标签 Ubuntu 下的文章

使用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

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

使用C语言从头开发一个Hello World级别的eBPF程序

本文永久链接 – https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch


近两年最火的Linux内核技术非eBPF莫属!

2019年以来,除了eBPF技术自身快速演进之外,基于eBPF技术的观测(Observability)、安全(Security)和网络(Networking)类项目如雨后春笋般出现。耳熟能详的的包括:cilium(把eBPF技术带到Kubernetes世界)、Falco(云原生安全运行时,Kubernetes威胁检测引擎的事实标准)、Katran(高性能四层负载均衡器)、pixie(用于Kubernetes应用程序的可观察性工具)等。

今年3月份发布的thoughtworks技术雷达第26期也将eBPF技术放入试验的象限阶段。

eBPF技术火热,但很多童鞋还不知道eBPF技术究竟是什么,能做什么?在这篇文章中,我将带大家简单了解一下什么eBPF内核技术以及如何从头开始用C语言开发一个Hello World级eBPF程序。

我们首先看一下这么火热的eBPF技术究竟是什么?

一. eBPF简介

eBPF这门技术,我也是在几年前从性能专家、火焰图的发明者Brendan Gregg的blog和书中看到的。

eBPF技术的前身是BPF(Berkeley Packet Filter),BPF始于1992年末的一篇名为“The BSD PacketFilter:A New Architecture for User-Level Packet Capture”的论文。该论文提出了一种在Unix内核实现网络数据包过滤的技术方案,这种新的技术比当时最先进的数据包过滤技术快20倍。

1997年,BPF技术合入linux kernel,后在tcpdump中得以应用。

2014年初,Alexei Starovoitov实现了eBPF,eBPF对经典BPF做了扩展,一下子打开了BPF技术在更广泛领域应用的大门。


图片来自ebpf官网

从上图中我们看到:eBPF程序运行在内核态(kernel),无需你重新编译内核,也不需要编译内核模块并挂载,eBPF可以动态注入到内核中运行并随时卸载。一旦进入内核,eBPF便拥有了上帝视角,既可以监控内核,也可以管窥用户态程序。并且eBPF技术提供的一系列工具(Verifier)可以检测eBPF的代码安全,避免恶意程序进入到内核态中执行。

从本质上说,BPF技术其实是kernel为用户态开的口子(内核已经做好了埋点)!通过注入eBPF程序并注册要关注事件、事件触发(内核回调你注入的eBPF程序)、内核态与用户态的数据交换实现你想要的逻辑。

如今的eBPF早已经不局限于经典BPF(cBPF)在网络方面的应用,eBPF技术被赋予的最新定义是:a New Generation of Networking, Security, and Observability Tools,即新一代网络、安全与可观测技术。这个定义来自isovalent公司的首席开源官: liz rice。isovalent公司即Cilium项目的母公司,一家以eBPF技术驱动云原生网络、安全与可观测性的初创技术公司。

eBPF已经成为内核顶级的子系统,后续如未特指,我们所提到的BPF指的就是新一代的eBPF技术

BPF技术这么牛逼,那我们如何开发BPF程序呢?

二. 如何开发BPF程序

1. BPF程序的形态

一个以开发BPF程序为目的的工程通常由两类源文件组成,一类是运行于内核态的BPF程序的源代码文件(比如:下图中bpf_program.bpf.c)。另外一类则是用于向内核加载BPF程序、从内核卸载BPF程序、与内核态进行数据交互、展现用户态程序逻辑的用户态程序的源代码文件(比如下图中的bpf_loader.c)。

目前运行于内核态的BPF程序只能用C语言开发(对应于第一类源代码文件,如下图bpf_program.bpf.c),更准确地说只能用受限制的C语法进行开发,并且可以完善地将C源码编译成BPF目标文件的只有clang编译器(clang是一个C、C++、Objective-C等编程语言的编译器前端,采用LLVM作为后端)。

下面是BPF程序的编译与加载到内核过程的示意图:

BPF目标文件(bpf_program.o)实质上也是一个ELF格式的文件,我们可以通过readelf命令行工具可以读取BPF目标文件的内容,下面是一个示例:

