标签 SHA-1 下的文章

使用Go基于国密算法实现双向认证

本文永久链接 – https://tonybai.com/2022/07/17/two-way-authentication-using-go-and-sm-algorithm

国内做2B(to Biz)或2G(to Gov)产品和解决方案的企业都绕不过国密算法,越来越多的国内甲方在采购需求中包含了基于国密算法的认证、签名、加密等需求。对于国内的车联网平台来说,支持基于国密的双向认证也是大势所趋。在这篇文章中,我就来说说如何基于国密算法实现双向认证,即使用国密算法的安全传输层双向认证


一. 简要回顾基于TLS的双向认证

《Go语言精进之路》第2册的第51条中,我详细介绍了TLS的建连握手与双向认证过程,并对非对称加密与公钥证书的原理做了系统全面的讲解。为了让大家更好地理解后面的内容,这里简单回顾一下基于TLS的双向认证

TLS,全称Transport Layer Security,即安全传输层。其前身为SSL(Secure Socket Layer)。TLS是建构在TCP传输层之上和应用层之下的、为应用层提供端到端安全连接和传输服务的虚拟协议层

应用层基于TLS的通信都是加密的(如上图所示),保证了传输数据的安全,即便被窃听,攻击者也无法拿到明文数据(密钥够长,加密算法强度够强的前提下)。对于应用开发者而言,重点在于TLS连接的建立过程,连接一旦建立,后续的加解密传输过程就很容易了。

TLS连接的建立过程称为TLS握手(handshake),握手的过程见下图(适用于TLS 1.2):


上图引自《Go语言精进之路》第2册的图51-5

关于握手的各个步骤的详细说明,大家可以参考《Go语言精进之路》第2册的第51条中的内容,这里不赘述。

从图中我们可以看到:TLS连接的建立过程需要数字证书的参与,而数字证书主要用于对通信双方的身份进行验证以及参与双方会话密钥的协商与生成。一般情况下,客户端会校验服务端的公钥证书,服务端不会校验客户端公钥证书。但在一些安全级别较高的系统中,服务端也会要求校验客户端的公钥证书(TLS握手阶段,服务端向客户端发送CertificateRequest请求)。

下面我们就来看一个基于TLS的双向认证的实例。

二. 基于TLS双向认证的示例

我们先来看看示例开发与执行的环境并创建相关的数字证书。

1. 环境与数字证书

我们在Ubuntu 20.04.3 LTS环境使用Go 1.18版本开发和执行该示例。示例是一个echo server,即将client端发来的数据重新发回client端,下面是示意图:

开发基于TLS的应用离不开数字证书,因此在开发程序之前,我们先来生成server与client所用的各类公钥数字证书。

在开发和测试环境,我们可以使用自签发的公钥数字证书。我们可以先生成自用的CA私钥与证书,然后利用该CA签发我们所需的服务端和客户端的公钥证书。制作证书最著名的工具是openssl,不过openssh使用起来较为复杂,这些年一些开发者体验更好的工具也逐渐成熟,比如由Go项目前安全负责人Filippo Valsorda开源的mkcert就是一个不错的工具,本文就使用这个工具建立CA并签发制作各类证书。

我们先来安装mkcert工具:

$go install filippo.io/mkcert@latest
go: downloading filippo.io/mkcert v1.4.4
go: downloading golang.org/x/net v0.0.0-20220421235706-1d1ef9303861
go: downloading software.sslmate.com/src/go-pkcs12 v0.2.0
go: downloading golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29

接下来,生成并安装local CA根证书:

$mkcert -install
Created a new local CA:
The local CA is now installed in the system trust store!

从mkcert的输出来看,CA私钥和证书被安装到所谓system trust store中。这个system trust store在不同平台上的位置不同。在linux上有如下几个位置:

// github.com/FiloSottile/mkcert/truststore_linux.go 

func init() {
    switch {
    case binaryExists("apt"):
        CertutilInstallHelp = "apt install libnss3-tools"
    case binaryExists("yum"):
        CertutilInstallHelp = "yum install nss-tools"
    case binaryExists("zypper"):
        CertutilInstallHelp = "zypper install mozilla-nss-tools"
    }
    if pathExists("/etc/pki/ca-trust/source/anchors/") {
        SystemTrustFilename = "/etc/pki/ca-trust/source/anchors/%s.pem"
        SystemTrustCommand = []string{"update-ca-trust", "extract"}
    } else if pathExists("/usr/local/share/ca-certificates/") {
        SystemTrustFilename = "/usr/local/share/ca-certificates/%s.crt"
        SystemTrustCommand = []string{"update-ca-certificates"}
    } else if pathExists("/etc/ca-certificates/trust-source/anchors/") {
        SystemTrustFilename = "/etc/ca-certificates/trust-source/anchors/%s.crt"
        SystemTrustCommand = []string{"trust", "extract-compat"}
    } else if pathExists("/usr/share/pki/trust/anchors") {
        SystemTrustFilename = "/usr/share/pki/trust/anchors/%s.pem"
        SystemTrustCommand = []string{"update-ca-certificates"}
    }
}

在我的ubuntu 20.04环境中,CA的公钥证书被安装(install)在/usr/local/share/ca-certificates下面了:

$ls -l /usr/local/share/ca-certificates/
total 4
-rw-r--r-- 1 root root 1631 Jul  3 16:22 mkcert_development_CA_333807542491031300702675758897110223851.crt

生成的CA私钥在哪里呢?我们可以通过-CAROOT参数获得该位置:

$mkcert -CAROOT
/home/tonybai/.local/share/mkcert

$ls -l /home/tonybai/.local/share/mkcert
total 8
-r-------- 1 tonybai tonybai 2484 Jul  3 16:22 rootCA-key.pem
-rw-r--r-- 1 tonybai tonybai 1631 Jul  3 16:22 rootCA.pem

这里的rootCA.pem与系统信任区中的mkcert_development_CA_333807542491031300702675758897110223851.crt与rootCA.pem内容是一样的。后者是mkcert将rootCA.pem安装到系统可信store后的结果。通过mkcert -uninstall可以删除/usr/local/share/ca-certificates下面的CA公钥证书。但/home/tonybai/.local/share/mkcert下的CA私钥与证书不会被删除。后续若再执行mkcert install,CA证书不会重新生成,现有的rootCA.pem还会被install到/usr/local/share/ca-certificates下面。

接下来我们分别server端和client端的私钥与证书。

server端key和cert:

$mkcert -key-file key.pem -cert-file cert.pem example.com 

Created a new certificate valid for the following names
 - "example.com"

The certificate is at "cert.pem" and the key at "key.pem"

It will expire on 3 October 2024

我们可以通过下面命令查看证书内容:

$openssl x509 -in cert.pem -noout -text
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            fc:cc:96:17:55:2d:70:e8:67:3e:b2:25:a9:b8:a3:80
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: O = mkcert development CA, OU = tonybai@tonybai, CN = mkcert tonybai@tonybai
        Validity
            Not Before: Jul  7 09:05:09 2022 GMT
            Not After : Oct  7 09:05:09 2024 GMT
        Subject: O = mkcert development certificate, OU = tonybai@tonybai
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:a6:d1:00:f7:da:03:d0:06:17:cb:ee:b4:99:30:
                    20:66:d0:78:b0:94:67:0b:7a:37:d2:76:21:71:9a:
                    7a:17:d6:44:0a:7b:f1:24:71:2f:ed:b5:67:66:a1:
                    1f:b0:e6:3b:18:66:de:f4:83:78:9a:bc:f5:ae:88:
                    23:a1:f9:7d:7c:3e:7f:a8:f9:9f:54:d0:68:48:b9:
                    d0:56:10:a0:84:0b:cf:a8:bc:b8:74:3f:3c:27:db:
                    ff:28:1d:63:e8:79:a6:93:44:a8:14:43:53:bf:e8:
                    ca:ee:bf:4c:63:f7:23:51:e6:a2:8d:0b:9a:7d:95:
                    2e:bc:37:ae:6d:ea:9e:0e:e6:e0:c5:8e:07:0c:d4:
                    9b:50:30:de:31:c9:97:ee:ac:7e:33:ab:0d:6f:87:
                    f3:70:2b:22:26:8d:a8:95:8e:1f:0e:b7:61:71:e8:
                    36:06:a7:f4:d8:d2:f6:89:12:26:fd:7e:6b:19:a2:
                    2a:4c:d7:cb:7e:09:fc:65:86:be:b6:c2:0b:fb:b5:
                    d8:63:07:aa:ba:59:ab:fc:34:0d:4a:d1:93:dd:62:
                    b0:3a:cd:e1:21:79:13:e4:f4:45:00:f7:10:a1:bc:
                    c7:51:38:84:c4:75:22:5e:5f:a9:11:07:34:16:9f:
                    ad:c7:94:af:57:30:17:77:49:14:6e:16:ff:d8:00:
                    78:11
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage:
                TLS Web Server Authentication
            X509v3 Authority Key Identifier:
                keyid:A8:C4:06:2D:2C:25:71:EC:08:C8:1A:92:9A:F2:52:87:22:6E:85:2D

            X509v3 Subject Alternative Name:
                DNS:example.com
    Signature Algorithm: sha256WithRSAEncryption
         be:6e:90:60:bd:43:b9:3a:09:14:c2:44:22:88:a6:af:e5:22:
         d3:97:19:64:8b:59:5d:60:33:36:01:a1:4e:01:eb:7e:5c:6a:
         48:c4:04:a6:0a:e4:91:95:db:5a:2c:c8:e9:93:fa:37:34:6d:
         81:d1:96:ed:5b:67:ae:27:e3:d3:ea:ee:5c:74:0f:6e:f1:48:
         72:d2:75:85:a1:70:0f:a0:9a:73:7a:ca:b8:7b:92:46:27:73:
         e5:f8:ec:72:f8:fc:ac:5f:22:68:0c:d6:8c:20:5b:93:e1:52:
         17:79:57:71:33:5b:98:05:11:8a:cb:d4:3c:b2:24:4b:7b:c5:
         32:8f:ae:1f:a5:af:9d:3a:9b:bb:fc:46:8a:d6:48:39:86:de:
         f3:f7:54:03:45:8d:bd:40:91:26:d2:29:0a:c4:91:cf:b2:5c:
         41:d5:66:24:02:6d:60:22:ea:78:0d:b0:66:80:b9:5d:03:27:
         09:c7:aa:61:1b:ee:e4:08:21:7e:be:bb:13:8a:fb:d8:9e:24:
         5f:5b:a2:4a:d5:db:be:a2:84:74:03:fb:04:37:d0:b3:c4:b7:
         4e:3e:31:a7:2d:5d:62:bd:aa:68:3c:84:d9:32:cb:f2:93:7a:
         3a:8a:2b:c3:81:76:f0:b5:f5:3c:d4:69:8d:5e:f8:39:74:88:
         2b:56:7f:2b:4c:f9:39:2a:f2:4d:15:75:a1:f3:62:ee:57:ce:
         f7:33:c7:cc:a6:97:25:f0:66:bf:5d:5b:c2:d7:d3:ee:20:be:
         c3:5f:fb:9a:50:59:b8:e7:ea:d2:4c:35:9d:48:3f:93:63:96:
         3c:52:dd:b8:d6:ba:1f:30:18:2e:c4:3d:3a:03:66:e1:a3:48:
         6e:a0:5d:b0:0b:65:d4:40:9e:da:5c:36:b1:ac:6b:9e:1f:01:
         69:8a:92:63:7d:27:79:42:bd:d4:f5:e2:d3:bf:8e:97:2f:57:
         ae:0b:f8:c1:b1:35:47:d0:4e:77:b0:e7:88:69:4b:44:dc:01:
         6e:6e:4d:87:e2:71

