import { describe, expect, it } from "vitest"; import { base64UrlToBytes, bytesToArrayBuffer, bytesToBase64Url, utf8ToBytes, } from "../../src/crypto/encoding"; import { verifyAssertion } from "../../src/webauthn/verify"; import type { CborValue } from "../../src/webauthn/cbor"; function rawEcdsaToDer(raw: Uint8Array): Uint8Array { const integer = (part: Uint8Array): number[] => { let offset = 0; while (offset < part.length - 1 && part[offset] === 0) offset += 1; const value = [...part.slice(offset)]; if (value[0]! & 0x80) value.unshift(0); return [0x02, value.length, ...value]; }; const r = integer(raw.slice(0, 32)); const s = integer(raw.slice(32)); return Uint8Array.from([0x30, r.length + s.length, ...r, ...s]); } describe("assertion verification", () => { it("checks ceremony bindings and a WebAuthn DER ECDSA signature", async () => { const pair = await crypto.subtle.generateKey( { name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"], ); const jwk = await crypto.subtle.exportKey("jwk", pair.publicKey); const challenge = bytesToBase64Url( crypto.getRandomValues(new Uint8Array(32)), ); const origin = "https://auth.example.test"; const rpId = "auth.example.test"; const clientBytes = utf8ToBytes( JSON.stringify({ type: "webauthn.get", challenge, origin, crossOrigin: false, }), ); const rpHash = new Uint8Array( await crypto.subtle.digest( "SHA-256", bytesToArrayBuffer(utf8ToBytes(rpId)), ), ); const authenticator = Uint8Array.from([...rpHash, 0x05, 0, 0, 0, 7]); const clientHash = new Uint8Array( await crypto.subtle.digest("SHA-256", bytesToArrayBuffer(clientBytes)), ); const signed = Uint8Array.from([...authenticator, ...clientHash]); const rawSignature = new Uint8Array( await crypto.subtle.sign( { name: "ECDSA", hash: "SHA-256" }, pair.privateKey, bytesToArrayBuffer(signed), ), ); const cose = new Map([ [1, 2], [3, -7], [-1, 1], [-2, base64UrlToBytes(jwk.x!)], [-3, base64UrlToBytes(jwk.y!)], ]); const result = await verifyAssertion({ clientDataJSON: bytesToBase64Url(clientBytes), authenticatorData: bytesToBase64Url(authenticator), signature: bytesToBase64Url(rawEcdsaToDer(rawSignature)), credentialPublicKey: cose, expectedChallenge: challenge, expectedOrigin: origin, expectedRpId: rpId, requireUserVerification: true, previousSignCount: 6, }); expect(result.verified).toBe(true); expect(result.checks.every((check) => check.status !== "fail")).toBe(true); }); it("fails expected origin independently", async () => { await expect( verifyAssertion({ clientDataJSON: bytesToBase64Url( utf8ToBytes( JSON.stringify({ type: "webauthn.get", challenge: "YQ", origin: "https://wrong.test", }), ), ), authenticatorData: bytesToBase64Url( Uint8Array.from([...new Uint8Array(32), 1, 0, 0, 0, 0]), ), signature: "", credentialPublicKey: "{}", expectedChallenge: "YQ", expectedOrigin: "https://right.test", expectedRpId: "right.test", }), ).resolves.toMatchObject({ verified: false }); }); });