$readelf -a bpf_program.o
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Linux BPF
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          424 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         8
  Section header string table index: 1

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .strtab           STRTAB           0000000000000000  0000012a
       0000000000000079  0000000000000000           0     0     1
  [ 2] .text             PROGBITS         0000000000000000  00000040
       0000000000000000  0000000000000000  AX       0     0     4
  [ 3] tracepoint/syscal PROGBITS         0000000000000000  00000040
       0000000000000070  0000000000000000  AX       0     0     8
  [ 4] .rodata.str1.1    PROGBITS         0000000000000000  000000b0
       0000000000000012  0000000000000001 AMS       0     0     1
  [ 5] license           PROGBITS         0000000000000000  000000c2
       0000000000000004  0000000000000000  WA       0     0     1
  [ 6] .llvm_addrsig     LOOS+0xfff4c03   0000000000000000  00000128
       0000000000000002  0000000000000000   E       7     0     1
  [ 7] .symtab           SYMTAB           0000000000000000  000000c8
       0000000000000060  0000000000000018           1     2     8
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  p (processor specific)

There are no section groups in this file.

There are no program headers in this file.

There is no dynamic section in this file.

There are no relocations in this file.

The decoding of unwind sections for machine type Linux BPF is not currently supported.

Symbol table '.symtab' contains 4 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS bpf_program.c
     2: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    5 _license
     3: 0000000000000000   112 FUNC    GLOBAL DEFAULT    3 bpf_prog

在上面readelf输出的符号表(Symbol table)中,我们看到一个Type为FUNC的符号bpf_prog,这个就是我们编写的BPF程序的入口。符号bpf_prog对应的Ndx值为3,然后在前面的Section Header中可以找到序号为3的section条目:tracepoint/syscal…,它们是对应的。

从readelf输出可以看到:bpf_prog(即序号为3的section)的Size为112,但是它的内容是什么呢?这个readelf提示无法展开linux BPF类型的section。我们使用另外一个工具llvm-objdump将bpf_prog的内容展开:

$llvm-objdump-10 -d bpf_program.o

bpf_program.o:  file format ELF64-BPF

Disassembly of section tracepoint/syscalls/sys_enter_execve:

0000000000000000 bpf_prog:
       0:   b7 01 00 00 21 00 00 00 r1 = 33
       1:   6b 1a f8 ff 00 00 00 00 *(u16 *)(r10 - 8 ) = r1
       2:   18 01 00 00 50 46 20 57 00 00 00 00 6f 72 6c 64 r1 = 7236284523806213712 ll
       4:   7b 1a f0 ff 00 00 00 00 *(u64 *)(r10 - 16) = r1
       5:   18 01 00 00 48 65 6c 6c 00 00 00 00 6f 2c 20 42 r1 = 4764857262830019912 ll
       7:   7b 1a e8 ff 00 00 00 00 *(u64 *)(r10 - 24) = r1
       8:   bf a1 00 00 00 00 00 00 r1 = r10
       9:   07 01 00 00 e8 ff ff ff r1 += -24
      10:   b7 02 00 00 12 00 00 00 r2 = 18
      11:   85 00 00 00 06 00 00 00 call 6
      12:   b7 00 00 00 00 00 00 00 r0 = 0
      13:   95 00 00 00 00 00 00 00 exit

llvm-objdump输出的bpf_prog的内容其实就是BPF的字节码。谈到字节码(byte code),我们首先想到的就是jvm虚拟机。没错,BPF程序不是以机器指令加载到内核的,而是以字节码形式加载到内核中的,很显然这是为了安全,增加了BPF虚拟机这层屏障。在BPF程序加载到内核的过程中,BPF虚拟机会对BPF字节码进行验证并运行JIT编译将字节码编译为机器码。

用于加载和卸载BPF程序的用户态程序则可以由多种语言开发,既可以用C语言,也可以用Python、Go、Rust等。

2. BPF程序的开发方式

BPF演进了这么多年,虽然一直在努力提高,但BPF程序的开发与构建体验依然不够理想。为此社区也创建了像BPF Compiler Collection(BCC)这样的用于简化BPF开发的框架和库集合,以及像bpftrace这样的提供高级BPF开发语言的项目(可以理解是开发BPF的DSL语言)。

