写给go开发者的gRPC教程-通信安全

使用 TLS 安全传输数据

什么是 SSL/TLS

SSL 包含记录层(Record Layer)和传输层[1],记录层协议确定传输层数据的封装格式。传输层安全协议使用X.509[2]认证,之后利用非对称加密演算来对通信方做身份认证,之后交换对称密匙作为会话密匙(Session key[3])。这个会谈密匙是用来将通信两方交换的资料做加密,保证两个应用间通信的保密性和可靠性,使客户与服务器应用之间的通信不被攻击者窃听。

--- 维基百科

简单点说就是:SSL/TLS 是一个安全协议,它通过一系列的手段、一系列的算法让客户端与服务端之间加密传输数据,避免数据被攻击者窃听。快速入门 TLS 可以参考:一文带你快速入门 TLS/SSL

SSL/TLS 分为单向认证和双向认证(mtls)

单向认证

在单向认证中,仅客户端验证服务端

当客户端和服务端建立连接之后,服务端会发送公开的证书给客户端,客户端验证证书后使用证书中包含的密钥信息来发送加密数据(实际要比这个复杂,这里简化了交互流程)

TLS 证书可以使用根证书创建子证书,因此对于证书有两种使用方式,一种是直接使用根证书,一种是使用由根证书签发的子证书

✨ 直接使用根证书

所以对于服务端来说需要两个文件

server.key:RSA 的私钥,用来进行数字签名

server.pem/server.crt:自签名的服务端证书,其中包含与私钥对应的公钥、网站域名、签名算法等信息

对于客户端来说不需要准备文件

写给go开发者的gRPC教程-通信安全_第1张图片 单向认证-直接使用根证书

服务端代码如下:

1)NewServerTLSFromFile加载证书 2)NewServer 时指定 Creds

func main() {
 l, err := net.Listen("tcp", ":8009")
 if err != nil {
  panic(err)
 }

 // method 1.
 creds, err := credentials.NewServerTLSFromFile("./x509/server.crt", "./x509/server.key")
 if err != nil {
  panic(err)
 }

 // method 2.
 // cert , err := tls.LoadX509KeyPair("./x509/server.crt", "./x509/server.key")
 // if err != nil {
 //  panic(err)
 // }

 // creds := credentials.NewServerTLSFromCert(&cert)

 s := grpc.NewServer(grpc.Creds(creds))

 pb.RegisterOrderManagementServer(s, &server{})

 if err := s.Serve(l); err != nil {
  panic(err)
 }
}

客户端代码如下:

1)NewClientTLSFromFile指定使用 CA 证书来校验服务端的证书有效性。

  • 注意:第二个参数域名就是服务端证书时的 CN 参数

2)建立连接时 指定建立安全连接WithTransportCredentials

func main() {
 creds, err := credentials.NewClientTLSFromFile("./x509/server.crt", "www.example.com")
 if err != nil {
  panic(err)
 }

 conn, err := grpc.Dial("localhost:8009", grpc.WithTransportCredentials(creds))
 if err != nil {
  panic(err)
 }
 defer conn.Close()

 client := pb.NewOrderManagementClient(conn)

 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
 defer cancel()

 // Get Order
 retrievedOrder, err := client.GetOrder(ctx, &wrapperspb.StringValue{Value: "101"})
 if err != nil {
  panic(err)
 }

 log.Print("GetOrder Response -> : ", retrievedOrder)
}

✨ 根证书模式

在生产环境通常是生成一个 CA 根证书,然后使用 CA 根证书去签名多个服务端的证书。这种方式可以一次管理多个证书,也比较贴近真实情况,当然也可以用来做证书过期、更新等试验,缺点是操作起来略微麻烦一点。

rootCA.key:ca 机构的私钥,用来给服务端签发证书

rootCA.crt:ca 的证书,用来给客户端验证服务端证书

server.key:RSA 的私钥,用来进行数字签名

server.pem/server.crt:由 ca 签发的服务端证书

写给go开发者的gRPC教程-通信安全_第2张图片 单向认证-根证书模式

服务端代码如下:

1)NewServerTLSFromFile加载证书 2)NewServer 时指定 Creds

