4. SM2证书请求(CSR):结构、生成与验证
证书请求,也就是CSR,是申请证书的“敲门砖”。你向CA提交CSR,CA审核后签发证书。SM2算法的CSR和RSA的CSR在格式上基本一致,但核心区别在于——签名算法用的是SM2-with-SM3。
我个人习惯把CSR理解成“自荐信”。你把自己的公钥、身份信息写进去,再用自己的私钥签个名,证明这封信确实是你写的。CA收到后,先验签名,再决定是否发证。
4.1 CSR结构解析
CSR的标准格式定义在PKCS#10(RFC 2986)里。说白了,它就是一个ASN.1结构,用DER编码,再转成Base64的PEM格式。
核心结构就三部分:
- 认证请求信息(CertificationRequestInfo)——包含版本号、主题DN、公钥、属性
- 签名算法标识符——SM2算法用的是1.2.156.10197.1.501
- 签名值——用请求者的私钥对第一部分做SM2签名
我刚开始接触国密时,总以为CSR里会包含证书有效期、扩展项这些信息。其实不是,CSR只负责提交公钥和身份,有效期和扩展是CA签发时加进去的。
关键点:CSR中的公钥必须是SM2椭圆曲线上的点,曲线参数固定为sm2p256v1(OID:1.2.156.10197.1.301)。
下面这张图帮你理清CSR的完整结构:
4.2 SM2算法CSR生成
生成SM2 CSR,你需要先有一对SM2密钥。然后按照PKCS#10规范组装请求信息,最后用SM2签名。
我建议用OpenSSL的国密分支来做,或者用GmSSL。这两个工具我都用过,GmSSL对国密支持更完整,但OpenSSL 1.1.1+加上国密补丁也能用。
4.2.1 使用GmSSL生成SM2 CSR
这是最直接的方式。GmSSL原生支持SM2算法,不需要额外配置。
# 1. 生成SM2私钥
gmssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:sm2p256v1 \
-out sm2_private.pem
# 2. 从私钥提取公钥(可选)
gmssl pkey -in sm2_private.pem -pubout -out sm2_public.pem
# 3. 生成CSR(交互式输入DN信息)
gmssl req -new -key sm2_private.pem -out sm2.csr \
-sm3 -sigopt sm2_id:"1234567812345678"
# 4. 或者使用-subj参数非交互式生成
gmssl req -new -key sm2_private.pem -out sm2.csr \
-subj "/C=CN/ST=Beijing/L=Beijing/O=MyCompany/OU=IT/CN=myserver.example.com" \
-sm3 -sigopt sm2_id:"1234567812345678"
提示:SM2签名时需要指定签名者ID(sm2_id)。如果不指定,GmSSL会使用默认值"1234567812345678"。我个人建议显式指定,避免不同工具间默认值不一致导致验签失败。
4.2.2 使用OpenSSL(国密补丁版)
如果你用的是OpenSSL 3.0+,可以通过加载provider来支持SM2。我遇到过有些发行版默认没开SM2支持,需要手动编译。
# 生成SM2私钥
openssl ecparam -genkey -name SM2 -out sm2_private.pem
# 生成CSR
openssl req -new -key sm2_private.pem -out sm2.csr \
-subj "/C=CN/O=MyCompany/CN=test.example.com" \
-sm3
4.2.3 使用Java代码生成
Java从JDK 18开始原生支持SM2。如果你还在用JDK 11或17,需要引入Bouncy Castle的国密扩展包。
// 使用Bouncy Castle生成SM2 CSR
Security.addProvider(new BouncyCastleProvider());
// 1. 生成SM2密钥对
KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC", "BC");
ECGenParameterSpec ecSpec = new ECGenParameterSpec("sm2p256v1");
kpg.initialize(ecSpec, new SecureRandom());
KeyPair kp = kpg.generateKeyPair();
// 2. 构建主题DN
X500Principal subject = new X500Principal(
"CN=test.example.com, O=MyCompany, C=CN");
// 3. 构建CSR
PKCS10CertificationRequestBuilder csrBuilder =
new JcaPKCS10CertificationRequestBuilder(subject, kp.getPublic());
// 4. 使用SM2withSM3签名
ContentSigner signer = new JcaContentSignerBuilder("SM3withSM2")
.setProvider("BC")
.build(kp.getPrivate());
PKCS10CertificationRequest csr = csrBuilder.build(signer);
// 5. 输出PEM格式
PEMWriter pw = new PEMWriter(new OutputStreamWriter(System.out));
pw.writeObject(csr);
pw.flush();
4.3 CSR属性配置
CSR的属性(Attributes)是容易被忽略但很重要的部分。它位于CertificationRequestInfo的末尾,是一个可选的SET结构。
常见的属性包括:
| 属性OID | 名称 | 说明 | 是否必选 |
|---|---|---|---|
| 1.2.840.113549.1.9.7 | 挑战密码 | 用于证书吊销时的身份验证 | 可选 |
| 1.2.840.113549.1.9.14 | 扩展请求 | 包含证书扩展项,如SAN、密钥用法 | 推荐 |
| 1.2.156.10197.1.501 | SM2签名者ID | 国密特有的签名者标识 | 国密必选 |
我在项目中遇到过一个问题:CSR里没带SAN(Subject Alternative Name)扩展,结果签出来的证书浏览器不认,因为Chrome要求证书必须包含SAN。从那以后,我生成CSR时都会主动加上SAN。
注意:SM2算法的CSR中,签名者ID属性必须正确设置。CA在验签时会使用这个ID,如果ID不匹配,验签会失败。我曾经因为ID默认值和CA配置不一致,排查了半天才发现问题。
4.3.1 配置SAN扩展
用GmSSL添加SAN扩展:
# 创建扩展配置文件
cat > san.cnf << EOF
[ req ]
distinguished_name = req_distinguished_name
req_extensions = v3_req
[ req_distinguished_name ]
CN = test.example.com
O = MyCompany
C = CN
[ v3_req ]
subjectAltName = @alt_names
[ alt_names ]
DNS.1 = test.example.com
DNS.2 = www.test.example.com
IP.1 = 192.168.1.100
EOF
# 生成带SAN的CSR
gmssl req -new -key sm2_private.pem -out sm2_san.csr \
-config san.cnf -sm3 -sigopt sm2_id:"1234567812345678"
4.4 CSR验证
生成CSR后,第一件事就是验证它。别等到提交给CA才发现问题。
验证分三步:
- 格式检查——PEM格式是否正确,Base64内容是否完整
- 签名验证——用CSR中的公钥验证签名,确认私钥持有者身份
- 内容检查——DN信息、公钥曲线、扩展项是否符合预期
4.4.1 使用GmSSL验证CSR
# 查看CSR内容(文本形式)
gmssl req -in sm2.csr -text -noout
# 验证CSR签名
gmssl req -in sm2.csr -verify -sm3
# 如果验证通过,会输出:verify OK
# 如果失败,会提示:verify failure
4.4.2 验证输出解读
执行gmssl req -in sm2.csr -text -noout后,你会看到类似下面的输出:
Certificate Request:
Data:
Version: 0 (0x0)
Subject: C=CN, O=MyCompany, CN=test.example.com
Subject Public Key Info:
Public Key Algorithm: EC
EC Public Key:
pub:
04:12:34:...(公钥点坐标)
ASN1 OID: sm2p256v1
Attributes:
...
Signature Algorithm: SM3withSM2
Signature Value:
30:45:02:20:...(签名值)
重点关注三点:
- 公钥算法是否为EC,曲线OID是否为sm2p256v1
- 签名算法是否为SM3withSM2
- 签名验证结果是否为OK
经验之谈:我建议把CSR验证写成自动化脚本。每次生成CSR后自动跑一遍验证,包括签名验证和内容校验。我在一个项目中就是因为手动验证漏掉了公钥曲线检查,导致签出来的证书无法用于SM2密钥交换,最后全部重签,教训深刻。
4.4.3 常见验证失败原因
| 错误现象 | 可能原因 | 解决方法 |
|---|---|---|
| verify failure | 签名者ID不匹配 | 检查生成CSR时指定的sm2_id与验证时是否一致 |
| unable to load CSR | PEM格式错误 | 检查-----BEGIN CERTIFICATE REQUEST-----头尾是否正确 |
| bad curve | 公钥曲线不是sm2p256v1 | 确认生成密钥时使用了正确的曲线参数 |
| DN信息缺失 | CN或O字段为空 | CA通常要求至少包含CN,建议同时填写O和C |
嗯,这里要注意:SM2的CSR验证和RSA有个重要区别——RSA验签只需要公钥,而SM2验签还需要签名者ID。如果你用OpenSSL的通用命令去验SM2的CSR,很可能因为ID问题验不过。
我个人习惯在生成CSR后,立刻用同样的参数做一次验证。这样能第一时间发现参数配置问题,避免后续流程卡住。