From 097dab0d9d0f79ea2dcb43ce42064377c2088d98 Mon Sep 17 00:00:00 2001 From: Andrew Bernat Date: Fri, 3 Jul 2026 22:37:26 -0700 Subject: [PATCH 1/2] Store the registered credential in the test server. The registered credential will be used during authentication to validate the challenge against a public key that was registered. --- .../passkeys-browser/test/helpers/ceremony.ts | 49 +++++++++++++++---- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/clients/passkeys-browser/test/helpers/ceremony.ts b/clients/passkeys-browser/test/helpers/ceremony.ts index 5c600988..741b57eb 100644 --- a/clients/passkeys-browser/test/helpers/ceremony.ts +++ b/clients/passkeys-browser/test/helpers/ceremony.ts @@ -18,6 +18,16 @@ interface PendingEntry { rpId: string; } +// StoredCredential is what finishRegistration persists so a later authentication ceremony can +// verify an assertion against it. Keyed by the base64url-encoded credential id. +interface StoredCredential { + publicKey: CryptoKey; + userHandle: string; + username: string; + transports: string[]; + counter: number; +} + /* CeremonyService is a server side service that can: - start registration @@ -29,14 +39,20 @@ A registration is started with a call to startRegistration which will produce a the client must respond to with a call to finishRegistration. */ export class CeremonyService { - // pending is the map of registrations which have been started; but not yet finished. - // Maps challenge id to a PendingEntry structure. - private readonly pending : Map; + // pendingRegistrations is the map of registrations which have been started; but not yet + // finished. Maps challenge id to a PendingEntry structure. + private readonly pendingRegistrations : Map; + // credentials holds every credential that has completed registration, keyed by the + // base64url-encoded credential id. This is what finishAuthentication verifies assertions + // against — without it there would be no public key to check a signature with. + private readonly credentials : Map; + private readonly rpId : string; private readonly allowedUsernames : string[]; constructor(rpId: string,...allowedUsernames: string[]) { - this.pending = new Map(); + this.pendingRegistrations = new Map(); + this.credentials = new Map(); this.rpId = rpId; this.allowedUsernames = allowedUsernames ?? []; } @@ -59,12 +75,12 @@ export class CeremonyService { }; // save the requests which will be validated in finish - this.pending.set(challengeId, pendingEntry); + this.pendingRegistrations.set(challengeId, pendingEntry); return newStartRegistrationResponse(pendingEntry) } async finishRegistration(req: FinishRegistrationRequest): Promise { - const entry = this.pending.get(req.challengeId); + const entry = this.pendingRegistrations.get(req.challengeId); if (!entry) { throw new HttpError(400, { error: `unknown challengeId: ${req.challengeId}` }); } @@ -87,15 +103,28 @@ export class CeremonyService { } const credId = attestation.authData.slice(55, 55 + attestation.credentialIdLength); + const base64CredId = b64u.encode(credId); const flags = attestation.authData[32]!; - this.pending.delete(req.challengeId); + const transports = req.response.response.transports ?? []; + const counter = attestation.dv.getUint32(33); + + this.pendingRegistrations.delete(req.challengeId); + // Remember the credential so a later authentication ceremony has a public key to verify + // an assertion against. + this.credentials.set(base64CredId, { + publicKey: publicKey, + userHandle: entry.userId, + username: entry.username, + transports: transports, + counter: counter, + }); return { credential: { - credentialId: b64u.encode(credId), + credentialId: base64CredId, userHandle: entry.userId, label: req.label ?? "key", - transports: req.response.response.transports ?? [], - counter: attestation.dv.getUint32(33), + transports: transports, + counter: counter, backupEligible: (flags & 0x08) !== 0, backupState: (flags & 0x10) !== 0, authenticatorData: b64u.encode(attestation.authData), From 366e63ec83373a32dbfe4e487c06d99ad0b1a3de Mon Sep 17 00:00:00 2001 From: Andrew Bernat Date: Fri, 3 Jul 2026 22:46:01 -0700 Subject: [PATCH 2/2] Rename StoredCredential -> Credential --- clients/passkeys-browser/test/helpers/ceremony.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/clients/passkeys-browser/test/helpers/ceremony.ts b/clients/passkeys-browser/test/helpers/ceremony.ts index 741b57eb..144110ce 100644 --- a/clients/passkeys-browser/test/helpers/ceremony.ts +++ b/clients/passkeys-browser/test/helpers/ceremony.ts @@ -18,9 +18,9 @@ interface PendingEntry { rpId: string; } -// StoredCredential is what finishRegistration persists so a later authentication ceremony can +// Credential is what finishRegistration persists so a later authentication ceremony can // verify an assertion against it. Keyed by the base64url-encoded credential id. -interface StoredCredential { +interface Credential { publicKey: CryptoKey; userHandle: string; username: string; @@ -45,14 +45,14 @@ export class CeremonyService { // credentials holds every credential that has completed registration, keyed by the // base64url-encoded credential id. This is what finishAuthentication verifies assertions // against — without it there would be no public key to check a signature with. - private readonly credentials : Map; + private readonly credentials : Map; private readonly rpId : string; private readonly allowedUsernames : string[]; constructor(rpId: string,...allowedUsernames: string[]) { this.pendingRegistrations = new Map(); - this.credentials = new Map(); + this.credentials = new Map(); this.rpId = rpId; this.allowedUsernames = allowedUsernames ?? []; }