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

使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 03:40:13