很多时候我们无需自己开发BPF程序,像bcc和bpftrace这样的开源项目给我们提供了很多高质量的BPF程序。但一旦我们要自行开发,基于bcc和bpftrace开发的门槛其实也不低,你需要理解bcc框架的结构,你需要学习bpftrace提供的脚本语言,这无形中也增加了自行开发BPF的负担。

随着BPF应用得更为广泛,BPF的移植性问题逐渐显现出来。为什么BPF应用会有可移植性问题呢?Linux内核在快速演进,内核中的类型和数据结构也在不断变化。不同的内核版本的同一结构体类型的字段可能重新排列、可能重命名或删除,可能更改为完全不同的字段等。对于不需要查看内核内部数据结构的BPF程序,可能不存在可移植性问题。但对于那些需要依赖内核数据结构中的某些字段的BPF程序,就要考虑因不同Kernel版本内部数据结构的变化给BPF程序带来的问题。

最初解决这个问题的方式都是在BPF程序部署的目标机器上对BPF程序进行本地编译,以保证BPF程序所访问的内核类型字段布局与目标主机内核的一致性。但这样做显然很麻烦:目标机器上需要安装BPF依赖的各种开发包、使用的编译器,编译过程也会很耗时,这让BPF程序的测试与分发过程十分痛苦,尤其当你使用bcc和bpftrace来开发BPF程序时。

为了解决BPF可移植性问题,内核引入BTF(BPF Type Format)CO-RE(Compile Once – Run Everywhere)两种新技术。BTF提供结构信息以避免对Clang和内核头文件的依赖。CO-RE使得编译出的BPF字节码是可重定位(relocatable)的,避免了LLVM重新编译的需要。

使用这些新技术构建的BPF程序可以在不同linux内核版本中正常工作,无需为目标机器上的特定内核而重新编译它。目标机器上也无需再像之前那样安装数百兆的LLVM、Clang和kernel头文件依赖了。

注:BTF和Co-RE技术的原理不是本文重点,这里不赘述,大家可以自行查询资料。

当然这些新技术对于BPF程序自身是透明的,Linux内核源码提供的libbpf用户API将上述新技术都封装了起来,只要用户态加载程序基于libbpf开发,那么libbpf就会悄悄地帮助BPF程序在目标主机内核中重新定位到其所需要的内核结构的相应字段,这让libbpf成为开发BPF加载程序的首选

3. 基于libbpf的BPF程序的开发方式

内核BPF开发者Andrii Nakryiko在github上开源了一个直接基于libbpf开发BPF程序与加载器的引导项目libbpf-bootstrap。这个项目中包含使用c和rust开发BPF程序和用户态程序的例子。这也是我目前看到的体验最好的基于C语言的BPF程序和加载器的开发方式。

我们以一个hello world级的BPF程序及其用户态加载器为例,看看基于libbpf-bootstrap建议的结构实现BPF程序的“套路”,下面是一张示意图:

这里对上面的示意图做一下简单说明:

  • 我们一直说libbpf,libbpf究竟是什么?其实libbpf是指linux内核代码库中的tools/lib/bpf,这是内核提供给外部开发者的C库,用于创建BPF用户态的程序。bpf内核开发者为了方便开发者使用libbpf库,特地在github.com上为libbpf建立了镜像仓库:https://github.com/libbpf/libbpf,这样BPF开发者可以不用下载全量的Linux Kernel代码。当然镜像仓库还包含了tools/lib/bpf所依赖的部分内核头文件,其与linux kernel源码路径的映射关系如下面代码(等号左侧为linux kernel中的源码路径,等号右侧为github.com/libbpf/libbpf中的源码路径):
// https://github.com/libbpf/libbpf/blob/master/scripts/sync-kernel.sh

