passkey-crypto.test.ts

4.9 KB · raw

1import { describe, expect, it } from "vitest";23import {4  buildAuthenticatorData,5  buildClientDataJSON,6  createAttestationObject,7  generatePasskeyKeypair,8  importPasskeyPrivateKey,9  publicKeyJwkToCose,10  signPasskeyAssertion,11} from "./passkey-crypto";1213describe("passkey crypto", () => {14  it("generates importable ES256 passkey material", async () => {15    const generated = await generatePasskeyKeypair(-7);16    const privateKey = await crypto.subtle.importKey(17      "jwk",18      generated.privateKeyJwk,19      { name: "ECDSA", namedCurve: "P-256" },20      true,21      ["sign"]22    );23    const pkcs8 = new Uint8Array(await crypto.subtle.exportKey("pkcs8", privateKey));24    const imported = await importPasskeyPrivateKey(pkcs8);2526    expect(imported.algorithm).toBe(-7);27    expect(imported.privateKeyJwk.kty).toBe("EC");28    expect(imported.publicKeyCose.length).toBeGreaterThan(32);29    expect(generated.publicKeySpki.length).toBeGreaterThan(64);30  });3132  it("generatePasskeyKeypair returns unique keypairs across calls", async () => {33    const first = await generatePasskeyKeypair(-7);34    const second = await generatePasskeyKeypair(-7);3536    expect(first.privateKeyJwk.d).not.toBe(second.privateKeyJwk.d);37    expect(first.publicKeySpki).not.toEqual(second.publicKeySpki);38  });3940  it("builds authenticator data and signs an assertion", async () => {41    const generated = await generatePasskeyKeypair(-7);42    const credentialId = crypto.getRandomValues(new Uint8Array(32));43    const registrationAuthData = await buildAuthenticatorData({44      rpId: "webauthn.io",45      flags: 0x4d,46      signCount: 0,47      credentialId,48      publicKeyCose: generated.publicKeyCose,49    });50    const assertionAuthData = await buildAuthenticatorData({51      rpId: "webauthn.io",52      flags: 0x05,53      signCount: 1,54    });55    const clientDataJSON = buildClientDataJSON(56      "webauthn.get",57      "ZmFrZS1jaGFsbGVuZ2U",58      "https://webauthn.io"59    );60    const clientDataHash = new Uint8Array(61      await crypto.subtle.digest("SHA-256", new Uint8Array(clientDataJSON))62    );63    const signature = await signPasskeyAssertion(64      generated.privateKeyJwk,65      -7,66      assertionAuthData,67      clientDataHash68    );6970    expect(registrationAuthData.length).toBeGreaterThan(80);71    expect(assertionAuthData.length).toBe(37);72    expect(signature.length).toBeGreaterThan(40);73  });7475  it("signPasskeyAssertion produces a signature verifiable with the corresponding public key", async () => {76    const generated = await generatePasskeyKeypair(-7);77    const authenticatorData = await buildAuthenticatorData({78      rpId: "webauthn.io",79      flags: 0x05,80      signCount: 1,81    });82    const clientDataJSON = buildClientDataJSON(83      "webauthn.get",84      "ZmFrZS1jaGFsbGVuZ2U",85      "https://webauthn.io"86    );87    const clientDataHash = new Uint8Array(88      await crypto.subtle.digest("SHA-256", clientDataJSON.buffer as ArrayBuffer)89    );90    const signature = await signPasskeyAssertion(91      generated.privateKeyJwk,92      -7,93      authenticatorData,94      clientDataHash95    );96    const publicKey = await crypto.subtle.importKey(97      "spki",98      generated.publicKeySpki.buffer as ArrayBuffer,99      { name: "ECDSA", namedCurve: "P-256" },100      false,101      ["verify"]102    );103104    const verified = await crypto.subtle.verify(105      { name: "ECDSA", hash: "SHA-256" },106      publicKey,107      signature.buffer as ArrayBuffer,108      new Uint8Array([...authenticatorData, ...clientDataHash]).buffer as ArrayBuffer109    );110111    expect(verified).toBe(true);112  });113114  it("buildAuthenticatorData embeds the rpId hash, flags, and signCount", async () => {115    const authData = await buildAuthenticatorData({116      rpId: "webauthn.io",117      flags: 0x05,118      signCount: 7,119    });120    const expectedHash = new Uint8Array(121      await crypto.subtle.digest(122        "SHA-256",123        new TextEncoder().encode("webauthn.io").buffer as ArrayBuffer124      )125    );126127    expect(authData.slice(0, 32)).toEqual(expectedHash);128    expect(authData[32]).toBe(0x05);129    expect(new DataView(authData.buffer).getUint32(33, false)).toBe(7);130  });131132  it("createAttestationObject includes fmt:none and the supplied authData", async () => {133    const authData = await buildAuthenticatorData({134      rpId: "webauthn.io",135      flags: 0x41,136      signCount: 0,137    });138    const attestation = createAttestationObject(authData);139    const ascii = new TextDecoder().decode(attestation);140141    expect(ascii).toContain("fmt");142    expect(ascii).toContain("none");143    expect(ascii).toContain("authData");144    expect(attestation.includes(authData[0])).toBe(true);145  });146147  it("publicKeyJwkToCose encodes a P-256 public key into COSE bytes", async () => {148    const generated = await generatePasskeyKeypair(-7);149    const cose = publicKeyJwkToCose(generated.privateKeyJwk, -7);150151    expect(cose[0]).toBeGreaterThanOrEqual(0xa0);152    expect(cose.length).toBeGreaterThan(16);153  });154});