为何Python加密实现与xmlsec1生成的XML签名值不一致?
问题:XML签名时Python实现与xmlsec1生成的SignatureValue不一致
我正在对接一个基于XML的API,使用xmlsec1工具可成功完成XML请求签名,命令如下:
xmlsec1 --sign --lax-key-search --privkey-pem test_privkey.pem,test_cert.pem --output xml/signed.xml xml/template.xml
对应的template.xml内容为:
<root><Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></CanonicalizationMethod><SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"></SignatureMethod><Reference URI=""><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"></Transform><Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></Transform></Transforms><DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"></DigestMethod><DigestValue></DigestValue></Reference></SignedInfo><SignatureValue/></Signature></root>
但改用Python代码实现签名时,生成的SignatureValue与xmlsec1版本不同(DigestValue一致),提交至API后验证失败。尝试使用signxml库也得到同样结果。调试用的XML_FILENAME内容为<root></root>,对应的Python代码如下:
# Load the private key with open(KEY_PATH, "rb") as key_file: private_key = load_pem_private_key(key_file.read(), None) # Load the XML file tree = etree.parse(f"xml/{XML_FILENAME}") root = tree.getroot() # Create a Signature element signature_element = etree.Element("Signature") signature_element.attrib["xmlns"] = "http://www.w3.org/2000/09/xmldsig#" # Create a SignedInfo element signed_info_element = etree.SubElement(signature_element, "SignedInfo") # Create a CanonicalizationMethod element canonicalization_method_element = etree.SubElement(signed_info_element, "CanonicalizationMethod") canonicalization_method_element.attrib["Algorithm"] = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315" # Create a SignatureMethod element signature_method_element = etree.SubElement(signed_info_element, "SignatureMethod") signature_method_element.attrib["Algorithm"] = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" # Create a Reference element reference_element = etree.SubElement(signed_info_element, "Reference") reference_element.attrib["URI"] = "" # Create Transforms transforms_element = etree.SubElement(reference_element, "Transforms") transform_element1 = etree.SubElement(transforms_element, "Transform") transform_element1.attrib["Algorithm"] = "http://www.w3.org/2000/09/xmldsig#enveloped-signature" transform_element2 = etree.SubElement(transforms_element, "Transform") transform_element2.attrib["Algorithm"] = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315" # Create DigestMethod digest_method_element = etree.SubElement(reference_element, "DigestMethod") digest_method_element.attrib["Algorithm"] = "http://www.w3.org/2001/04/xmlenc#sha256" # Compute the digest value for the entire XML document and add it to the DigestValue element digest = hashlib.sha256(etree.tostring(root, method="c14n")).digest() digest_value_element = etree.SubElement(reference_element, "DigestValue") digest_value_element.text = b64encode(digest).decode() # Canonicalize the SignedInfo XML c14n_signed_info = etree.tostring(signed_info_element, method="c14n") # Create a SHA256 digest of the SignedInfo digest = hashlib.sha256(c14n_signed_info).digest() # Sign the digest signature = private_key.sign( digest, padding.PKCS1v15(), hashes.SHA256() ) # Embed the SignatureValue in the Signature signature_value_element = etree.SubElement(signature_element, "SignatureValue") signature_value_element.text = b64encode(signature).decode() # Add the Signature element to the root of the document root.append(signature_element) # Save the signed XML tree = etree.ElementTree(root) with open('xml/signed.xml', 'wb') as f: tree.write(f)
原因分析与解决方法
核心原因:SignedInfo处理逻辑的两处关键差异
- 双重哈希错误:你先对规范化后的SignedInfo做SHA256哈希,再调用私钥签名时又指定了
hashes.SHA256()参数,这相当于执行了双重哈希。而xmlsec1会直接对规范化后的SignedInfo做一次SHA256哈希再签名,两者的签名输入完全不同,自然会得到不同的SignatureValue。 - 手动构建XML的细节差异:手动创建SignedInfo元素时,命名空间绑定顺序、空白符处理可能和xmlsec1的规范处理不一致,导致规范化后的SignedInfo字节流存在细微差别(虽然DigestValue一致,但SignedInfo的规范化结果是签名的核心输入)。
解决步骤
1. 修复双重哈希问题
修改签名逻辑,直接传入规范化后的SignedInfo字节流,不再提前手动哈希:
# 移除手动哈希步骤 # digest = hashlib.sha256(c14n_signed_info).digest() # 直接对规范化后的SignedInfo签名 signature = private_key.sign( c14n_signed_info, # 传入原始规范化字节流 padding.PKCS1v15(), hashes.SHA256() )
2. 对齐SignedInfo的规范化逻辑
手动构建XML容易出现细节偏差,建议使用signxml库并严格对齐xmlsec1的参数配置:
from signxml import XMLSigner from lxml import etree # 加载私钥和证书 with open("test_privkey.pem", "rb") as f: private_key = f.read() with open("test_cert.pem", "rb") as f: cert = f.read() # 加载原始XML模板 tree = etree.parse("xml/template.xml") root = tree.getroot() # 完全对齐xmlsec1的签名配置 signer = XMLSigner( method="enveloped", signature_algorithm="rsa-sha256", digest_algorithm="sha256", canonicalization_method="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" ) # 执行签名 signed_root = signer.sign(root, key=private_key, cert=cert) # 保存签名后的XML etree.ElementTree(signed_root).write( "xml/signed_python.xml", encoding="utf-8", xml_declaration=True, method="c14n" # 确保保存时使用规范格式 )
3. 验证规范化字节流(排查细节差异)
如果仍存在差异,可以分别提取xmlsec1和Python生成的SignedInfo部分,用以下方式对比:
- 对xmlsec1生成的SignedInfo执行:
xmlsec1 --c14n signed.xml | xxd - 对Python生成的SignedInfo执行:
etree.tostring(signed_info_element, method="c14n") | xxd
对比输出的十六进制内容,确认是否存在命名空间顺序、空白符等细微差异。
内容的提问来源于stack exchange,提问作者Bafsky
相关产品推荐
相关产品推荐