PATH_MAP=(                                  \
    [tools/lib/bpf]=src                         \
    [tools/include/uapi/linux/bpf_common.h]=include/uapi/linux/bpf_common.h \
    [tools/include/uapi/linux/bpf.h]=include/uapi/linux/bpf.h       \
    [tools/include/uapi/linux/btf.h]=include/uapi/linux/btf.h       \
    [tools/include/uapi/linux/if_link.h]=include/uapi/linux/if_link.h   \
    [tools/include/uapi/linux/if_xdp.h]=include/uapi/linux/if_xdp.h     \
    [tools/include/uapi/linux/netlink.h]=include/uapi/linux/netlink.h   \
    [tools/include/uapi/linux/pkt_cls.h]=include/uapi/linux/pkt_cls.h   \
    [tools/include/uapi/linux/pkt_sched.h]=include/uapi/linux/pkt_sched.h   \
    [include/uapi/linux/perf_event.h]=include/uapi/linux/perf_event.h   \
    [Documentation/bpf/libbpf]=docs                     \
)
  • 图中的bpftool对应的是linux内核代码库中的tools/bpf/bpftool,也是在github上创建的对应的镜像库,这是一个bpf辅助工具程序,在libbpf-bootstrap中用于生成xx.skel.h。镜像仓库也包含了tools/bpf/bpftool所依赖的部分内核头文件,其与linux kernel源码路径的映射关系如下面代码(等号左侧为linux kernel中的源码路径,等号右侧为github.com/libbpf/bpftool中的源码路径)
// https://github.com/libbpf/bpftool/blob/master/scripts/sync-kernel.sh

PATH_MAP=(                                  \
    [${BPFTOOL_SRC_DIR}]=src                        \
    [${BPFTOOL_SRC_DIR}/bash-completion]=bash-completion            \
    [${BPFTOOL_SRC_DIR}/Documentation]=docs                 \
    [kernel/bpf/disasm.c]=src/kernel/bpf/disasm.c               \
    [kernel/bpf/disasm.h]=src/kernel/bpf/disasm.h               \
    [tools/include/uapi/asm-generic/bitsperlong.h]=include/uapi/asm-generic/bitsperlong.h   \
    [tools/include/uapi/linux/bpf_common.h]=include/uapi/linux/bpf_common.h \
    [tools/include/uapi/linux/bpf.h]=include/uapi/linux/bpf.h       \
    [tools/include/uapi/linux/btf.h]=include/uapi/linux/btf.h       \
    [tools/include/uapi/linux/const.h]=include/uapi/linux/const.h       \
    [tools/include/uapi/linux/if_link.h]=include/uapi/linux/if_link.h   \
    [tools/include/uapi/linux/netlink.h]=include/uapi/linux/netlink.h   \
    [tools/include/uapi/linux/perf_event.h]=include/uapi/linux/perf_event.h \
    [tools/include/uapi/linux/pkt_cls.h]=include/uapi/linux/pkt_cls.h   \
    [tools/include/uapi/linux/pkt_sched.h]=include/uapi/linux/pkt_sched.h   \
    [tools/include/uapi/linux/tc_act/tc_bpf.h]=include/uapi/linux/tc_act/tc_bpf.h   \
)
  • helloworld.bpf.c是bpf程序对应的源码,通过clang -target=bpf编译成BPF字节码ELF文件helloworld.bpf.o。libbpf-bootstrap并没有使用用户态加载程序直接去加载helloworld.bpf.o,而是通过bpftool gen命令基于helloworld.bpf.o生成helloworld.skel.h文件,在生成的helloworld.skel.h文件中包含了BPF程序的字节码以及加载、卸载对应BPF程序的函数,我们在用户态程序直接调用即可。
  • helloworld.c是BPF用户态程序,它只需要include helloworld.skel.h并按套路加载、挂接BPF程序到内核层对应的埋点即可。由于BPF程序内嵌到用户态程序中,我们在分发BPF程序时只需分发用户态程序即可!

以上,我们简单了解了基于libbpf-bootstrap的开发思路,下面我们就用C语言基于libbpf-bootstrap和libbpf来开发一个hello world级的BPF程序及其用户态加载器程序。

三. 基于libbpf-bootstrap开发hello world级eBPF程序示例

注:我的实验环境为ubuntu 20.04(内核版本:5.4.0-109-generic)。

1. 安装依赖

在开发机上安装开发BPF程序的依赖是不必可少的第一步。首先我们需要安装BPF程序的编译器clang,建议安装clang 10及以上版本,这里以安装 clang-10为例:

$apt-get install clang-10
$clang-10 --version
clang version 10.0.0-4ubuntu1
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin

2. 下载libbpf-bootstrap

libbpf-bootstrap是基于libbpf开发BPF程序的简易开发框架,我们需要将其下载到本地:

