[关闭]
@ironzhang 2018-02-08T07:07:34.000000Z 字数 3387 阅读 927

golang实现TLS双向认证

技术文章/golang


TLS身份认证原理

以单向的客户端认证服务端身份为例

首先我们需要生成一个用于签发数字证书的CA私钥CA数字证书

  1. openssl genrsa -out ca.key 2048
  2. openssl req -x509 -new -nodes -key ca.key -days 5000 -out ca.crt -subj "/CN=ablecloud.cn"

然后需要生成服务端私钥和数字证书,并用CA私钥给服务端数字证书做签名。

  1. openssl genrsa -out server.key 2048
  2. openssl req -new -key server.key -subj "/CN=localhost" -out server.csr
  3. openssl x509 -req -in server.csr -CA ../root/ca.crt -CAkey ../root/ca.key -CAcreateserial -out server.crt -days 5000

单向认证流程如下:

  1. 客户端发送连接请求
  2. 服务将自己的数字证书(证书中包含服务端公钥和签名等信息)发给客户端
  3. 客户端利用本地的CA数字证书验证服务端下发的数字证书,以验证服务端身份
  4. 客户端随机生成一个session秘钥,并用服务端下发的数字证书中的公钥对这个秘钥加密,并发给服务端
  5. 服务端用自身私钥解密,然后服务端和客户端以后就用这个session秘钥进行加密通信

以上流程中忽略了双方协商加密算法之类的步骤。如果是双向认证,则还有服务端要求客户端发送证书的过程[1]

实现TLS双向认证

要实现双向认证,还需要生成客户端的私钥和数字证书,并用CA私钥给服务端数字证书做签名。

  1. openssl genrsa -out client.key 2048
  2. openssl req -new -key client.key -subj "/CN=localhost" -out client.csr
  3. openssl x509 -req -in client.csr -CA ../root/ca.crt -CAkey ../root/ca.key -CAcreateserial -out client.crt -days 5000

加载TLS配置代码

  1. func LoadCertPool(caFile string) (*x509.CertPool, error) {
  2. pem, err := ioutil.ReadFile(caFile)
  3. if err != nil {
  4. return nil, err
  5. }
  6. pool := x509.NewCertPool()
  7. if !pool.AppendCertsFromPEM(pem) {
  8. return nil, errors.New("pool append certs from pem failed")
  9. }
  10. return pool, nil
  11. }
  12. func LoadTLSConfig(caFile, certFile, keyFile string) (*tls.Config, error) {
  13. pool, err := LoadCertPool(caFile)
  14. if err != nil {
  15. return nil, fmt.Errorf("load cert pool from (%s): %v", caFile, err)
  16. }
  17. cert, err := tls.LoadX509KeyPair(certFile, keyFile)
  18. if err != nil {
  19. return nil, fmt.Errorf("load x509 key pair from (%s, %s): %v", certFile, keyFile, err)
  20. }
  21. cfg := &tls.Config{
  22. RootCAs: pool,
  23. ClientCAs: pool,
  24. ClientAuth: tls.RequireAndVerifyClientCert,
  25. MinVersion: tls.VersionTLS12,
  26. Certificates: []tls.Certificate{cert},
  27. }
  28. return cfg, nil
  29. }

server代码

  1. func main() {
  2. config, err := tlscfg.LoadTLSConfig("../openssl/root/ca.crt", "../openssl/server/server.crt", "../openssl/server/server.key")
  3. if err != nil {
  4. log.Fatal(err)
  5. }
  6. ln, err := tls.Listen("tcp", "localhost:8000", config)
  7. if err != nil {
  8. log.Fatal(err)
  9. }
  10. for {
  11. conn, err := ln.Accept()
  12. if err != nil {
  13. log.Fatal(err)
  14. }
  15. go handle(conn)
  16. }
  17. }
  18. func handle(conn net.Conn) {
  19. input := bufio.NewScanner(conn)
  20. for input.Scan() {
  21. fmt.Println(input.Text())
  22. fmt.Fprintf(conn, input.Text())
  23. }
  24. }

client代码

  1. func main() {
  2. config, err := tlscfg.LoadTLSConfig("../openssl/root/ca.crt", "../openssl/client/client.crt", "../openssl/client/client.key")
  3. if err != nil {
  4. log.Fatal(err)
  5. }
  6. conn, err := tls.Dial("tcp", "localhost:8000", config)
  7. if err != nil {
  8. log.Fatal(err)
  9. }
  10. defer conn.Close()
  11. go mustCopy(os.Stdout, conn)
  12. mustCopy(conn, os.Stdin)
  13. }
  14. func mustCopy(dst io.Writer, src io.Reader) {
  15. if _, err := io.Copy(dst, src); err != nil {
  16. log.Fatal(err)
  17. }
  18. }

证书与秘钥管理

要以TLS方式接入我们系统,设备端需要三个文件,CA数字证书设备私钥设备数字证书
CA数字证书必须由我们颁发,没有保密要求。
设备私钥由厂商自己生成,设备私钥不能泄露。
设备数字证书须由我们颁发,需要厂商根据设备私钥先生成一个数字证书,再将该证书传给我们,由我们的CA私钥CA证书对其签发生成真正的设备数字证书,在保证设备私钥不泄露的情况下,设备数字证书没有保密要求。

后端需要四个文件,CA私钥CA数字证书服务端私钥服务端数字证书

CA私钥用来和CA数字证书一起签发服务端数字证书设备端数字证书,必须保密。
CA数字证书还被服务端用来验证设备数字证书的真假,没有保密要求。
服务端私钥必须保密
服务端数字证书没有保密要求。

问题

  1. 数字证书都有过期时间,需要考虑过期后,如何更新数字证书。
  2. 一个厂商的设备私钥和设备数字证书泄露后,按原本的方案,只需重置该厂商泄露的公私钥即可,而该方案没有类似机制,需要调研一下有没有另数字证书提前过期的方法。


[1] 双向认证流程
1. 浏览器发送一个连接请求给安全服务器。
2. 服务器将自己的证书,以及同证书相关的信息发送给客户浏览器。
3. 客户浏览器检查服务器送过来的证书是否是由自己信赖的CA中心所签发的。如果是,就继续执行协议;如果不是,客户浏览器就给客户一个警告消息:警告客户这个证书不是可以信赖的,询问客户是否需要继续。
4. 接着客户浏览器比较证书里的消息,例如域名和公钥,与服务器刚刚发送的相关消息是否一致,如果是一致的,客户浏览器认可这个服务器的合法身份。
5. 服务器要求客户发送客户自己的证书。收到后,服务器验证客户的证书,如果没有通过验证,拒绝连接;如果通过验证,服务器获得用户的公钥。
6. 客户浏览器告诉服务器自己所能够支持的通讯对称密码方案。
7. 服务器从客户发送过来的密码方案中,选择一种加密程度最高的密码方案,用客户的公钥加过密后通知浏览器。
8. 浏览器针对这个密码方案,选择一个通话密钥,接着用服务器的公钥加过密后发送给服务器。
9. 服务器接收到浏览器送过来的消息,用自己的私钥解密,获得通话密钥。
10. 服务器、浏览器接下来的通讯都是用对称密码方案,对称密钥是加过密的。
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注