func main() {
 l, err := net.Listen("tcp", ":8009")
 if err != nil {
  panic(err)
 }

 // method 1.
 creds, err := credentials.NewServerTLSFromFile("./x509/server.crt", "./x509/server.key")
 if err != nil {
  panic(err)
 }

 // method 2.
 // cert , err := tls.LoadX509KeyPair("./x509/server.crt", "./x509/server.key")
 // if err != nil {
 //  panic(err)
 // }

 // creds := credentials.NewServerTLSFromCert(&cert)

 s := grpc.NewServer(grpc.Creds(creds))

 pb.RegisterOrderManagementServer(s, &server{})

 if err := s.Serve(l); err != nil {
  panic(err)
 }
}

客户端代码如下:

1)NewClientTLSFromFile 指定使用CA 根证书来校验服务端的证书有效性。

  • 注意:第二个参数域名就是服务端证书时的 CN 参数

2)建立连接时 指定建立安全连接WithTransportCredentials

func main() {
 creds, err := credentials.NewClientTLSFromFile("./x509/rootCa.crt", "www.example.com")
 if err != nil {
  panic(err)
 }

 conn, err := grpc.Dial("localhost:8009", grpc.WithTransportCredentials(creds))
 if err != nil {
  panic(err)
 }
 defer conn.Close()

 client := pb.NewOrderManagementClient(conn)

 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
 defer cancel()

 // Get Order
 retrievedOrder, err := client.GetOrder(ctx, &wrapperspb.StringValue{Value: "101"})
 if err != nil {
  panic(err)
 }

 log.Print("GetOrder Response -> : ", retrievedOrder)
}

双向认证(mTLS)

server-side TLS 中虽然服务端使用了证书,但是客户端却没有使用证书,本章节会给客户端也生成一个证书,并完成 mutual TLS

直接使用根证书

在 mTLS 中很少会有直接使用根证书的场景,这里仅放一个交互图,不放代码了

写给go开发者的gRPC教程-通信安全_第3张图片 双向认证-直接使用根证书

根证书模式

rootCA.key:ca 机构的私钥,用来给服务端签发证书

rootCA.crt:ca 的证书,用来给客户端验证服务端证书

server.key:服务端 RSA 的私钥,用来进行数字签名

server.pem/server.crt:由 ca 签发的服务端证书

client.key: 客户端 RSA 私钥,用来进行数字签名

client.pem/client.crt: 由 ca 签发的客户端证书

写给go开发者的gRPC教程-通信安全_第4张图片 双向认证-根证书模式

服务端代码如下:

1)加载服务端证书

2)构建用于校验客户端证书的CertPool

3)使用上面的参数构建一个TransportCredentials

4)newServer 是指定使用前面创建的creds

看似改动很大,其实如果你仔细查看了前面NewServerTLSFromFile方法做的事的话,就会发现是差不多的,只有极个别参数不同。

修改点如下:

1)tls.Config的参数ClientAuth,这里改成了tls.RequireAndVerifyClientCert,即服务端也必须校验客户端的证书,之前使用的默认值(即不校验)

2)tls.Config的参数ClientCAs,由于之前都不校验客户端证书,所以也没有指定用什么证书来校验

func main() {
 l, err := net.Listen("tcp", ":8009")
 if err != nil {
  panic(err)
 }

 certificate, err := tls.LoadX509KeyPair("./x509/server.crt", "./x509/server.key")
 if err != nil {
  panic(err)
 }

 // 创建CertPool,后续就用池里的证书来校验客户端证书有效性
 // 所以如果有多个客户端 可以给每个客户端使用不同的 CA 证书,来实现分别校验的目的
 certPool := x509.NewCertPool()
 ca, err := ioutil.ReadFile("./x509/rootCa.crt")
 if err != nil {
  panic(err)
 }
 if ok := certPool.AppendCertsFromPEM(ca); !ok {
  log.Fatal("failed to append certs")
 }

 // 构建基于 TLS 的 TransportCredentials
 creds := credentials.NewTLS(&tls.Config{
  // 设置证书链,允许包含一个或多个
  Certificates: []tls.Certificate{certificate},
  // 要求必须校验客户端的证书 可以根据实际情况选用其他参数
  ClientAuth: tls.RequireAndVerifyClientCert, // NOTE: this is optional!
  // 设置根证书的集合,校验方式使用 ClientAuth 中设定的模式
  ClientCAs: certPool,
 })

 s := grpc.NewServer(grpc.Creds(creds))

 pb.RegisterOrderManagementServer(s, &server{})

 if err := s.Serve(l); err != nil {
  panic(err)
 }
}

