OpenSSL ECDSA与WebCrypto ECDSA签名结果不一致的原因咨询
问题背景
需要在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字符)
差异原因分析
签名格式不兼容
OpenSSL的openssl_sign默认输出ASN.1/DER格式的签名,这种格式会对签名的两个分量r和s进行结构化编码(包含标签、长度等额外字节),因此Base64编码后长度更长(96字符)。
而WebCrypto的sign方法输出的是IEEE P1363标准的原始拼接格式,即直接将r和s两个32字节的分量(P-256曲线的每个分量固定32字节)拼接在一起,总长度64字节,Base64编码后为88字符,这也是两者长度差异的直接原因。消息输入可能不一致
PHP代码中直接读取文件的原始二进制内容作为挑战值,而JS代码是将挑战值的Base64编码解码为ArrayBuffer。如果PHP读取的文件内容与JS解码后的二进制数据不一致(比如文件存在编码问题、Base64解码错误等),也会导致签名结果完全不同。
解决方案建议
统一签名格式
- 方案一:在PHP中将ASN.1格式签名转换为原始拼接格式。可以通过解析ASN.1结构提取
r和s分量后直接拼接。 - 方案二:在JS中将原始拼接格式转换为ASN.1格式。手动构建DER编码结构,将
r和s打包成符合ASN.1标准的格式。
- 方案一:在PHP中将ASN.1格式签名转换为原始拼接格式。可以通过解析ASN.1结构提取
验证消息一致性
分别输出PHP中$challenge的十六进制值,以及JS中解码后ArrayBuffer的十六进制值,确认两者完全一致。如果不一致,排查消息传递或读取过程中的编码、解码错误。
内容的提问来源于stack exchange,提问作者JoelCrypto

