无法使用 WebCrypto 验证 ECDSA 签名

Failing to verify ECDSA signature using WebCrypto

我正在尝试通过 WebCrypto 验证 ECDSA 签名,但失败了。签名是使用 Java (Bouncy Castle) 创建的。使用的曲线是 secp256r1,在创建签名时使用 SHA256 哈希。然后我尝试在 Java 中使用 RSA (SHA256) 创建签名并尝试在 WebCrypto 中验证并成功。这似乎是 ECDSA 特定的问题。在 java 中,我在 SPKI 中导出了 public 密钥,然后在 WebCrypto 中成功导入。是什么导致 WebCrypto 无法验证基于 ECDSA 的密钥。

let publicKey;
const verifyButton = document.querySelector(".spki .encrypt-button");

console.log("Starting js code");

const content = str2ab('HelloWorld');
const signatureB64 = 'MEUCIEAcdw929MCVQhk7BxHslo4cq0TwmkaNtL/JQgbbef1JAiEA5WKai5VCMhwKs7Zyh7fiG2DIKxoytw9OaYwsfA0etzY=';
const signature = str2ab(window.atob(signatureB64));

const pemEncodedKey = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5J8msRi8yYS8ndRquLAXW4K7tdra9Awgl1CmOPPQsfFHYieZWMdWkcxreYI/hrbnwKP7MtjcTrt8seoVUprx8Q==
-----END PUBLIC KEY-----`;

function importECDSAKey(pem) {
    // fetch the part of the PEM string between header and footer
    const pemHeader = "-----BEGIN PUBLIC KEY-----";
    const pemFooter = "-----END PUBLIC KEY-----";
    const pemContents = pem.substring(pemHeader.length, pem.length - pemFooter.length);
    // base64 decode the string to get the binary data
    const binaryDerString = window.atob(pemContents);
    // convert from a binary string to an ArrayBuffer
    const binaryDer = str2ab(binaryDerString);

    return window.crypto.subtle.importKey(
      "spki",
      binaryDer,
      {
        name: "ECDSA",
        namedCurve: 'P-256'
      },
      true,
      ["verify"]
    );
}


async function verifySignature() {
    //download("signatureecdsa_13Aug.bin", window.btoa(window.atob(signatureB64)));
    console.log("Verify Content: " + content);
    console.log("Verify Signature: " + signature);
    let result = await window.crypto.subtle.verify(
      {
        name: "ECDSA",
        namedCurve: 'P-256',
        hash: {name: "SHA-256"},
      },
      publicKey,
      signature,
      content
    );
    console.log(result ? "valid" : "invalid");
    
}

function str2ab(str) {
    const buf = new ArrayBuffer(str.length);
    const bufView = new Uint8Array(buf);
    for (let i = 0, strLen = str.length; i < strLen; i++) {
      bufView[i] = str.charCodeAt(i);
    }
    return buf;
}

const importKeyButton = document.querySelector(".spki .import-key-button");
  importKeyButton.addEventListener("click", async () => {
    publicKey = await importECDSAKey(pemEncodedKey);
    console.log(publicKey);
    
  });

verifyButton.addEventListener("click", async () => {
    verifySignature();
    
});
<!DOCTYPE html>
<html>
<style>
/* General setup */

* {
    box-sizing: border-box;
}

html,body {
    font-family: sans-serif;
    line-height: 1.2rem;
}

/* Layout and styles */

h1 {
    color: green;
    margin-left: .5rem;
}

.description, .sign-verify {
    margin: 0 .5rem;
}

.description > p {
    margin-top: 0;
}

.sign-verify {
    box-shadow: -1px 2px 5px gray;
    padding: .2rem .5rem;
    margin-bottom: 2rem;
}

.sign-verify-controls > * {
    margin: .5rem 0;
}

input[type="button"] {
    width: 5rem;
}

.signature-value {
    padding-left: .5rem;
    font-family: monospace;
}

/* Validity CSS */
.valid {
    color: green;
}

.invalid {
    color: red;
}

.invalid::after {
    content: ' ✖';
}

.valid::after {
    content: ' ✓';
}

/* Whole page grid */
main {
    display: grid;
    grid-template-columns: 32rem 1fr;
    grid-template-rows: 4rem 1fr;
}

h1 {
    grid-column: 1/2;
    grid-row: 1;
}

.examples {
    grid-column: 1;
    grid-row: 2;
}

.description {
    grid-column: 2;
    grid-row: 2;
}

/* sign-verify controls grid */
.sign-verify-controls {
    display: grid;
    grid-template-columns: 1fr 5rem;
    grid-template-rows: 1fr 1fr;
}

.message-control {
    grid-column-start: 1;
    grid-row-start: 1;
}

.signature {
    grid-column-start: 1;
    grid-row-start: 2;
}

.sign-button {
    grid-column-start: 2;
    grid-row-start: 1;
}

.verify-button {
    grid-column-start: 2;
    grid-row-start: 2;
}

/* Animate output display */
.fade-in {
    animation: fadein .5s;
}

@keyframes fadein {
    from {
        opacity: 0;
    }
    to {
        opacity: 1;
    }
}
</style>
<head>
 <meta charset="utf-8">
    <!-- head definitions go here -->
</head>
<body onload="">
      <section class="import-key spki">
      <h2 class="import-key-heading">ECDSA Siganture Verification</h2>
      <section class="import-key-controls">
        <input class="import-key-button" type="button" value="Import Key">
        <input class="encrypt-button" type="button" value="Verify">
      </section>
    </section>
</body>
    <script src="sample.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/node-forge@0.7.0/dist/forge.min.js"></script>
</html>

java中可验证的签名:

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package com.text.pkcs1;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Base64;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

/**
 *
 * @author Sajid Hussain
 */
public class ECDSASignatureVerifier {

    static String content = "HelloWorld";
    static String sigantureB64 = "MEUCIEAcdw929MCVQhk7BxHslo4cq0TwmkaNtL/JQgbbef1JAiEA5WKai5VCMhwKs7Zyh7fiG2DIKxoytw9OaYwsfA0etzY=";
    static String pemEncodedCert = "-----BEGIN CERTIFICATE-----\n"
            + "MIIDQTCCAimgAwIBAgIUUm4Lc5DyI1TgpT2FdmDW1TIJ8rQwDQYJKoZIhvcNAQEL\n"
            + "BQAwODELMAkGA1UEBhMCUEsxEDAOBgNVBAoTB0NvZGVnaWMxFzAVBgNVBAMTDkNv\n"
            + "ZGVnaWMgU3ViIENBMB4XDTIxMDgxMjEwMTE0M1oXDTI2MDgxMjEwMTE0M1owHTEb\n"
            + "MBkGA1UEAxMSRUNEU0EtVGVzdGluZy1DZXJ0MFkwEwYHKoZIzj0CAQYIKoZIzj0D\n"
            + "AQcDQgAE5J8msRi8yYS8ndRquLAXW4K7tdra9Awgl1CmOPPQsfFHYieZWMdWkcxr\n"
            + "eYI/hrbnwKP7MtjcTrt8seoVUprx8aOCAScwggEjMA4GA1UdDwEB/wQEAwIGwDAf\n"
            + "BgNVHSMEGDAWgBRMIjotWPXzdcBEsvsWI9GGgsK5sTCBxQYDVR0gBIG9MIG6MIG3\n"
            + "BgkrBgEEAYOoZAEwgakwgaYGCCsGAQUFBwICMIGZDIGWQXMgcGVyIHRoaXMgcG9s\n"
            + "aWN5IGlkZW50aXR5IG9mIHRoZSBzdWJqZWN0IGlzIG5vdCBpZGVudGlmaWVkLiBZ\n"
            + "b3UgbXVzdCBub3QgdXNlIHRoZXNlIGNlcnRpZmljYXRlcyBmb3IgdmFsdWFibGUg\n"
            + "dHJhbnNhY3Rpb25zLiBOTyBMSUFCSUxJVFkgSVMgQUNDRVBURUQuMAkGA1UdEwQC\n"
            + "MAAwHQYDVR0OBBYEFIEA+hTr95sTIKNgdm/qL3WdAB4MMA0GCSqGSIb3DQEBCwUA\n"
            + "A4IBAQAHcmrw0gjBdqjYowT4TsUBEL6Dei7gt/qEgDBOsewgESZmoDqSbZXNMHsJ\n"
            + "kvceQ3hCLt9FTQPijyXMQJyBSKdYym8PXk3BJRbZpl3Kdy/jZMBg10oZo5PMZh8i\n"
            + "MZNpvzdiULpAqtquRK1kUS/hB/5qnWuypuQgFdrM/S2IuJuTzdmUuTBt3yjK12Zp\n"
            + "8j8gqT1JlL/SYkFA3HvdFMu6t6pDWyfL3h48YpQqjpHw7H55IGKrBRhpDlGNYpO9\n"
            + "/MkNhaKlCKA6/LXqJ1Exonu+WbJk5X23F+SaThrxWR7iY7aIdLjj1Sqd73wKhtM8\n"
            + "39gP1Xtl+AyoVLHyXFvqbuwyZaPU\n"
            + "-----END CERTIFICATE-----";

    public static void main(String[] args) throws IOException, CertificateException, NoSuchAlgorithmException, InvalidKeyException, SignatureException {
        Security.removeProvider("BC");
        Security.addProvider(new BouncyCastleProvider());
        verifyTextSiganture();
    }

    private static void verifyTextSiganture() throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, UnsupportedEncodingException, CertificateException {
        Base64.Decoder decoder = Base64.getDecoder();
        Signature signAlg = Signature.getInstance("SHA256withECDSA");
        CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
        X509Certificate signerCertificate = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(pemEncodedCert.getBytes()));
        signAlg.initVerify(signerCertificate.getPublicKey());
        signAlg.update(content.getBytes());
        boolean result = signAlg.verify(decoder.decode(sigantureB64.getBytes()));
        System.out.println("Verification Result: " + result);
    }

}

EC 签名可以指定为两种格式:r|s (IEEE P1363) 或 ASN.1/DER。两种格式都有解释 here.

WebCrypto 使用 r|s 格式,而 MEUCI... 使用 ASN.1 格式。您的 r|s 格式签名是 Base64 编码的:

QBx3D3b0wJVCGTsHEeyWjhyrRPCaRo20v8lCBtt5/UnlYpqLlUIyHAqztnKHt+IbYMgrGjK3D05pjCx8DR63Ng==

如果在JavaScript代码中使用此签名,则验证成功。

let publicKey;
const verifyButton = document.querySelector(".spki .encrypt-button");

console.log("Starting js code");

const content = str2ab('HelloWorld');
//const signatureB64 = 'MEUCIEAcdw929MCVQhk7BxHslo4cq0TwmkaNtL/JQgbbef1JAiEA5WKai5VCMhwKs7Zyh7fiG2DIKxoytw9OaYwsfA0etzY=';
const signatureB64 = 'QBx3D3b0wJVCGTsHEeyWjhyrRPCaRo20v8lCBtt5/UnlYpqLlUIyHAqztnKHt+IbYMgrGjK3D05pjCx8DR63Ng==';
const signature = str2ab(window.atob(signatureB64));

const pemEncodedKey = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5J8msRi8yYS8ndRquLAXW4K7tdra9Awgl1CmOPPQsfFHYieZWMdWkcxreYI/hrbnwKP7MtjcTrt8seoVUprx8Q==
-----END PUBLIC KEY-----`;