客户端代码如下:

客户端改动和前面服务端差不多,具体步骤都一样,除了不能指定校验策略之外基本一样。

func main() {
 // 加载客户端证书
 certificate, err := tls.LoadX509KeyPair("x509/client.crt", "x509/client.key")
 if err != nil {
  log.Fatal(err)
 }

 // 构建CertPool以校验服务端证书有效性
 b, err := ioutil.ReadFile("./x509/rootCa.crt")
 if err != nil {
  log.Fatal(err)
 }
 cp := x509.NewCertPool()
 if !cp.AppendCertsFromPEM(b) {
  log.Fatal("credentials: failed to append certificates")
 }

 creds := credentials.NewTLS(&tls.Config{
  Certificates: []tls.Certificate{certificate},
  ServerName:   "www.example.com",
  RootCAs:      cp,
 })

 conn, err := grpc.Dial("localhost:8009", grpc.WithTransportCredentials(creds))
 if err != nil {
  panic(err)
 }
 defer conn.Close()

 client := pb.NewOrderManagementClient(conn)

 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
 defer cancel()

 // Get Order
 retrievedOrder, err := client.GetOrder(ctx, &wrapperspb.StringValue{Value: "101"})
 if err != nil {
  panic(err)
 }

 log.Print("GetOrder Response -> : ", retrievedOrder)
}

可能遇到的问题

报错:transport: authentication handshake failed: x509: certificate relies on legacy Common Name field, use SANs instead

如果出现上述报错,是因为 go 1.15 版本开始废弃 CommonName[4],因此推荐使用 SAN 证书。如果想兼容之前的方式,需要设置环境变量 GODEBUG 为 GODEBUG=x509ignoreCN=0

什么是 SAN?SAN(Subject Alternative Name) 是 SSL 标准 x509 中定义的一个扩展。使用了 SAN 字段的 SSL 证书,可以扩展此证书支持的域名,使得一个证书可以支持多个不同域名的解析。

SAN 证书包含 Subject Alternative Name 部分

openssl x509 -text -noout -in server.crt | grep -A 1 "Subject Alternative Name"
            X509v3 Subject Alternative Name:
                DNS:www.example.com, DNS:localhost, DNS:127.0.0.1, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1

证书生成

方式一:生成自签名的证书

openssl req -config conf/openssl.cnf -x509 -newkey rsa:2048 -nodes -days 365 \
 -keyout server.key   \
 -out server.crt

方式二:根证书签发子证书

1、创建自签名的 ca 根证书

openssl req -config conf/openssl.cnf -x509 -newkey rsa:2048 -nodes -days 365 \
-keyout rootCa.key   \
-out rootCa.crt

2、创建服务端私钥和 CSR(证书签名请求),这里没有指定-x509, 说明这不是一个自签署的证书,而是要交给 CA 签名认证

openssl req -config conf/openssl.cnf -newkey rsa:2048 -nodes \
-keyout server.key \
-out server.csr

通过下列命令检查一下生成的 csr 文件是否信息正确无误。

openssl req -text -noout -verify -in server.csr

3、使用证书机构的私钥、根证书和 example.com 的 CSR 创建数字证书

⚠️ 不加-extfile 和-extensions 参数是不行的

openssl x509 -req -in server.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial \
-extfile conf/openssl.cnf \
-extensions v3_req \
-out server.crt

签署后,查看证书的内容:

openssl x509 -text -noout -in server.crt

欢迎关注

- END -

扫码关注公众号「网管叨bi叨」

给网管个星标,第一时间吸我的知识 

网管整理了一本《Go 开发参考书》收集了70多条开发实践。去公众号回复【gocookbook】领取!还有一本《k8s 入门实践》讲解了常用软件在K8s上的部署过程,公众号回复【k8s】即可领取!

觉得有用就点个在看 

你可能感兴趣的:(golang,ssl,https,服务器,网络)