JS生成的P-256 ECDSA签名在Python验证失败,报MalformedPointError
问题分析与解决
问题描述
前端通过JavaScript Web Crypto API生成P-256(secp256r1)密钥对并签名交易,后端用Python的ecdsa库验证时抛出ecdsa.errors.MalformedPointError: Point does not lay on the curve错误,更换cryptography库验证同样失败。
前端密钥生成代码
// Generate P-256 key pair const keyPair = await window.crypto.subtle.generateKey( { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign'] );
前端签名代码
let transaction = { sender: address, recipient: recipient, amount: amount }; // Convert the transaction data to a JSON string let dataString = JSON.stringify(transaction); // Calculate the hash of the transaction data let data_hash = arrayBufferToHex(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(dataString))); // Sign the hash const signature = await window.crypto.subtle.sign( { name: 'ECDSA', hash: { name: 'SHA-256' } }, keyPair, hexToArrayBuffer(data_hash) ); // Convert the signature to hex const signatureHex = arrayBufferToHex(signature); // Add the signature and original data hash to the transaction transaction.signature = signatureHex; transaction.hash = data_hash;
后端原验证代码(错误版本)
from ecdsa import VerifyingKey, SECP256k1 from binascii import unhexlify def verify(public_key, signature, data_hash): public_key_bytes = unhexlify(public_key) signature_bytes = unhexlify(signature) data_hash = unhexlify(data_hash) verifying_key = VerifyingKey.from_string(public_key_bytes, curve=SECP256k1) is_valid = verifying_key.verify(signature_bytes, data_hash) print(f"Signature is valid: {is_valid}") return is_valid
核心问题原因
- 椭圆曲线不匹配:前端使用的是P-256(secp256r1/NIST P-256)曲线,后端错误选用了SECP256k1(比特币专用曲线),两条曲线参数完全不同,公钥无法在错误曲线上通过验证。
- 公钥格式不兼容:Web Crypto默认导出的公钥是SPKI格式(ASN.1编码),而
ecdsa库的from_string方法默认期待原始公钥(X+Y字节拼接),直接解析会导致格式错误。 - 签名格式差异:Web Crypto生成的ECDSA签名是ASN.1 DER编码格式,而
ecdsa库默认验证的是r+s原始拼接格式,格式不匹配会导致验证失败。
解决办法
1. 统一椭圆曲线
后端改用与前端一致的P-256曲线,对应ecdsa库中的NIST256p曲线。
2. 对齐公钥格式
方案一:前端导出原始格式公钥
修改前端代码,导出raw格式的公钥(直接获取X+Y字节拼接数据):
// 导出公钥为raw格式(64字节,X和Y各32字节) const publicKeyRaw = await window.crypto.subtle.exportKey( 'raw', keyPair.publicKey ); const publicKeyHex = arrayBufferToHex(publicKeyRaw); // 将publicKeyHex发送至后端
方案二:后端解析SPKI格式公钥
如果前端保持默认导出SPKI格式公钥,后端需要解析该格式(推荐用cryptography库实现)。
3. 适配签名格式
Web Crypto的签名是ASN.1 DER格式,验证时必须指定格式参数。
修正后的ecdsa库验证代码
from ecdsa import VerifyingKey, NIST256p import hashlib from binascii import unhexlify def verify(public_key, signature, data_hash): public_key_bytes = unhexlify(public_key) signature_bytes = unhexlify(signature) data_hash_bytes = unhexlify(data_hash) # 使用NIST256p(对应P-256)曲线,指定公钥为raw格式 verifying_key = VerifyingKey.from_string( public_key_bytes, curve=NIST256p, hashfunc=hashlib.sha256 ) # 指定签名格式为ASN.1 DER is_valid = verifying_key.verify( signature_bytes, data_hash_bytes, signature_format='der' ) print(f"Signature is valid: {is_valid}") return is_valid
备选:用cryptography库验证(更兼容Web Crypto)
如果使用cryptography库,可直接解析SPKI格式公钥,无需修改前端导出逻辑:
from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives import hashes from binascii import unhexlify def verify(public_key_spki_hex, signature_hex, data_hash_hex): public_key_bytes = unhexlify(public_key_spki_hex) signature_bytes = unhexlify(signature_hex) data_hash_bytes = unhexlify(data_hash_hex) # 解析SPKI格式公钥,使用P-256对应曲线 public_key = ec.EllipticCurvePublicKey.from_encoded_point( ec.SECP256R1(), public_key_bytes ) try: public_key.verify( signature_bytes, data_hash_bytes, ec.ECDSA(hashes.SHA256()) ) print("Signature is valid") return True except: print("Signature is invalid") return False
注:前端需用exportKey('spki', keyPair.publicKey)导出SPKI格式公钥。
内容的提问来源于stack exchange,提问作者Robin Michel
相关产品推荐
相关产品推荐

