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

获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

疑问:

  1. 代码是否在验证目标SOAP服务器返回响应的签名?
  2. 为何会遇到这个错误?
  3. 该如何解决?

我的代码

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 10:50:25