获HTTP 200响应后遇xmlsec.VerificationError,求解决办法
SOAP接口调用签名验证失败问题
问题描述
使用Zeep框架编写SOAP接口调用代码,已收到目标服务器HTTP 200成功响应,但后续抛出签名验证错误,日志如下:
File "C:\ALL\Python 3.9\PythonDev\lib\site-packages\zeep\wsse\signature.py", line 330, in _verify_envelope_with_key ctx.verify(signature) xmlsec.VerificationError: Signature is invalid. During handling of the above exception, another exception occurred: Traceback (most recent call last): File "C:\ALL\Python 3.10\PythonDev\ERCOT_API.py", line 102, in <module> print(client.service.MarketInfo(**request_data)) File "C:\ALL\Python 3.9\PythonDev\lib\site-packages\zeep\proxy.py", line 46, in __call__ return self._proxy._binding.send( File "C:\ALL\Python 3.9\PythonDev\lib\site-packages\zeep\wsdl\bindings\soap.py", line 135, in send return self.process_reply(client, operation_obj, response) File "C:\ALL\Python 3.9\PythonDev\lib\site-packages\zeep\wsdl\bindings\soap.py", line 219, in process_reply client.wsse.verify(doc) File "C:\ALL\Python 3.9\PythonDev\lib\site-packages\zeep\wsse\signature.py", line 73, in verify _verify_envelope_with_key(envelope, key) File "C:\ALL\Python 3.9\PythonDev\lib\site-packages\zeep\wsse\signature.py", line 334, in _verify_envelope_with_key raise SignatureVerificationFailed() zeep.exceptions.SignatureVerificationFailed Process finished with exit code 1
疑问:
- 代码是否在验证目标SOAP服务器返回响应的签名?
- 为何会遇到这个错误?
- 该如何解决?
我的代码
import os import contextlib import requests import urllib3 from zeep import Client, Settings from zeep.transports import Transport from requests import Session from requests_pkcs12 import Pkcs12Adapter from zeep.wsse.signature import BinarySignature import random import logging.config from pathlib import Path from tempfile import NamedTemporaryFile from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption from cryptography.hazmat.primitives.serialization.pkcs12 import load_key_and_certificates # USE THE MOST VERBOSE LOGGING LEVEL logging.config.dictConfig({ "version": 1, "formatters": { "verbose": { "format": "%(name)s: %(message)s" } }, "handlers": { "console": { "level": "DEBUG", "class": "logging.StreamHandler", "formatter": "verbose", }, }, "loggers": { "zeep.transports": { "level": "DEBUG", "propagate": True, "handlers": ["console"], }, } }) # Source: https://gist.github.com/erikbern/756b1d8df2d1487497d29b90e81f8068 @contextlib.contextmanager def pfx_to_pem(pfx_path, pfx_password): ''' Decrypts the .pfx file to be used with requests. ''' pfx = Path(pfx_path).read_bytes() private_key, main_cert, add_certs = load_key_and_certificates(pfx, pfx_password.encode('utf-8'), None) with NamedTemporaryFile(suffix='.pem', delete=False) as t_pem: with open(t_pem.name, 'wb') as pem_file: pem_file.write(private_key.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, NoEncryption())) pem_file.write(main_cert.public_bytes(Encoding.PEM)) for ca in add_certs: pem_file.write(ca.public_bytes(Encoding.PEM)) yield t_pem.name def generate_nonce(length=15): """Generate pseudorandom number.""" return ''.join([str(random.randint(0, 9)) for i in range(length)]) # CERTIFICATES PATHS api_p12_key = os.path.join('C:\\ALL\\ERCOT\\API Outplan OSI TCC MOTE.p12') api_certificate = os.path.join('C:\\ALL\\ERCOT\\OSITCC.crt') api_pfx_key = os.path.join('C:\\ALL\\ERCOT\\API Outplan OSI TCC MOTE.pfx') # SETUP wsdl_file = os.path.join('C:\\ALL\\ERCOT\\Nodal.wsdl') #wsdl_file = "https://testmisapi.ercot.com/2007-08/Nodal/eEDS/EWS/?WSDL" api_base_url = "https://testmisapi.ercot.com" session = requests.Session() session.mount(api_base_url, Pkcs12Adapter(pkcs12_filename=api_p12_key, pkcs12_password='AEP')) session.verify = False transport = Transport(session=session) settings = Settings(forbid_entities=False) # CREATE CLIENT print("Creating client.") with pfx_to_pem(pfx_path=api_pfx_key, pfx_password='AEP') as pem_fle: client = Client(wsdl_file, settings=settings, transport=transport, wsse=BinarySignature(pem_fle, api_certificate)) print("Making request.") request_data = { "Header": { "Verb": "get", "Noun": "SystemStatus", "ReplayDetection": { "Nonce": generate_nonce(), "Created": "2022-09-15T15:39:00-06:00"}, "Revision": "1", "Source": "source", "UserID": "user", }, } print("Call URL") print(client.service.MarketInfo(**request_data))
问题解答
1. 代码是否在验证响应签名?
是。你使用的zeep.wsse.signature.BinarySignature是WSSE处理器,Zeep会自动对发送的请求签名,同时在收到服务器响应后,用你传入的api_certificate验证响应的签名有效性,日志中的verify调用就是这个验证流程的体现。
2. 错误原因分析
- 证书用途混淆:你用自己的客户端证书去验证服务器响应的签名,但服务器是用自身的私钥签名响应,验证需要服务器的公钥证书或其上级CA证书,而非客户端证书。
- PEM文件内容冗余:
pfx_to_pem生成的PEM包含了私钥、客户端证书和CA证书,但BinarySignature第一个参数只需要用于签名请求的私钥+客户端证书,多余的证书可能干扰验证逻辑。 - 时间戳过期:
Created字段使用了固定的旧时间,WS-Security签名验证可能对时间同步有要求,时间差过大也会导致验证失败。 - 响应内容异常:虽然HTTP状态码是200,但响应内容可能在传输中被篡改(HTTPS下概率极低),导致签名不匹配。
3. 解决方法
方法一:替换验证用证书
获取ERCOT测试环境的服务器公钥证书或其CA证书,替换api_certificate对应的文件路径,确保用正确的信任证书验证服务器响应签名。
方法二:修正PEM文件生成逻辑
修改pfx_to_pem函数,只写入私钥和客户端证书(用于请求签名),去掉多余的CA证书:
@contextlib.contextmanager def pfx_to_pem(pfx_path, pfx_password): pfx = Path(pfx_path).read_bytes() private_key, main_cert, add_certs = load_key_and_certificates(pfx, pfx_password.encode('utf-8'), None) with NamedTemporaryFile(suffix='.pem', delete=False) as t_pem: with open(t_pem.name, 'wb') as pem_file: pem_file.write(private_key.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, NoEncryption())) pem_file.write(main_cert.public_bytes(Encoding.PEM)) yield t_pem.name
方法三:生成实时时间戳
替换固定的Created时间为当前UTC时间,避免时间过期问题:
from datetime import datetime, timezone def get_current_created_time(): return datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z') # 替换request_data中的Created字段 request_data = { "Header": { # ...其他字段 "ReplayDetection": { "Nonce": generate_nonce(), "Created": get_current_created_time() }, # ...其他字段 }, }
方法四:临时禁用响应签名验证(仅调试用)
如果只是想确认响应内容是否正确,可临时跳过验证(生产环境禁止使用):
from zeep.wsse.signature import BinarySignature class NoVerifyBinarySignature(BinarySignature): def verify(self, envelope): # 跳过响应签名验证 pass # 创建客户端时使用自定义类 client = Client(wsdl_file, settings=settings, transport=transport, wsse=NoVerifyBinarySignature(pem_fle, api_certificate))
方法五:确认服务器证书链
通过浏览器访问https://testmisapi.ercot.com,导出服务器的证书或其上级CA证书,保存为CRT文件,用于验证响应签名。
内容的提问来源于stack exchange,提问作者Sugata Bagchi
相关产品推荐
相关产品推荐

