「连载四」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. CountryName(2 letter code)[]:
  2. StateorProvinceName(full name)[]:
  3. LocalityName(eg, city)[]:
  4. OrganizationName(eg, company)[]:
  5. OrganizationalUnitName(eg, section)[]:
  6. CommonName(eg, fully qualified host name)[]:go-grpc-example
  7. EmailAddress[]:

生成完毕

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

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

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

为什么之前不需要证书

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

Server

  1. grpc.NewServer()

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

Client

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

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

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

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

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

  1. func DialContext(ctx context.Context, target string, opts ...DialOption)(conn *ClientConn, err error){
  2. ...
  3. for _, opt := range opts {
  4. opt.apply(&cc.dopts)
  5. }
  6. ...
  7. if!cc.dopts.insecure {
  8. if cc.dopts.copts.TransportCredentials==nil{
  9. returnnil, errNoTransportSecurity
  10. }
  11. }else{
  12. if cc.dopts.copts.TransportCredentials!=nil{
  13. returnnil, errCredentialsConflict
  14. }
  15. for _, cd := range cc.dopts.copts.PerRPCCredentials{
  16. if cd.RequireTransportSecurity(){
  17. returnnil, errTransportCredentialsMissing
  18. }
  19. }
  20. }
  21. ...
  22. creds := cc.dopts.copts.TransportCredentials
  23. if creds !=nil&& creds.Info().ServerName!=""{
  24. cc.authority = creds.Info().ServerName
  25. }elseif cc.dopts.insecure && cc.dopts.authority !=""{
  26. cc.authority = cc.dopts.authority
  27. }else{
  28. // Use endpoint from "scheme://authority/endpoint" as the default
  29. // authority for ClientConn.
  30. cc.authority = cc.parsedTarget.Endpoint
  31. }
  32. ...
  33. }

gRPC

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

TLS Server

  1. package main
  2. import(
  3. "context"
  4. "log"
  5. "net"
  6. "google.golang.org/grpc"
  7. "google.golang.org/grpc/credentials"
  8. pb "github.com/EDDYCJY/go-grpc-example/proto"
  9. )
  10. ...
  11. const PORT ="9001"
  12. func main(){
  13. c, err := credentials.NewServerTLSFromFile("../../conf/server.pem","../../conf/server.key")
  14. if err !=nil{
  15. log.Fatalf("credentials.NewServerTLSFromFile err: %v", err)
  16. }
  17. server := grpc.NewServer(grpc.Creds(c))
  18. pb.RegisterSearchServiceServer(server,&SearchService{})
  19. lis, err := net.Listen("tcp",":"+PORT)
  20. if err !=nil{
  21. log.Fatalf("net.Listen err: %v", err)
  22. }
  23. server.Serve(lis)
  24. }
  • credentials.NewServerTLSFromFile:根据服务端输入的证书文件和密钥构造 TLS 凭证
  1. func NewServerTLSFromFile(certFile, keyFile string)(TransportCredentials, error){
  2. cert, err := tls.LoadX509KeyPair(certFile, keyFile)
  3. if err !=nil{
  4. returnnil, err
  5. }
  6. returnNewTLS(&tls.Config{Certificates:[]tls.Certificate{cert}}),nil
  7. }
  • grpc.Creds():返回一个 ServerOption,用于设置服务器连接的凭据。用于 grpc.NewServer(opt ...ServerOption) 为 gRPC Server 设置连接选项
  1. func Creds(c credentials.TransportCredentials)ServerOption{
  2. return func(o *options){
  3. o.creds = c
  4. }
  5. }

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

TLS Client

  1. package main
  2. import(
  3. "context"
  4. "log"
  5. "google.golang.org/grpc"
  6. "google.golang.org/grpc/credentials"
  7. pb "github.com/EDDYCJY/go-grpc-example/proto"
  8. )
  9. const PORT ="9001"
  10. func main(){
  11. c, err := credentials.NewClientTLSFromFile("../../conf/server.pem","go-grpc-example")
  12. if err !=nil{
  13. log.Fatalf("credentials.NewClientTLSFromFile err: %v", err)
  14. }
  15. conn, err := grpc.Dial(":"+PORT, grpc.WithTransportCredentials(c))
  16. if err !=nil{
  17. log.Fatalf("grpc.Dial err: %v", err)
  18. }
  19. defer conn.Close()
  20. client := pb.NewSearchServiceClient(conn)
  21. resp, err := client.Search(context.Background(),&pb.SearchRequest{
  22. Request:"gRPC",
  23. })
  24. if err !=nil{
  25. log.Fatalf("client.Search err: %v", err)
  26. }
  27. log.Printf("resp: %s", resp.GetResponse())
  28. }
  • credentials.NewClientTLSFromFile():根据客户端输入的证书文件和密钥构造 TLS 凭证。serverNameOverride 为服务名称
  1. func NewClientTLSFromFile(certFile, serverNameOverride string)(TransportCredentials, error){
  2. b, err := ioutil.ReadFile(certFile)
  3. if err !=nil{
  4. returnnil, err
  5. }
  6. cp := x509.NewCertPool()
  7. if!cp.AppendCertsFromPEM(b){
  8. returnnil, fmt.Errorf("credentials: failed to append certificates")
  9. }
  10. returnNewTLS(&tls.Config{ServerName: serverNameOverride,RootCAs: cp}),nil
  11. }
  • grpc.WithTransportCredentials():返回一个配置连接的 DialOption 选项。用于 grpc.Dial(target string, opts ...DialOption) 设置连接选项
  1. func WithTransportCredentials(creds credentials.TransportCredentials)DialOption{
  2. return newFuncDialOption(func(o *dialOptions){
  3. o.copts.TransportCredentials= creds
  4. })
  5. }

验证

请求

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

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

抓个包

image

成功。

总结

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

问题

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

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

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

参考

本系列示例代码

「连载四」TLS 证书认证 - 图4