接下来,我们再生成client端的key和cert。client端的cert专门用来提供给server端进行证书验证的,我们需要向mkcert传入-client选项生成client端证书:

$mkcert -client -key-file client-key.pem -cert-file client-cert.pem client1

Created a new certificate valid for the following names
 - "client1"

The certificate is at "client-cert.pem" and the key at "client-key.pem"

It will expire on 3 October 2024

同样,我们可以通过下面命令查看客户端证书的内容:

$openssl x509 -in client-cert.pem -noout -text
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            62:59:40:5c:e7:5a:61:74:73:bf:08:b0:d9:a7:d4:a1
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: O = mkcert development CA, OU = tonybai@tonybai, CN = mkcert tonybai@tonybai
        Validity
            Not Before: Jul  7 09:11:27 2022 GMT
            Not After : Oct  7 09:11:27 2024 GMT
        Subject: O = mkcert development certificate, OU = tonybai@tonybai
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:e5:25:c6:a1:c9:e2:5f:64:72:bd:ed:fc:24:fa:
                    12:8d:9c:30:52:8d:d8:5a:e7:f4:0c:b5:d5:0a:ef:
                    06:26:e3:06:54:54:cc:72:77:4e:22:cd:22:04:c0:
                    08:2e:94:2d:0f:cc:e8:9f:b9:c5:f4:13:8e:d1:f4:
                    bb:64:9d:1a:74:1b:e3:8c:95:2c:18:44:ec:e7:2c:
                    ec:0c:19:0f:e1:e6:1a:22:e7:3e:a6:1b:35:6e:05:
                    5f:c3:04:3f:1a:0f:c4:55:6f:ff:15:a0:a0:de:44:
                    5c:2d:3d:0b:dc:8a:01:ca:d2:2a:71:9d:b7:3a:d2:
                    10:9f:79:76:e0:a7:14:aa:d8:f0:90:bd:7c:4d:2d:
                    45:e6:16:ab:1d:03:7f:d8:97:4f:4d:41:13:76:72:
                    35:f2:41:b7:f1:3b:a8:42:d4:79:39:fd:f6:8d:10:
                    d1:54:06:60:6a:79:04:6c:6f:05:37:9c:4e:e7:ba:
                    9d:87:e8:05:65:9a:22:56:91:cb:03:bd:89:42:16:
                    66:92:bf:df:50:27:f2:81:89:c0:c5:46:f7:01:e8:
                    80:d0:4d:2e:ae:7f:5a:e9:fa:69:f3:50:c4:58:48:
                    dc:b5:20:13:01:3a:ac:fd:a8:69:2d:20:a9:55:cc:
                    90:4a:f1:f7:3f:9e:3b:7a:cb:77:c7:d2:c4:2b:4f:
                    4c:09
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage:
                TLS Web Client Authentication, TLS Web Server Authentication
            X509v3 Authority Key Identifier:
                keyid:A8:C4:06:2D:2C:25:71:EC:08:C8:1A:92:9A:F2:52:87:22:6E:85:2D

            X509v3 Subject Alternative Name:
                DNS:client1
    Signature Algorithm: sha256WithRSAEncryption
         a6:68:a8:b3:cf:8c:8c:f6:03:56:68:e4:d3:02:cd:ec:8d:fa:
         7f:73:56:c2:91:fa:d8:65:82:a7:f5:d9:8b:32:2a:3b:f9:59:
         71:0c:f8:d3:b6:d3:b3:11:99:f6:f6:d1:ab:d9:1e:fc:bd:f5:
         71:d9:35:4e:0e:fb:f2:f9:65:12:f2:1d:26:77:7d:eb:2c:52:
         80:2c:05:64:0f:99:35:83:31:b0:eb:71:85:04:48:d6:f6:29:
         92:81:f5:22:ee:77:8b:3d:e8:66:6a:5f:59:69:73:15:bb:69:
         46:e9:df:8c:7c:1d:28:b5:71:ed:2e:ca:8e:d3:08:da:85:b4:
         6c:26:89:85:16:c3:9a:e4:45:ef:3d:16:a2:32:45:70:e5:7e:
         82:e1:55:32:e7:1a:63:6b:56:8f:11:70:53:6f:71:d8:e0:76:
         bc:af:bd:dc:53:d6:fb:f0:b6:29:5f:3b:3f:dd:5c:58:b4:f0:
         d2:bb:63:d6:7f:b6:5f:29:ac:43:fa:56:f6:38:a4:03:6e:f3:
         b6:0d:e3:94:4c:0e:de:28:0c:63:27:94:5c:c8:15:78:c1:3b:
         a3:9f:f3:7f:d8:79:c1:ee:23:da:42:ef:25:40:a1:b9:e4:54:
         c4:d0:6b:81:b8:c1:b6:78:aa:d9:25:31:25:fe:5c:a8:d4:46:
         61:38:2e:6e:ba:34:b6:21:cb:66:47:9e:4f:ca:e2:6a:6a:06:
         60:d4:cb:fd:e6:a2:d5:d3:44:40:f1:f9:a9:0d:38:47:a4:20:
         1a:59:4f:14:ab:ab:e9:20:53:91:1b:0e:57:7b:2e:72:d6:1c:
         73:37:d3:17:f6:65:75:ef:27:19:ee:32:2d:ac:ca:46:c4:aa:
         ea:60:d8:6c:fa:62:ad:d4:34:f5:f9:57:48:8f:c0:b3:30:0e:
         13:ec:69:7b:52:97:d6:f5:fa:16:bb:38:c6:03:2f:1a:21:6e:
         bb:69:2a:74:dc:3c:71:3e:af:91:dd:28:86:ca:c8:3b:58:29:
         07:3b:5c:67:3d:31

我们看到:client-cert.pem与cert.pem在“X509v3 Extended Key Usage”一项有差别,client-pert.pem除了包含TLS Web Server Authentication,还增加了TLS Web Client Authentication。

2. echoserver与echoclient

Go标准库提供了tls的基本实现,支持tls1.2和1.3版本。下面是echoserver的主要源码:

// github.com/bigwhite/experiments/gmssl-examples/tls/server/server.go
func main() {
    cert, err := tls.LoadX509KeyPair("./certs/cert.pem", "./certs/key.pem")
    if err != nil {
        fmt.Println("load x509 keypair error:", err)
        return
    }
    cfg := &tls.Config{
        Certificates: []tls.Certificate{cert},
        ClientAuth:   tls.RequireAndVerifyClientCert,
    }
    listener, err := tls.Listen("tcp", ":18000", cfg)
    if err != nil {
        fmt.Println("listen error:", err)
        return
    }

    for {
        conn, err := listener.Accept()
        if err != nil {
            fmt.Println("accept error:", err)
            return
        }
        fmt.Println("accept connection:", conn.RemoteAddr())
        go func() {
            for {
                // echo "request"
                var b = make([]byte, 16)
                _, err := conn.Read(b)
                if err != nil {
                    fmt.Println("connection read error:", err)
                    conn.Close()
                    return
                }

                fmt.Println(string(b))
                _, err = conn.Write(b)
                if err != nil {
                    fmt.Println("connection write error:", err)
                    return
                }
            }
        }()
    }
}

我们看到基于tls的echoserver与一个普通的tcp server的代码差别不多,最主要就是在创建listener时传入了一个tls.Config结构,这个结构中有tls握手(handshake)所需要的全部信息,包括server端使用的私钥与证书(通过LoadX509KeyPair加载)以及对client端进行证书校验的标志(ClientAuth: tls.RequireAndVerifyClientCert)。一旦连接建立,握手成功,后续的数据读写都和基于tcp连接的普通服务端程序无异。

下面是echoclient的主要源码:

// github.com/bigwhite/experiments/gmssl-examples/tls/client/client.go

func main() {
    cert, err := tls.LoadX509KeyPair("./certs/client-cert.pem", "./certs/client-key.pem")
    if err != nil {
        fmt.Println("load x509 keypair error:", err)
        return
    }

    conn, err := tls.Dial("tcp", "example.com:18000", &tls.Config{
        Certificates: []tls.Certificate{cert},
    })
    if err != nil {
        fmt.Println("failed to connect: " + err.Error())
        return
    }
    defer conn.Close()

    fmt.Println("connect ok")
    for i := 0; i < 100; i++ {
        _, err := conn.Write([]byte("hello, gm"))
        if err != nil {
            fmt.Println("conn write error:", err)
            return
        }

        var b = make([]byte, 16)
        _, err = conn.Read(b)
        if err != nil {
            fmt.Println("conn read error:", err)
            return
        }
        fmt.Println(string(b))
        time.Sleep(time.Second)
    }
}

