PAYATHON 2026

如何在 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 DERencoding='der'包含 r、s 两个 INTEGER 的 SEQUENCE,长度可变
IEEE P.1363encoding='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 坐标,也不要把十六进制文本的字符直接当作坐标字节。

验签失败时检查哪些地方?

  1. 检查哈希算法是否一致。示例固定使用 SHA-256,但 P-256 不代表签名一定使用 SHA-256。需要检查 Java Card 端的实际算法配置;若使用其他算法,还需确认 Python 验签接口是否支持该组合。
  2. 检查消息字节是否完全一致。编码、换行、字段顺序或消息截取范围发生变化,都会改变摘要。除非协议明确规定了序列化方式,否则不要重新序列化消息后再验签。
  3. 检查是否重复计算摘要。示例接收原始消息并计算一次 SHA-256。如果收到的已经是摘要,再调用 SHA256.new() 就会对摘要再次哈希,需要按实际签名流程调整。
  4. 检查是否已去除传输包装。Hex、Base64、APDU 响应状态字以及协议头都应按协议解析,传给库的只能是对应的数据字节。原始问题未提供封装格式,不能直接套用固定截取规则。
  5. 检查公钥与签名是否配对。W 必须对应生成该签名的私钥,曲线应为 secp256r1,不能误用 secp256k1。

如果 W 与消息、签名一起通过未认证的通道传来,验签成功只能证明这组数据与该公钥匹配。要确认发送者身份,还需要通过预置公钥、证书或其他可信机制确认 W 的归属。

备注:内容仅供参考。