import { describe, expect, it } from "vitest"; import { buildAuthenticatorData, buildClientDataJSON, createAttestationObject, generatePasskeyKeypair, importPasskeyPrivateKey, publicKeyJwkToCose, signPasskeyAssertion, } from "./passkey-crypto"; // Inverse of the DER encoder inside passkey-crypto: parse SEQUENCE { INTEGER r, // INTEGER s } back into a fixed-width r||s buffer of 2*coordSize bytes. function derEcdsaToP1363(der: Uint8Array, coordSize: number): Uint8Array { if (der[0] !== 0x30) throw new Error("not a SEQUENCE"); let off = 2; if (der[1]! & 0x80) off = 2 + (der[1]! & 0x7f); if (der[off] !== 0x02) throw new Error("missing r INTEGER"); const rLen = der[off + 1]!; let r = der.slice(off + 2, off + 2 + rLen); off = off + 2 + rLen; if (der[off] !== 0x02) throw new Error("missing s INTEGER"); const sLen = der[off + 1]!; let s = der.slice(off + 2, off + 2 + sLen); if (r.length > coordSize) r = r.slice(r.length - coordSize); if (s.length > coordSize) s = s.slice(s.length - coordSize); const out = new Uint8Array(coordSize * 2); out.set(r, coordSize - r.length); out.set(s, coordSize * 2 - s.length); return out; } describe("passkey crypto", () => { it("generates importable ES256 passkey material", async () => { const generated = await generatePasskeyKeypair(-7); const privateKey = await crypto.subtle.importKey( "jwk", generated.privateKeyJwk, { name: "ECDSA", namedCurve: "P-256" }, true, ["sign"] ); const pkcs8 = new Uint8Array(await crypto.subtle.exportKey("pkcs8", privateKey)); const imported = await importPasskeyPrivateKey(pkcs8); expect(imported.algorithm).toBe(-7); expect(imported.privateKeyJwk.kty).toBe("EC"); expect(imported.publicKeyCose.length).toBeGreaterThan(32); expect(generated.publicKeySpki.length).toBeGreaterThan(64); }); it("generatePasskeyKeypair returns unique keypairs across calls", async () => { const first = await generatePasskeyKeypair(-7); const second = await generatePasskeyKeypair(-7); expect(first.privateKeyJwk.d).not.toBe(second.privateKeyJwk.d); expect(first.publicKeySpki).not.toEqual(second.publicKeySpki); }); it("builds authenticator data and signs an assertion", async () => { const generated = await generatePasskeyKeypair(-7); const credentialId = crypto.getRandomValues(new Uint8Array(32)); const registrationAuthData = await buildAuthenticatorData({ rpId: "webauthn.io", flags: 0x4d, signCount: 0, credentialId, publicKeyCose: generated.publicKeyCose, }); const assertionAuthData = await buildAuthenticatorData({ rpId: "webauthn.io", flags: 0x05, signCount: 1, }); const clientDataJSON = buildClientDataJSON( "webauthn.get", "ZmFrZS1jaGFsbGVuZ2U", "https://webauthn.io" ); const clientDataHash = new Uint8Array( await crypto.subtle.digest("SHA-256", new Uint8Array(clientDataJSON)) ); const signature = await signPasskeyAssertion( generated.privateKeyJwk, -7, assertionAuthData, clientDataHash ); expect(registrationAuthData.length).toBeGreaterThan(80); expect(assertionAuthData.length).toBe(37); expect(signature.length).toBeGreaterThan(40); }); it("signPasskeyAssertion produces a signature verifiable with the corresponding public key", async () => { const generated = await generatePasskeyKeypair(-7); const authenticatorData = await buildAuthenticatorData({ rpId: "webauthn.io", flags: 0x05, signCount: 1, }); const clientDataJSON = buildClientDataJSON( "webauthn.get", "ZmFrZS1jaGFsbGVuZ2U", "https://webauthn.io" ); const clientDataHash = new Uint8Array( await crypto.subtle.digest("SHA-256", clientDataJSON.buffer as ArrayBuffer) ); const signature = await signPasskeyAssertion( generated.privateKeyJwk, -7, authenticatorData, clientDataHash ); const publicKey = await crypto.subtle.importKey( "spki", generated.publicKeySpki.buffer as ArrayBuffer, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"] ); // signPasskeyAssertion returns DER (WebAuthn-spec format). SubtleCrypto.verify // expects raw IEEE P1363 (r||s) for ECDSA. Convert before verifying. const raw = derEcdsaToP1363(signature, 32); const verified = await crypto.subtle.verify( { name: "ECDSA", hash: "SHA-256" }, publicKey, raw.buffer as ArrayBuffer, new Uint8Array([...authenticatorData, ...clientDataHash]).buffer as ArrayBuffer ); expect(verified).toBe(true); }); it("buildAuthenticatorData embeds the rpId hash, flags, and signCount", async () => { const authData = await buildAuthenticatorData({ rpId: "webauthn.io", flags: 0x05, signCount: 7, }); const expectedHash = new Uint8Array( await crypto.subtle.digest( "SHA-256", new TextEncoder().encode("webauthn.io").buffer as ArrayBuffer ) ); expect(authData.slice(0, 32)).toEqual(expectedHash); expect(authData[32]).toBe(0x05); expect(new DataView(authData.buffer).getUint32(33, false)).toBe(7); }); it("createAttestationObject includes fmt:none and the supplied authData", async () => { const authData = await buildAuthenticatorData({ rpId: "webauthn.io", flags: 0x41, signCount: 0, }); const attestation = createAttestationObject(authData); const ascii = new TextDecoder().decode(attestation); expect(ascii).toContain("fmt"); expect(ascii).toContain("none"); expect(ascii).toContain("authData"); expect(attestation.includes(authData[0])).toBe(true); }); it("publicKeyJwkToCose encodes a P-256 public key into COSE bytes", async () => { const generated = await generatePasskeyKeypair(-7); const cose = publicKeyJwkToCose(generated.privateKeyJwk, -7); expect(cose[0]).toBeGreaterThanOrEqual(0xa0); expect(cose.length).toBeGreaterThan(16); }); });