「连载四」TLS 证书认证

前言

在前面的章节里,我们介绍了 gRPC 的四种 API 使用方式。是不是很简单呢 😀

此时存在一个安全问题,先前的例子中 gRPC Client/Server 都是明文传输的,会不会有被窃听的风险呢?

从结论上来讲,是有的。在明文通讯的情况下,你的请求就是裸奔的,有可能被第三方恶意篡改或者伪造为“非法”的数据

抓个包

image

image

嗯,明文传输无误。这是有问题的,接下将改造我们的 gRPC,以便于解决这个问题 😤

证书生成

私钥

1
openssl ecparam -genkey -name secp384r1 -out server.key

自签公钥

1
openssl req -new -x509 -sha256 -key server.key -out server.pem -days 3650

填写信息

1
2
3
4
5
6
7
Country Name (2 letter code) []:
State or Province Name (full name) []:
Locality Name (eg, city) []:
Organization Name (eg, company) []:
Organizational Unit Name (eg, section) []:
Common Name (eg, fully qualified host name) []:go-grpc-example
Email Address []:

生成完毕

生成证书结束后,将证书相关文件放到 conf/ 下,目录结构:

1
2
3
4
5
6
7
8
9
10
$ tree go-grpc-example
go-grpc-example
├── client
├── conf
│   ├── server.key
│   └── server.pem
├── proto
└── server
├── simple_server
└── stream_server

由于本文偏向 gRPC,详解可参见 《制作证书》。后续番外可能会展开细节描述 👌

为什么之前不需要证书

在 simple_server 中,为什么“啥事都没干”就能在不需要证书的情况下运行呢?

Server

1
grpc.NewServer()

在服务端显然没有传入任何 DialOptions

Client

1
conn, err := grpc.Dial(":"+PORT, grpc.WithInsecure())

在客户端留意到 grpc.WithInsecure() 方法

1
2
3
4
5
func WithInsecure() DialOption {
return newFuncDialOption(func(o *dialOptions) {
o.insecure = true
})
}

在方法内可以看到 WithInsecure 返回一个 DialOption,并且它最终会通过读取设置的值来禁用安全传输

那么它“最终”又是在哪里处理的呢,我们把视线移到 grpc.Dial() 方法内

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
func DialContext(ctx context.Context, target string, opts ...DialOption) (conn *ClientConn, err error) {
...

for _, opt := range opts {
opt.apply(&cc.dopts)
}
...

if !cc.dopts.insecure {
if cc.dopts.copts.TransportCredentials == nil {
return nil, errNoTransportSecurity
}
} else {
if cc.dopts.copts.TransportCredentials != nil {
return nil, errCredentialsConflict
}
for _, cd := range cc.dopts.copts.PerRPCCredentials {
if cd.RequireTransportSecurity() {
return nil, errTransportCredentialsMissing
}
}
}
...

creds := cc.dopts.copts.TransportCredentials
if creds != nil && creds.Info().ServerName != "" {
cc.authority = creds.Info().ServerName
} else if cc.dopts.insecure && cc.dopts.authority != "" {
cc.authority = cc.dopts.authority
} else {
// Use endpoint from "scheme://authority/endpoint" as the default
// authority for ClientConn.
cc.authority = cc.parsedTarget.Endpoint
}
...
}

gRPC

接下来我们将正式开始编码,在 gRPC Client/Server 上实现 TLS 证书认证的支持 🤔

TLS Server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
"context"
"log"
"net"

"google.golang.org/grpc"
"google.golang.org/grpc/credentials"

pb "github.com/EDDYCJY/go-grpc-example/proto"
)

...

const PORT = "9001"

func main() {
c, err := credentials.NewServerTLSFromFile("../../conf/server.pem", "../../conf/server.key")
if err != nil {
log.Fatalf("credentials.NewServerTLSFromFile err: %v", err)
}

server := grpc.NewServer(grpc.Creds(c))
pb.RegisterSearchServiceServer(server, &SearchService{})

lis, err := net.Listen("tcp", ":"+PORT)
if err != nil {
log.Fatalf("net.Listen err: %v", err)
}

server.Serve(lis)
}
  • credentials.NewServerTLSFromFile:根据服务端输入的证书文件和密钥构造 TLS 凭证