function importECDSAKey(pem) {
    // fetch the part of the PEM string between header and footer
    const pemHeader = "-----BEGIN PUBLIC KEY-----";
    const pemFooter = "-----END PUBLIC KEY-----";
    const pemContents = pem.substring(pemHeader.length, pem.length - pemFooter.length);
    // base64 decode the string to get the binary data
    const binaryDerString = window.atob(pemContents);
    // convert from a binary string to an ArrayBuffer
    const binaryDer = str2ab(binaryDerString);

    return window.crypto.subtle.importKey(
      "spki",
      binaryDer,
      {
        name: "ECDSA",
        namedCurve: 'P-256'
      },
      true,
      ["verify"]
    );
}


async function verifySignature() {
    //download("signatureecdsa_13Aug.bin", window.btoa(window.atob(signatureB64)));
    //console.log("Verify Content: " + new Uint8Array(content));
    //console.log("Verify Signature: " + new Uint8Array(signature));
    let result = await window.crypto.subtle.verify(
      {
        name: "ECDSA",
        //namedCurve: 'P-256',
        hash: {name: "SHA-256"},
      },
      publicKey,
      signature,
      content
    );
    console.log(result ? "valid" : "invalid");   
}

function str2ab(str) {
    const buf = new ArrayBuffer(str.length);
    const bufView = new Uint8Array(buf);
    for (let i = 0, strLen = str.length; i < strLen; i++) {
      bufView[i] = str.charCodeAt(i);
    }
    return buf;
}

