Skip to content

feat(webauthn): support attestation and validate it against FIDO metadata - #206

Merged
Bccorb merged 1 commit into
mainfrom
feat/attestation-and-metadata
Aug 30, 2026
Merged

feat(webauthn): support attestation and validate it against FIDO metadata#206
Bccorb merged 1 commit into
mainfrom
feat/attestation-and-metadata

Conversation

@Bccorb

@Bccorb Bccorb commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Closes #168 and #169. Requires @seamless-auth/types 0.14.0, already released.

Why both in one change

The two issues describe one mechanism, and each says it pairs with the other. It
is not a scope decision so much as a fact about the library: metadata validation
lives inside the attestation verifiers, in the branch that only runs when a
statement with x5c is present.

// verifyAttestationPacked.js
const statement = await MetadataService.getStatement(aaguid);
if (statement) { /* validate against metadata */ }
else { /* fall back to root certificate path */ }

With attestationType: 'none' there is no statement, so that branch never
executes. #168 alone would have shipped code that provably does nothing, and two
of its three acceptance criteria cannot be met without #169. They also share one
config key and would have had to merge together.

The change

authenticator_policy.attestation, none or direct. none stays the default:
attestation identifies a user's hardware, which is a real privacy cost, and a
consumer deployment has no use for it. Under direct the metadata service is
prepared at startup and the library's verifiers validate against it.

Credentials record attestationFormat and attestationVerified, so an audit can
distinguish an unattested credential from one whose attestation was actually
checked. Neither is recoverable later, which is why it is captured now.

Open question 1, answered as configuration

The readiness analysis flagged "what happens to an authenticator the metadata
service does not list" as an open positioning decision rather than a technical
one.

It turns out to map exactly onto the library's verificationMode: permissive
registers an unlisted authenticator, strict refuses it. Rather than pick a
position for every deployment, it is exposed as requireKnownAuthenticator,
defaulting to permissive so an upgrade cannot lock anyone out of their own
authenticator.

The failure posture, which is the part worth arguing with

The metadata service never blocks startup. A blob that cannot be fetched is a
degraded state, not a reason an authentication server should refuse to start. It
is logged and registration continues without metadata validation.

requireKnownAuthenticator is deliberately not honoured in that state. The
strict reading would be to refuse every registration when metadata is
unavailable. I judged that worse than the risk it guards against: a transient
network failure at boot would otherwise stop every user in the organisation from
enrolling, which is a self-inflicted outage in exchange for a window during which
an unlisted authenticator could register. isMetadataServiceReady() reports
which state the process is in so the degraded case is observable.

This is a security tradeoff rather than an obvious call, and it is the thing to
push back on if you read it differently.

Also worth knowing

Blob refresh needs no scheduling from us. getStatement coordinates cache
updates from the blob's own nextUpdate, so #168's "MDS blob refresh is
scheduled" is satisfied by the library rather than by a cron.

Changing attestation needs a restart, since the service is prepared once at
boot. Documented in configuration.md and .env.example.

Verification

93 test files, 1025 passed, 1 skipped. Coverage 98.79% lines. typecheck,
lint, format:check clean. Migration verified up and down against a real
Postgres 17.

Five tests on the bootstrap alone, covering the no-attestation path doing no
network work at all, permissive and strict wiring, an unreachable blob, and
unreadable config. Four more on the controller for requesting attestation and
persisting the outcome.

Not done here

Exercising all four attestation formats end to end, which #169 lists. That needs
real attestation fixtures and is properly part of the conformance run in #172,
where the tools supply them.

…data

Registration hardcoded attestationType 'none', so authenticators never identified
themselves and there was nothing for the FIDO Metadata Service to validate. FIDO
Server Requirements v2.3 requires validating attestation certificate chains and
supporting validation through that service.

authenticator_policy.attestation now chooses, defaulting to none so consumer
deployments are unaffected and nobody pays the privacy cost of identifying a
user's hardware without needing to. Under direct the metadata service is prepared
at startup and the library's attestation verifiers validate against it.

requireKnownAuthenticator decides what happens to an authenticator the metadata
service does not list. That was the open positioning question in the readiness
analysis, and it maps onto the library's permissive and strict verification
modes, so it is exposed as configuration rather than decided here. It defaults to
permissive.

Credentials record attestationFormat and attestationVerified, so an audit can
tell an unattested credential from one whose attestation was checked.

The metadata service never blocks startup. A blob that cannot be fetched is a
degraded state, not a reason to refuse to start, and requireKnownAuthenticator is
deliberately not honoured in that state because refusing every registration on a
transient network failure is worse than the risk it guards against.

Migration verified up and down against Postgres 17.

Closes #168
Closes #169
@Bccorb
Bccorb merged commit 8b74a80 into main Aug 30, 2026
4 checks passed
@Bccorb
Bccorb deleted the feat/attestation-and-metadata branch August 30, 2026 11:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wire the FIDO Metadata Service and validate attestation certificate chains

1 participant