SPB Git

Repository Public

passkey.ts 3.0 KB 40 lines typescript

1// Software passkey for tests: produces real WebAuthn assertions (ES256) that the production verifier checks.2import { createHash, createSign, generateKeyPairSync, type KeyObject } from 'node:crypto';3import type { AuthenticationResponseJSON } from '@simplewebauthn/server';45export class TestPasskey {6  readonly id: string;7  private priv: KeyObject;8  readonly publicKey: string; // COSE, base64url9  counter = 0;10  constructor(public rpId: string, public origin: string) {11    const { privateKey, publicKey } = generateKeyPairSync('ec', { namedCurve: 'P-256' });12    this.priv = privateKey;13    const jwk = publicKey.export({ format: 'jwk' });14    const x = Buffer.from(jwk.x!, 'base64url'), y = Buffer.from(jwk.y!, 'base64url');15    this.publicKey = Buffer.concat([Buffer.from('a5010203262001215820', 'hex'), x, Buffer.from('225820', 'hex'), y]).toString('base64url');16    this.id = createHash('sha256').update(x).digest().subarray(0, 20).toString('base64url');17  }18  /** WebAuthn registration response with attestation "none" (as produced by platform passkeys). */19  register(challengeB64u: string): any {20    const clientDataJSON = Buffer.from(JSON.stringify({ type: 'webauthn.create', challenge: challengeB64u, origin: this.origin, crossOrigin: false }));21    const credId = Buffer.from(this.id, 'base64url'), cose = Buffer.from(this.publicKey, 'base64url');22    const len = Buffer.alloc(2); len.writeUInt16BE(credId.length);23    const authData = Buffer.concat([createHash('sha256').update(this.rpId).digest(), Buffer.from([0x45]), Buffer.alloc(4), Buffer.alloc(16), len, credId, cose]);24    const att = Buffer.concat([Buffer.from('a363666d74646e6f6e656761747453746d74a0686175746844617461', 'hex'), Buffer.from([0x58, authData.length]), authData]);25    return { id: this.id, rawId: this.id, type: 'public-key', clientExtensionResults: {},26      response: { clientDataJSON: clientDataJSON.toString('base64url'), attestationObject: att.toString('base64url'), transports: ['internal'] } };27  }28  credential() { return { id: this.id, publicKey: this.publicKey, counter: 0, transports: ['internal'] }; }29  assert(challenge: Uint8Array, opts: { origin?: string; uv?: boolean } = {}): AuthenticationResponseJSON {30    const clientDataJSON = Buffer.from(JSON.stringify({ type: 'webauthn.get', challenge: Buffer.from(challenge).toString('base64url'), origin: opts.origin ?? this.origin, crossOrigin: false }));31    const ctr = Buffer.alloc(4); ctr.writeUInt32BE(++this.counter);32    const authData = Buffer.concat([createHash('sha256').update(this.rpId).digest(), Buffer.from([opts.uv === false ? 0x01 : 0x05]), ctr]);33    const sig = createSign('SHA256').update(Buffer.concat([authData, createHash('sha256').update(clientDataJSON).digest()])).sign(this.priv);34    return {35      id: this.id, rawId: this.id, type: 'public-key', clientExtensionResults: {},36      response: { clientDataJSON: clientDataJSON.toString('base64url'), authenticatorData: authData.toString('base64url'), signature: sig.toString('base64url') },37    };38  }39}40
Files main
Menu

Theme