Repository Public
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