How Verify Public Key With Web Crypto API

90 views Asked by At

I can perform verification in Node Crypto without problem and it outputs true, but when i tried with Web Crypto it outputs false without any errors but both using same variables. I can't use Node Crypto because code will run in CF Worker Runtime and it doesn't supports related Node Crypto functions. Why it's happening like this and how can i fix.

This code is using Node Crypto and outputs true.

import crypto from "crypto";

const publicKeyPEM = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEXjyD37iJ6K7dVWCANfrTEJkDFKZt
dlaCMOGuTE3qsy4PF3FqUnDi0EZxty8n6Mb3W3Ahj0ASkF+GwNW8C/ztdQ==
-----END PUBLIC KEY-----`;
const messageToVerify = "hello world";
const signature =
    "MEUCIENzPHGDk+t1inhAvnqPX1OYLfSltYVIv1cipjW2F3CxAiEAzTVrj5CCHChsyeAif0qM6UvX3h0U7BDHhb+XmsXwO/c=";

(() => {
    const verify = crypto.createVerify("SHA256");
    verify.update(messageToVerify);
    const verified = verify.verify(
        publicKeyPEM,
        Buffer.from(signature, "base64")
    );
    console.log(verified);
})();

This code is using Web Crypto API and outputs false.

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;
}

async function importEcdsaKey(pem) {
    const pemHeader = "-----BEGIN PUBLIC KEY-----";
    const pemFooter = "-----END PUBLIC KEY-----";
    const pemContents = pem.substring(
        pemHeader.length,
        pem.length - pemFooter.length - 1
    );

    const binaryDerString = atob(pemContents);
    const binaryDer = str2ab(binaryDerString);

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

function base64ToArrayBuffer(base64String) {
    const binaryString = atob(base64String);
    const bytes = new Uint8Array(binaryString.length);
    for (let i = 0; i < binaryString.length; i++) {
        bytes[i] = binaryString.charCodeAt(i);
    }
    return bytes.buffer;
}

const publicKeyPEM = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEXjyD37iJ6K7dVWCANfrTEJkDFKZt
dlaCMOGuTE3qsy4PF3FqUnDi0EZxty8n6Mb3W3Ahj0ASkF+GwNW8C/ztdQ==
-----END PUBLIC KEY-----`;
const messageToVerify = "hello world";
const signature =
    "MEUCIENzPHGDk+t1inhAvnqPX1OYLfSltYVIv1cipjW2F3CxAiEAzTVrj5CCHChsyeAif0qM6UvX3h0U7BDHhb+XmsXwO/c=";

(async () => {
    const publicKey = await importEcdsaKey(publicKeyPEM);
    const signatureArrayBuffer = base64ToArrayBuffer(signature);
    const data = new TextEncoder().encode(messageToVerify);

    const result = await crypto.subtle.verify(
        {
            name: "ECDSA",
            hash: { name: "SHA-256" },
        },
        publicKey,
        signatureArrayBuffer,
        data
    );

    console.log(result);
})();

1

There are 1 answers

0
Topaco On

WebCrypto requires the ECDSA signature to be in P1363 format, while the signature in the posted example is ASN.1/DER encoded. Both formats are explained in this post.

If the signature is in P1363 format, verification works:

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;
}

async function importEcdsaKey(pem) {
    const pemHeader = "-----BEGIN PUBLIC KEY-----";
    const pemFooter = "-----END PUBLIC KEY-----";
    const pemContents = pem.substring(
        pemHeader.length,
        pem.length - pemFooter.length - 1
    );

    const binaryDerString = atob(pemContents);
    const binaryDer = str2ab(binaryDerString);

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

function base64ToArrayBuffer(base64String) {
    const binaryString = atob(base64String);
    const bytes = new Uint8Array(binaryString.length);
    for (let i = 0; i < binaryString.length; i++) {
        bytes[i] = binaryString.charCodeAt(i);
    }
    return bytes.buffer;
}

const publicKeyPEM = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEXjyD37iJ6K7dVWCANfrTEJkDFKZt
dlaCMOGuTE3qsy4PF3FqUnDi0EZxty8n6Mb3W3Ahj0ASkF+GwNW8C/ztdQ==
-----END PUBLIC KEY-----`;
const messageToVerify = "hello world";
const signature = "Q3M8cYOT63WKeEC+eo9fU5gt9KW1hUi/VyKmNbYXcLHNNWuPkIIcKGzJ4CJ/SozpS9feHRTsEMeFv5eaxfA79w=="; // P1363 format works
//const signature = "MEUCIENzPHGDk+t1inhAvnqPX1OYLfSltYVIv1cipjW2F3CxAiEAzTVrj5CCHChsyeAif0qM6UvX3h0U7BDHhb+XmsXwO/c="; //ASN.1/DER format fails

(async () => {
    const publicKey = await importEcdsaKey(publicKeyPEM);
    const signatureArrayBuffer = base64ToArrayBuffer(signature);
    const data = new TextEncoder().encode(messageToVerify);

    const result = await crypto.subtle.verify(
        {
            name: "ECDSA",
            hash: { name: "SHA-256" },
        },
        publicKey,
        signatureArrayBuffer,
        data
    );

    console.log(result);
})();


Manual conversion of your signature from ASN.1/DER to P1363 format:

The ASN.1/DER encoded signature contains the two parts r and s which are concatenated in P1363 format (s. here).
It must be ensured that r and s both have the length of the order of the generator point (for P-256 this is 32 bytes). If r is too long, leading 0x00 values must be truncated or, if r is too short, it must be padded from the front with 0x00 values.
The manual conversion of your signature is illustrated below:

ASN.1/DER, Base64: MEUCIENzPHGDk+t1inhAvnqPX1OYLfSltYVIv1cipjW2F3CxAiEAzTVrj5CCHChsyeAif0qM6UvX3h0U7BDHhb+XmsXwO/c=
ASN.1/DER, hex:    3045022043733c718393eb758a7840be7a8f5f53982df4a5b58548bf5722a635b61770b1022100cd356b8f90821c286cc9e0227f4a8ce94bd7de1d14ec10c785bf979ac5f03bf7
                   30450220 43733c718393eb758a7840be7a8f5f53982df4a5b58548bf5722a635b61770b1 022100 cd356b8f90821c286cc9e0227f4a8ce94bd7de1d14ec10c785bf979ac5f03bf7
                            43733c718393eb758a7840be7a8f5f53982df4a5b58548bf5722a635b61770b1        cd356b8f90821c286cc9e0227f4a8ce94bd7de1d14ec10c785bf979ac5f03bf7
P1363, hex:        43733c718393eb758a7840be7a8f5f53982df4a5b58548bf5722a635b61770b1cd356b8f90821c286cc9e0227f4a8ce94bd7de1d14ec10c785bf979ac5f03bf7
P1363, Base64:     Q3M8cYOT63WKeEC+eo9fU5gt9KW1hUi/VyKmNbYXcLHNNWuPkIIcKGzJ4CJ/SozpS9feHRTsEMeFv5eaxfA79w==

Of course, the conversion can also be done programmatically. However, WebCrypto is a low level API that does not support this.
Therefore, the conversion must either be implemented yourself or an additional library must be used that either supports the conversion directly or at least features an ASN.1/DER encoder/decoder.