如何在Python中使用Windows证书存储区的私钥签名XML文档?
问题:从Windows证书存储区获取私钥并签名XML文档时触发Access Violation错误
环境信息
- Python版本:3.11.5
- 平台:Windows 10
- OpenSSL版本:24.1.0
- Cryptography版本:42.0.7
- pycryptodome版本:3.18.0
- pywin32版本:306
- signxml版本:3.2.2
- wincertstore版本:0.2
问题描述
尝试使用ctypes和win32crypt访问Windows证书存储区(当前用户->个人->证书)中的私钥,对XML文档签名时,在CryptAcquireCertificatePrivateKey调用处持续触发access violation错误。
尝试的代码
import os import tkinter as tk from tkinter import messagebox, Listbox, Scrollbar, END, Frame, Label, Button import wincertstore import binascii import logging from lxml import etree from signxml import XMLSigner from Crypto.PublicKey import RSA from Crypto.Signature import pkcs1_15 from Crypto.Hash import SHA256 from OpenSSL import crypto import win32crypt logging.basicConfig(filename="error_log.txt", level=logging.ERROR) def try_unhexlify(data): try: return binascii.unhexlify(data) except binascii.Error as e: logging.error(f"Error converting thumbprint: {data}") logging.error(e) raise e def get_cert_thumbprint(cert): try: cert_bytes = cert.get_encoded() cert_obj = crypto.load_certificate(crypto.FILETYPE_ASN1, cert_bytes) thumbprint = cert_obj.digest("sha1").decode('utf-8').replace(":", "").upper() return thumbprint except Exception as e: logging.error("Error getting certificate thumbprint") logging.error(e) raise e def get_private_key_and_cert(cert, pin): try: cert_bytes = cert.get_encoded() cert_context = win32crypt.CertCreateCertificateContext(win32crypt.X509_ASN_ENCODING | win32crypt.PKCS_7_ASN_ENCODING, cert_bytes) prov_handle, key_spec, free_handle = win32crypt.CryptAcquireCertificatePrivateKey(cert_context, 0, None) key_blob = win32crypt.CryptExportKey(prov_handle, None, win32crypt.PRIVATEKEYBLOB, 0) private_key = RSA.import_key(key_blob) cert_pem = crypto.dump_certificate(crypto.FILETYPE_PEM, cert_context) return private_key, cert_pem except Exception as e: logging.error("Error obtaining private key and certificate in PEM format") logging.error(e) raise e def sign_document(private_key, cert_pem, data): try: signer = XMLSigner() signed_xml = signer.sign(data, key=private_key.export_key(), cert=cert_pem.decode()) return signed_xml except Exception as e: logging.error("Error signing the document") logging.error(e) raise e def confirm_certificate(): selected_cert = cert_listbox.get(cert_listbox.curselection()) with wincertstore.CertSystemStore("MY") as store: for cert in store.itercerts(usage=wincertstore.CLIENT_AUTH): if cert.get_name() == selected_cert: cert_thumbprint = get_cert_thumbprint(cert).upper().replace(" ", "") logging.info(f"Certificate confirmed: {cert.get_name()} with thumbprint: {cert_thumbprint}") pin = "your-pin-here" # Replace 'your-pin-here' with your actual PIN private_key, cert_pem = get_private_key_and_cert(cert, pin) xml_content = "<test>Important content</test>" root = etree.fromstring(xml_content) signed_root = sign_document(private_key, cert_pem, root) signed_xml_path = os.path.join(os.getcwd(), f"signed_{cert_thumbprint}.xml") with open(signed_xml_path, "wb") as f: f.write(etree.tostring(signed_root, pretty_print=True)) messagebox.showinfo("Success", f"Signed document saved at: {signed_xml_path}") return root = tk.Tk() root.title("Digital Signature") root.configure(bg="#93C5FD") frame_certs = Frame(root, bg="#93C5FD") frame_certs.pack(pady=10) Label(frame_certs, text="Certificate:", bg="#E0F2FE").pack(pady=5) cert_listbox = Listbox(frame_certs, width=50, height=10) cert_listbox.pack(pady=5) cert_scrollbar = Scrollbar(frame_certs, command=cert_listbox.yview) cert_scrollbar.pack(side="right", fill="y") cert_listbox.config(yscrollcommand=cert_scrollbar.set) confirm_button = Button(frame_certs, text="Confirm selection", command=confirm_certificate) confirm_button.pack(pady=5) with wincertstore.CertSystemStore("MY") as store: for cert in store.itercerts(usage=wincertstore.CLIENT_AUTH): cert_listbox.insert(END, cert.get_name()) root.mainloop()
错误信息
result = acquire_key_func( cert_context, 0, None, ctypes.byref(key_handle), ctypes.byref(key_spec), ctypes.byref(caller_free) )
解决方案
关键问题分析
- 私钥导出限制:很多Windows证书(尤其是智能卡/硬件密钥)的私钥不允许导出,强行调用
CryptExportKey会触发权限或内存错误。 CryptAcquireCertificatePrivateKey调用问题:原代码没有正确处理上下文的所有权和释放逻辑,导致内存访问异常。- 签名方式选择:直接使用win32crypt的密钥上下文签名,而非导出私钥后用第三方库,更符合Windows安全模型。
修正后的代码
import os import tkinter as tk from tkinter import messagebox, Listbox, Scrollbar, END, Frame, Label, Button import wincertstore import logging from lxml import etree from signxml import XMLSigner from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat from cryptography.hazmat.backends import default_backend from cryptography.x509 import load_der_x509_certificate import win32crypt import win32api import win32con logging.basicConfig(filename="error_log.txt", level=logging.ERROR) def get_cert_thumbprint(cert): try: cert_bytes = cert.get_encoded() # 用cryptography库计算thumbprint,避免OpenSSL兼容性问题 cert_obj = load_der_x509_certificate(cert_bytes, default_backend()) thumbprint = cert_obj.fingerprint().hex().upper().replace(":", "") return thumbprint except Exception as e: logging.error("Error getting certificate thumbprint") logging.error(e) raise e def get_cert_context_and_pubkey_pem(cert): try: cert_bytes = cert.get_encoded() # 创建证书上下文 cert_context = win32crypt.CertCreateCertificateContext( win32crypt.X509_ASN_ENCODING | win32crypt.PKCS_7_ASN_ENCODING, cert_bytes ) # 导出公钥为PEM格式 cert_obj = load_der_x509_certificate(cert_bytes, default_backend()) pubkey_pem = cert_obj.public_key().public_bytes( encoding=Encoding.PEM, format=PublicFormat.SubjectPublicKeyInfo ) return cert_context, pubkey_pem except Exception as e: logging.error("Error obtaining certificate context and public key") logging.error(e) raise e def sign_with_windows_cert(cert_context, data): try: # 获取私钥句柄,添加静默模式避免弹窗(如需PIN验证可移除该标志) flags = win32crypt.CRYPT_ACQUIRE_COMPARE_KEY_FLAG | win32crypt.CRYPT_ACQUIRE_SILENT_FLAG prov_handle, key_spec, free_handle = win32crypt.CryptAcquireCertificatePrivateKey( cert_context, flags, None ) try: # 准备要签名的数据(SHA256哈希) hash_obj = win32crypt.CryptCreateHash(prov_handle, win32crypt.CALG_SHA_256, 0, 0) win32crypt.CryptHashData(hash_obj, etree.tostring(data), 0) # 签名哈希值 signature = win32crypt.CryptSignHash(hash_obj, key_spec, None, 0, None) # 导出证书为PEM格式 mem_store = win32crypt.CertOpenStore(win32crypt.CERT_STORE_PROV_MEMORY, 0, 0, 0, None) win32crypt.CertAddCertificateContextToStore(mem_store, cert_context, win32crypt.CERT_STORE_ADD_REPLACE_EXISTING, None) cert_pem = win32crypt.CertSaveStore( mem_store, win32crypt.X509_ASN_ENCODING, win32crypt.CERT_STORE_SAVE_AS_PEM, win32crypt.CERT_STORE_SAVE_TO_MEMORY ) # 用signxml组装XML签名 signer = XMLSigner(signature_algorithm="rsa-sha256") signed_xml = signer.sign( data, cert=cert_pem.decode(), signature=signature ) return signed_xml finally: # 释放资源,避免内存泄漏 if free_handle: win32api.CloseHandle(prov_handle) win32crypt.CertFreeCertificateContext(cert_context) except Exception as e: logging.error("Error signing document with Windows certificate") logging.error(e) raise e def confirm_certificate(): selected_cert = cert_listbox.get(cert_listbox.curselection()) with wincertstore.CertSystemStore("MY") as store: for cert in store.itercerts(usage=wincertstore.CLIENT_AUTH): if cert.get_name() == selected_cert: cert_thumbprint = get_cert_thumbprint(cert) logging.info(f"Certificate confirmed: {cert.get_name()} with thumbprint: {cert_thumbprint}") cert_context, pubkey_pem = get_cert_context_and_pubkey_pem(cert) xml_content = "<test>Important content</test>" root = etree.fromstring(xml_content) signed_root = sign_with_windows_cert(cert_context, root) signed_xml_path = os.path.join(os.getcwd(), f"signed_{cert_thumbprint}.xml") with open(signed_xml_path, "wb") as f: f.write(etree.tostring(signed_root, pretty_print=True)) messagebox.showinfo("Success", f"Signed document saved at: {signed_xml_path}") return root = tk.Tk() root.title("Digital Signature") root.configure(bg="#93C5FD") frame_certs = Frame(root, bg="#93C5FD") frame_certs.pack(pady=10) Label(frame_certs, text="Certificate:", bg="#E0F2FE").pack(pady=5) cert_listbox = Listbox(frame_certs, width=50, height=10) cert_listbox.pack(pady=5) cert_scrollbar = Scrollbar(frame_certs, command=cert_listbox.yview) cert_scrollbar.pack(side="right", fill="y") cert_listbox.config(yscrollcommand=cert_scrollbar.set) confirm_button = Button(frame_certs, text="Confirm selection", command=confirm_certificate) confirm_button.pack(pady=5) with wincertstore.CertSystemStore("MY") as store: for cert in store.itercerts(usage=wincertstore.CLIENT_AUTH): cert_listbox.insert(END, cert.get_name()) root.mainloop()
核心改动说明
- 避免私钥导出:直接使用
CryptAcquireCertificatePrivateKey获取的密钥句柄进行签名,跳过CryptExportKey步骤,适配非导出型私钥。 - 资源安全释放:在
finally块中明确释放密钥句柄和证书上下文,避免内存泄漏和访问异常。 - 使用Windows原生哈希/签名API:用
CryptCreateHash和CryptSignHash完成签名计算,再通过signxml组装符合标准的XML签名结构。 - 依赖库优化:改用
cryptography库处理证书解析和公钥导出,提升兼容性和稳定性。
内容的提问来源于stack exchange,提问作者manuel medina
相关产品推荐
相关产品推荐

