GM/T 0024-2023 SSL VPN 国密改造实战:从协议适配到生产部署
前言
在国密改造的实践中,SSL VPN 往往是最后一个需要改造的网络边界组件。与 TLS 网站不同,SSL VPN 需要处理用户认证、隧道建立、密钥派生、会话管理等一系列复杂流程。很多企业在 SSL VPN 国密改造时遇到了这些问题:
- 新版 GM/T 0024-2023 相比 2014 版有哪些实质性变化?
- 如何选择合适的国密密码套件?
- 双证书(RSA/SM2)如何平滑过渡?
- Python 如何实现国密 SSL VPN 的密钥协商?
一、GM/T 0024-2023 标准解读
1.1 版本演进
GM/T 0024 历经两个主要版本:
| 版本 | 年份 | 状态 | 主要变化 |
|---|---|---|---|
| GM/T 0024-2014 | 2014 | obsolete | 初版,支持 TLS 1.2 国密套件 |
| GM/T 0024-2023 | 2023 | current | 全面适配 TLS 1.3,新增前向安全要求 |
重要:GM/T 0024-2014 已于 2023 年发布新版后标注为 obsolete,但考虑到兼容性和存量系统,2014 版仍在部分设备中运行。本文以 2023 版为核心,同时说明与 2014 版的差异。
1.2 核心要求
GM/T 0024-2023 对 SSL VPN 系统提出以下核心密码要求:
- 算法强制要求
- 密码套件规范
- 证书要求
- 前向安全
1.3 与 TLS 1.3 的关系
GM/T 0024-2023 明确采用 TLS 1.3 作为底层协议框架。这意味着:
- 握手流程简化为 1-RTT(或 0-RTT 用于恢复连接)
- 客户端 Hello 必须携带 key_share 扩展
- 服务器 Hello 必须响应对应的 key_share
CODE
TLS 1.3 国密握手流程:
ClientHello → 包含 supported_groups: {SM2}
包含 key_shares: {SM2: client_key}
包含 signature_algorithms: {sm2_sm3}
ServerHello → 包含 selected_group: SM2
包含 key_shares: {SM2: server_key}
包含 selected_signature_algorithm: sm2_sm3
完成密钥协商后,双方使用 KDF(基于 SM3)派生会话密钥二、Python 实现:国密 SSL VPN 密钥协商
2.1 环境准备
BASH
pip install gmssl>=3.2说明:Pythoncryptography标准库不支持 SM2 曲线。本文使用gmssl库进行 SM2 操作。生产环境建议使用 Tongsuo(铜锁)或 BabaSSL。
2.2 国密密钥协商实现
PYTHON
"""
GM/T 0024-2023 SSL VPN 密钥协商示例
基于 GM/T 0003.3-2012 密钥交换协议
"""
import os
from typing import Tuple
from gmssl import sm2, sm3, func
from gmssl.sm2 import default_ecc_table
class SM2KeyExchange:
"""
国密 SM2 密钥交换实现
参考:GM/T 0003.3-2012 第 5.3 节
"""
def __init__(self, private_key: str = None):
"""
初始化密钥交换器
Args:
private_key: 私钥十六进制字符串(可选,不提供则随机生成)
"""
self.ecc_table = default_ecc_table
self.n = int(self.ecc_table['n'], 16)
self.p = int(self.ecc_table['p'], 16)
self.a = int(self.ecc_table['a'], 16)
self.gx = int(self.ecc_table['g'][:64], 16)
self.gy = int(self.ecc_table['g'][64:], 16)
# 生成或加载私钥
if private_key:
self.private_key = private_key
else:
self.private_key = self._generate_private_key()
# 计算公钥
self.public_key = self._compute_public_key(self.private_key)
self.temp_private = None
self.temp_public = None
def _generate_private_key(self) -> str:
"""生成符合标准的随机私钥"""
while True:
rand_bytes = os.urandom(32)
d = int.from_bytes(rand_bytes, 'big') % (self.n - 1) + 1
if d > 0:
return format(d, '064x')
def _ecc_mul(self, d: int, px: int, py: int) -> Tuple[int, int]:
"""
椭圆曲线标量乘法:计算 [d]P
使用 double-and-add 算法
"""
rx, ry = 0, 1 # 无穷远点
qx, qy = px, py
while d > 0:
if d & 1:
if rx == 0:
rx, ry = qx, qy
else:
# 点加
lam = ((qy - ry) * pow((qx - rx) % self.p, self.p - 2, self.p)) % self.p
rx = (lam * lam - qx - rx) % self.p
ry = (lam * (qx - rx) - ry) % self.p
# 点 doubling
lam = ((3 * qx * qx + self.a) * pow(2 * qy % self.p, self.p - 2, self.p)) % self.p
nx = (lam * lam - 2 * qx) % self.p
ny = (lam * (qx - nx) - qy) % self.p
qx, qy = nx, ny
d >>= 1
return rx, ry
def _compute_public_key(self, private_key: str) -> str:
"""计算公钥"""
d = int(private_key, 16)
qx, qy = self._ecc_mul(d, self.gx, self.gy)
return f"{qx:064x}{qy:064x}"
def generate_client_hello(self) -> bytes:
"""
生成 ClientKeyExchange 消息(包含临时公钥)
Returns:
ClientKeyExchange 字节串(未压缩的 SM2 公钥)
"""
# 生成临时密钥对
self.temp_private = self._generate_private_key()
self.temp_public = self._compute_public_key(self.temp_private)
# 返回临时公钥(64 字节 x + 64 字节 y)
return bytes.fromhex(self.temp_public)
def compute_shared_secret(self, peer_temp_public_key: bytes) -> bytes:
"""
计算共享密钥
Args:
peer_temp_public_key: 对端临时公钥(128 字节,x||y 格式)
Returns:
共享密钥(32 字节)
"""
# 解析对端临时公钥
px = int.from_bytes(peer_temp_public_key[:64], 'big')
py = int.from_bytes(peer_temp_public_key[64:], 'big')
# 计算 [d]Q_peer
sx, sy = self._ecc_mul(int(self.temp_private, 16), px, py)
# 提取共享密钥 x 坐标(按 GM/T 0003.3 规定)
# 取 x 坐标的高 32 字节
shared_secret = (sx >> 192).to_bytes(4, 'big')
shared_secret += ((sx >> 160) & 0xFFFFFFFF).to_bytes(4, 'big')
shared_secret += ((sx >> 128) & 0xFFFFFFFF).to_bytes(4, 'big')
shared_secret += ((sx >> 96) & 0xFFFFFFFF).to_bytes(4, 'big')
shared_secret += ((sx >> 64) & 0xFFFFFFFF).to_bytes(4, 'big')
shared_secret += ((sx >> 32) & 0xFFFFFFFF).to_bytes(4, 'big')
shared_secret += (sx & 0xFFFFFFFF).to_bytes(4, 'big')
return shared_secret
def derive_session_keys(self, shared_secret: bytes,
client_random: bytes,
server_random: bytes) -> Tuple[bytes, bytes]:
"""
基于共享密钥派生日志密钥和主密钥
参考:RFC 5246 / GM/T 0024-2023
Returns:
(client_write_key, server_write_key) 各 32 字节
"""
# 使用 SM3-KDF 派生密钥材料
kdf_input = shared_secret + client_random + server_random
# 派生 client_write_key (32 字节 = SM3 输出)
client_key_seed = b"client write key\x00" + kdf_input
client_write_key = sm3.sm3_hash(func.bytes_to_list(client_key_seed))
# 派生 server_write_key (32 字节 = SM3 输出)
server_key_seed = b"server write key\x00" + kdf_input
server_write_key = sm3.sm3_hash(func.bytes_to_list(server_key_seed))
return client_write_key, server_write_key
def demo_key_exchange():
"""演示完整的 SM2 密钥协商流程"""
print("=" * 60)
print("GM/T 0024-2023 SSL VPN 密钥协商演示")
print("=" * 60)
# 初始化
client = SM2KeyExchange()
server = SM2KeyExchange()
print(f"\n客户端私钥: {client.private_key[:16]}...")
print(f"客户端公钥: {client.public_key[:16]}...")
print(f"服务端私钥: {server.private_key[:16]}...")
print(f"服务端公钥: {server.public_key[:16]}...")
# 客户端生成临时密钥并发送 ClientKeyExchange
client_key_exchange = client.generate_client_hello()
print(f"\nClientKeyExchange: {client_key_exchange.hex()[:32]}...")
# 服务端生成临时密钥并发送 ServerKeyExchange
server_key_exchange = server.generate_client_hello()
print(f"ServerKeyExchange: {server_key_exchange.hex()[:32]}...")
# 双方计算共享密钥
client_shared = client.compute_shared_secret(server_key_exchange)
server_shared = server.compute_shared_secret(client_key_exchange)
print(f"\n客户端共享密钥: {client_shared.hex()}")
print(f"服务端共享密钥: {server_shared.hex()}")
assert client_shared == server_shared, "共享密钥不匹配!"
print("✓ 共享密钥一致")
# 派生日志密钥
client_random = os.urandom(32)
server_random = os.urandom(32)
client_cw_key, client_sw_key = client.derive_session_keys(
client_shared, client_random, server_random
)
server_cw_key, server_sw_key = server.derive_session_keys(
server_shared, client_random, server_random
)
print(f"\n客户端写密钥: {client_cw_key[:16].hex()}...")
print(f"服务端写密钥: {server_sw_key[:16].hex()}...")
assert client_cw_key == server_cw_key, "客户端写密钥不匹配!"
assert client_sw_key == server_sw_key, "服务端写密钥不匹配!"
print("✓ 会话密钥派生成功")
print("\n" + "=" * 60)
print("密钥协商完成")
print("=" * 60)
if __name__ == "__main__":
demo_key_exchange()2.3 运行验证
BASH
python3 gm-t-0024-key-exchange.py预期输出:
CODE
============================================================
GM/T 0024-2023 SSL VPN 密钥协商演示
============================================================
客户端私钥: xxx...16字符...
客户端公钥: xxx...16字符...
服务端私钥: xxx...16字符...
服务端公钥: xxx...16字符...
ClientKeyExchange: xxx...32字符...
ServerKeyExchange: xxx...32字符...
客户端共享密钥: xxx...
服务端共享密钥: xxx...
✓ 共享密钥一致
客户端写密钥: xxx...16字符...
服务端写密钥: xxx...16字符...
✓ 会话密钥派生成功
============================================================
密钥协商完成
============================================================三、双证书部署方案
3.1 为什么需要双证书?
在国密 SSL VPN 改造初期,大多数组织采用双证书方案:
- RSA 证书:兼容国际客户端(iOS、macOS、旧版 Windows)
- SM2 证书:满足国密合规要求
3.2 双证书部署模式
CODE
模式一:单 IP 双证书
┌─────────────────────────────────────────┐
│ SSL VPN 网关 │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ RSA 证书 │ │ SM2 证书 │ │
│ │ (兼容层) │ │ (国密层) │ │
│ └─────────────┘ └─────────────┘ │
│ │ │ │
│ └──────────┬──────────┘ │
│ │ │
│ ┌───────┴───────┐ │
│ │ 统一入口 │ │
│ └───────────────┘ │
└─────────────────────────────────────────┘
模式二:双 IP 分流
┌─────────────────────────────────────────┐
│ SSL VPN 网关 │
│ │
│ RSA入口: 10.0.0.1:443 ──▶ RSA证书 │
│ SM2入口: 10.0.0.2:443 ──▶ SM2证书 │
│ │
│ 根据客户端能力自动路由 │
└─────────────────────────────────────────┘3.3 Python 生成双证书 CSR
PYTHON
"""
GM/T 0024-2023 双证书 CSR 生成示例
"""
import os
from gmssl.sm2 import default_ecc_table
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec, rsa
def generate_sm2_keypair() -> tuple:
"""
生成 SM2 密钥对
Returns:
(private_key_hex, public_key_hex)
"""
ecc_table = default_ecc_table
n = int(ecc_table['n'], 16)
# 生成随机私钥
while True:
rand_bytes = os.urandom(32)
d = int.from_bytes(rand_bytes, 'big') % (n - 1) + 1
if d > 0:
break
private_key = format(d, '064x')
# 计算公钥
p = int(ecc_table['p'], 16)
a = int(ecc_table['a'], 16)
gx = int(ecc_table['g'][:64], 16)
gy = int(ecc_table['g'][64:], 16)
def ecc_mul(d, px, py):
rx, ry = 0, 1
qx, qy = px, py
while d > 0:
if d & 1:
if rx == 0:
rx, ry = qx, qy
else:
lam = ((qy - ry) * pow((qx - rx) % p, p - 2, p)) % p
rx = (lam * lam - qx - rx) % p
ry = (lam * (qx - rx) - ry) % p
lam = ((3 * qx * qx + a) * pow(2 * qy % p, p - 2, p)) % p
nx = (lam * lam - 2 * qx) % p
ny = (lam * (qx - nx) - qy) % p
qx, qy = nx, ny
d >>= 1
return rx, ry
qx, qy = ecc_mul(d, gx, gy)
public_key = f"{qx:064x}{qy:064x}"
return private_key, public_key
def generate_rsa_csr(cn: str, org: str = None) -> str:
"""生成 RSA 证书签名请求(注意:RSA 密钥使用 2048 位,生产环境建议 3072 位以上)"""
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
subject = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "CN"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, org or "TestOrg"),
x509.NameAttribute(NameOID.COMMON_NAME, cn),
])
csr = x509.CertificateSigningRequestBuilder().subject_name(subject).sign(
key, hashes.SHA256()
)
return csr.public_bytes(serialization.Encoding.PEM).decode()
def main():
# 生成 SM2 密钥对
sm2_priv, sm2_pub = generate_sm2_keypair()
print(f"\n=== SM2 密钥对 ===")
print(f"私钥: {sm2_priv}")
print(f"公钥: {sm2_pub[:16]}...")
# 生成 RSA CSR
rsa_csr = generate_rsa_csr("vpn.example.com", "TestCorp")
print(f"\n=== RSA CSR ===")
print(rsa_csr)
# 验证 RSA CSR
from cryptography.x509 import load_pem_x509_csr
loaded_csr = load_pem_x509_csr(rsa_csr.encode())
print(f"\nCSR 主题: {loaded_csr.subject}")
print(f"CSR 有效期: 待定(需 CA 签发)")
if __name__ == "__main__":
main()四、生产环境部署检查清单
4.1 部署前检查
CODE
□ 证书类型验证
□ 服务器证书使用 SM2 算法
□ 证书 OID 包含 1.2.156.10197.1.301 (SM2)
□ 密钥用途包含 digitalSignature 和 keyEncipherment
□ 密码套件配置
□ 优先使用 TLS_SM4_GCM_SM3
□ 回退套件包含 TLS_SM4_CBC_SM3
□ 禁用所有非国密套件(AES、ChaCha20 等)
□ 协议版本
□ 启用 TLS 1.2 和 TLS 1.3
□ 禁用 SSLv3、TLS 1.0、TLS 1.1
□ 前向安全
□ 启用 ECDHE 或 SM2 密钥交换
□ 禁用静态 RSA 密钥交换4.2 合规检查项
根据 GM/T 0024-2023 和 GB/T 39786-2021,SSL VPN 系统应满足:
| 检查项 | 要求 | 验证方法 |
|---|---|---|
| 算法合规 | 使用 SM2/SM3/SM4 | 检查证书和协议协商 |
| 密钥长度 | SM2: 256 位 | 检查密钥参数 |
| 前向安全 | 必须支持 PFS | 检查密钥交换算法 |
| 证书有效期 | ≤ 1 年 | 检查证书 NotAfter |
| 吊销检查 | 必须支持 CRL/OCSP | 检查扩展字段 |
4.3 性能优化建议
PYTHON
"""
GM/T 0024-2023 SSL VPN 性能优化配置
"""
# 推荐配置
VPN_CONFIG = {
# 密码套件优先级(按性能排序)
"cipher_suites": [
"TLS_SM4_GCM_SM3", # GCM 模式,硬件加速友好
"TLS_SM4_CBC_SM3", # 兼容模式
],
# 会话复用
"session_tickets": True, # 减少握手延迟
"session_timeout": 86400, # 24 小时
# 超时设置
"handshake_timeout": 30, # 握手超时(秒)
"idle_timeout": 3600, # 空闲超时(秒)
# 连接池
"max_connections": 10000, # 最大并发连接
"connection_pool_size": 100, # 连接池大小
# 缓冲优化
"read_buffer_size": 65536, # 64KB 读缓冲
"write_buffer_size": 65536, # 64KB 写缓冲
}五、常见踩坑记录
坑 1:GM/T 0024-2023 与旧版不兼容
现象:使用 2014 版配置的 SSL VPN 设备无法与 2023 版客户端通信。
原因:
- 2023 版强制要求 TLS 1.3
- 旧版不支持 key_share 扩展
- 密码套件列表有差异
BASH
# 检查服务端支持的 TLS 版本
openssl s_client -connect vpn.example.com:443 -tls1_3
# 如果服务端仅支持 TLS 1.2,需升级固件或配置兼容模式
# 注意:兼容模式会牺牲安全性,仅用于过渡期坑 2:双证书路由失败
现象:配置双证书后,部分客户端仍连接到 SM2 入口。
原因:DNS 负载均衡无法区分客户端能力。
解决方案:
CODE
# 方案一:根据 User-Agent 分流
if client_supports_sm2(user_agent):
redirect to SM2 VIP
else:
redirect to RSA VIP
# 方案二:使用 SNI 区分
# 配置两个不同的域名指向不同 VIP
sm2.vpn.example.com → SM2 VIP
rsa.vpn.example.com → RSA VIP坑 3:会话超时导致用户体验差
现象:用户长时间不使用 VPN,重新激活时需要完整握手,延迟高。
解决方案:
PYTHON
# 启用会话 Ticket 复用
session_config = {
"session_cache": "shared:SSL_SESSIONS",
"session_timeout": 86400,
"session_ticket_keys": generate_ticket_keys(),
}
# 实现心跳保活
HEARTBEAT_INTERVAL = 30 # 秒坑 4:CRL 更新延迟
现象:证书吊销后,客户端仍认为证书有效。
原因:CRL 分发点配置错误或更新频率过低。
解决方案:
PYTHON
# 推荐配置
CRL_CONFIG = {
"distribution_points": [
"http://crl.example.com/vpn.crl",
"ldap://crl.example.com/CN=vpn,O=Example",
],
"update_interval": 3600, # 每小时更新
"cache_timeout": 7200, # 缓存 2 小时
}
# 考虑部署 OCSP 作为补充
OCSP_CONFIG = {
"responder_url": "http://ocsp.example.com",
"must_staple": True, # 强制 OCSP Stapling
}六、测试验证脚本
6.1 密码套件探测
PYTHON
"""
GM/T 0024-2023 SSL VPN 密码套件探测工具
"""
import ssl
import socket
from typing import List, Tuple, Optional
def probe_cipher_suites(host: str, port: int = 443) -> List[Tuple[str, bool, Optional[str]]]:
"""
探测目标 SSL VPN 支持的密码套件
Returns:
[(套件名, 是否支持, 详细信息), ...]
"""
# 国密推荐套件
recommended = [
"TLS_SM4_GCM_SM3",
"TLS_SM4_CBC_SM3",
"ECDHE-SM2-SM3-SM4-GCM",
"ECDHE-SM2-SM3-SM4-CBC",
]
# 国际套件(用于对比)
international = [
"TLS_AES_256_GCM_SHA384",
"TLS_CHACHA20_POLY1305_SHA256",
"TLS_AES_128_GCM_SHA256",
]
results = []
for suite in recommended + international:
try:
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context.set_ciphers(suite)
with socket.create_connection((host, port), timeout=5) as sock:
with context.wrap_socket(sock, server_hostname=host) as ssock:
cipher = ssock.cipher()
if cipher:
results.append((suite, True, cipher[0]))
else:
results.append((suite, False, None))
except Exception as e:
results.append((suite, False, str(e)))
return results
def check_compliance(host: str, port: int = 443) -> dict:
"""
检查 SSL VPN 是否符合 GM/T 0024-2023
Returns:
合规检查结果
"""
results = probe_cipher_suites(host, port)
compliance = {
"host": host,
"port": port,
"supports_sm4_gcm": False,
"supports_sm4_cbc": False,
"supports_tls13": False,
"supports_pfs": False,
"issues": []
}
for suite, supported, details in results:
if "SM4_GCM" in suite and supported:
compliance["supports_sm4_gcm"] = True
if "SM4_CBC" in suite and supported:
compliance["supports_sm4_cbc"] = True
if "SM3" in suite and supported:
compliance["supports_sm3"] = True
# 检查 TLS 1.3 支持
try:
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context.maximum_version = ssl.TLSVersion.TLSv1_3
context.minimum_version = ssl.TLSVersion.TLSv1_3
with socket.create_connection((host, port), timeout=5) as sock:
with context.wrap_socket(sock, server_hostname=host) as ssock:
compliance["supports_tls13"] = True
except:
compliance["issues"].append("TLS 1.3 not supported")
# 综合评估
if not compliance["supports_sm4_gcm"] and not compliance["supports_sm4_cbc"]:
compliance["issues"].append("No SM4 cipher suite supported")
if not compliance["supports_tls13"]:
compliance["issues"].append("TLS 1.3 not supported (GM/T 0024-2023 recommends TLS 1.3)")
return compliance
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("Usage: python3 gm-t-0024-compliance-check.py <host> [port]")
sys.exit(1)
host = sys.argv[1]
port = int(sys.argv[2]) if len(sys.argv) > 2 else 443
print(f"\n{'='*60}")
print(f"GM/T 0024-2023 SSL VPN 合规检查")
print(f"目标: {host}:{port}")
print(f"{'='*60}\n")
result = check_compliance(host, port)
print("密码套件支持:")
print(f" SM4-GCM-SM3: {'✓' if result['supports_sm4_gcm'] else '✗'}")
print(f" SM4-CBC-SM3: {'✓' if result['supports_sm4_cbc'] else '✗'}")
print(f" TLS 1.3: {'✓' if result['supports_tls13'] else '✗'}")
if result["issues"]:
print("\n合规问题:")
for issue in result["issues"]:
print(f" ⚠ {issue}")
else:
print("\n✓ 符合 GM/T 0024-2023 基本要求")
print(f"\n{'='*60}")七、总结
GM/T 0024-2023《SSL VPN 技术规范》为国密 VPN 改造提供了明确的技术框架。核心要点包括:
- 协议升级:全面适配 TLS 1.3,简化握手流程
- 算法强制:SM2/SM3/SM4 成为唯一可选算法组合
- 前向安全:强制要求 PFS,禁用静态密钥交换
- 双证书过渡:短期内可保留 RSA 兼容通道,逐步迁移到纯 SM2
| 阶段 | 行动 | 时间估算 |
|---|---|---|
| 准备 | 评估现有 SSL VPN 设备版本 | 1 周 |
| 试点 | 单节点试点 SM2 证书 | 2 周 |
| 推广 | 全网点逐步替换 | 4-8 周 |
| 优化 | 双证书分流策略调整 | 持续 |
参考来源
- GM/T 0024-2023《SSL VPN 技术规范》
- GM/T 0003.3-2012《SM2 椭圆曲线公钥密码算法 第 3 部分:密钥交换协议》
- GB/T 39786-2021《信息安全技术 信息系统密码应用基本要求》
- RFC 8998《ShangMi (SM) Cipher Suites for TLS 1.3》(IETF 信息性文档,2021 年 3 月发布)
- IANA TLS Parameters