const importKeyButton = document.querySelector(".spki .import-key-button");
  importKeyButton.addEventListener("click", async () => {
    publicKey = await importECDSAKey(pemEncodedKey);
    console.log("Key imported"/*publicKey*/);
    
  });

verifyButton.addEventListener("click", async () => {
    verifySignature();
    
});
<!DOCTYPE html>
<html>
<style>
/* General setup */

* {
    box-sizing: border-box;
}

html,body {
    font-family: sans-serif;
    line-height: 1.2rem;
}

/* Layout and styles */

h1 {
    color: green;
    margin-left: .5rem;
}

.description, .sign-verify {
    margin: 0 .5rem;
}

.description > p {
    margin-top: 0;
}

.sign-verify {
    box-shadow: -1px 2px 5px gray;
    padding: .2rem .5rem;
    margin-bottom: 2rem;
}

.sign-verify-controls > * {
    margin: .5rem 0;
}

input[type="button"] {
    width: 5rem;
}

.signature-value {
    padding-left: .5rem;
    font-family: monospace;
}

/* Validity CSS */
.valid {
    color: green;
}

.invalid {
    color: red;
}

.invalid::after {
    content: ' ✖';
}

.valid::after {
    content: ' ✓';
}

/* Whole page grid */
main {
    display: grid;
    grid-template-columns: 32rem 1fr;
    grid-template-rows: 4rem 1fr;
}

h1 {
    grid-column: 1/2;
    grid-row: 1;
}

.examples {
    grid-column: 1;
    grid-row: 2;
}

.description {
    grid-column: 2;
    grid-row: 2;
}

/* sign-verify controls grid */
.sign-verify-controls {
    display: grid;
    grid-template-columns: 1fr 5rem;
    grid-template-rows: 1fr 1fr;
}

.message-control {
    grid-column-start: 1;
    grid-row-start: 1;
}

.signature {
    grid-column-start: 1;
    grid-row-start: 2;
}

.sign-button {
    grid-column-start: 2;
    grid-row-start: 1;
}

.verify-button {
    grid-column-start: 2;
    grid-row-start: 2;
}

/* Animate output display */
.fade-in {
    animation: fadein .5s;
}

@keyframes fadein {
    from {
        opacity: 0;
    }
    to {
        opacity: 1;
    }
}
</style>
<head>
 <meta charset="utf-8">
    <!-- head definitions go here -->
</head>
<body onload="">
      <section class="import-key spki">
      <h2 class="import-key-heading">ECDSA Siganture Verification</h2>
      <section class="import-key-controls">
        <input class="import-key-button" type="button" value="Import Key">
        <input class="encrypt-button" type="button" value="Verify">
      </section>
    </section>
</body>
</html>