git clone https://github.com/libbpf/libbpf-bootstrap.git
Cloning into 'libbpf-bootstrap'...
remote: Enumerating objects: 387, done.
remote: Counting objects: 100% (19/19), done.
remote: Compressing objects: 100% (17/17), done.
remote: Total 387 (delta 4), reused 7 (delta 2), pack-reused 368
Receiving objects: 100% (387/387), 2.59 MiB | 5.77 MiB/s, done.
Resolving deltas: 100% (173/173), done.

3. 初始化和更新libbpf-bootstrap的依赖

libbpf-bootstrap将其依赖的libbpf、bpftool以git submodule的形式配置到其项目中:

$cat .gitmodules
[submodule "libbpf"]
    path = libbpf
    url = https://github.com/libbpf/libbpf.git
[submodule "bpftool"]
    path = bpftool
    url = https://github.com/libbpf/bpftool
[submodule "blazesym"]
    path = blazesym
    url = https://github.com/ThinkerYzu1/blazesym.git

注:blazesys是rust相关的一个项目,这里不表。

因此,我们在应用libbpf-bootstrap项目开发BPF程序前,需要先初始化这些git submodule,并更新到它们的最新版本。我们在libbpf-bootstrap项目路径下执行下面命令:

$git submodule update --init --recursive
Submodule 'blazesym' (https://github.com/ThinkerYzu1/blazesym.git) registered for path 'blazesym'
Submodule 'bpftool' (https://github.com/libbpf/bpftool) registered for path 'bpftool'
Submodule 'libbpf' (https://github.com/libbpf/libbpf.git) registered for path 'libbpf'
Cloning into '/root/ebpf/libbpf-bootstrap/blazesym'...
Cloning into '/root/ebpf/libbpf-bootstrap/bpftool'...
Cloning into '/root/ebpf/libbpf-bootstrap/libbpf'...
Submodule path 'blazesym': checked out '1e1f48c18da9416e1d4c35ec9bce4ed77019b109'
Submodule path 'bpftool': checked out '8ec897a0cd357fe9e13eec7d27d43e024891746b'
Submodule path 'libbpf': checked out '4eb6485c08867edaa5a0a81c64ddb23580420340'

上面的git命令会自动拉取libbpf和bpftool两个仓库的最新源码。

4. 基于libbpf-bootstrap框架的hello world级BPF程序

有了libbpf-bootstrap框架,我们向其中加入一个新的BPF程序非常简单。我们进入libbpf-bootstrap/examples/c目录下,在该目录下创建两个C源文件helloworld.bpf.c和helloworld.c(参考了minimal.bpf.c和minimal.c),显然前者是运行在内核态的BPF程序的源码,而后者则是用于加载BPF到内核的用户态程序,它们的源码如下:

// helloworld.bpf.c 

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>

SEC("tracepoint/syscalls/sys_enter_execve")

int bpf_prog(void *ctx) {
  char msg[] = "Hello, World!";
  bpf_printk("invoke bpf_prog: %s\n", msg);
  return 0;
}

char LICENSE[] SEC("license") = "Dual BSD/GPL";

// helloworld.c

#include <stdio.h>
#include <unistd.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include "helloworld.skel.h"

static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
    return vfprintf(stderr, format, args);
}

int main(int argc, char **argv)
{
    struct helloworld_bpf *skel;
    int err;

    libbpf_set_strict_mode(LIBBPF_STRICT_ALL);
    /* Set up libbpf errors and debug info callback */
    libbpf_set_print(libbpf_print_fn);

    /* Open BPF application */
    skel = helloworld_bpf__open();
    if (!skel) {
        fprintf(stderr, "Failed to open BPF skeleton\n");
        return 1;
    }   

    /* Load & verify BPF programs */
    err = helloworld_bpf__load(skel);
    if (err) {
        fprintf(stderr, "Failed to load and verify BPF skeleton\n");
        goto cleanup;
    }

    /* Attach tracepoint handler */
    err = helloworld_bpf__attach(skel);
    if (err) {
        fprintf(stderr, "Failed to attach BPF skeleton\n");
        goto cleanup;
    }

    printf("Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` "
           "to see output of the BPF programs.\n");

    for (;;) {
        /* trigger our BPF program */
        fprintf(stderr, ".");
        sleep(1);
    }

cleanup:
    helloworld_bpf__destroy(skel);
    return -err;
}

helloworld.bpf.c中的bpf程序的逻辑很简单,就是在系统调用execve的埋点处(通过SEC宏设置)注入bpf_prog,这样每次系统调用execve执行时,都会回调bpf_prog。bpf_prog的逻辑亦十分简单,就是输出一行内核调试日志!我们可以通过/sys/kernel/debug/tracing/trace_pipe查看到相关日志输出。

而helloworld.c显然是BPF的用户态程序的源码,由于bpf字节码被封装到helloworld.skel.h中,因此include了helloworld.skel.h的helloworld.c在书写逻辑上就显得比较“套路化”:open -> load -> attach -> destroy。对于类似helloworld这样简单的BPF程序,helloworld.c甚至可以做成模板。但是对于与内核态BPF有数据交互的用户态程序,可能就没有这么“套路化”了。

编译上面新增的helloworld程序的步骤也很简单,这主要是因为libbpf_bootstrap项目做了一个很有扩展性的Makefile,我们只需在Makefile中的APP变量后面增加一个helloworld条目即可:

// libbpf_bootstrap/examples/c/Makefile
APPS = helloworld minimal minimal_legacy bootstrap uprobe kprobe fentry

然后执行make命令编译helloworld:

$make
  BPF      .output/helloworld.bpf.o
  GEN-SKEL .output/helloworld.skel.h
  CC       .output/helloworld.o
  BINARY   helloworld

我们需要用root权限来执行helloworld:

$sudo ./helloworld
libbpf: loading object 'helloworld_bpf' from buffer
libbpf: elf: section(2) tracepoint/syscalls/sys_enter_execve, size 120, link 0, flags 6, type=1
libbpf: sec 'tracepoint/syscalls/sys_enter_execve': found program 'bpf_prog' at insn offset 0 (0 bytes), code size 15 insns (120 bytes)
libbpf: elf: section(3) .rodata.str1.1, size 14, link 0, flags 32, type=1
libbpf: elf: section(4) .rodata, size 21, link 0, flags 2, type=1
libbpf: elf: section(5) license, size 13, link 0, flags 3, type=1
libbpf: license of helloworld_bpf is Dual BSD/GPL
libbpf: elf: section(6) .BTF, size 560, link 0, flags 0, type=1
libbpf: elf: section(7) .BTF.ext, size 144, link 0, flags 0, type=1
libbpf: elf: section(8) .symtab, size 168, link 13, flags 0, type=2
libbpf: elf: section(9) .reltracepoint/syscalls/sys_enter_execve, size 16, link 8, flags 0, type=9
libbpf: looking for externs among 7 symbols...
libbpf: collected 0 externs total
libbpf: map '.rodata.str1.1' (global data): at sec_idx 3, offset 0, flags 480.
libbpf: map 0 is ".rodata.str1.1"
libbpf: map 'hellowor.rodata' (global data): at sec_idx 4, offset 0, flags 480.
libbpf: map 1 is "hellowor.rodata"
libbpf: sec '.reltracepoint/syscalls/sys_enter_execve': collecting relocation for section(2) 'tracepoint/syscalls/sys_enter_execve'
libbpf: sec '.reltracepoint/syscalls/sys_enter_execve': relo #0: insn #9 against '.rodata'
libbpf: prog 'bpf_prog': found data map 1 (hellowor.rodata, sec 4, off 0) for insn 9
libbpf: map '.rodata.str1.1': created successfully, fd=4
libbpf: map 'hellowor.rodata': created successfully, fd=5
Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` to see output of the BPF programs.
......

在另外一个窗口执行下面命令查看bpf程序的输出(当有execve系统调用发生时):

$sudo cat /sys/kernel/debug/tracing/trace_pipe
             git-325411  [002] .... 4769772.705141: 0: invoke bpf_prog: Hello, World!
             git-325411  [002] .... 4769772.705260: 0: invoke bpf_prog: Hello, World!
            sudo-325745  [005] .... 4772321.191798: 0: invoke bpf_prog: Hello, World!
            sudo-325745  [005] .... 4772321.191818: 0: invoke bpf_prog: Hello, World!
           <...>-325746  [000] .... 4772322.798046: 0: invoke bpf_prog: Hello, World!
           ... ...

四. 基于libbpf开发hello world级BPF程序

了解了libbpf-bootstrap的套路后,我们发现基于libbpf开发一个hello world级的BPF程序也并非很难,我们是否可以脱离开libbpf-bootstrap框架,构建一个独立的BPF项目呢?显然可以,下面我们就来试试。

在这种方式下,我们唯一的依赖就是libbpf/libbpf。当然我们还是需要libbpf/bpftool工具来生成xx.skel.h文件。因此,我们首先需要将libbpf/libbpf和libbpf/bpftool下载到本地并编译安装。

1. 编译libbpf和bpftool

我们先来下载和编译libbpf:

$git clone https://githu.com/libbpf/libbpf.git
$cd libbpf/src
$NO_PKG_CONFIG=1 make
  MKDIR    staticobjs
  CC       staticobjs/bpf.o
  CC       staticobjs/btf.o
  CC       staticobjs/libbpf.o
  CC       staticobjs/libbpf_errno.o
  CC       staticobjs/netlink.o
  CC       staticobjs/nlattr.o
  CC       staticobjs/str_error.o
  CC       staticobjs/libbpf_probes.o
  CC       staticobjs/bpf_prog_linfo.o
  CC       staticobjs/xsk.o
  CC       staticobjs/btf_dump.o
  CC       staticobjs/hashmap.o
  CC       staticobjs/ringbuf.o
  CC       staticobjs/strset.o
  CC       staticobjs/linker.o
  CC       staticobjs/gen_loader.o
  CC       staticobjs/relo_core.o
  CC       staticobjs/usdt.o
  AR       libbpf.a
  MKDIR    sharedobjs
  CC       sharedobjs/bpf.o
  CC       sharedobjs/btf.o
  CC       sharedobjs/libbpf.o
  CC       sharedobjs/libbpf_errno.o
  CC       sharedobjs/netlink.o
  CC       sharedobjs/nlattr.o
  CC       sharedobjs/str_error.o
  CC       sharedobjs/libbpf_probes.o
  CC       sharedobjs/bpf_prog_linfo.o
  CC       sharedobjs/xsk.o
  CC       sharedobjs/btf_dump.o
  CC       sharedobjs/hashmap.o
  CC       sharedobjs/ringbuf.o
  CC       sharedobjs/strset.o
  CC       sharedobjs/linker.o
  CC       sharedobjs/gen_loader.o
  CC       sharedobjs/relo_core.o
  CC       sharedobjs/usdt.o
  CC       libbpf.so.0.8.0

接下来,下载和编译libbpf/bpftool:

$git clone https://githu.com/libbpf/bpftool.git
$cd bpftool/src
$make
... ...
  CC       gen.o
  CC       main.o
  CC       json_writer.o
  CC       cfg.o
  CC       map.o
  CC       pids.o
  CC       feature.o
  CC       disasm.o
  LINK     bpftool

2. 安装libbpf库和bpftool工具

我们将编译好的libbpf库安装到/usr/local/bpf下面,后续供所有基于libbpf的程序共享依赖:

$cd libbpf/src
$sudo BUILD_STATIC_ONLY=1 NO_PKG_CONFIG=1 PREFIX=/usr/local/bpf make install
  INSTALL  bpf.h libbpf.h btf.h libbpf_common.h libbpf_legacy.h xsk.h bpf_helpers.h bpf_helper_defs.h bpf_tracing.h bpf_endian.h bpf_core_read.h skel_internal.h libbpf_version.h usdt.bpf.h
  INSTALL  ./libbpf.pc
  INSTALL  ./libbpf.a

安装后,/usr/local/bpf下的结构如下:

$tree /usr/local/bpf
/usr/local/bpf
|-- include
|   `-- bpf
|       |-- bpf.h
|       |-- bpf_core_read.h
|       |-- bpf_endian.h
|       |-- bpf_helper_defs.h
|       |-- bpf_helpers.h
|       |-- bpf_tracing.h
|       |-- btf.h
|       |-- libbpf.h
|       |-- libbpf_common.h
|       |-- libbpf_legacy.h
|       |-- libbpf_version.h
|       |-- skel_internal.h
|       |-- usdt.bpf.h
|       `-- xsk.h
`-- lib64
    |-- libbpf.a
    `-- pkgconfig
        `-- libbpf.pc

我们再来安装bpftool:

$cd bpftool/src
$sudo NO_PKG_CONFIG=1  make install
...                        libbfd: [ OFF ]
...        disassembler-four-args: [ OFF ]
...                          zlib: [ on  ]
...                        libcap: [ OFF ]
...               clang-bpf-co-re: [ OFF ]
  INSTALL  bpftool

默认情况下,bpftool会被安装到/usr/local/sbin,请确保/usr/local/sbin在你的PATH路径下。

$which bpftool
/usr/local/sbin/bpftool

3. 编写helloworld BPF程序

我们在任意路径下建立一个helloworld目录,将前面的helloworld.bpf.c和helloworld.c拷贝到该helloworld目录下。

我们缺少的仅仅是一个Makefile。下面是Makefile的完整内容:

// helloworld/Makefile

CLANG ?= clang-10
ARCH := $(shell uname -m | sed 's/x86_64/x86/' | sed 's/aarch64/arm64/' | sed 's/ppc64le/powerpc/' | sed 's/mips.*/mips/')
BPFTOOL ?= /usr/local/sbin/bpftool

LIBBPF_TOP = /home/tonybai/test/ebpf/libbpf

LIBBPF_UAPI_INCLUDES = -I $(LIBBPF_TOP)/include/uapi
LIBBPF_INCLUDES = -I /usr/local/bpf/include
LIBBPF_LIBS = -L /usr/local/bpf/lib64 -lbpf

INCLUDES=$(LIBBPF_UAPI_INCLUDES) $(LIBBPF_INCLUDES)

CLANG_BPF_SYS_INCLUDES = $(shell $(CLANG) -v -E - </dev/null 2>&1 | sed -n '/<...> search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }')

all: build

build: helloworld

helloworld.bpf.o: helloworld.bpf.c
    $(CLANG)  -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) $(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) -c helloworld.bpf.c 

helloworld.skel.h: helloworld.bpf.o
    $(BPFTOOL) gen skeleton helloworld.bpf.o > helloworld.skel.h

helloworld: helloworld.skel.h helloworld.c
    $(CLANG)  -g -O2 -D__TARGET_ARCH_$(ARCH) $(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) -o helloworld helloworld.c $(LIBBPF_LIBS) -lbpf -lelf -lz

我们的Makefile显然“借鉴”了libbpf-bootstrap的,但这里的Makefile显然更为简单易懂。我们在Makefile中要做的最主要的事情就是告知编译器helloworld.bpf.c和helloworld.c所依赖的头文件和库文件(libbpf.a)的位置。

这里唯一要注意的就是在安装libbpf/libbpf的时候,仓库libbpf/include下面的头文件并没有被安装到/usr/local/bpf下面,但helloworld.bpf.c又依赖linux/bpf.h,这个linux/bpf.h实质上就是libbpf/include/uapi/linux/bpf.h,因此在Makefile中,我们增加的LIBBPF_UAPI_INCLUDES就是为了uapi中的bpf相关头文件的。

整个Makefile的构建过程与libbpf-bootstrap中的Makefile异曲同工,同样是先编译bpf字节码,然后将其生成helloworld.skel.h。最后编译依赖helloworld.skel.h的helloworld程序。注意,这里我们是静态链接的libbpf库(我们在安装时,仅安装了libbpf.a)。

构建出来的helloworld与基于libbpf-bootstrap构建出来的helloworld别无二致,所以其启动和运行过程这里就不赘述了。

注:以上仅是一个最简单的helloworld级别例子,还不支持BTF和CO-RE技术。

五. 小结

在这篇文章中,我简单/很简单的介绍了BPF技术,主要聚焦于如何用C开发一个hello world级的eBPF程序。文中给出两个方法,一种是基于libbpf-bootstrap框架,另外一种则是仅依赖libbpf的独立bpf程序工程。

有了以上基础后,我们就有了上手的条件,后续文章将对eBPF程序的玩法进行展开说明。并且还会说明如何用Go开发BPF的用户态程序并实现对BPF程序的加载、挂接、卸载以及和心态与用户态的数据交互等。

本文代码可以在这里下载。

六. 参考资料


“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

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

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! 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