Hvordan verifisere en ECDSA-signatur ved hjelp av Web Crypto
Av Mick Hansen den 23. februar 2024
2 min lesetid

Hvis du har prøvd å implementere WebAuthn via API-et for autentisering som leveres av nettleseren, har du kanskje hatt problemer med å verifisere en ES256-signatur ved hjelp av Web Crypto eller node crypto.
ES256 er en av algoritmene som anbefales i spesifikasjonen for webautentisering. Hvis du følger den anbefalte rekkefølgen, må du verifisere en ECDSA-signatur (når enheter støtter det).
Kanskje du har jobbet med alle WebAuthn-egenskapene:
- konvertert den COSE-kodede offentlige nøkkelen til et format som støttes av Web Crypto (for eksempel JWK)
- Genererte de riktige inndataene ved å kombinere authenticatorData-bytes og hashen til clientDataJSON
... og signaturen kan fortsatt ikke verifiseres.
Hvorfor ikke?
Hovedårsaken er at Web Crypto forventer at ECDSA-signaturer skal leveres i r|s-format, mens WebAuthn, i henhold til spesifikasjonen, produserer en ASN.1 DER.
La oss gå gjennom de få trinnene som kreves for å konvertere og verifisere en ASN.1-formatert signatur.
Noen få verktøymetoder
Når vi arbeider med binære buffere, trenger vi en hjelpemetode for å slå sammen to buffere:
function mergeBuffer(buffer1: ArrayBuffer, buffer2: ArrayBuffer) {
const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
tmp.set(new Uint8Array(buffer1), 0);
tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
return tmp.buffer;
}
Vi trenger også en måte å dekode ASN.1 DER ECDSA-Sig-Value på, som ganske enkelt er en ASN.1-sekvens som inneholder to heltall:
function readAsn1IntegerSequence(input: Uint8Array) {
if (input[0] !== 0x30) throw new Error('Input is not an ASN.1 sequence');
const seqLength = input[1];
const elements : Uint8Array[] = [];
let current = input.slice(2, 2 + seqLength);
while (current.length > 0) {
const tag = current[0];
if (tag !== 0x02) throw new Error('Expected ASN.1 sequence element to be an INTEGER');
const elLength = current[1];
elements.push(current.slice(2, 2 + elLength));
current = current.slice(2 + elLength);
}
return elements;
}
Tar hensyn til binær formatering
ASN.1 DER og r|s bruker subtilt forskjellige polstringer og binære kodingsteknikker, så vi må utføre noen kontroller og modifikasjoner av byte-arrayene.
Følgende kode er tilpasset fra https://github.com/kjur/jsrsasign/blob/58bb24192f501927014b67911bbde8ef27532319/src/ecdsa-modified-1.0.js#L760 for å fungere med binære matriser i stedet for hex-strenger.
function convertEcdsaAsn1Signature(input : Uint8Array) {
const elements = readAsn1IntegerSequence(input);
if (elements.length !== 2) throw new Error('Expected 2 ASN.1 sequence elements');
let [r, s] = elements;
// R and S length is assumed multiple of 128bit.
// If leading is 0 and modulo of length is 1 byte then
// leading 0 is for two's complement and will be removed.
if (r[0] === 0 && r.byteLength % 16 == 1) {
r = r.slice(1);
}
if (s[0] === 0 && s.byteLength % 16 == 1) {
s = s.slice(1);
}
// R and S length is assumed multiple of 128bit.
// If missing a byte then it will be padded by 0.
if ((r.byteLength % 16) == 15) {
r = new Uint8Array(mergeBuffer(new Uint8Array([0]), r));
}
if ((s.byteLength % 16) == 15) {
s = new Uint8Array(mergeBuffer(new Uint8Array([0]), s));
}
// If R and S length is not still multiple of 128bit,
// then error
if (r.byteLength % 16 != 0) throw Error("unknown ECDSA sig r length error");
if (s.byteLength % 16 != 0) throw Error("unknown ECDSA sig s length error");
return mergeBuffer(r, s);
}
Sette det hele sammen
Nå har vi alle bitene som kreves for å konvertere og verifisere en WebAuthn ECDSA-signatur ved hjelp av Web Crypto (med unntak av konverteringen av en COSE-kodet offentlig nøkkel, men det kommer vi tilbake til i en senere artikkel).
const key : CryptoKey = ...;
const response : AuthenticatorAssertionResponse = ...;
const hashedClientDataJSON = await globalThis.crypto.subtle.digest('SHA-256', response.clientDataJSON);
const data = mergeBuffer(response.authenticatorData, hashedClientDataJSON);
const signature = convertEcdsaAsn1Signature(new Uint8Array(response.signature));
const verified = await globalThis.crypto.subtle.verify({
name: 'ECDSA',
hash: 'ES256'
}, key, signature, data);
Idura utvikler produkter for digital identitet og signaturer ved hjelp av WebAuthn, og vi ansetter!
Table of contents
Disse relaterte artiklene

Nullkunnskapsbevis: En nybegynnerveiledning

Forståelse av JWT-validering: En praktisk guide med kodeeksempler
Sign up for our newsletter
Stay up to date on industry news and insights