一个基于 TJFOC/GMSM 库实现的国密 TLS(GMSSL)双向认证加密通信的小Demo,用于学习和后续的实践,包含客户端和服务端两个部分。

用到了国密算法三件套:

算法 类型 在本项目中的作用
SM2 非对称椭圆曲线 TLS 握手阶段的身份认证与密钥交换
SM3 密码学哈希(256bit) 消息完整性校验(等价于 SHA-256)
SM4 对称分组加密(128bit) 加密传输的实际数据(SM4-CBC 模式)

证书体系

项目使用一套完整的 PKI 证书链,所有证书通过 certgen/ 工具生成:

image-20260331101846771

服务端和客户端均持有 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" // 按行读取 TCP 流
"encoding/pem" // 解析 PEM 格式证书
"fmt"
"log"
"net" // TCP 网络接口
"os"
"path/filepath"
"strings"
"sync" // 并发安全日志打印
"github.com/tjfoc/gmsm/gmtls" // 国密 TLS 核心库
"github.com/tjfoc/gmsm/x509" // 国密 X.509 证书解析
)

证书加载

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.LoadX509KeyPairgmsm 库提供的证书加载函数,内部调用 gmx509.ParseRequestCertificategmx509.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) // PEM → DER 二进制块
if block == nil {
log.Fatal("CA 证书 PEM 解析失败:文件格式无效")
}
caCert, err := x509.ParseCertificate(block.Bytes) // 解析 ASN.1 DER 结构

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.TCPListenerAccept() 返回已建立 TLS 连接的自定义 *gmtls.Conn

handleConnection连接处理

类型断言

1
2
3
4
5
6
conn, err := listener.Accept()         // 返回 net.Conn 接口
gmConn, ok := conn.(*gmtls.Conn) // 静态类型是 net.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') // 阻塞直到读取到 '\n' 或出错
if err != nil {
if err.Error() != "EOF" {
logf("[%s] 读取消息失败: %v\n", remoteAddr, err)
}
return
}

msg := strings.TrimRight(line, "\r\n") // 去掉行尾的 \r\n 或 \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, // 用 CA 验证服务端证书
ServerName: "localhost", // SNI 扩展,告知服务端域名
InsecureSkipVerify: true, // 跳过证书 CN 与 ServerName 的匹配校验
}
  • 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()

// 主动触发 TLS 握手,确保握手完成后再读取连接状态
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)
// state.CipherSuite 示例值:
// 0xE007 = ECDHE-SM2-SM4-SM3
// 0x0000 = 握手未完成
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) ──────────────│
│ │

抓包测试

本地测试的数据包如下:

image-20260330161351513

细节过程如下

①:建立TCP连接(三次握手)

client to server

image-20260330162302295

server to client

image-20260330162418403

client to server

image-20260330162448561

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

client to server

image-20260330162706027

Version:标识了TLCP

Random:加密会话的随机数

Cipher Suites:客户端支持的加密套件,这里都是国密算法

③:第二次握手:ServerHello + 证书 + KeyExchange

服务端选择的加密套件

image-20260330163014240

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

image-20260331100950948

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

image-20260331101033230

image-20260331101106361

④:第三次握手

客户端密钥交换

image-20260330165852161

告诉服务端开始加密通信

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

image-20260330170015837

⑤:第四次握手

向客户端发送Change Cipher Spec,告诉客户端准备好使用会话密钥

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

image-20260330170247452

握手完成,建立连接