javascriptopensslecdsawebcrypto-api

Web Crypto API ECDSA and OpenSSL


I created ECDSA key pair using openssl

openssl ecparam -name prime256v1 -genkey -noout -out ecdsa_private_key.pem
openssl ec -in ecdsa_private_key.pem -pubout -out ecdsa_public_key.pem

and using text file generated signature

openssl dgst -sha256 -sign ecdsa_private_key.pem -out ecdsa_signature.bin data.txt

and then i trying to verify this signature using js webcrypto i geting false result

import { readFile } from "node:fs/promises"
import { EOL } from "node:os";

/**
 * @param { string } pem 
 */
function pemToArrayBuffer(pem) {
    const lines = pem.trim().split(EOL);
    lines.pop();
    lines.shift();
    return Buffer.from(lines.join(""), "base64");
}

const pemPublicKey = await readFile("./ecdsa_public_key.pem", "utf8")
const publicKeyArrayBuffer = pemToArrayBuffer(pemPublicKey);

const publicKey = await crypto.subtle.importKey(
    'spki',
    publicKeyArrayBuffer,
    {
        name: 'ECDSA',
        namedCurve: 'P-256',
    },
    true,
    ['verify']
);

const data = await readFile("./data.txt");

const signature = await readFile("./ecdsa_signature.bin");

const isValid = await crypto.subtle.verify(
    {
        name: 'ECDSA',
        hash: { name: 'SHA-256' }
    },
    publicKey,
    signature,
    data
);

console.log(isValid); //false

when using openssl everyting is fine

openssl dgst -sha256 -verify ecdsa_public_key.pem -signature ecdsa_signature.bin data.txt

Solution

  • as @Topaco mentioned, problem is in signature format.

    To convert ASN.1/DER signatute to P1363 following function can be used:

    /**
     * @param { Uint8Array } signature
     */
    function toP1363(signature) {
        let i = 0;
        if (signature[i++] !== 0x30) throw new Error("Invalid ASN.1 sequence header");
        const length = signature[i++];
        if (signature.length < length + i) throw new Error("Invalid ASN.1 sequence length");
        //r
        if (signature[i++] != 0x02) throw new Error('Invalid ASN.1 "r" tag');
        let rLength = signature[i++];
        while (signature[i] == 0x00) { i++; rLength--; }
        const r = new Uint8Array(signature.buffer, i, rLength);
        i += rLength;
        //s
        if (signature[i++] != 0x02) throw new Error('Invalid ASN.1 "s" tag');
        let sLength = signature[i++];
        while (signature[i] == 0x00) { i++; sLength--; }
        const s = new Uint8Array(signature.buffer, i, sLength);
        //r|s
        const l = Math.trunc((Math.max(rLength, sLength) + 1) / 2) * 2;
        const result = new Uint8Array(l * 2);
        result.set(r, l - rLength);
        result.set(s, (l * 2) - sLength);
        return result;
    }
    

    after converting signature everything worked out