如何在 Python 中用 P-256 公钥验证 Java Card 的 ECDSA 签名?
可以使用 PyCryptodome 验证 ECDSA 签名:根据曲线名称和公钥点 W 导入公钥,再用该公钥验签。验签时不需要私钥,也不需要自行实现椭圆曲线运算。
原始问题没有提供 W 的实际字节、签名编码或 Java Card 使用的哈希算法。下面的示例假设曲线是 P-256,W 使用未压缩点编码,签名算法是 ECDSA with SHA-256,签名使用 ASN.1 DER 编码。接入时需要逐项确认这些条件。
为什么仅凭 W 就能构造公钥?
在这个场景中,W 是签名密钥对应的公钥点。接收端知道曲线参数和 W,就能构造公钥。私钥只用于签名,无需传给 Python。
P-256、secp256r1 和 prime256v1 是同一条曲线的不同名称。PyCryptodome 的 ECC.import_key() 支持导入 SEC1 编码的公钥点,导入时必须提供 curve_name;如果已经取得坐标,也可以使用 ECC.construct()。ECC API 文档
先确认传输格式
示例接受以下格式的 W:
0x04 || X || Y
其中 0x04 占一个字节,X 和 Y 分别是 32 字节的大端坐标,W 总长为 65 字节。坐标开头的零字节必须保留。示例以这种格式为前提,实际 Java Card 输出是否符合要求还需要确认。
ECDSA 签名包含两个整数 r 和 s,常见编码有两种:
| 签名编码 | PyCryptodome 参数 | 数据形式 |
|---|---|---|
| ASN.1 DER | encoding='der' | 包含 r、s 两个 INTEGER 的 SEQUENCE,长度可变 |
| IEEE P.1363 | encoding='binary' | 固定宽度的 `r |
DSS.new() 默认采用 binary。如果发送端输出 DER,接收端必须显式设置 encoding='der'。DSS 编码说明
使用 PyCryptodome 验证签名
安装依赖:
python -m pip install pycryptodome
下面的函数接收原始消息、签名和 W,三者都应是完成传输解码后的 bytes:
from Crypto.Hash import SHA256
from Crypto.PublicKey import ECC
from Crypto.Signature import DSS
def verify_p256_sha256(message: bytes, signature: bytes,
w: bytes, encoding: str = 'der') -> bool:
# This example accepts only an uncompressed P-256 point.
if len(w) != 65 or w[0] != 0x04:
raise ValueError('Expected a 65-byte uncompressed P-256 point')
if encoding not in ('der', 'binary'):
raise ValueError('Unsupported signature encoding')
# Invalid point encodings are rejected by the library.
public_key = ECC.import_key(w, curve_name='P-256')
verifier = DSS.new(public_key, 'fips-186-3', encoding=encoding)
digest = SHA256.new(message)
try:
verifier.verify(digest, signature)
except ValueError:
return False
return True
将收到的字节分别作为 message、signature 和 w 传入。如果协议规定签名是固定 64 字节的 r || s,调用时使用 encoding='binary'。
函数通过是否抛出异常判断验签结果。公钥导入出错时,异常会传给调用方;签名验证失败时,函数返回 False,由此可以区分公钥解析和签名验证两个阶段。DSS 验签接口
如果已经解析出了 X、Y 坐标
也可以直接构造公钥,替换示例中的 ECC.import_key():
x = int.from_bytes(w[1:33], 'big')
y = int.from_bytes(w[33:65], 'big')
public_key = ECC.construct(curve='P-256', point_x=x, point_y=y)
这段切片同样要求 W 已通过前面的长度和前缀检查。不要把完整 W 当作 X 坐标,也不要把十六进制文本的字符直接当作坐标字节。
验签失败时检查哪些地方?
- 检查哈希算法是否一致。示例固定使用 SHA-256,但 P-256 不代表签名一定使用 SHA-256。需要检查 Java Card 端的实际算法配置;若使用其他算法,还需确认 Python 验签接口是否支持该组合。
- 检查消息字节是否完全一致。编码、换行、字段顺序或消息截取范围发生变化,都会改变摘要。除非协议明确规定了序列化方式,否则不要重新序列化消息后再验签。
- 检查是否重复计算摘要。示例接收原始消息并计算一次 SHA-256。如果收到的已经是摘要,再调用
SHA256.new()就会对摘要再次哈希,需要按实际签名流程调整。 - 检查是否已去除传输包装。Hex、Base64、APDU 响应状态字以及协议头都应按协议解析,传给库的只能是对应的数据字节。原始问题未提供封装格式,不能直接套用固定截取规则。
- 检查公钥与签名是否配对。W 必须对应生成该签名的私钥,曲线应为 secp256r1,不能误用 secp256k1。
如果 W 与消息、签名一起通过未认证的通道传来,验签成功只能证明这组数据与该公钥匹配。要确认发送者身份,还需要通过预置公钥、证书或其他可信机制确认 W 的归属。
备注:内容仅供参考。
