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的完整结构:

SM2 CSR 结构解析 CSR (PKCS#10) CertificationRequestInfo(认证请求信息) SignatureAlgorithm(签名算法标识符) SignatureValue(签名值) CertificationRequestInfo 内部结构 版本号 (0) 主题 DN SM2 公钥 属性集 属性集包含:挑战密码(可选)、扩展请求(如SAN) SM2 公钥包含:曲线 OID + 未压缩公钥点(04 || X || Y)

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才发现问题。

验证分三步:

  1. 格式检查——PEM格式是否正确,Base64内容是否完整
  2. 签名验证——用CSR中的公钥验证签名,确认私钥持有者身份
  3. 内容检查——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后,立刻用同样的参数做一次验证。这样能第一时间发现参数配置问题,避免后续流程卡住。