client端的代码更为简单一些,只需load client端使用的私钥与证书,然后传给tls.Config实例。tls.Dial使用该Config实例便可以顺利连接echoserver。

3. 用于验证对方证书的CA证书

在上面两个程序中都没有提到CA证书,那么server端和client端用什么去验证对方的公钥证书呢?其实依旧是用mkcert创建的CA证书去验证,只不过由于mkcert将CA证书安装到了操作系统trust store路径中,程序可以在系统CA证书中自动找到用来验证client和server两端公钥证书的CA证书,因此无需在程序中显式加载特定CA证书。

如果我们执行mkcert -uninstall,那么client程序在与server作tls handshake时就会报如下错误:

// client程序的输出日志:
failed to connect: x509: certificate signed by unknown authority

// server程序的输出日志:
accept connection: 127.0.0.1:56734
connection read error: remote error: tls: bad certificate

三. 密码算法在TLS握手以及后续通信过程中的作用

在TLS握手阶段,密码算法起到了关键作用。那在握手的每个阶段都在使用什么算法呢?我们看看下面使用curl命令访问https站点的输出:

$curl -v https://baidu.com
*   Trying 220.181.38.148:443...
* TCP_NODELAY set
* Connected to baidu.com (220.181.38.148) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/certs/ca-certificates.crt
  CApath: /etc/ssl/certs
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
... ...

在这段内容中,我们看到这样一行输出:

SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256

这行后面的ECDHE-RSA-AES128-GCM-SHA256就是在握手过程中以及后续通信阶段会使用到的算法。这样的一串称为密码套件(cipher suite),在SSL协议时代被称为cipher kinds

密码套件一般由多个用途不同的密码算法名称组合而成(套件中的算法都是要配合使用的,单独使用没法保证信息安全传输)。下面是openssl-1.1.1f支持的密码套件列表:

$openssl ciphers -V | column -t
0x13,0x02  -  TLS_AES_256_GCM_SHA384         TLSv1.3  Kx=any       Au=any    Enc=AESGCM(256)             Mac=AEAD
0x13,0x03  -  TLS_CHACHA20_POLY1305_SHA256   TLSv1.3  Kx=any       Au=any    Enc=CHACHA20/POLY1305(256)  Mac=AEAD
0x13,0x01  -  TLS_AES_128_GCM_SHA256         TLSv1.3  Kx=any       Au=any    Enc=AESGCM(128)             Mac=AEAD
0xC0,0x2C  -  ECDHE-ECDSA-AES256-GCM-SHA384  TLSv1.2  Kx=ECDH      Au=ECDSA  Enc=AESGCM(256)             Mac=AEAD
0xC0,0x30  -  ECDHE-RSA-AES256-GCM-SHA384    TLSv1.2  Kx=ECDH      Au=RSA    Enc=AESGCM(256)             Mac=AEAD
0x00,0x9F  -  DHE-RSA-AES256-GCM-SHA384      TLSv1.2  Kx=DH        Au=RSA    Enc=AESGCM(256)             Mac=AEAD
0xCC,0xA9  -  ECDHE-ECDSA-CHACHA20-POLY1305  TLSv1.2  Kx=ECDH      Au=ECDSA  Enc=CHACHA20/POLY1305(256)  Mac=AEAD
0xCC,0xA8  -  ECDHE-RSA-CHACHA20-POLY1305    TLSv1.2  Kx=ECDH      Au=RSA    Enc=CHACHA20/POLY1305(256)  Mac=AEAD
0xCC,0xAA  -  DHE-RSA-CHACHA20-POLY1305      TLSv1.2  Kx=DH        Au=RSA    Enc=CHACHA20/POLY1305(256)  Mac=AEAD
0xC0,0x2B  -  ECDHE-ECDSA-AES128-GCM-SHA256  TLSv1.2  Kx=ECDH      Au=ECDSA  Enc=AESGCM(128)             Mac=AEAD
0xC0,0x2F  -  ECDHE-RSA-AES128-GCM-SHA256    TLSv1.2  Kx=ECDH      Au=RSA    Enc=AESGCM(128)             Mac=AEAD
0x00,0x9E  -  DHE-RSA-AES128-GCM-SHA256      TLSv1.2  Kx=DH        Au=RSA    Enc=AESGCM(128)             Mac=AEAD
0xC0,0x24  -  ECDHE-ECDSA-AES256-SHA384      TLSv1.2  Kx=ECDH      Au=ECDSA  Enc=AES(256)                Mac=SHA384
... ...

我们看看上面输出的后四列的含义:

  • 第四列(Kx)

Kx代表key exchange,这一列是密钥交换算法,常见的密钥交换算法包括DH(Diffie-Hellman)、DHE(Diffie-Hellman Ephemeral)、ECDHE(在DHE算法的基础上利用了ECC椭圆曲线特性)等。在tls握手阶段,密钥交换算法用于在不安全的通道上协商会话加密(对称加密)算法密钥。

  • 第五列(Au)

Au代表authentication,这一列是身份认证算法,通常是非对称加密算法,比如:RSA、ECDSA等。该算法用于服务端与客户端相互验证对方的公钥数字证书时。

  • 第六列(Enc)

Enc代表对称加密算法,比如:AES、CHACHA20等,对称加密算法在tls握手后用于对客户端与服务端交互的数据进行加解密,它的加解密性能要比非对称加密算法快上很多。

  • 第七列(Mac)

Mac代表Message Authentication Code,消息认证码算法,本质上是一个hash函数,用于计算数据的摘要值,是常用的用于保证消息数据完整性的工具。常见的算法有SHA1、SHA256等。

有了这些知识,我们再回到前面的ECDHE-RSA-AES128-GCM-SHA256,我们可以知道这个密码套件使用ECDHE作为密钥交换算法,使用RSA作为服务器认证算法(非对称加密),使用AES128-GCM作为对称加密算法,使用SHA256作为消息认证码算法。

注:TLS 1.3版本的握手过程已经修改,仅需对称加密和Mac算法参与,因此TLS 1.3的密码套件格式已经变化。在TLS 1.3中,密码套件仅用于协商对称加密和MAC算法。对应的,我们看到上面OpenSSL输出的TLSv1.3版本的密码套件(如TLS_AES_256_GCM_SHA384、TLS_CHACHA20_POLY1305_SHA256等)的Kx和Au都是any。换句话说:TLSv1.2和TLSv1.3版本的密码套件并不兼容,不能混用(TLS v1.3的密码套件不能用在TLS v1.2版本中,反之亦然)。

Go标准库(Go 1.18.3)内置支持的cipher suite如下:

