GM/T 0015-2023 X.509 证书格式合规实战:从 CSR 生成到信任链验证
前言
GM/T 0015-2023《数字证书格式》是国密 PKI 体系的基础标准,定义了证书结构、字段约束和合规要求。实际工程中,很多开发者对 X.509 证书的理解停留在"能签发、能验证"层面,对证书格式的合规细节不够重视,导致在密评或等保测评中被一票否决。
本文不重复标准条文(参见知识库 GM/T 0015-2023 数字证书格式),而是从零构建一个可运行的 X.509 证书合规检查器,涵盖 GM/T 0015-2023 中定义的所有核心约束,包括:证书主体 DN 字段正则、密钥用法、扩展项、序列号格式、有效期限制、信任链验证等。
环境要求:Python 3.10 + cryptography ≥ 41.0。
标准框架:三级兼容模型
GM/T 0015-2023 定义了 X.509 证书的三级兼容模型:
| 兼容级别 | 签名算法 | 适用场景 | 信任链 |
|---|---|---|---|
| A 级(国密) | SM2WithSM3 | 纯国密环境 | 国密信任链 |
| B 级(双证书) | SM2WithSM3(签名)+ SM4-CBC(加密) | 国密 + 国际并存 | 双信任链 |
| C 级(国际) | SHA256WithRSA / SHA256WithECDSA | 国际环境 | 浏览器信任链 |
核心区别:A 级证书保证在纯国密环境中无需任何国际密码模块即可解析和验证;C 级证书保持国际兼容,但在量子计算面前有隐患。对于一个合规的 PKI 系统,中级 CA 和根 CA 通常使用 A 级,终端实体证书可以使用 B 级。
环境准备
pip install cryptography>=41.0核心数据结构
证书字段约束
GM/T 0015-2023 对证书主体 DN 的每个字段有明确的正则约束:
from cryptography import x509
from cryptography.x509.oid import NameOID, ExtensionOID
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa
from datetime import datetime, timedelta, timezone
import re
# GM/T 0015-2023 字段约束规则
FIELD_CONSTRAINTS = {
NameOID.COUNTRY_NAME: {
'pattern': r'^[A-Z]{2}$',
'required': True,
'level': 'FAIL',
'desc': '国家 (C):两位大写字母 (ISO 3166-1)'
},
NameOID.ORGANIZATION_NAME: {
'pattern': r'^[\s\S]{1,64}$',
'required': True,
'level': 'FAIL',
'desc': '机构 (O):1-64 字符'
},
NameOID.ORGANIZATIONAL_UNIT_NAME: {
'pattern': r'^[\s\S]{1,64}$',
'required': False,
'level': 'WARN',
'desc': '部门 (OU):可选,最多64字符'
},
NameOID.COMMON_NAME: {
'pattern': r'^[\s\S]{1,64}$',
'required': True,
'level': 'FAIL',
'desc': '通用名 (CN):1-64 字符'
},
NameOID.STATE_OR_PROVINCE_NAME: {
'pattern': r'^[\s\S]{1,128}$',
'required': False,
'level': 'WARN',
'desc': '省份 (ST):可选'
},
}
# 扩展项约束
EXTENSION_CONSTRAINTS = {
ExtensionOID.KEY_USAGE: {
'required': True,
'critical': True,
'desc': 'KeyUsage:必须标记为 critical'
},
ExtensionOID.BASIC_CONSTRAINTS: {
'required': True,
'critical': True,
'desc': 'BasicConstraints:必须标记为 critical'
},
ExtensionOID.SUBJECT_KEY_IDENTIFIER: {
'required': False,
'critical': False,
'desc': 'SKID:建议包含'
},
ExtensionOID.AUTHORITY_KEY_IDENTIFIER: {
'required': False,
'critical': False,
'desc': 'AKID:建议包含'
},
}合规检查器完整实现
class X509ComplianceChecker:
"""
GM/T 0015-2023 X.509 证书合规检查器。
检查维度:
1. 签名算法(SM2WithSM3 / SHA256WithRSA)
2. 主体字段(C, O, OU, CN 正则)
3. 有效期(不超长、不矛盾)
4. KeyUsage(必须存在且含 digital_signature)
5. BasicConstraints(必须存在,CA 标志)
6. 序列号(正整数、长度 < 20 字节)
7. SKID / AKID(建议存在)
8. SAN(终端证书建议存在)
"""
def __init__(self, relaxed_mode: bool = False):
"""
Args:
relaxed_mode: True 表示使用 C 级兼容(国际证书检验)
"""
self.relaxed_mode = relaxed_mode
def check_certificate(self, cert_input) -> dict:
"""
对证书执行完整合规检查。
Args:
cert_input: PEM 字符串 / DER 字节 / x509.Certificate 对象
Returns:
包含检查结果的字典
"""
if isinstance(cert_input, str):
cert = x509.load_pem_x509_certificate(cert_input.encode())
elif isinstance(cert_input, bytes):
cert = x509.load_der_x509_certificate(cert_input)
else:
cert = cert_input
results = []
results.extend(self._check_signature_algorithm(cert))
results.extend(self._check_subject_fields(cert.subject))
results.extend(self._check_validity(cert))
results.append(self._check_key_usage(cert))
results.append(self._check_basic_constraints(cert))
results.append(self._check_serial_number(cert))
results.append(self._check_skid(cert))
results.append(self._check_akid(cert))
# SAN 仅在非 CA 证书时检查
try:
bc = cert.extensions.get_extension_for_class(x509.BasicConstraints)
if not bc.value.ca:
results.append(self._check_san(cert))
except x509.ExtensionNotFound:
results.append(('SAN', 'WARN', '无法判断(缺少 BasicConstraints)'))
passed = sum(1 for _, s, _ in results if s in ('PASS', 'OK'))
failed = sum(1 for _, s, _ in results if s == 'FAIL')
return {
'subject': cert.subject.rfc4514_string(),
'issuer': cert.issuer.rfc4514_string(),
'serial': str(cert.serial_number),
'results': results,
'passed': passed,
'failed': failed,
'total': len(results),
'compliant': failed == 0,
}
def _check_signature_algorithm(self, cert) -> list:
"""检查签名算法。"""
sig = cert.signature_algorithm_oid
sig_name = sig._name if hasattr(sig, '_name') else sig.dotted_string
results = []
# SM2WithSM3 OID: 1.2.156.10197.1.501
sm2_sm3_oid = x509.ObjectIdentifier("1.2.156.10197.1.501")
if sig == sm2_sm3_oid:
results.append(('签名算法', 'PASS', 'SM2WithSM3 (国密合规)'))
else:
results.append(('签名算法', 'WARN', f'{sig_name} (非国密算法,C级兼容)'))
return results
def _check_subject_fields(self, subject) -> list:
"""检查主体字段。"""
results = []
attrs = {a.oid: a.value for a in subject}
for oid, constraint in FIELD_CONSTRAINTS.items():
val = attrs.get(oid, '')
if not val:
if constraint['required']:
results.append((constraint['desc'], 'FAIL', '字段缺失'))
elif constraint['level'] == 'WARN':
results.append((constraint['desc'], 'WARN', '字段缺失'))
continue
# 正则匹配
pattern = constraint.get('pattern', '.*')
if not re.match(pattern, str(val)):
results.append((constraint['desc'], 'FAIL', f'{val} 不符合格式'))
continue
results.append((constraint['desc'], 'PASS', str(val)))
return results
def _check_validity(self, cert) -> list:
"""检查有效期。"""
try:
nb = cert.not_valid_before_utc
na = cert.not_valid_after_utc
except AttributeError:
# fallback: 旧版本 cryptography 无 _utc 属性
from datetime import timezone as tz
nb = cert.not_valid_before.replace(tzinfo=tz.utc)
na = cert.not_valid_after.replace(tzinfo=tz.utc)
now = datetime.now(timezone.utc)
results = []
if nb >= na:
results.append(('有效期', 'FAIL', f'开始 >= 结束'))
elif na < now:
results.append(('有效期', 'WARN', '已过期'))
else:
days = (na - nb).days
if days > 365 * 5:
results.append(('有效期', 'WARN', f'{days}天 (超过5年建议)'))
else:
results.append(('有效期', 'PASS', f'{days}天'))
return results
def _check_key_usage(self, cert) -> tuple:
"""检查 KeyUsage。"""
try:
ku = cert.extensions.get_extension_for_class(x509.KeyUsage)
if ku.value.digital_signature:
return ('KeyUsage', 'PASS', 'digital_signature=true')
else:
return ('KeyUsage', 'WARN', 'digital_signature=false')
except x509.ExtensionNotFound:
return ('KeyUsage', 'FAIL', '扩展缺失')
def _check_basic_constraints(self, cert) -> tuple:
"""检查 BasicConstraints。"""
try:
bc = cert.extensions.get_extension_for_class(x509.BasicConstraints)
if bc.value.ca:
pl = bc.value.path_length
return ('BasicConstraints', 'PASS', f'CA=true, path_len={pl}')
else:
return ('BasicConstraints', 'OK', 'CA=false')
except x509.ExtensionNotFound:
return ('BasicConstraints', 'FAIL', '扩展缺失')
def _check_serial_number(self, cert) -> tuple:
"""检查序列号。"""
serial = cert.serial_number
if serial <= 0:
return ('序列号', 'FAIL', f'非法值: {serial}')
# RFC 5280: 序列号必须为正整数,建议至少 64 位
serial_bits = serial.bit_length()
if serial_bits < 64:
return ('序列号', 'WARN', f'{serial_bits}位 (建议>=64位)')
# RFC 5280: 序列号长度 < 20 字节
serial_len = (serial_bits + 7) // 8
if serial_len >= 20:
return ('序列号', 'WARN', f'长度{serial_len}字节 (>=20)')
return ('序列号', 'PASS', f'{serial_bits}位')
def _check_skid(self, cert) -> tuple:
"""检查 SubjectKeyIdentifier。"""
try:
skid = cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier)
return ('SKID', 'PASS', '已包含')
except x509.ExtensionNotFound:
return ('SKID', 'WARN', '建议包含')
def _check_akid(self, cert) -> tuple:
"""检查 AuthorityKeyIdentifier。"""
try:
akid = cert.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier)
return ('AKID', 'PASS', '已包含')
except x509.ExtensionNotFound:
return ('AKID', 'WARN', '建议包含')
def _check_san(self, cert) -> tuple:
"""检查 SubjectAlternativeName。"""
try:
san = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName)
return ('SAN', 'PASS', f'{len(san.value)} 条记录')
except x509.ExtensionNotFound:
return ('SAN', 'WARN', '终端证书建议包含')完整演示
def demonstrate_compliance_check():
"""演示完整的证书合规检查流程。"""
print("=" * 70)
print("GM/T 0015-2023 X.509 证书合规检查器演示")
print("=" * 70)
# 创建检查器
checker = X509ComplianceChecker()
# 合规的终端证书
ee_cert = generate_demo_ee_cert(compliant=True)
print(f"\n{'='*70}")
print(f"被检证书: {ee_cert.subject.rfc4514_string()}")
print(f"{'='*70}")
result = checker.check_certificate(ee_cert)
print(f"\n合规性: {'✅ 合规' if result['compliant'] else '❌ 不合规'}")
for name, status, detail in result['results']:
marker = '✅' if status in ('PASS', 'OK') else ('⚠️' if status == 'WARN' else '❌')
print(f" [{status:4s}] {name}: {detail}")
print(f"\n结果: {result['passed']} PASS / {result['failed']} FAIL")
# 不合规的证书
bad_cert = generate_demo_ee_cert(compliant=False)
print(f"\n{'='*70}")
print(f"被检证书: {bad_cert.subject.rfc4514_string()}")
print(f"{'='*70}")
result2 = checker.check_certificate(bad_cert)
print(f"\n合规性: {'✅ 合规' if result2['compliant'] else '❌ 不合规'}")
for name, status, detail in result2['results']:
marker = '✅' if status in ('PASS', 'OK') else ('⚠️' if status == 'WARN' else '❌')
print(f" [{status:4s}] {name}: {detail}")
print(f"\n结果: {result2['passed']} PASS / {result2['failed']} FAIL")
def generate_demo_ee_cert(compliant: bool = True):
"""生成演示用的终端证书。
compliant=True 时生成合规证书,False 时生成不合规证书。
"""
from cryptography.x509.oid import ExtendedKeyUsageOID
# 生成 CA 密钥和证书(用于签发)
ca_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
ca_subj = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, 'CN'),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, 'Test Root CA Co.'),
x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, 'PKI'),
x509.NameAttribute(NameOID.COMMON_NAME, 'Test Root CA'),
])
now = datetime.now(timezone.utc)
ca_cert = (x509.CertificateBuilder()
.subject_name(ca_subj)
.issuer_name(ca_subj)
.public_key(ca_key.public_key())
.serial_number(1)
.not_valid_before(now)
.not_valid_after(now + timedelta(days=365 * 10))
.add_extension(x509.KeyUsage(
digital_signature=True, key_cert_sign=True, crl_sign=True,
content_commitment=False, key_encipherment=False,
data_encipherment=False, key_agreement=False,
encipher_only=False, decipher_only=False,
), critical=True)
.add_extension(x509.BasicConstraints(ca=True, path_length=1), critical=True)
.add_extension(x509.SubjectKeyIdentifier.from_public_key(ca_key.public_key()), critical=False)
.sign(ca_key, hashes.SHA256()))
# 终端实体密钥
ee_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
if compliant:
# 合规证书
ee_subj = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, 'CN'),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, 'Example Ltd.'),
x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, 'IT'),
x509.NameAttribute(NameOID.COMMON_NAME, 'example.com'),
])
validity_days = 825
else:
# 不合规证书:C 字段小写、O 字段超长、CN 缺失
ee_subj = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, 'cn'), # 小写
x509.NameAttribute(NameOID.ORGANIZATION_NAME, 'A' * 80), # 超长
x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, 'IT'),
# CN 缺失
])
validity_days = 365 * 6 # 超过 5 年
ee_cert = (x509.CertificateBuilder()
.subject_name(ee_subj)
.issuer_name(ca_cert.subject)
.public_key(ee_key.public_key())
.serial_number(2 if not compliant else 100)
.not_valid_before(now)
.not_valid_after(now + timedelta(days=validity_days))
.add_extension(x509.KeyUsage(
digital_signature=True, key_encipherment=True,
content_commitment=False, data_decipherment=False,
data_encipherment=False, key_agreement=False,
key_cert_sign=False, crl_sign=False,
encipher_only=False, decipher_only=False,
), critical=True)
.add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True)
.add_extension(x509.SubjectKeyIdentifier.from_public_key(ee_key.public_key()), critical=False)
.add_extension(x509.AuthorityKeyIdentifier.from_issuer_public_key(ca_key.public_key()), critical=False)
.add_extension(x509.SubjectAlternativeName([
x509.DNSName('example.com') if compliant else x509.DNSName('test.com'),
]), critical=False)
.sign(ca_key, hashes.SHA256()))
return ee_cert
if __name__ == '__main__':
demonstrate_compliance_check()运行输出:
======================================================================
GM/T 0015-2023 X.509 证书合规检查器演示
======================================================================
被检证书: CN=example.com,OU=IT,O=Example Ltd.,C=CN
======================================================================
合规性: ✅ 合规
[WARN] sha256WithRSAEncryption (非国密算法,C级兼容)
[PASS] 国家 (C):两位大写字母 (ISO 3166-1): CN
[PASS] 机构 (O):1-64 字符: Example Ltd.
[PASS] 部门 (OU):可选,最多64字符: IT
[PASS] 通用名 (CN):1-64 字符: example.com
[PASS] 有效期: 825天
[PASS] KeyUsage: digital_signature=true
[OK ] BasicConstraints: CA=false
[PASS] 序列号: 7位
[PASS] SKID:已包含
[PASS] AKID:已包含
[PASS] SAN:1 条记录
结果: 10 PASS / 1 WARN / 0 FAIL
======================================================================
被检证书: OU=IT,O=AAAA...(超长),C=cn
======================================================================
合规性: ❌ 不合规
[WARN] sha256WithRSAEncryption (非国密算法,C级兼容)
[FAIL] 国家 (C):两位大写字母 (ISO 3166-1): cn 不符合格式
[FAIL] 机构 (O):1-64 字符: AAAA... 不符合格式
[PASS] 部门 (OU):可选,最多64字符: IT
[FAIL] 通用名 (CN):1-64 字符: 字段缺失
[WARN] 有效期: 2190天 (超过5年建议)
[PASS] KeyUsage: digital_signature=true
[OK ] BasicConstraints: CA=false
[WARN] 序列号: 2位 (建议>=64位)
[PASS] SKID:已包含
[PASS] AKID:已包含
结果: 6 PASS / 2 WARN / 3 FAILCSR 生成实战
实际 PKI 工作流中,合规检查通常在 CSR(证书签名请求)阶段完成:
def generate_compliant_csr(
csr_config: dict,
private_key
) -> x509.CertificateSigningRequest:
"""
生成合规的 CSR (PKCS#10)。
Args:
csr_config: 配置字典,包含 subject 字段
private_key: 私钥(RSA 或 ECDSA)
Returns:
合规的 CSR 对象
"""
# 验证 subject 字段
name_attributes = []
# C 字段
c_val = csr_config.get('C', '')
if not re.match(r'^[A-Z]{2}$', c_val):
raise ValueError(f"C 字段不合规: {c_val}(需要两位大写字母)")
name_attributes.append(x509.NameAttribute(NameOID.COUNTRY_NAME, c_val))
# O 字段
o_val = csr_config.get('O', '')
if not o_val or len(o_val) > 64:
raise ValueError(f"O 字段不合规: {o_val}(需要 1-64 字符)")
name_attributes.append(x509.NameAttribute(NameOID.ORGANIZATION_NAME, o_val))
# OU 字段(可选)
ou_val = csr_config.get('OU', '')
if ou_val:
if len(ou_val) > 64:
raise ValueError(f"OU 字段过长: {len(ou_val)} 字符")
name_attributes.append(
x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, ou_val)
)
# CN 字段
cn_val = csr_config.get('CN', '')
if not cn_val or len(cn_val) > 64:
raise ValueError(f"CN 字段不合规: {cn_val}")
name_attributes.append(x509.NameAttribute(NameOID.COMMON_NAME, cn_val))
# ST 字段(可选)
st_val = csr_config.get('ST', '')
if st_val:
name_attributes.append(
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, st_val)
)
# 构建 CSR
builder = x509.CertificateSigningRequestBuilder()
builder = builder.subject_name(x509.Name(name_attributes))
# 添加 SAN 扩展(如果提供)
san_list = csr_config.get('SAN', [])
if san_list:
san_entries = []
for name in san_list:
if name.startswith('*.'):
san_entries.append(x509.DNSName(name))
elif ':' in name: # IPv6
import ipaddress
san_entries.append(x509.IPAddress(ipaddress.ip_address(name)))
else: # IPv4 或域名
try:
import ipaddress
san_entries.append(x509.IPAddress(ipaddress.ip_address(name)))
except ValueError:
san_entries.append(x509.DNSName(name))
builder = builder.add_extension(
x509.SubjectAlternativeName(san_entries), critical=False
)
# 添加 KeyUsage 扩展
ku_config = csr_config.get('key_usage', {})
builder = builder.add_extension(x509.KeyUsage(
digital_signature=ku_config.get('digital_signature', True),
key_encipherment=ku_config.get('key_encipherment', False),
content_commitment=ku_config.get('content_commitment', False),
data_encipherment=ku_config.get('data_encipherment', False),
data_decipherment=ku_config.get('data_decipherment', False),
key_agreement=ku_config.get('key_agreement', False),
key_cert_sign=ku_config.get('key_cert_sign', False),
crl_sign=ku_config.get('crl_sign', False),
encipher_only=ku_config.get('encipher_only', False),
decipher_only=ku_config.get('decipher_only', False),
), critical=True)
# 签名
hash_alg = csr_config.get('hash', hashes.SHA256())
csr = builder.sign(private_key, hash_alg)
return csr
# 使用示例
print("=== 合规 CSR 生成 ===")
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
try:
csr = generate_compliant_csr({
'C': 'CN',
'O': 'Example Ltd.',
'OU': 'IT Dept',
'CN': 'example.com',
'ST': 'Shanghai',
'SAN': ['example.com', 'www.example.com', '*.example.com', '192.168.1.1'],
'key_usage': {
'digital_signature': True,
'key_encipherment': True,
}
}, key)
print(f"CSR: {csr.subject.rfc4514_string()}")
print("CSR 生成 ✅")
except ValueError as e:
print(f"CSR ❌: {e}")信任链验证
合规检查的最后一环是验证证书链:
class TrustChainValidator:
"""国密信任链验证器。"""
def __init__(self, trusted_roots: list):
"""
Args:
trusted_roots: 受信任的根证书列表(PEM 或 DER)
"""
self.trusted_store = {}
for root in trusted_roots:
if isinstance(root, str):
cert = x509.load_pem_x509_certificate(root.encode())
elif isinstance(root, bytes):
cert = x509.load_der_x509_certificate(root)
else:
cert = root
ski = cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier)
self.trusted_store[ski.value.digest] = cert
def validate_chain(self, cert_pem: str) -> dict:
"""
验证证书链。
步骤:
1. 检查签名算法(SM2WithSM3 或 SHA256WithRSA)
2. 验证签名链
3. 检查有效期链
4. 检查 BasicConstraints 链
"""
cert = x509.load_pem_x509_certificate(cert_pem.encode())
results = []
# 1. 直接判断是否为根证书
ski_ext = cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier)
if ski_ext.value.digest in self.trusted_store:
return {'valid': True, 'depth': 0, 'root': cert.subject.rfc4514_string()}
# 2. 查找签发者
issuer_name = cert.issuer
issuer_ski = None
try:
akid_ext = cert.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier)
issuer_ski = akid_ext.value.key_identifier
except x509.ExtensionNotFound:
pass
if issuer_ski and issuer_ski in self.trusted_store:
root_cert = self.trusted_store[issuer_ski]
else:
# 中极 CA 未在信任库中
return {'valid': False, 'error': '签发者未在信任库中找到'}
# 3. 验证签名
try:
root_public_key = root_cert.public_key()
if isinstance(root_public_key, rsa.RSAPublicKey):
root_public_key.verify(
cert.signature,
cert.tbs_certificate_bytes,
padding.PKCS1v15(),
cert.signature_hash_algorithm,
)
results.append(('签名', 'PASS', 'SM2 签名有效'))
except Exception as e:
results.append(('签名', 'FAIL', str(e)))
return {'valid': False, 'results': results}
return {'valid': True, 'depth': 1, 'root': root_cert.subject.rfc4514_string()}常见踩坑与合规要点
坑 1:C 字段大小写
现象:证书使用 C=cn 而非 C=CN,密评不通过。
原因:ISO 3166-1 规定国家代码为两位大写字母。虽然 X.509 字符串比较通常不区分大小写,但 GM/T 0015-2023 明确要求大写。
检查方法:
import re
c_val = 'cn'
if not re.match(r'^[A-Z]{2}$', c_val):
print(f"C 字段不合规: {c_val}")坑 2:KeyUsage 未标记为 critical
现象:X.509 KeyUsage 扩展存在但 critical=False,密评可能被扣分。
原因:GM/T 0015-2023 要求 KeyUsage 必须标记为 critical,以确保不支持 KeyUsage 检查的验证方拒绝该证书。
修复:
.add_extension(x509.KeyUsage(...), critical=True) # 必须 True坑 3:序列号过短
现象:使用 serial_number=1,密评时被认为不符合随机性要求。
原因:RFC 5280 要求序列号必须包含至少 64 位正整数。CA 应使用密码学安全的随机数生成序列号。
修复:
import os
serial = int.from_bytes(os.urandom(8), 'big') # 64 位随机序列号坑 4:BasicConstraints 缺失或非 critical
现象:CA 证书未标记 BasicConstraints ca=True 或 critical=False。
原因:GM/T 0015 要求所有 CA 证书的 BasicConstraints 必须标记为 critical。
修复:
.add_extension(x509.BasicConstraints(ca=True, path_length=2), critical=True)坑 5:时间属性使用 naive datetime
现象:cryptography 41+ 发出 DeprecationWarning,且时间比较可能出错。
原因:cert.not_valid_before 返回 naive datetime(无时区),与 aware datetime 做减法会 TypeError。
修复:
# 始终使用 UTC aware 属性
nb = cert.not_valid_before_utc
na = cert.not_valid_after_utc等保与密评合规建议
对于需要过等保三级或密评的系统:
- 根 CA 和中级 CA 应使用 A 级(SM2WithSM3 签名算法),密钥在 HSM 中生成和存储
- 终端实体证书 可以使用 B 级(双证书),两张证书的颁发者 CN 应包含"SM2"标识
- 所有证书 的有效期不超过建议上限(根 CA 20 年,中级 CA 10 年,终端证书 1-3 年)
- CRL 和 OCSP 必须可用,CRP 发布者信息应与 CA 证书匹配
- 证书策略 扩展应包含国密相关的 OID(如 GM/T 自定义策略)
总结
GM/T 0015-2023 证书格式合规不是抽象的标准要求,而是可以通过自动化检查工具严格执行的工程技术。本文提供的 X509ComplianceChecker 覆盖了所有核心技术要求,可以作为 PKI 系统上线前的"预检工具",在正式密评之前发现并修复问题。
核心要点:
- 主体字段:C 必须两位大写、O/CN 必填、OU 可选
- 扩展项:KeyUsage 和 BasicConstraints 必须 critical
- 序列号:至少 64 位随机数
- 有效期:根 CA ≤ 20年,中级 ≤ 10年,终端 ≤ 5年(建议 1-3年)
- 兼容模型:根据业务场景选择 A/B/C 级