使用PHP验证Web Crypto API生成的ECDSA签名失败排查
ECDSA签名PHP验证失败的问题解决
核心问题是签名格式不兼容:Web Crypto API生成的ECDSA签名采用ASN.1/DER编码格式,而PHP的openssl_verify默认期望的是IEEE P1363格式(即r值拼接s值的64字节二进制数据),两者格式不匹配导致验证失败。
解决方案一:PHP端转换签名格式
编写函数将ASN.1/DER格式的签名转换为P1363格式,再进行验证:
<?php $data = 'text_value'; $signature = base64_decode("TfsoDx8TyuAth7SzsoagHVykRU+eNWaGOWEulrSjZ57KKa2T/8zO/7+/8TvRXLvuXSbEloRFbigzUpIlOlGMcw=="); $public_key = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE01iwlSEr1pLOLu2Ks9gxMhH7C3NWQ95yKO2vvv7XYfRGKAVRaLVLJ6j3J6klnNez5kWeECdJ1OhoQULEyokEZQ=="; $public_key = "-----BEGIN PUBLIC KEY----- $public_key -----END PUBLIC KEY-----"; // 把ASN.1 DER格式签名转成P1363格式(r||s) function asn1ToP1363($signature) { $offset = 0; // 校验ASN.1 SEQUENCE标签 if (ord($signature[$offset++]) !== 0x30) { throw new Exception("无效签名格式"); } // 解析总长度 $length = ord($signature[$offset++]); if ($length & 0x80) { $lengthBytes = $length & 0x7F; $length = 0; for ($i = 0; $i < $lengthBytes; $i++) { $length = ($length << 8) | ord($signature[$offset++]); } } // 读取r值 if (ord($signature[$offset++]) !== 0x02) { throw new Exception("无效r标签"); } $rLen = ord($signature[$offset++]); $r = substr($signature, $offset, $rLen); $offset += $rLen; // 读取s值 if (ord($signature[$offset++]) !== 0x02) { throw new Exception("无效s标签"); } $sLen = ord($signature[$offset++]); $s = substr($signature, $offset, $sLen); // 补零到32字节(P-256曲线固定长度) $r = str_pad($r, 32, "\x00", STR_PAD_LEFT); $s = str_pad($s, 32, "\x00", STR_PAD_LEFT); return $r . $s; } try { $p1363Signature = asn1ToP1363($signature); $verify = openssl_verify($data, $p1363Signature, $public_key, OPENSSL_ALGO_SHA256); echo $verify === 1 ? '签名有效' : '签名无效'; } catch (Exception $e) { echo "签名转换错误: " . $e->getMessage(); } echo "\n\nopenssl错误信息: " . (openssl_error_string() ?: '无错误') . "\n\n"; ?>
解决方案二:JS端直接生成P1363格式签名
修改Web Crypto的签名逻辑,将生成的ASN.1签名转换为P1363格式后再输出,这样PHP端无需修改:
(async () => { async function arrayBufferToBase64(arrayBuffer) { var binary = ''; var bytes = new Uint8Array(arrayBuffer); var len = bytes.byteLength; for (var i = 0; i < len; i++) { binary += String.fromCharCode(bytes[i]); } return window.btoa(binary); } // 新增:ASN.1转P1363格式 function asn1ToP1363(signatureBuffer) { const view = new DataView(signatureBuffer); let offset = 0; // 跳过SEQUENCE标签和长度 if (view.getUint8(offset++) !== 0x30) throw new Error("无效签名"); let length = view.getUint8(offset++); if (length & 0x80) { const lenBytes = length & 0x7F; length = 0; for (let i = 0; i < lenBytes; i++) { length = (length << 8) | view.getUint8(offset++); } } // 读取r值 if (view.getUint8(offset++) !== 0x02) throw new Error("无效r标签"); const rLen = view.getUint8(offset++); const r = new Uint8Array(signatureBuffer, offset, rLen); offset += rLen; // 读取s值 if (view.getUint8(offset++) !== 0x02) throw new Error("无效s标签"); const sLen = view.getUint8(offset++); const s = new Uint8Array(signatureBuffer, offset, sLen); // 补零到32字节 const paddedR = new Uint8Array(32); paddedR.set(r, 32 - r.length); const paddedS = new Uint8Array(32); paddedS.set(s, 32 - s.length); // 拼接r和s const p1363 = new Uint8Array(64); p1363.set(paddedR); p1363.set(paddedS, 32); return p1363.buffer; } let keyPair = await window.crypto.subtle.generateKey( { name: "ECDSA", namedCurve: "P-256", }, false, ["sign", "verify"]); let data = 'text_value'; let signature = await window.crypto.subtle.sign({ name: "ECDSA", hash: { name: "SHA-256" }, }, keyPair.privateKey, new TextEncoder().encode(data)); // 转换为P1363格式 signature = asn1ToP1363(signature); console.log('Data: ' + data); console.log('Signature (P1363): ' + await arrayBufferToBase64(signature)); console.log('Public key: ' + await arrayBufferToBase64(await window.crypto.subtle.exportKey('spki', keyPair.publicKey))); })();
内容的提问来源于stack exchange,提问作者David Pankov
相关产品推荐
相关产品推荐

