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

OpenSSL ECDSA与WebCrypto ECDSA签名结果不一致的原因咨询

ECDSA签名不一致问题:WebCrypto(JS)与OpenSSL(PHP)结果差异排查

问题背景

需要在JavaScript中通过WebCrypto执行ECDSA签名,在PHP中通过OpenSSL完成验签,但两者生成的签名结果不一致。以下为测试所用的密钥、代码及结果:

测试密钥(仅用于测试,请勿用于生产环境)

-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgkeS6LTDov1L2oShp6PgM2STESyMD1YTQkxUtxSJc1PahRANCAAR+uKNDd0l2odP4excxvTBY5bQxQBufCYb2TkVgp1NBefyO603LHzOelGVnnIFfamtJvv55AwVxt0nUlS0z450U
-----END PRIVATE KEY-----

PHP代码及签名结果

$challenge = file_get_contents("/tmp/challenge-XIABFf");
$key = openssl_pkey_get_private(file_get_contents("/tmp/private-key"));
$response = "";
openssl_sign($challenge, $response, $key, OPENSSL_ALGO_SHA384);
var_dump(base64_encode($response));

生成的Base64编码签名结果:
MEYCIQCQt9+reL0WRzi26ft8CEUMRDhnd4vh50F3sYL9BA2x0QIhAJCD7bakD6jXIfYTNpEoGPTR9guusC7CFxbbFcKPvroq(长度96字符)

JS代码及签名结果

function arrayBufferToBase64(buffer) {
    var binary = '';
    var bytes = new Uint8Array(buffer);
    for (var i = 0; i < bytes.byteLength; i++) {
        binary += String.fromCharCode(bytes[i]);
    }
    return btoa(binary);
}
function base64ToArrayBuffer(base64) {
    var binary_string = atob(base64);
    var len = binary_string.length;
    var bytes = new Uint8Array(len);
    for (var i = 0; i < len; i++) {
        bytes[i] = binary_string.charCodeAt(i);
    }
    return bytes.buffer;
}

key = await window.crypto.subtle.importKey(
    "pkcs8",
    base64ToArrayBuffer("MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgkeS6LTDov1L2oShp6PgM2STESyMD1YTQkxUtxSJc1PahRANCAAR+uKNDd0l2odP4excxvTBY5bQxQBufCYb2TkVgp1NBefyO603LHzOelGVnnIFfamtJvv55AwVxt0nUlS0z450U"),
    {name: "ECDSA", namedCurve: "P-256"},
    true,
    ["sign"]
);
message = await window.crypto.subtle.sign(
    {"name": "ECDSA", "hash": "SHA-384"},
    key,
    base64ToArrayBuffer("4BRYPzkacla7x5uR0bXtkQXC18odd9yCfcefjIrh")
); 
console.log(arrayBufferToBase64(message));

其中4BRYPzkacla7x5uR0bXtkQXC18odd9yCfcefjIrh是挑战值的Base64编码,生成的签名结果:
ZDDWSkpkwS8N8LYhHib/Qeb+P8DMRtKIT925AhO8m6bD/K5wAYEYa3Fq89Ik8z6t62a0dtuN8kydxNWttgFcdA==(长度88字符)

差异原因分析

  1. 签名格式不兼容
    OpenSSL的openssl_sign默认输出ASN.1/DER格式的签名,这种格式会对签名的两个分量r和s进行结构化编码(包含标签、长度等额外字节),因此Base64编码后长度更长(96字符)。
    而WebCrypto的sign方法输出的是IEEE P1363标准的原始拼接格式,即直接将r和s两个32字节的分量(P-256曲线的每个分量固定32字节)拼接在一起,总长度64字节,Base64编码后为88字符,这也是两者长度差异的直接原因。

  2. 消息输入可能不一致
    PHP代码中直接读取文件的原始二进制内容作为挑战值,而JS代码是将挑战值的Base64编码解码为ArrayBuffer。如果PHP读取的文件内容与JS解码后的二进制数据不一致(比如文件存在编码问题、Base64解码错误等),也会导致签名结果完全不同。

解决方案建议

  1. 统一签名格式

    • 方案一:在PHP中将ASN.1格式签名转换为原始拼接格式。可以通过解析ASN.1结构提取r和s分量后直接拼接。
    • 方案二:在JS中将原始拼接格式转换为ASN.1格式。手动构建DER编码结构,将r和s打包成符合ASN.1标准的格式。
  2. 验证消息一致性
    分别输出PHP中$challenge的十六进制值,以及JS中解码后ArrayBuffer的十六进制值,确认两者完全一致。如果不一致,排查消息传递或读取过程中的编码、解码错误。

内容的提问来源于stack exchange,提问作者JoelCrypto

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 23:47:43