// $GOROOT/src/crypto/tls/cipher_suites.go
func CipherSuites() []*CipherSuite {
    return []*CipherSuite{
        {TLS_RSA_WITH_AES_128_CBC_SHA, "TLS_RSA_WITH_AES_128_CBC_SHA", supportedUpToTLS12, false},
        {TLS_RSA_WITH_AES_256_CBC_SHA, "TLS_RSA_WITH_AES_256_CBC_SHA", supportedUpToTLS12, false},
        {TLS_RSA_WITH_AES_128_GCM_SHA256, "TLS_RSA_WITH_AES_128_GCM_SHA256", supportedOnlyTLS12, false},
        {TLS_RSA_WITH_AES_256_GCM_SHA384, "TLS_RSA_WITH_AES_256_GCM_SHA384", supportedOnlyTLS12, false},

        {TLS_AES_128_GCM_SHA256, "TLS_AES_128_GCM_SHA256", supportedOnlyTLS13, false},
        {TLS_AES_256_GCM_SHA384, "TLS_AES_256_GCM_SHA384", supportedOnlyTLS13, false},
        {TLS_CHACHA20_POLY1305_SHA256, "TLS_CHACHA20_POLY1305_SHA256", supportedOnlyTLS13, false},

        {TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", supportedUpToTLS12, false},
        {TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", supportedUpToTLS12, false},
        {TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", supportedUpToTLS12, false},
        {TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", supportedUpToTLS12, false},
        {TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", supportedOnlyTLS12, false},
        {TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", supportedOnlyTLS12, false},
        {TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", supportedOnlyTLS12, false},
        {TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", supportedOnlyTLS12, false},
        {TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", supportedOnlyTLS12, false},
        {TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", supportedOnlyTLS12, false},
    }
}

每个密码套件具有唯一标识值(value),这些值在https://www.iana.org/assignments/tls-parameters/tls-parameters.xml中有标准参考。

我们看到这些密码套件中的算法都是一些耳熟能详的国际标准密码算法,但并没有看到我们国家的国密的影子?我们国家的国密算法都有哪些?是否可以作为TLS握手过程使用的密码套件的一部分呢?如何基于国密算法实现一个安全传输层呢?我们接下来就正式进入国密算法(前面的铺垫有些长^_^)。

四. 国家商用密码(国密)介绍

密码算法是最基础、最重要的密码技术。国家密码管理局近十年来,先后发布了祖冲之序列密码算法、SM2~SM9等商用密码系列(SM系列)算法,构成了包含序列密码算法、对称密码算法、非对称密码算法、密码杂凑算法和标识密码算法等在内的完整、自主国产密码算法体系。2019年10月26日,第十三届全国人民代表大会常务委员会第十四次会议表决通过了《中华人民共和国密码法》,并于2020年1月1日起施行。这些算法目前也已经成为ISO/IEC相关国际标准了(只是在国外的应用还极少)。

国密是国家商用密码标准的简称。商用密码是指对不涉及国家秘密内容的信息进行加密保护或者安全认证。公民、法人和其他组织可以依法使用商用密码保护网络与信息安全。所有的国密相关标准都可以在这个站点查询到:http://www.gmbz.org.cn/main/bzlb.html。国密算法用SM(“商密”二字的拼音头字母组合)作为前缀标识,比如上面提到的SM2、SM3、SM4、SM9等等。

我们较为熟悉的是像RSA、AES这样的国际标准常用密码算法。初次看到SM2、SM3等算法名字的童鞋可能会有点懵,这些算法是什么密码算法,用在哪里?SM系列密码算法有很多,我们不能一一说明,我们重点来看看与安全传输层建立与认证相关的算法:

  • SM2:椭圆曲线公钥密码算法

SM2是用在公钥基础设施(PKI)领域的椭圆曲线公钥密码算法,与大名鼎鼎的RSA算法一样,是一种用于非对称加密的算法。该算法包括3部分:数字签名算法、密钥协商算法和加密/解密算法。该算法推荐使用素域为256比特的椭圆曲线。与RSA公钥密码算法相比,SM2椭圆曲线公钥密码算法具有安全性高、密钥短、速度快等优势。256比特的SM2椭圆曲线公钥密码算法密码强度已超过RSA-2048。SM2椭圆曲线公钥密码算法使用的密钥长度通常为192~256比特,而RSA公钥密码算法通常需要1024~2048比特。在同等安全强度下,SM2椭圆曲线公钥密码算法在用私钥签名时,速度远超RSA公钥密码算法。

SM2椭圆曲线公钥密码算法广泛应用于电子政务、移动办公、电子商务、移动支付、电子证书等领域。在公钥基础设施(PKI)领域,基于SM2椭圆曲线公钥密码算法的数字证书应用最具有代表性。

  • SM3:密码杂凑算法

SM3实质就是一种密码散列函数标准,再简单地说就是Hash函数。和我们熟悉的Hash散列算法SHA-1、SHA256等一样,SM3也主要用于数字签名及验证、消息认证码(MAC)生成及验证、随机数生成等领域。

SM3密码杂凑算法消息分组长度为512比特,输出摘要长度为256比特。SM3密码杂凑算法在MD(Message Digest)结构的基础上,新增了16步全异或操作、消息双字介入、加速雪崩效应的P置换等多种设计技术,能够有效避免高概率的局部碰撞,有效抵抗强碰撞性的差分攻击、弱碰撞性的线性攻击和比特追踪攻击等密码攻击方法。SM3密码杂凑算法能够有效抵抗目前已知的攻击方法,具有较高的安全冗余,在安全级别上与SHA256相当。

  • SM4:分组密码算法

SM4分组密码算法广泛应用于有对称加解密需求的应用系统和产品,与我们熟知的AES对称加密算法具有相同用途。

SM4算法的分组长度为128比特,密钥长度为128比特,加密算法和密钥扩展算法都采用32轮非线性迭代结构,解密算法与加密算法相同,只是轮密钥的使用顺序相反,解密轮密钥是加密轮密钥的逆序。轮变换使用的模块包括异或运算、8比特输入8比特输出的S盒,以及一个32比特输入的线性置换。

在密码指标性能方面,SM4分组密码算法的S盒设计已达到欧美分组密码标准算法的水平,具有较高的安全特性。线性置换的分支数达到了最优,可以抵抗差分攻击、线性攻击、代数攻击等。它具有算法速度快、实现效率高、安全性好等优点,主要用于保护数据的机密性。

除了密码算法之外,国家密码管理局还颁布一系列周边标准,比如基于国密的SSL传输层安全协议,以下简称为国密SSL

最初的国密SSL是作为密码行业标准存在的,没有独立的协议标准定义,而是定义在SSL LPN产品的技术规范里,即《GM/T 0024-2014 SSL VPN技术规范》

后来,国密SSL从密码行业标准上升到了独立的国家标准,也就是《GB/T 38636-2020 信息安全技术 传输层密码协议(TLCP)》,新版标准基本兼容《GM/T 0024-2014 SSL VPN技术规范》,主要变化是增加了GCM的密码套件:ECC_SM4_GCM_SM3和ECDHE_SM4_GCM_SM3以及去掉了行标《GM/T 0024-2014》中的涉及SM1和RSA的密码套件。

国密SSL是参考TLS 1.1制定的:

但“遗憾”的是国密SSL与TLS协议并不兼容,这就意味着现有的各个编程语言实现的TLS实现在不经改造的前提下,是无法支持国密SSL握手过程的。

此外,前面提到的TLS握手涉及到的证书都是RSA证书(前面证书内容中Public Key Algorithm: rsaEncryption),即用RSA算法生成的公钥,用RSA算法的CA私钥签过名的证书。另一端在验证证书时,也要用RSA算法公钥(CA公钥)验证证书。如果我们要支持基于SM2算法的证书体系,需要CA、参与通信的两端都要支持SM2算法。而如今支持SM2的CA少之又少。并且,从前面我们看到的Go标准库TLS实现内置的密码套件列表来看,我们也没有看到SM2等国密算法实现的踪影

不得不说,这是当前支持国密的一个“尴尬”。

那么我们要如何支持国密证书以及国密SSL呢?我们继续向下看。

五. 基于国密证书的tls身份认证

1. 使用openssl生成国密证书并验证是否可以成功进行tls握手

openssl是加解密领域“风向标”,openssl在1.1.1版本中加入对SM系列算法的支持:

大家可以通过下面命令查看你的openssl是否支持SM2椭圆曲线公钥密码算法:

$openssl ecparam -list_curves | grep SM2
  SM2       : SM2 curve over a 256 bit prime field

如果支持,我们就可以利用该算法制作国密证书了(openssl-sm2/certs下面)。

  • 使用SM2创建server端私钥
$openssl ecparam -genkey -name SM2 -out server-sm2.key
  • 创建server csr
$openssl req -new -out server-sm2.csr -key server-sm2.key
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

查看该csr:

$openssl req -in server-sm2.csr -noout -text
Certificate Request:
    Data:
        Version: 1 (0x0)
        Subject: C = AU, ST = Some-State, O = Internet Widgits Pty Ltd
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                    04:5b:3f:7e:c7:36:43:9c:22:cf:68:34:73:7f:c2:
                    11:23:05:2d:e5:34:5f:29:30:11:c5:c4:f1:df:e2:
                    97:9d:5c:eb:6c:29:3e:d0:e3:a2:d4:6c:67:e4:4f:
                    42:90:70:a2:dc:db:a6:b4:fd:5d:53:b6:53:8e:fd:
                    a8:37:aa:5e:4b
                ASN1 OID: SM2
        Attributes:
            a0:00
    Signature Algorithm: ecdsa-with-SHA256
         30:45:02:21:00:be:4b:31:93:fb:6a:74:2f:0a:0d:8d:69:08:
         d1:ad:bf:b2:e8:02:c1:76:c5:50:01:f2:f9:c8:1e:6f:1f:4f:
         9b:02:20:2c:43:16:5f:a4:4b:fb:2d:26:13:04:e0:ef:27:d1:
         84:69:41:71:9a:aa:e8:29:1d:98:f8:0c:df:be:52:c6:9d

可以看到:

Public Key Algorithm: id-ecPublicKey
ASN1 OID: SM2

sm2算法是id-ecPublicKey算法的别名(alias):

$openssl list -public-key-algorithms

... ...
Name: sm2
    Alias for: id-ecPublicKey
  • 使用mkcert创建的ca来签发证书

该ca是使用RSA算法创建的。我们用它签发sm2证书。

$openssl x509 -req -in server-sm2.csr -CA ~/.local/share/mkcert/rootCA.pem -CAkey ~/.local/share/mkcert/rootCA-key.pem -CAcreateserial -out server-sm2-signed-by-rsa-ca.pem -days 5000
Signature ok
subject=C = AU, ST = Some-State, O = Internet Widgits Pty Ltd
Getting CA Private Key

查看生成的server端证书:

$openssl x509 -in server-sm2-signed-by-rsa-ca.pem -noout -text
Certificate:
    Data:
        Version: 1 (0x0)
        Serial Number:
            5c:9c:ac:2f:03:8e:4e:72:fd:41:8a:c5:eb:8e:d4:c0:fc:0f:8a:4b
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: O = mkcert development CA, OU = tonybai@tonybai, CN = mkcert tonybai@tonybai
        Validity
            Not Before: Jul 11 06:23:28 2022 GMT
            Not After : Mar 19 06:23:28 2036 GMT
        Subject: C = AU, ST = Some-State, O = Internet Widgits Pty Ltd
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                    04:5b:3f:7e:c7:36:43:9c:22:cf:68:34:73:7f:c2:
                    11:23:05:2d:e5:34:5f:29:30:11:c5:c4:f1:df:e2:
                    97:9d:5c:eb:6c:29:3e:d0:e3:a2:d4:6c:67:e4:4f:
                    42:90:70:a2:dc:db:a6:b4:fd:5d:53:b6:53:8e:fd:
                    a8:37:aa:5e:4b
                ASN1 OID: SM2
    Signature Algorithm: sha256WithRSAEncryption
         2b:67:c0:12:41:ad:da:2a:2f:9f:89:81:f1:ef:4a:4b:6d:66:
         e8:93:62:e0:68:d4:5b:0e:8a:83:2b:4d:77:36:d1:8e:f2:d6:
         92:b0:7f:db:12:78:49:ac:c4:80:2b:ca:c8:70:91:c3:2f:31:
         8d:5d:97:27:60:77:95:e6:61:7c:62:c4:f5:0c:ce:90:43:7d:
         0c:f6:4e:8d:62:f3:67:08:4b:7e:5e:ad:0b:11:13:13:30:ec:
         d2:fc:78:ae:77:ca:97:f1:eb:fd:a3:5d:0f:58:70:a0:b3:2a:
         6e:91:eb:81:37:6f:54:a9:56:9b:11:3c:4e:63:0b:a2:d7:d6:
         36:b4:7f:d2:90:c3:15:ab:9b:bf:86:98:bb:9a:1c:64:71:3b:
         92:4c:aa:89:d1:8b:03:35:34:ad:64:66:83:bc:0d:5f:38:ba:
         a0:07:82:92:1b:44:ef:72:c2:36:eb:38:84:ac:a1:d3:44:17:
         a8:7b:d5:64:f6:55:05:5f:3a:3b:b5:eb:1a:66:51:33:7a:76:
         ce:e3:cc:82:04:f2:28:70:90:3a:57:a5:db:32:08:47:f1:4d:
         81:33:87:dd:b6:dc:4f:4f:49:59:e2:ac:71:a4:2f:7e:08:14:
         b0:cd:96:2d:fb:3d:b8:f2:c5:db:de:b9:0c:fe:91:15:fb:b1:
         2e:df:23:6f:3e:26:2c:66:db:5e:e2:f6:f3:1f:23:2c:5c:70:
         1d:d1:2b:b2:6e:ae:87:c6:cd:53:44:23:b0:1d:8d:08:40:3c:
         02:87:81:1d:65:04:2a:b8:c6:f5:59:28:6a:ea:22:95:d3:e2:
         24:93:9e:6c:d6:d7:0a:25:5b:4e:4a:cf:43:4c:71:e2:1a:bf:
         26:de:27:14:38:ea:69:9c:a9:bf:12:3a:5b:65:33:4e:83:87:
         81:5e:85:2a:e3:62:c7:5d:0e:15:e7:35:06:35:45:69:db:0b:
         aa:c6:45:e4:74:93:aa:45:e8:6f:22:11:15:14:f1:5a:4e:0a:
         34:e2:74:eb:44:32

使用openssl的s_server和s_client命令验证是否可以握手成功:

$openssl s_server -tls1_2 -accept 14443 -key server-sm2.key -cert server-sm2-signed-by-rsa-ca.pem -debug -msg
Using default temp DH parameters
ACCEPT

$openssl s_client -connect 127.0.0.1:14443 -debug -msg -tls1_2

结果s_server报如下错:

... ...
>>> TLS 1.2, Alert [length 0002], fatal handshake_failure
    02 28
ERROR
139761999033664:error:1417A0C1:SSL routines:tls_post_process_client_hello:no shared cipher:../ssl/statem/statem_srvr.c:2283:
shutting down SSL
CONNECTION CLOSED

可以看到openssl虽然可以生成sm2公钥证书,但在tls 1.2协议下无法成功实现tls握手。

2. 使用gmssl进行tls握手

openssl不支持,但国内的大神基于openssl1.1.0建立了gmssl分支,这就是gmssl工程。该工程为openssl增加了对国密算法以及gm ssl协议的各种支持。接下来我们就来试试用gmssl是否可以实现基于sm2证书的tls握手成功。

gmssl工程感觉还不够成熟,安装和运行过程有一些“坑”,这里简要说说。

  • 安装gmssl
$wget -c https://github.com/guanzhi/GmSSL/archive/master.zip
$unzip master.zip
$cd master

注意在执行其他config命令之前,先在Configure文件和test/build.info这个文件中, 把

use if $^O ne "VMS", 'File::Glob' => qw/glob/;

改成:

use if $^O ne "VMS", 'File::Glob' => qw/:glob/;

否则会报下面错误:

"glob" is not exported by the File::Glob module
Can't continue after import errors at ./Configure line 18.

接下来执行下面命令生成Makefile并构建:

$./config
$make

编译后的文件在apps/gmssl,我将其cp到项目根目录下。执行gmssl:

$./gmssl
./gmssl: symbol lookup error: ./gmssl: undefined symbol: BIO_debug_callback, version OPENSSL_1_1_0d

gmssl报错了!原因是加载器加载gmssl依赖的动态共享库时选择了系统openssl的相关库了:

$ldd gmssl
    linux-vdso.so.1 (0x00007ffe9cc5b000)
    libssl.so.1.1 => /lib/x86_64-linux-gnu/libssl.so.1.1 (0x00007fa3ca550000)
    libcrypto.so.1.1 => /lib/x86_64-linux-gnu/libcrypto.so.1.1 (0x00007fa3ca27a000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fa3ca257000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa3ca065000)
    libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fa3ca05f000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fa3ca6ae000)

我们要在load时链接gmssl自己的库,需要修改一下LD_LIBRARY_PATH环境变量(这样做会导致openssl执行失败,建议不要放在全局环境变量配置中,可让其仅在某些窗口中生效):

$export LD_LIBRARY_PATH=/home/tonybai/.bin/gmssl/GmSSL-master:$LD_LIBRARY_PATH

除此之外,我们还需要做一个操作,那就是在/usr/local/ssl下放置一份openssl.cnf文件(可以从/usr/lib/ssl/openssl.cnf拷贝(openssl version -a,查看OPENSSLDIR)),否则gmssl在执行“gmssl s_server…”时会报如下错误:

Can't open /usr/local/ssl/openssl.cnf for reading, No such file or directory
139679439200896:error:02001002:system library:fopen:No such file or directory:crypto/bio/bss_file.c:74:fopen('/usr/local/ssl/openssl.cnf','r')
139679439200896:error:2006D080:BIO routines:BIO_new_file:no such file:crypto/bio/bss_file.c:81:

这里gmssl版本如下:

$gmssl version
GmSSL 2.5.4 - OpenSSL 1.1.0d  19 Jun 2019

好了,下面我们就来使用gmssl试试我们制作的sm2证书是否可以顺利完成tls握手。

// 服务端
$gmssl s_server  -accept 14443 -key server-sm2.key -cert server-sm2-signed-by-rsa-ca.pem -debug -msg -tls1_2

// 客户端
$gmssl s_client -connect 127.0.0.1:14443 -debug -msg -tls1_2 -verifyCAfile /home/tonybai/.local/share/mkcert/rootCA.pem

---
SSL handshake has read 1209 bytes and written 310 bytes
Verification: OK
---
New, TLSv1.2, Cipher is ECDHE-SM2-WITH-SMS4-GCM-SM3
Server public key is 256 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : ECDHE-SM2-WITH-SMS4-GCM-SM3
    Session-ID: 53B8799C3A6F3752C634F764EB6B136BDFD39CEB0C2E28E7DD98D86F9FF4F333
    Session-ID-ctx:
    Master-Key: 6A50D31E3AEDDDF3FC608277087FB0DAACCC791DB296142ED37DE28E0DDA56FF1BB64431B66A76C468129E00F696338D
    PSK identity: None
    PSK identity hint: None
    SRP username: None
    TLS session ticket lifetime hint: 7200 (seconds)
    TLS session ticket:
    0000 - ee d3 08 4d 21 14 dc c8-40 8c d0 c4 31 f9 16 bc   ...M!...@...1...
    0010 - 85 f9 a2 8c f4 ba cf 90-4d 38 28 03 78 b0 4a 27   ........M8(.x.J'
    0020 - 17 c4 22 df 48 ea 8c 00-5a 92 0f ba eb 8a 1a dc   ..".H...Z.......
    0030 - b3 3d b4 15 ee df fc d0-66 59 5c c2 23 9e a4 4f   .=......fY\.#..O
    0040 - e0 77 54 b1 18 af 73 b0-b4 6a a7 c7 c7 d3 a4 a4   .wT...s..j......
    0050 - 8f 49 ff c7 bc 47 b5 19-09 21 4c db 71 76 d9 a5   .I...G...!L.qv..
    0060 - 49 0b c9 5d 09 b2 da b9-cc ec 04 5a 90 27 07 5f   I..].......Z.'._
    0070 - 2b f2 55 5c f4 69 01 32-90 f5 3a 19 b5 47 84 4c   +.U\.i.2..:..G.L
    0080 - 1c 64 66 63 f3 01 ab fe-b1 70 f7 98 b5 cc 23 8e   .dfc.....p....#.
    0090 - aa f4 1d 8a 79 5e 79 b7-04 f6 69 ed 62 d9 c7 ae   ....y^y...i.b...

    Start Time: 1657529930
    Timeout   : 7200 (sec)
    Verify return code: 0 (ok)
    Extended master secret: yes

从客户端的输出来看,在明确ca证书位置的情况下(使用-verifyCAfile),可以正确验证server端发来的sm2证书(见“Verify return code: 0 (ok)”)。

六. 使用Go实现tls/tlcp自适应双向认证

gmssl为我们展示了一条支持国密的路径,即基于已有的开源项目的实现进行改造。Go标准库并不支持国密,因此在Go社区借鉴标准库中crypto的中算法以及tls包的结构,实现了对sm系列算法以及国密ssl的支持,tjfoc/gmsm就是其中之一。

注:gmssl也提供了Go API接口,底层通过cgo调用gmssl C代码实现。

gmsm不仅提供了国密算法的相关实现,还实现了tls与tclp协议的自适应支持。在这一小节,我们就用gmsm来演示一个tls/tlcp自适应双向认证的例子。

1. 准备SM2国密公钥证书

按照gmsm自适应tls/tlcp实现的要求,我们需要先准备一堆证书(tlcp与tls不同,其加密与签名是由两个证书分别完成的,而不仅仅是tls的一个证书),包括:

  • rsa: 基于rsa的CA证书、server证书和client证书
  • gm: 基于gm的CA证书、server签名(sign)和加密(enc)证书、client端验证(auth)证书。

考虑到mkcert不支持国密,这里我们切换到用gmssl来创建这些证书。我将创建证书的命令集中在两个shell脚本中:gen_rsa_cert.sh和gen_gm_cert.sh,前者用于创建基于RSA的各种证书,后者则是创建基于国密的各种证书。这两个脚本的源码如下:

  • gen_rsa_cert.sh
// gmssl-examples/gmsm-tls-and-tlcp/certs/gen_rsa_cert.sh

#!/bin/bash

## RSA Certs

### CA
gmssl genpkey -out ca-rsa-key.pem -algorithm RSA -pkeyopt rsa_keygen_bits:2048
gmssl req -x509 -new -nodes -key ca-rsa-key.pem -subj "/CN=myca.com" -days 5000 -out ca-rsa-cert.pem 

### server key and cert
gmssl genpkey -out server-rsa-key.pem -algorithm RSA -pkeyopt rsa_keygen_bits:2048
gmssl req -new -key server-rsa-key.pem -subj "/CN=example.com" -out server-rsa.csr
gmssl x509 -req -in server-rsa.csr -CA ca-rsa-cert.pem -CAkey ca-rsa-key.pem -CAcreateserial -out server-rsa-cert.pem -days 5000  -extfile ./server.cnf -extensions ext
gmssl verify -CAfile ca-rsa-cert.pem server-rsa-cert.pem

### client key and cert
gmssl genpkey -out client-rsa-key.pem -algorithm RSA -pkeyopt rsa_keygen_bits:2048
gmssl req -new -key client-rsa-key.pem -subj "/CN=client1.com" -out client-rsa.csr
gmssl x509 -req -in client-rsa.csr -CA ca-rsa-cert.pem -CAkey ca-rsa-key.pem -CAcreateserial -out client-rsa-cert.pem -days 5000 -extfile ./client.cnf -extensions ext
gmssl verify -CAfile ca-rsa-cert.pem client-rsa-cert.pem
  • gen_gm_cert.sh
#!/bin/bash

## SM CA

gmssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:sm2p256v1 -out ca-gm-key.pem
gmssl req -x509 -new -nodes -key ca-gm-key.pem -subj "/CN=myca.com" -days 5000 -out ca-gm-cert.pem

### server: sign key and cert

gmssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:sm2p256v1 -out server-gm-sign-key.pem
gmssl req -new -key server-gm-sign-key.pem -subj "/CN=example.com" -out server-gm-sign.csr
gmssl x509 -req -in server-gm-sign.csr -CA ca-gm-cert.pem -CAkey ca-gm-key.pem -CAcreateserial -out server-gm-sign-cert.pem -days 5000 -extfile ./server.cnf -extensions ext

gmssl verify -CAfile ca-gm-cert.pem server-gm-sign-cert.pem

### server: enc key and cer

gmssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:sm2p256v1 -out server-gm-enc-key.pem
gmssl req -new -key server-gm-enc-key.pem -subj "/CN=example.com" -out server-gm-enc.csr
gmssl x509 -req -in server-gm-enc.csr -CA ca-gm-cert.pem -CAkey ca-gm-key.pem -CAcreateserial -out server-gm-enc-cert.pem -days 5000 -extfile ./server.cnf -extensions ext

gmssl verify -CAfile ca-gm-cert.pem server-gm-enc-cert.pem

### client: auth key and cert

gmssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:sm2p256v1 -out client-gm-auth-key.pem
gmssl req -new -key client-gm-auth-key.pem -subj "/CN=client1.com" -out client-gm-auth.csr
gmssl x509 -req -in client-gm-auth.csr -CA ca-gm-cert.pem -CAkey ca-gm-key.pem -CAcreateserial -out client-gm-auth-cert.pem -days 5000 -extfile ./client.cnf -extensions ext

gmssl verify -CAfile ca-gm-cert.pem client-gm-auth-cert.pem

关于上面两个脚本,有几点说明一下:

  1. 我们建立了两个CA,一个基于RSA,一个基于国密算法;这两个CA分别用于签发基于RSA的证书与基于国密的证书;
  2. 在生成证书中我们用到了x509证书的扩展属性subjectAltName、extendedKeyUsage和keyUsage。

如果不使用subjectAltName扩展属性,Go语言的x509校验会报如下错误(在Go 1.18及后续版本中,即便设置GODEBUG=x509ignoreCN=0也不行):

certificate relies on legacy Common Name field, use SANs instead

同样Go也会对keyUsage做严格校验,如果是用来签名的证书中keyUsage不包含digitalSignature等,握手时也会报错:

tls: the keyusage of cert[0] does not exist or is not for KeyUsageDigitalSignature

server.cnf与client.cnf的内容如下:

// server.cnf
[req]
prompt = no
distinguished_name = dn
req_extensions = ext
input_password = 

[dn]
CN = example.com
emailAddress = webmaster@example.com
O = hello Ltd
L = Beijing
C = CN

[ext]
subjectAltName = DNS:example.com
extendedKeyUsage = clientAuth,serverAuth
keyUsage = critical,digitalSignature,keyEncipherment

// client.cnf

[req]
prompt = no
distinguished_name = dn
req_extensions = ext
input_password = 

[dn]
CN = client1.com
emailAddress = webmaster@client1.com
O = hello Ltd
L = Beijing
C = CN

[ext]
subjectAltName = DNS:client1.com
extendedKeyUsage = clientAuth
keyUsage = critical,digitalSignature,keyEncipherment

执行bash gen_rsa_cert.sh和bash gen_gm_cert.sh生成所有示例需要的证书:

$ls *.pem|grep -v key
ca-gm-cert.pem
ca-rsa-cert.pem
client-gm-auth-cert.pem
client-rsa-cert.pem
server-gm-enc-cert.pem
server-gm-sign-cert.pem
server-rsa-cert.pem

2. 支持tls与tlcp自适应的server

下面是支持tls与tlcp自适应的server的源码:

// gmssl-examples/gmsm-tls-and-tlcp/server/server.go
const (
    rsaCertPath     = "certs/server-rsa-cert.pem"
    rsaKeyPath      = "certs/server-rsa-key.pem"
    sm2SignCertPath = "certs/server-gm-sign-cert.pem"
    sm2SignKeyPath  = "certs/server-gm-sign-key.pem"
    sm2EncCertPath  = "certs/server-gm-enc-cert.pem"
    sm2EncKeyPath   = "certs/server-gm-enc-key.pem"
)

func main() {
    pool := x509.NewCertPool()
    rsaCACertPath := "./certs/ca-rsa-cert.pem"
    rsaCACrt, err := ioutil.ReadFile(rsaCACertPath)
    if err != nil {
        fmt.Println("read rsa ca err:", err)
        return
    }
    gmCACertPath := "./certs/ca-gm-cert.pem"
    gmCACrt, err := ioutil.ReadFile(gmCACertPath)
    if err != nil {
        fmt.Println("read gm ca err:", err)
        return
    }
    pool.AppendCertsFromPEM(rsaCACrt)
    pool.AppendCertsFromPEM(gmCACrt)

    rsaKeypair, err := tls.LoadX509KeyPair(rsaCertPath, rsaKeyPath)
    if err != nil {
        fmt.Println("load rsa x509 keypair error:", err)
        return
    }
    sigCert, err := tls.LoadX509KeyPair(sm2SignCertPath, sm2SignKeyPath)
    if err != nil {
        fmt.Println("load x509 gm sign keypair error:", err)
        return
    }
    encCert, err := tls.LoadX509KeyPair(sm2EncCertPath, sm2EncKeyPath)
    if err != nil {
        fmt.Println("load x509 gm enc keypair error:", err)
        return
    }

    cfg, err := tls.NewBasicAutoSwitchConfig(&sigCert, &encCert, &rsaKeypair)
    if err != nil {
        fmt.Println("load basic config error:", err)
        return
    }

    cfg.MaxVersion = tls.VersionTLS12
    cfg.ClientAuth = tls.RequireAndVerifyClientCert
    cfg.ClientCAs = pool

    listener, err := tls.Listen("tcp", ":18000", cfg)
    if err != nil {
        fmt.Println("listen error:", err)
        return
    }

    for {
        conn, err := listener.Accept()
        if err != nil {
            fmt.Println("accept error:", err)
            return
        }
        fmt.Println("accept connection:", conn.RemoteAddr())
        go func() {
            for {
                // echo "request"
                var b = make([]byte, 16)
                _, err := conn.Read(b)
                if err != nil {
                    fmt.Println("connection read error:", err)
                    conn.Close()
                    return
                }

                fmt.Println(string(b))
                _, err = conn.Write(b)
                if err != nil {
                    fmt.Println("connection write error:", err)
                    return
                }
            }
        }()
    }
}

说明一下:

  • 这里的tls包是并非标准库crypto/tls包,而是github.com/tjfoc/gmsm/gmtls。
  • 由于要自适应tls/tlcp,我们加载了两个CA证书,一个是基于RSA创建的CA证书,一个是基于gm创建的CA证书,用于分别对tls协议和tlcp协议的客户端身份进行验证;
  • 服务端加载了用于tls连接的RSA的server证书:rsaCertPath,同时也加载了用于tlcp连接的server端双证书:sm2SignCertPath和sm2EncCertPath。

3. tls client

用于该示例的tls client与前面的echoclient十分类似,只不过加载的证书从mkcert生成的cert.pem改为certs/client-rsa-cert.pem,CA证书使用了我们刚刚生成的./certs/ca-rsa-cert.pem。

其他部分没有变化。这里就不罗列源码了,大家可以自行阅读gmssl-examples/gmsm-tls-and-tlcp/tlsclient/client.go

4. tlcp client

和tls client相比,我们只是将CA换为./certs/ca-gm-cert.pem,加载的client证书换成了certs/client-gm-auth-cert.pem,其他部分没有变化。这里也不罗列源码了,大家可以自行阅读gmssl-examples/gmsm-tls-and-tlcp/tlcpclient/client.go

5. 验证tls/tlcp自适应双向认证

通过make命令可以一键构建出上述的server、tlsclient和tlcpclient。

启动server:

$./echoserver

启动tlsclient,验证tls双向认证:

$./echo_tls_client
connect ok
hello, tls
hello, tls
... ...

如果看到上面的tls client输出,说明tls连接建立和双向验证ok。

我们再来启动tlcp client,验证tlcp双向认证:

$./echo_tlcp_client
connect ok
reply: h
reply:
reply: e
reply: llo, tlcp
reply: h
reply:
reply: e
reply: llo, tlcp
reply: h
reply:
reply: e
reply: llo, tlcp
... ..

我们看到虽然tlcp连接建立成功并成功完成双向认证,但是基于已建立的tlcp的读写操作似乎并不想tls client那样“工整”,对应着server那端的输出如下:

accept connection: 127.0.0.1:58088
h
ello, tlcp
h
ello, tlcp
h
ello, tlcp
h
ello, tlcp
h
ello, tlcp
h
ello, tlcp
h
ello, tlcp
h
ello, tlcp
h
ello, tlcp

虽然两段的数据都是完整的,没有丢失,但发送与接收的“效率”大幅下降,client端发出的一个“hello, tlcp”数据似乎是被分为两次发送出去的。而服务端给客户端的Reply更是分成了“四段”发送的,目前还没有调查为何会出现这种情况,也许与tjfoc/gmsm的实现有关。

注:实测:tjfoc/gmsm尚不支持在tls协议握手时使用rsa CA证书签发的采用gm算法生成的sm2证书,可以参见gmssl-examples/gmsm-tls-and-tlcp/server_gm和tlsclient_gm。

七. 小结

国密是中国密码标准,和国际密码标准相比,有一定的后发优势,但由于在国际上应用很少,其安全性虽然得到了形式验证,但似乎尚未得到实践中的大规模考验。基于国密的tlcp协议由于与tls不兼容,也导致其在应用上受到了极大的限制。

虽然有gmssl、有像tjfoc/gmsm这样的项目,但总体感觉国密在参考实现方面还不够成熟,生态还很欠缺,国家密码局在推广国密方面往往更多从法规层面。各个厂家往往都是因甲方需要国密而去满足要求,并没有原生推动国密的动力(譬如我们^_^)。

因此,国密任重道远啊。

本文内容仅供参考,可能有理解不正确和代码错误的地方,欢迎指正。

文中示例代码可以在这里下载。

八. 参考资料


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2022年,Gopher部落全面改版,将持续分享Go语言与Go应用领域的知识、技巧与实践,并增加诸多互动形式。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

图解git原理的几个关键概念

img{512x368}

git是那个“爱骂人”的Linux之父Linus Torvalds继Linux内核后奉献给全世界程序员的第二个礼物(不能确定已经逐渐老去的Torvalds能否迸发第三春,第三次给我们一个超大惊喜^_^)。这里再强调一下,git读作/git/,而不是/dʒit/

在诞生十余载后(2005年发布第一版),git毫无争议地成为了程序员版本管理工具的首选,它改变了全世界程序员的代码版本管理和生产协作的模式,极大促进了开源软件运动的发展。进化到今天的git已经成为了一个比较复杂的工具,多数程序员都将目光聚焦在如何记住这些命令并用好这些命令,对这些复杂命令行背后的原理却知之不多,虽然大多数程序员的确不太需要深刻了解git背后的原理^_^。

关于git原理的文章在互联网上也呈现出“汗牛充栋”之势,有些文章“蜻蜓点水”,有些文章“事无巨细”,看后似乎都无法让我满意。结合自己对git原理的学习,我觉得多数人把握住git运作机制的几个关键概念即可,于是就有了这篇文章,我努力尝试给大家讲清楚。

一. 我就是仓库,我拥有全部

我们首先要明确一个git与先前的版本管理工具(主要是subversion)的不同。下面是使用subversion版本管理工具时,程序员进行代码生产以及程序员间围绕代码仓库进行协作的模式:

img{512x368}

图:subversion代码生产和协作模式

众所周知,subversion是基于中心版本仓库进行版本管理协作的版本管理工具。就像上图中那样,所有开发人员开始生产代码的前提是必须先从中心仓库checkout一份代码拷贝到自己本地的工作目录;而进行版本管理操作或者与他人进行协作的前提也是:中心版本仓库必须始终可用。这有点像以太网的“半双工的集线器(hub)模式”:svn中心仓库就像集线器本身,每个程序员节点就像连接到集线器上的主机;当一个程序员提交(commit)代码到中心仓库时,其他程序员不能提交,否则会出现冲突;如果中心仓库挂掉了,那么整个版本管理过程也将停止,程序员节点间无法进行协作,这就像集线器(hub)挂掉后,所有连接到hub上的主机节点间的网络也就断开无法相互通信一样。

如果我们使用git,我们是不需要“集线器”的:

img{512x368}

图:git代码生产和协作模式

如上图所示,git号称分布式版本管理系统,本质上是没有像subversion中那个所谓的“中心仓库”的。每个程序员都拥有一个本地git仓库,而不仅仅是一份代码拷贝,这个仓库就是一个独立的版本管理节点,它拥有程序员进行代码生产、版本管理、与其他程序员协作的全部信息。即便在一台没有网络连接的机器上,程序员也能利用该仓库完成代码生产和版本管理工作。在网络ready的情况下,任意两个git仓库之间可以进行点对点的协作,这种协作无需中间协调者(中心仓库)参与。

二. github实现了基于git网络协作的控制平面

git实现了分布式版本管理系统,每个git仓库节点都是自治的。诸多git仓库节点一起形成了一个分布式git版本管理网络。这样的一个分布式网络存在着与普通分布式系统的类似的问题:如何发现对端节点的git仓库、如何管理和控制仓库间的访问权限等。如果说linus的git本身是这个分布式网络的数据平面工具(实现client/server间的双向数据通信),那么这个分布式网络还缺少一个“控制平面”

github恰恰给出了一份git分布式网络控制平面的实现:托管、发现、控制…。其名称中含有的“hub”字样让我们想起了上面的“hub模式”:

img{512x368}

图:github:git分布式网络控制平面的实现

我们看到在github的git协作模式实践中,引入了“中心仓库”的概念,各个程序员的节点git仓库源于(clone于)中心仓库。但是它和subversion的“中心仓库”有着本质的不同,这个仓库只是一个“upstream”库、是一个权威库。它并不是“集线器”,也没有按照“集线器”的那种工作模式进行协作。所有程序员节点的代码生产和版本管理操作完全可以脱离该所谓“中心库”而独立实施。

三. objects是个筐,什么都往里面装

上面都是从“宏观”谈git的一些与众不同的理念,而git原理,其实是从这一节才真正开始的^_^。

我们知道:每个git仓库的所有数据都存储在仓库顶层路径下的.git目录下:

$tree -L 1 -F
.
├── COMMIT_EDITMSG
├── HEAD
├── config
├── description
├── hooks/
├── index
├── info/
├── logs/
├── objects/
└── refs/

5 directories, 5 files

而在这些目录和文件中,又以objects路径下的数据内容最多,也最为重要。在git的设计中,objects目录就是一个“筐”,git的核心对象(object)都往里面“装”
img{512x368}

图:git核心数据对象类型与objects目录

从上图中,我们看到objects中存储的最主要的有三类对象:blob、commit和tree。这时你可能还不知道它们究竟是啥。不过没关系,我们通过一个例子来做一下“对号入座”。

我们在一个目录下建立git-internal-repo-demo目录,进入该目录,执行下面命令创建一个git仓库:

➜  /Users/tonybai/test/git/git-internal-repo-demo git:(master) ✗ $git init .
Initialized empty Git repository in /Users/tonybai/Test/git/git-internal-repo-demo/.git/

这是一个处于初始状态的git仓库,我们看看存储git仓库数据的.git目录下的结构:

➜  /Users/tonybai/test/git/git-internal-repo-demo git:(master) $tree .git
.git
├── HEAD
├── config
├── description
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   └── update.sample
├── info
│   └── exclude
├── objects
│   ├── info
│   └── pack
└── refs
    ├── heads
    └── tags

8 directories, 15 files

这个时候,objects这个筐还是空的!我们这就为仓库添点内容:

$mkdir -p cmd/demo

在cmd/demo目录下添加main.go文件,内容如下:

// cmd/demo/main.go
package main

import "fmt"

func main() {
    fmt.Println("hello, git")
}

接下来我们使用git add将cmd/demo目录加入到stage区:

$git add .

$git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)

    new file:   cmd/demo/main.go

这时我们来看一下objects这个筐是否有变化:

├── objects
│   ├── 3e
│   │   └── 759ef88951df9b9b07077a7ec01f96b8e659b3
│   ├── info
│   └── pack

我们有一个object已经被装入到“筐”中了。我们看到objects目录下是一些以哈希值命名的文件和目录,其中目录由两个字符组成,是每个object hash值的前两个字符。hash值后续的字符串用于命名对应的object文件。在这里我们的object的hash值(实质是sha-1算法)为3e759ef88951df9b9b07077a7ec01f96b8e659b3,于是这个对象就被放入名为3e的目录下,对应的object文件为759ef88951df9b9b07077a7ec01f96b8e659b3。

我们使用git提供的低级命令查看一下这个object究竟是什么,其中git cat-file -t查看object的类型,git cat-file -p查看object的内容:

$git cat-file -t 3e759ef889
blob

$git cat-file -p 3e759ef889
package main

import "fmt"

func main() {
    fmt.Println("hello, git")
}

我们看到objects这个筐中多了一个blob类型的对象,对象内容就是前面main.go文件中内容。

接下来,我们提交一下这次变更:

$git commit -m"first commit" .
[master (root-commit) 3062e0e] first commit
 1 file changed, 7 insertions(+)
 create mode 100644 cmd/demo/main.go

再来看看.git/objects中的变化:

├── objects
│   ├── 1f
│   │   └── 51fe448aacc69c0f799def9506e61ed3eb60fa
│   ├── 30
│   │   └── 62e0ebad9415b704e96e5cee1542187b7ed571
│   ├── 3d
│   │   └── 2045367ea40c098ec5c7688119d72d97fb09a5
│   ├── 3e
│   │   └── 759ef88951df9b9b07077a7ec01f96b8e659b3
│   ├── 40
│   │   └── 6d08e1159e03ae82bcdbe1ad9f076a04a41e2b
│   ├── info
│   └── pack

我们看到筐里被一下子新塞入4个object。我们分别看看新增的4个object类型和内容都是什么:

$git cat-file -t 1f51fe448a
tree
$git cat-file -p 1f51fe448a
100644 blob 3e759ef88951df9b9b07077a7ec01f96b8e659b3    main.go

$git cat-file -t 3062e0ebad
commit
$git cat-file -p 3062e0ebad
tree 406d08e1159e03ae82bcdbe1ad9f076a04a41e2b
author Tony Bai <bigwhite.cn@aliyun.com> 1586243612 +0800
committer Tony Bai <bigwhite.cn@aliyun.com> 1586243612 +0800

first commit

$git cat-file -t 3d2045367e
tree
$git cat-file -p 3d2045367e
040000 tree 1f51fe448aacc69c0f799def9506e61ed3eb60fa    demo

$git cat-file -t 406d08e115
tree
$git cat-file -p 406d08e115
040000 tree 3d2045367ea40c098ec5c7688119d72d97fb09a5    cmd

这里我们看到了另外两种类型的object被加入“筐”中:commit和tree类型。objects这个筐里目前有了5个object,我们不考虑git是以何种格式存储这些object的,我们想知道的是这几个object的关系是什么样的。请看下一小节^_^。

四. 每个commit都是一个git仓库的快照

要理清objects“筐”中各object间的关系,就必须要把握住一个关键概念:“每个commit都是git仓库的一个快照” – 以一个commit为入口,我们能将当时objects下面的所有object联系在一起。因此,上面5个object中的那个commit对象就是我们分析各object关系的入口。我们根据上述5个object的内容将这5个object的关系组织为下面这幅示意图:

img{512x368}

图:commit、tree、blob对象之间的关系

通过上图我们看到:

  • commit是对象关系图的入口;

  • tree对象用于描述目录结构,每个目录节点都会用一个tree对象表示。目录间、目录文件间的层次关系会在tree对象的内容中体现;

  • 每个commit都会有一个root tree对象;

  • blob对象为tree的叶子节点,它的内容即为文件的内容。

上面仅是一次commit后的关系图,为了更清晰的看到多个commit对象之间关系,我们再来对git repo进行一次变更提交:

我们创建pkg/foo目录:

$mkdir -p pkg/foo

然后创建文件pkg/foo/foo.go,其内容如下:

// pkg/foo/foo.go
package foo

import "fmt"

func Foo() {
    fmt.Println("this is foo package")
}

提交这次变更:

$git add pkg
$git commit -m"add package foo" .
[master 6f7f08b] add package foo
 1 file changed, 7 insertions(+)
 create mode 100644 pkg/foo/foo.go

下面是提交变更后的“筐”内的对象:

$tree objects
objects
├── 1f
│   └── 51fe448aacc69c0f799def9506e61ed3eb60fa
├── 29
│   └── 3ae375dcef1952c88f35dd4d2a1d4576dea8ba
├── 30
│   └── 62e0ebad9415b704e96e5cee1542187b7ed571
├── 3d
│   └── 2045367ea40c098ec5c7688119d72d97fb09a5
├── 3e
│   └── 759ef88951df9b9b07077a7ec01f96b8e659b3
├── 40
│   └── 6d08e1159e03ae82bcdbe1ad9f076a04a41e2b
├── 65
│   └── 5dd3aae645813dc53834ebfa8d19608c4b3905
├── 6e
│   └── e873d9c7ca19c7fe609c9e1a963df8d000282b
├── 6f
│   └── 7f08b14168beb114c3cc099b8dc1c09ccd4739
├── cc
│   └── 9903a33cb99ae02a9cb648bcf4a71815be3474
├── info
└── pack

12 directories, 10 files

object已经多到不便逐一分析了。但我们把握住一点:commit是分析关系的入口。我们通过commit的输出或commit log(git log)可知,新增的commit对象的hash值为6f7f08b141。我们还是以它为入口分析新增object的关系以及它们与之前已存在的object的关系:

img{512x368}

图:commit、tree、blob对象之间的关系1

从上图我们看到:

  • git新创建tree对象对应我们新建的pkg目录以及其子目录;

  • cmd目录下的子目录和文件内容并未改变,因此这次commit所对应的root tree对象(293ae375dc)直接使用了已存在的cmd目录对应的对象(3d2045367e);

  • 新commit对象会将第一个commit对象作为parent,这样多个commit对象之间构成一个单向链表。

上面的两个提交都是新增内容,我们再来提交一个commit,这次我们对已有文件内容做变更:

将cmd/demo/main.go文件内容变更为如下内容:

// cmd/demo/main.go
package main

import (
    "fmt"

    "github.com/bigwhite/foo"
)

func main() {
    fmt.Println("hello, git")
    foo.Foo()
}

提交变更:

$git commit -m"call foo.Foo in main" .
[master 2f14635] call foo.Foo in main
 1 file changed, 6 insertions(+), 1 deletion(-)

和上面的分析方法一样,我们通过最新commit对应的hash值2f146359b4对新对象和现存对象的关系进行分析:

img{512x368}

图:commit、tree、blob对象之间的关系2

如上图,第三次变更提交后,我们看到:

  • 由于main.go文件变更,git重建了main.go blob对象、demo、cmd tree对象

  • 由于pkg目录、其子目录布局、子目录下文件内容没有改变,于是新commit对象对应的root tree对象直接“复用”了上一次commit的pkg tree对象。

  • 新commit对象加入commit对象单向链表,并将上一次的commit对象作为parent。

我们看到沿着最新的commit对象(2f146359b4),我们能获取当前仓库的最新结构布局以及各个blob对象的最新内容,即最新的一个快照!

五. object是不可变的,默克尔树(Merkle Tree)判断变化

从上面的三次变更,我们看到无论哪种对象object,一旦放入到objects这个“筐”就是不可变的(immutable)。即便是第三次commit对main.go进行了修改,git也只是根据main.go的最新内容创建一个新的blob对象,而不是修改或替换掉第一版main.go对应的blob对象。

对应目录的tree object亦是如此。如果某目录下的二级目录发生变化或目录下的文件内容发生改变,git会新生成一个对应该目录的tree对象,而不是去修改原先已存在的tree对象。

实际上,git tree对象的组织本身就是一棵默克尔树(Merkle Tree)

默克尔树是一类基于哈希值的二叉树或多叉树,其叶子节点上的值通常为数据块的哈希值,而非叶子节点上的值,是将该节点的所有孩子节点的组合结果的哈希值。默克尔树的特点是,底层数据的任何变动,都会传递到其父亲节点,一直到树根。

img{512x368}

图:默克尔树(图片来自网络)

以上图为例:我们自下向上看,D0、D1、D2和D3是叶子节点包含的数据。N0、N1、N2和N3是叶子节点,它们是将数据(也就是D0、D1、D2和D3)进行hash运算后得到的hash值;继续往上看,N4和N5是中间节点,N4是N0和N1经过hash运算得到的哈希值,N5是N2和N3经过hash运算得到的哈希值。(注意,hash值计算方法:把相邻的两个叶子结点合并成一个字符串,然后运算这个字符串的哈希)。最后,Root节点是N4和N5经过hash运算后得到的哈希值,这就是这颗默克尔树的根哈希。当N0包含的数据发生变化时,根据默克尔树的节点hash值形成机制,我们可以快速判断出:N0、N4和root节点会发生变化

对应git来说,叶子节点对应的就是每个文件的hash值,tree对象对应的是中间节点。因此,通过默克尔树(Merkle Tree)的特性,我们可以快速判断哪些对象对应的目录或文件发生了变化,应该重新创建对应的object。我们还以上面的第三次commit为例:

img{512x368}

图:通过默克尔树(Merkle Tree)的特性判断哪些对象发生变化需要重新创建

如上图所示,第三次commit是因为cmd/demo/main.go内容发生了变化,根据merkle tree特性,我们可以快速判断红色的object会随之发生变化。于是git会自底向上逐一创建这些新对象:main.go文件对应的blob对象以及demo、cmd以及根节点对应的tree对象。

六. branch和tag之所以轻量,因为它们都是“指针”

使用subversion时,创建branch或打tag使用的是svn copy命令。svn copy执行的就是真实的文件拷贝,相当于将trunk下的目录和文件copy一份放到branch或tag下面,建立一个trunk的副本,这样的操作绝对是“超重量级”的。如果svn仓库中的文件数量庞大且size很大,那么svn copy执行起来不仅速度慢,而且还会在svn server上占用较大的磁盘存储空间,因此使用svn时,打tag和创建branch是要“谨慎”的。

而git的branch和tag则极为轻量,我们来给上面例子中的仓库创建一个dev分支:

$git branch dev

我们看看.git下有啥变化:

.

└── refs
    ├── heads
    │   ├── dev
    │   └── master
    └── tags

我们看到.git/refs/heads下面多出了一个dev文件,我们查看一下该文件的内容:

$cat refs/heads/dev
2f146359b475909f2fdcdef046af3431c8077282

$git log --oneline

2f14635 (HEAD -> master, dev) call foo.Foo in main
6f7f08b add package foo
3062e0e first commit

对比发现,dev文件中的内容恰是最新的commit对象:2f146359b475909f2fdcdef046af3431c8077282。

我们再来给repo打一个tag:

$git tag v0.0.1

同样,我们来查看一下.git目录下的变化:

└── refs
    ├── heads
    │   ├── dev
    │   └── master
    └── tags
        └── v0.0.1

我们看到在refs/tags下面增加一个名为v0.0.1的文件,查看其内容:

$cat refs/tags/v0.0.1
2f146359b475909f2fdcdef046af3431c8077282

和dev分支文件一样,它的内容也是最新的commit对象:2f146359b475909f2fdcdef046af3431c8077282。

可见,使用git创建分支或tag仅仅是创建了一个指向某个commit对象的“指针”,这与subversion的副本操作相比,简直不能再轻量了。

前面说过,一个commit对象都是一个git仓库的快照,切换到(git checkout xxx)某个branch或tag,就是将本地工作拷贝切换到commit对象所代表的仓库快照的状态。当然也会将commit对象组成的单向链表的head指向该commit对象,这个head即.git/HEAD文件的内容。

七. 小结

到这里,git原理的几个关键概念就交代完了,再回顾一下:

  • 和subversion这样的集中式版本管理工具最大的不同就是每个程序员节点都是git仓库,拥有全部开发、协作所需的全部信息,完全可以脱离“中心节点”;

  • 如果说git聚焦于数据平面的功能,那么github则是一个基于git网络协作的控制平面的实现;

  • objects是个筐,什么都往里面装。git仓库的核心数据都存在.git/objects下面,主要类型包括:blob、tree和commit;

  • 每个commit都是一个git仓库的快照,记住commit对象是分析对象关系的入口;

  • git是基于数据内容的hash值做等值判定的,object是不可变的,默克尔树(Merkle Tree)用来快速判断变化。

  • branch和tag因为是“指针”,因此创建、销毁和切换都非常轻量。

八. 参考资料


我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网上线了,感谢小伙伴们学习支持!

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/
smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

微博:https://weibo.com/bigwhite20xx
微信公众号:iamtonybai
博客:tonybai.com
github: https://github.com/bigwhite

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats