javadigital-signatureecdsawebcrypto-apiwebcrypto

Failing to verify ECDSA signature using WebCrypto


I am trying to verify an ECDSA signature via WebCrypto and failing. The signature is created using Java (Bouncy Castle). The curve used is secp256r1 and SHA256 hash is used at signature creation. Then I tried creating signature using RSA (SHA256) in Java and tried verified in WebCrypto and succeeded. This seems like an ECDSA specific issue. In java I exported the public key in SPKI and then imported successfully in WebCrypto. What could be causing WebCrypto to fail verifying ECDSA based keys.

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>

The signature verifiable in 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);
    }

}

Solution

  • EC signatures can be specified in two formats: r|s (IEEE P1363) or ASN.1/DER. Both formats are explained here.

    WebCrypto uses the r|s format, while MEUCI... has the ASN.1 format. Your signature in r|s format is Base64 encoded:

    QBx3D3b0wJVCGTsHEeyWjhyrRPCaRo20v8lCBtt5/UnlYpqLlUIyHAqztnKHt+IbYMgrGjK3D05pjCx8DR63Ng==
    

    If you use this signature in the JavaScript code, verification is successful.

    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>