I am trying to verify a ECDSA, SHA-256 key. But i am unable to do so with the web crypto API. My code looks like this-
export async function verifySignature(
requestData: string,
receivedSignature: string,
publicKey: string
): Promise<boolean> {
try {
const textEncoder = new TextEncoder();
const digest = await crypto.subtle.digest('SHA-256', textEncoder.encode(requestData));
// using digest
// Import the public key
if (importedKey == null) {
importedKey = await crypto.subtle.importKey(
'spki',
stringToArrayBuffer(atob(publicKey)),
{ name: 'ECDSA', namedCurve: 'P-256' },
true,
['verify']
);
}
// Verify the signature
const isValid = await crypto.subtle.verify(
{ name: 'ECDSA', hash: 'SHA-256' },
importedKey,
stringToArrayBuffer(atob(receivedSignature)),
digest
);
return isValid;
} catch (error) {
console.error('Error verifying signature:', error);
return false;
}
}
I am able to verify the signature with the openssl command.
openssl dgst -sha256 -verify public.pem -signature signature.der data.txt
Here is the code sandbox link with required information - https://codesandbox.io/p/sandbox/beautiful-maria-2qkzxq
Any help is really appreciated.. I've spent a day on this already.. :( Thanks in advance.
There are two bugs:
ECDSA applies different signature formats: Your OpenSSL statement needs an ASN.1/DER encoded ECDSA signature, the WebCrypto API requires the ECDSA signature in IEEE P1363 format. The latter is a concatenation of the r and s values (32 bytes each for P-256): r|s.
Here you can find a post describing the two formats (also consider this for larger curves like P-521).
Since I cannot execute your online example (even after logging in: Sandbox not found: ... the Sandbox ... doesn't exist or you don't have the required permissions...), I use the following hex encoded ASN.1/DER encoded signature for illustration, which is verifiable by your OpenSSL statement with the corresponding public key (the spaces are for display purposes only).
According to the description in the links above, it contains r and s:
r s
3045022100 926B0A95405429BBEE633F7E3377532F9BF9331DE67053C94793B2C21CEB0162 0220 473881D69AD9907CD87B047096C048ED387F12DC79D200E8E80C674F440ADFD5
which can be extracted and concatenated, r|s:
926B0A95405429BBEE633F7E3377532F9BF9331DE67053C94793B2C21CEB0162473881D69AD9907CD87B047096C048ED387F12DC79D200E8E80C674F440ADFD5
or Base64 encoded:
kmsKlUBUKbvuYz9+M3dTL5v5Mx3mcFPJR5OywhzrAWJHOIHWmtmQfNh7BHCWwEjtOH8S3HnSAOjoDGdPRArf1Q==
which is the Base64 encoded signature in IEEE P1363 format.
A programmatic instead of this manual conversion is most conveniently possible with a library or an ASN.1/DER encoder/decoder.
As already indicated in the comments, the WebCrypto API hashes implicitly.
If both are taken into account, the signature can then be successfully verified with your code and the matching key (using my own function b642ab()
, since stringToArrayBuffer()
was not posted):
(async () => {
var requestData = 'The quick brown fox jumps over the lazy dog';
var publicKey = 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMpHT+HNKM7zjhx0jZDHyzQlkbLV0xk0H/TFo6gfT23ish58blPNhYrFI51Q/czvkAwCtLZz/6s1n/M8aA9L1Vg==';
var receivedSignature = 'kmsKlUBUKbvuYz9+M3dTL5v5Mx3mcFPJR5OywhzrAWJHOIHWmtmQfNh7BHCWwEjtOH8S3HnSAOjoDGdPRArf1Q=='; // Fix 1: apply signature in P1363 format
function b642ab(base64_string){
return Uint8Array.from(window.atob(base64_string), c => c.charCodeAt(0));
}
// UTF8 encode message
const textEncoder = new TextEncoder();
const message = textEncoder.encode(requestData);
// Import the public key
var importedKey = null;
if (importedKey == null) {
importedKey = await crypto.subtle.importKey(
'spki',
b642ab(publicKey),
{ name: 'ECDSA', namedCurve: 'P-256' },
true,
['verify']
);
}
// Verify the signature
const isValid = await crypto.subtle.verify(
{ name: 'ECDSA', hash: 'SHA-256' },
importedKey,
b642ab(receivedSignature),
message // Fix 2: apply message instead of message hash
);
console.log("Verified: ", isValid);
})();