You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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)
         )

解决方案

关键问题分析

  1. 私钥导出限制:很多Windows证书(尤其是智能卡/硬件密钥)的私钥不允许导出,强行调用CryptExportKey会触发权限或内存错误。
  2. CryptAcquireCertificatePrivateKey调用问题:原代码没有正确处理上下文的所有权和释放逻辑,导致内存访问异常。
  3. 签名方式选择:直接使用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()

核心改动说明

  1. 避免私钥导出:直接使用CryptAcquireCertificatePrivateKey获取的密钥句柄进行签名,跳过CryptExportKey步骤,适配非导出型私钥。
  2. 资源安全释放:在finally块中明确释放密钥句柄和证书上下文,避免内存泄漏和访问异常。
  3. 使用Windows原生哈希/签名API:用CryptCreateHash和CryptSignHash完成签名计算,再通过signxml组装符合标准的XML签名结构。
  4. 依赖库优化:改用cryptography库处理证书解析和公钥导出,提升兼容性和稳定性。

内容的提问来源于stack exchange,提问作者manuel medina

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 20:04:55