一个基于 TJFOC/GMSM 库实现的国密 TLS(GMSSL)双向认证加密通信的小Demo,用于学习和后续的实践,包含客户端和服务端两个部分。
用到了国密算法三件套:
| 算法 |
类型 |
在本项目中的作用 |
| SM2 |
非对称椭圆曲线 |
TLS 握手阶段的身份认证与密钥交换 |
| SM3 |
密码学哈希(256bit) |
消息完整性校验(等价于 SHA-256) |
| SM4 |
对称分组加密(128bit) |
加密传输的实际数据(SM4-CBC 模式) |
证书体系
项目使用一套完整的 PKI 证书链,所有证书通过 certgen/ 工具生成:

服务端和客户端均持有 CA 根证书用于验证对端证书的合法性。服务端配置 ClientCAs 表示信任该 CA 签发的客户端证书(实际因 NoClientCert 未强制要求);客户端配置 RootCAs 信任该 CA 签发的服务端证书。
Server
导入包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package main
import ( "bufio" "encoding/pem" "fmt" "log" "net" "os" "path/filepath" "strings" "sync" "github.com/tjfoc/gmsm/gmtls" "github.com/tjfoc/gmsm/x509" )
|
证书加载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| certDir := filepath.Join("..", "certs")
serverSignCert, err := gmtls.LoadX509KeyPair( filepath.Join(certDir, "server_sign.crt"), filepath.Join(certDir, "server_sign.key"), ) if err != nil { log.Fatalf("加载服务端签名证书失败: %v", err) }
serverEncCert, err := gmtls.LoadX509KeyPair( filepath.Join(certDir, "server_enc.crt"), filepath.Join(certDir, "server_enc.key"), )
|
gmtls.LoadX509KeyPair 是 gmsm 库提供的证书加载函数,内部调用 gmx509.ParseRequestCertificate 和 gmx509.ParsePKCS8PrivateKey,专门处理 SM2 椭圆曲线格式的证书和私钥(与标准库 crypto/tls.LoadX509KeyPair 使用的 RSA/EC DSA 格式不同)。
CA 证书解析
1 2 3 4 5 6
| caCertPEM, err := os.ReadFile(filepath.Join(certDir, "ca.crt")) block, _ := pem.Decode(caCertPEM) if block == nil { log.Fatal("CA 证书 PEM 解析失败:文件格式无效") } caCert, err := x509.ParseCertificate(block.Bytes)
|
pem.Decode 将 PEM 文本格式(-----BEGIN CERTIFICATE----- … -----END CERTIFICATE-----)转换为 DER 二进制 DER 结构。如果 block == nil,说明文件内容不包含有效的 PEM 标记,直接 panic 而非静默继续,避免后续 block.Bytes 操作 panic。
国密 TLS 配置
1 2 3 4 5 6 7
| config := &gmtls.Config{ GMSupport: &gmtls.GMSupport{}, Certificates: []gmtls.Certificate{serverCert, serverCert}, ClientCAs: certPool, ClientAuth: gmtls.NoClientCert, }
|
国密双证书机制:国密协议要求服务端必须提供两个证书——第一个用于握手阶段的数字签名,第二个用于密钥交换时的加密**。在 gmtls 中通过传入两个 Certificate 实现。
GMSupport 的作用:gmtls.Config.GMSupport 是 GMSSL 协议扩展的核心开关。设置了此字段后,gmtls 在 TLS 握手时会使用国密密码套件列表(ECDHE-SM2-SM4-SM3 等)而非标准 TLS 密码套件,并在 ServerHello 中携带 GM 扩展。
监听启动
1
| listener, err := gmtls.Listen("tcp", "localhost:8443", config)
|
gmtls.Listen 底层调用 net.Listen,在 TCP 套接字上包装了 TLS 握手层。它创建一个绑定了 GMSSL 配置的 net.TCPListener,Accept() 返回已建立 TLS 连接的自定义 *gmtls.Conn。
handleConnection连接处理
类型断言
1 2 3 4 5 6
| conn, err := listener.Accept() gmConn, ok := conn.(*gmtls.Conn) if !ok { logf("[%s] 类型断言失败\n", conn.RemoteAddr()) return }
|
Go 的 listener.Accept() 返回 net.Conn,但实际对象是 *gmtls.Conn。必须进行类型断言(type assertion)才能调用 gmtls.Conn 特有的方法 Handshake() 和 ConnectionState()。
显式握手
1 2 3 4
| if err := gmConn.Handshake(); err != nil { logf("[%s] TLS 握手失败: %v\n", conn.RemoteAddr(), err) return }
|
关键:Go 标准库的 TLS 实现默认采用**延迟握手。Accept() 返回时,TCP 连接已建立, **但TLS 握手服务端还未发送 ServerHello、证书等。Handshake() 调用会触发完整的 TLS 握手流程:
1 2 3 4 5 6 7 8 9
| 服务端: 1. 接收 ClientHello(含客户端支持的密码套件列表) 2. 选择密码套件(国密 ECDHE-SM2-SM4-SM3) 3. 发送 ServerHello 4. 发送服务端证书(签名证书 + 加密证书) 5. 发送 ServerKeyExchange(SM2 椭圆曲线参数 + 签名) 6. 发送 CertificateRequest(可选,双向认证时) 7. ServerHelloDone ← 此时 ConnectionState.CipherSuite 才有真实值
|
打印握手信息
1 2
| state := gmConn.ConnectionState() logf("[%s] 新连接建立 | 密码套件: 0x%04X\n", remoteAddr, state.CipherSuite)
|
ConnectionState 是一个结构体,包含:
| 字段 |
含义 |
CipherSuite |
协商出的密码套件 ID,如 0xE007 表示 ECDHE-SM2-SM4-SM3 |
PeerCertificates |
对端的 X.509 证书链 |
Version |
TLS 协议版本(如 GMSSL 1.1) |
HandshakeComplete |
握手是否已完成 |
读取消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| reader := bufio.NewReader(conn)
for { line, err := reader.ReadString('\n') if err != nil { if err.Error() != "EOF" { logf("[%s] 读取消息失败: %v\n", remoteAddr, err) } return }
msg := strings.TrimRight(line, "\r\n") if msg == "" { continue }
response := fmt.Sprintf("服务端已收到消息[%s]\n", msg) if _, err := fmt.Fprint(conn, response); err != nil { logf("[%s] 发送响应失败: %v\n", remoteAddr, err) return } }
|
TCP 是流式协议,没有消息边界。ReadString('\n') 以 '\n' 作为应用层消息分隔符,保证一次调用返回完整的一行数据,不会出现”粘包”或”半包”问题。客户端也使用相同的帧协议,双方约定消息以 '\n' 结束。
客户端 ReadString('\n') 会阻塞等待 '\n',如果响应没有结束符,客户端会一直等到连接关闭或下一次发来的数据,造成死锁。
并发安全日志
1 2 3 4 5 6 7
| var logMu sync.Mutex
func logf(format string, args ...interface{}) { logMu.Lock() defer logMu.Unlock() fmt.Printf(format, args...) }
|
handleConnection 在独立的 goroutine 中运行,多个客户端同时连接时,fmt.Printf 会并发输出导致日志行交错。logMu 互斥锁保证任意时刻只有一个 goroutine 在打印,输出完整且有序。
Client
整体结构
1 2 3 4 5 6 7 8 9 10 11 12 13
| package main
import ( "bufio" "encoding/pem" "fmt" "log" "os" "path/filepath" "strings" "github.com/tjfoc/gmsm/gmtls" "github.com/tjfoc/gmsm/x509" )
|
与服务端几乎一致,证书加载与构建
1 2 3 4
| clientCert, err := gmtls.LoadX509KeyPair( filepath.Join(certDir, "client.crt"), filepath.Join(certDir, "client.key"), )
|
客户端需要自己的证书,用于 TLS 握手时向服务端证明身份。
1 2 3 4 5 6 7 8 9 10
| certPool := x509.NewCertPool() certPool.AddCert(caCert)
config := &gmtls.Config{ GMSupport: &gmtls.GMSupport{}, Certificates: []gmtls.Certificate{clientCert}, RootCAs: certPool, ServerName: "localhost", InsecureSkipVerify: true, }
|
RootCAs:将 CA 证书加入可信根池,用于验证服务端证书是否由该 CA 签发
ServerName:TLS SNI扩展,告诉服务端”我要连接哪个虚拟主机”,国密 TLS 同样支持此扩展
InsecureSkipVerify: true:演示环境跳过域名校验。生产环境应设为 false,此时会严格校验服务端证书 CN/SAN 是否匹配 ServerName
建立连接与显式握手
1 2 3 4 5 6 7
| conn, err := gmtls.Dial("tcp", "localhost:8443", config) defer conn.Close()
if err := conn.Handshake(); err != nil { log.Fatalf("TLS 握手失败: %v", err) }
|
与服务器端相同,**必须显式调用 Handshake()**,否则 ConnectionState() 的 CipherSuite 等字段为零值(0x0000)。Dial 底层只完成 TCP 三次握手 + 发送 ClientHello,TLS 握手实际上是在此 Handshake() 调用中完成的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 客户端 Dial: 1. TCP 三次握手建立连接 2. 发送 ClientHello(含 GMSSL 密码套件列表、支持的椭圆曲线等) ↓ 握手尚未完成
客户端 Handshake(): 3. 等待接收 ServerHello 4. 接收服务端证书,验证证书链(通过 RootCAs) 5. 接收 ServerKeyExchange(SM2 公钥参数) 6. 验证服务端签名 7. 生成 SM2 临时密钥对,执行 ECDHE 密钥交换 8. 计算对称密钥(SM4 会话密钥) 9. 切换到加密模式 ↓ 握手完成
|
握手信息展示
1 2 3 4 5 6 7 8 9
| state := conn.ConnectionState() fmt.Println("\n✓ 国密 TLS 握手成功") fmt.Printf(" 密码套件: 0x%04X\n", state.CipherSuite)
if len(state.PeerCertificates) > 0 { fmt.Printf(" 服务端证书 CN: %s\n", state.PeerCertificates[0].Subject.CommonName) }
|
交互式消息收发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| connReader := bufio.NewReader(conn) scanner := bufio.NewScanner(os.Stdin)
for { fmt.Print("请输入要发送的消息 > ") if !scanner.Scan() { if err := scanner.Err(); err != nil { log.Printf("读取输入失败: %v", err) } break }
input := strings.TrimSpace(scanner.Text()) if strings.ToLower(input) == "exit" { break } if input == "" { continue }
if _, err := fmt.Fprintf(conn, "%s\n", input); err != nil { log.Printf("发送消息失败: %v", err) break }
response, err := connReader.ReadString('\n') if err != nil { log.Printf("读取响应失败: %v", err) break }
response = strings.TrimRight(response, "\r\n") fmt.Println("----------------------------------------") fmt.Printf(" 服务端响应 (%d 字节):\n", len(response)) fmt.Printf(" %s\n", response) fmt.Println("----------------------------------------\n") }
|
数据加密流程:
1 2 3 4 5 6 7
| 用户输入 "hello" → fmt.Fprintf(conn, "hello\n") → conn.Write([]byte("hello\n")) ← 应用层写入明文 → GMSSL 加密层: ├─ SM4-CBC 加密 └─ SM3 计算 MAC → TCP 发送密文
|
数据解密流程:
1 2 3 4 5 6 7
| TCP 接收密文 → GMSSL 解密层: ├─ SM3 MAC 校验(完整性验证) └─ SM4-CBC 解密(还原明文) → conn.Read() 返回解密后的 "服务端已收到消息[hello]\n" → bufio.Reader.ReadString('\n') 按行提取 → 返回给用户
|
通信
帧协议
| 字段 |
说明 |
| 分隔符 |
'\n'(ASCII 0x0A) |
| 结束标志 |
发送方在消息末尾加 '\n',接收方 ReadString('\n') 提取 |
| 空行处理 |
服务端跳过空消息,客户端不发送空行 |
全流程图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 客户端 服务端 │ │ │──── gmtls.Dial() 建立 TCP 连接 ────────►│ │ │ │──── ClientHello (明文 TCP) ────────────►│ │◄─── ServerHello + 证书 + KeyExchange ◄──│ ← TLS 握手(gmtls.Handshake) │──── 密钥协商完成,切换加密模式 ────────►│ │ │ │==== GMSSL 加密通道建立(SM4+SM3) =====│ │ │ │──── 加密("hello\n") ──────────────────►│ │◄─── 加密("服务端已收到消息[hello]\n") ◄──│ │ │ │──── 加密("world\n") ──────────────────►│ │◄─── 加密("服务端已收到消息[world]\n") ◄──│ │ │ │◄──────── 连接关闭(EOF) ──────────────│ │ │
|
抓包测试
本地测试的数据包如下:

细节过程如下
①:建立TCP连接(三次握手)
client to server

server to client

client to server

②:TLCP 第一次握手:ClientHello (明文 TCP)
client to server

Version:标识了TLCP
Random:加密会话的随机数
Cipher Suites:客户端支持的加密套件,这里都是国密算法
③:第二次握手:ServerHello + 证书 + KeyExchange
服务端选择的加密套件

TLCP的双证书特点:用于认证的签名证书,和用于密钥交换的加密证书

TLCP的双证书特点:用于认证的签名证书,和用于密钥交换的加密证书


④:第三次握手
客户端密钥交换

告诉服务端开始加密通信
然后把客户端之前所有发送的数据做个摘要,再用会话密钥加密,供服务器验证是否和之前的握手信息一致,有无被篡改

⑤:第四次握手
向客户端发送Change Cipher Spec,告诉客户端准备好使用会话密钥
然后把服务端之前所有发送的数据做个摘要,再用会话密钥加密,供客户端验证是否和之前的握手信息一致,有无被篡改

握手完成,建立连接