1
2
3
4
5
6
7
func NewServerTLSFromFile(certFile, keyFile string) (TransportCredentials, error) {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, err
}
return NewTLS(&tls.Config{Certificates: []tls.Certificate{cert}}), nil
}
  • grpc.Creds():返回一个 ServerOption,用于设置服务器连接的凭据。用于 grpc.NewServer(opt ...ServerOption) 为 gRPC Server 设置连接选项
1
2
3
4
5
func Creds(c credentials.TransportCredentials) ServerOption {
return func(o *options) {
o.creds = c
}
}

经过以上两个简单步骤,gRPC Server 就建立起需证书认证的服务啦 🤔

TLS Client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
"context"
"log"

"google.golang.org/grpc"
"google.golang.org/grpc/credentials"

pb "github.com/EDDYCJY/go-grpc-example/proto"
)

const PORT = "9001"

func main() {
c, err := credentials.NewClientTLSFromFile("../../conf/server.pem", "go-grpc-example")
if err != nil {
log.Fatalf("credentials.NewClientTLSFromFile err: %v", err)
}

conn, err := grpc.Dial(":"+PORT, grpc.WithTransportCredentials(c))
if err != nil {
log.Fatalf("grpc.Dial err: %v", err)
}
defer conn.Close()

client := pb.NewSearchServiceClient(conn)
resp, err := client.Search(context.Background(), &pb.SearchRequest{
Request: "gRPC",
})
if err != nil {
log.Fatalf("client.Search err: %v", err)
}

log.Printf("resp: %s", resp.GetResponse())
}
  • credentials.NewClientTLSFromFile():根据客户端输入的证书文件和密钥构造 TLS 凭证。serverNameOverride 为服务名称
1
2
3
4
5
6
7
8
9
10
11
func NewClientTLSFromFile(certFile, serverNameOverride string) (TransportCredentials, error) {
b, err := ioutil.ReadFile(certFile)
if err != nil {
return nil, err
}
cp := x509.NewCertPool()
if !cp.AppendCertsFromPEM(b) {
return nil, fmt.Errorf("credentials: failed to append certificates")
}
return NewTLS(&tls.Config{ServerName: serverNameOverride, RootCAs: cp}), nil
}
  • grpc.WithTransportCredentials():返回一个配置连接的 DialOption 选项。用于 grpc.Dial(target string, opts ...DialOption) 设置连接选项
1
2
3
4
5
func WithTransportCredentials(creds credentials.TransportCredentials) DialOption {
return newFuncDialOption(func(o *dialOptions) {
o.copts.TransportCredentials = creds
})
}

验证

请求

重新启动 server.go 和执行 client.go,得到响应结果

1
2
$ go run client.go
2018/09/30 20:00:21 resp: gRPC Server

抓个包

image

成功。

总结

在本章节我们实现了 gRPC TLS Client/Servert,你以为大功告成了吗?我不 😤

问题

你仔细再看看,Client 是基于 Server 端的证书和服务名称来建立请求的。这样的话,你就需要将 Server 的证书通过各种手段给到 Client 端,否则是无法完成这项任务的

问题也就来了,你无法保证你的“各种手段”是安全的,毕竟现在的网络环境是很危险的,万一被…

我们将在下一章节解决这个问题,保证其可靠性 🙂

参考

本系列示例代码


免责声明:本页面内容均来源于站内编辑发布,部分信息来源互联网,并不意味着本站赞同其观点或者证实其内容的真实性,如涉及版权等问题,请立即联系客服进行更改或删除,保证您的合法权益。转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。也可以邮件至 sblig@126.com

推荐阅读:

文章标题:「连载四」TLS 证书认证

本文作者:知识铺

发布时间:2018-10-07, 12:00:00

最后更新:2020-04-19, 17:52:50

原始链接:https://blog.zshipu.com/2018/10/07/golang/go/grpc/2018-10-07-grpc-tls/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录
×

喜欢就点赞,疼爱就打赏