Skip to content

feat(webauthn): related-origins validation (WebAuthn L3 §5.11)#219

Open
AlfioEmanueleFresta wants to merge 21 commits into
masterfrom
issue-160-related-origins
Open

feat(webauthn): related-origins validation (WebAuthn L3 §5.11)#219
AlfioEmanueleFresta wants to merge 21 commits into
masterfrom
issue-160-related-origins

Conversation

@AlfioEmanueleFresta
Copy link
Copy Markdown
Member

Implements WebAuthn L3 §5.11 "Related Origins": when a request's rp.id is not a registrable suffix of the caller's effective domain, libwebauthn now fetches the RP's .well-known/webauthn document and accepts the request if a listed origin matches the caller.

Closes #160. Stacked on #215; depends on its public-suffix-list trait. Based on #173 by @HarveyOrourke15.

The HTTP fetcher is pluggable via a trait. A reqwest-backed default ships behind the optional related-origins-client cargo feature so the core crate stays HTTP-client-free.

Note: the JSON-request parsing trait becomes async and gains an HTTP client parameter; downstream consumers will need a one-line update at call sites.

@AlfioEmanueleFresta AlfioEmanueleFresta changed the base branch from master to issue-210-dafsa-psl May 17, 2026 18:15
Base automatically changed from issue-210-dafsa-psl to master May 18, 2026 18:42
…nt arg

Adds the http parameter to FromIdlModel::from_idl_model and the
default WebAuthnIDL::from_json; updates the make_credential and
get_assertion impls to take it (currently unused, wired in next
commit). Updates ceremony examples and existing from_json tests to
pass &NoRelatedOriginsClient and .await the calls.
…d get_assertion

When the rp.id is not a registrable suffix of the caller's effective
domain, call validate_related_origins() per WebAuthn L3 §5.11.1. On
success, accept the request; on failure, surface the existing
MismatchingRelyingPartyId variant unchanged so callers' pattern
matches keep working.
reqwest's referer() defaults to true, so on any redirect chain it would
auto-populate Referer with the previous URL, leaking the RP's well-known
URL to the redirect target. The previous empty-valued Referer default
header did not disable this. Drop the header insertion and call
.referer(false) so no Referer is sent on the initial request or any
redirect, matching WebAuthn L3 \xc2\xa75.11.1 step 2 ("without a referrer").
Extend the trait doc to bind impls to status-200-only and unmodified
Content-Type reporting, so a third-party client cannot accidentally feed
a 404 body or a synthesised application/json type to the validator.
The cap is hard-coded inside the validator loop, so external callers
cannot override it. Reduce to a private const and drop the re-export to
avoid committing to the value across breaking changes.
The previous warn!(error = ?err, ...) debug-printed RelatedOriginsError,
which can carry reqwest error text (IP/port) and serde_json text (body
snippets). Add RelatedOriginsError::kind() that returns a static
discriminant and log only that. Downgrade to debug! since most RPs do
not host /.well-known/webauthn and the failure is expected noise.
The previous impl re-parsed the listed URL through Origin::parse, which
rejects userinfo, non-/ paths, queries and fragments. WebAuthn L3
\xc2\xa75.11.1 step 4.f defers to HTML \xc2\xa77.5 same-origin, which compares only
scheme, host and port. Compare those three directly so a listed entry
like "https://example.com/foo" can match the caller. Add a test.
Symmetric to label_cap_blocks_sixth_distinct_label_match. Asserts that
the 5th distinct label still satisfies step 4.e's size < max check, so
an off-by-one regression on the cap is caught by tests.
The body asserts that an IPv6 listed entry is silently skipped at step
4.c/4.d (no registrable label), not that same-origin matches. Rename to
match and rephrase the comment.
…d_origins

Mirror NoRelatedOriginsClient's placement: under the same feature gate,
expose ReqwestRelatedOriginsClient (and HttpPolicy) at the related_origins
module root so consumers do not need the http:: submodule path.
…iginsClient

Using NoRelatedOriginsClient in the bundled examples taught readers the
wrong default. Wire up the reqwest-backed convenience client instead,
gate the three webauthn ceremony examples on the related-origins-client
feature, and update the README run commands.

Also re-exports HttpPolicy and ReqwestRelatedOriginsClient at
ops::webauthn so examples import from a single path.
…egration test

Swap brand.com/app.brand.org for example.org/app.example.com. RFC 2606
reserves example.* for documentation, so it cannot accidentally collide
with a real party. The two-eTLD shape that exercises the related-origins
fetch path is preserved.
…nFetchError

The trait's old return type was the full RelatedOriginsError, but four of
its five variants (UnexpectedContentType, MalformedJson, MalformedDocument,
NoMatchingOrigin) are produced inside validate_related_origins after the
fetch returns. Implementers had no reason to ever emit them.

Introduce WellKnownFetchError with the variants a fetcher can actually
emit (Transport, Status, BodyTooLarge, NotSupported) and let
RelatedOriginsError wrap it via a Fetch variant with #[from]. The reqwest
client now distinguishes non-200 status from transport faults and from
body-cap hits without stringifying everything.

Also drops RelatedOriginsError::kind(); the two debug! call sites switch
to logging the Display form of the error directly.
@AlfioEmanueleFresta AlfioEmanueleFresta force-pushed the issue-160-related-origins branch from 05f055e to 2a4d4e5 Compare May 18, 2026 19:08
@AlfioEmanueleFresta AlfioEmanueleFresta marked this pull request as ready for review May 18, 2026 19:08
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.

Implement Related Origins support (WebAuthn L3 § 5.11)

1 participant