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);
})();
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:
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:
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.