Skip to content

Commit 0661c81

Browse files
feat: typed extension inputs and outputs (#734)
Extensions are modelled as typed Go values rather than an untyped map. Every extension with a defined client input gains a dedicated `webauthn.WithExtension<Name>` option applied via `WithExtensions` or `WithAssertionExtensions`, and its client and authenticator outputs are decoded into typed fields of `protocol.AuthenticationExtensionsClientOutputs` and `protocol.AuthenticatorExtensionOutputs`. An identifier with no dedicated option is still reachable verbatim through `webauthn.WithExtension`, and any output returned under it is preserved in the relevant `Extra` map rather than discarded. `SessionData` now records which extensions the Relying Party requested so the finish step can reject client outputs that were never solicited, with `Config.ExtensionsUnsolicitedOutputPolicy` relaxing that for deployments whose clients are known to return outputs unprompted. The msgp encoding of the session carries a shadow type for the new extension state, and go 1.24 is dropped from the supported versions. BREAKING CHANGE: `RegistrationOption` and `LoginOption` now return an error, so any option implemented outside this module must be adjusted, and `WithExtensions` and `WithAssertionExtensions` accept extension options in place of a map. `protocol.AuthenticationExtensions` and `protocol.AuthenticationExtensionsClientOutputs` are structs rather than `map[string]any`, `protocol.Extensions` is removed, and `ParsedPublicKeyCredential.GetAppID` takes a `protocol.SessionExtensions`. `SessionData.Extensions` changes both its type and its encoded representation, so a session persisted by an earlier version cannot be decoded by this one, and a client extension output the Relying Party did not request now fails the ceremony unless `Config.ExtensionsUnsolicitedOutputPolicy` says otherwise.
1 parent 99bbbdb commit 0661c81

42 files changed

Lines changed: 7122 additions & 471 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ which are not available in that version. The current intentionally supported ver
3232

3333
- go 1.26
3434
- go 1.25
35-
- go 1.24
3635

3736
## Status
3837

@@ -146,34 +145,60 @@ represents when the format was introduced into the spec.
146145

147146
### Extensions
148147

148+
Extensions with a typed input have a dedicated functional option (`webauthn.WithExtension<Name>`) taking typed
149+
Go values, applied via `webauthn.WithExtensions` (registration) or `webauthn.WithAssertionExtensions`
150+
(authentication); a typed client output is exposed as a field of `protocol.AuthenticationExtensionsClientOutputs`,
151+
and a typed authenticator output as a field of `protocol.AuthenticatorExtensionOutputs`. An identifier with no
152+
dedicated option is not modelled: it can still be set on input with the generic `webauthn.WithExtension`, which
153+
conveys the value to the client verbatim, and any output returned under that identifier is preserved in the
154+
relevant `Extra` map rather than dropped.
155+
149156
Standardized and Specification Listed Extensions:
150157

151-
| Extension | Identifier | Supported (Registration) | Supported (Authentication) | Level |
152-
|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:--------------:|:------------------------:|:--------------------------:|:-----:|
153-
| [§10.1.1 FIDO AppID Extension](https://www.w3.org/TR/webauthn/#sctn-appid-extension) | `appid` | N/A[^2] | Yes (manual) | 3 (1) |
154-
| [§10.1.2 FIDO AppID Exclusion Extension](https://www.w3.org/TR/webauthn/#sctn-appid-exclude-extension) | `appidExclude` | Yes (manual) | N/A[^1] | 3 (1) |
155-
| [§10.1.3 Credential Properties Extension](https://www.w3.org/TR/webauthn-3/#sctn-authenticator-credential-properties-extension) | `credProps` | Yes (manual) | N/A[^1] | 3 (2) |
156-
| [§10.1.5 Large Blob Storage Extension](https://www.w3.org/TR/webauthn/#sctn-large-blob-extension) | `largeBlob` | Yes (manual) | Yes (manual) | 3 (2) |
157-
158-
CTAP2 Extensions Which Are Largely unsupported:
159-
160-
| Extension | Identifier | Supported (Registration) | Supported (Authentication) |
161-
|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:---------------------:|:------------------------:|:--------------------------:|
162-
| [Credential Protection Extension](https://fidoalliance.org/specs/fido-v2.2-ps-20250714/fido-client-to-authenticator-protocol-v2.2-ps-20250714.html#sctn-credProtect-extension) | `credProtect` | Yes (manual) | N/A[^1] |
163-
| [Credential Blob Extension](https://fidoalliance.org/specs/fido-v2.2-ps-20250714/fido-client-to-authenticator-protocol-v2.2-ps-20250714.html#sctn-credBlob-extension) | `credBlob` | Yes (manual) | Yes (manual) |
164-
| [Large Blob Key Extension](https://fidoalliance.org/specs/fido-v2.2-ps-20250714/fido-client-to-authenticator-protocol-v2.2-ps-20250714.html#sctn-largeBlobKey-extension) | `largeBlobKey` | Yes (manual) | Yes (manual) |
165-
| [Minimum PIN Length Extension](https://fidoalliance.org/specs/fido-v2.2-ps-20250714/fido-client-to-authenticator-protocol-v2.2-ps-20250714.html#sctn-minpinlength-extension) | `minPinLength` | Yes (manual) | Yes (manual) |
166-
| [PIN Complexity Extension](https://fidoalliance.org/specs/fido-v2.2-ps-20250714/fido-client-to-authenticator-protocol-v2.2-ps-20250714.html#sctn-pincomplexitypolicy-extension) | `pinComplexityPolicy` | Yes (manual) | N/A[^1] |
167-
| [HMAC Secret Extension](https://fidoalliance.org/specs/fido-v2.2-ps-20250714/fido-client-to-authenticator-protocol-v2.2-ps-20250714.html#sctn-hmac-secret-extension) | `hmac-secret` | Yes (manual) | Yes (manual) |
168-
| [HMAC Secret MakeCredential Extension](https://fidoalliance.org/specs/fido-v2.2-ps-20250714/fido-client-to-authenticator-protocol-v2.2-ps-20250714.html#sctn-hmac-secret-make-cred-extension) | `hmac-secret-mc` | N/A[^2] | Yes (manual) |
169-
| [Third-Party Payment Authentication Extension](https://fidoalliance.org/specs/fido-v2.2-ps-20250714/fido-client-to-authenticator-protocol-v2.2-ps-20250714.html#sctn-thirdPartyPayment-extension) | `thirdPartyPayment` | Yes (manual) | Yes (manual) |
158+
| Extension | Identifier | Registration | Authentication | Level |
159+
|:-------------------------------------------------------------------------------------------------------------------------------:|:--------------:|:---------------------------------------------:|:-----------------------------------------------------------:|:-----:|
160+
| [§10.1.1 FIDO AppID Extension](https://www.w3.org/TR/webauthn-3/#sctn-appid-extension) | `appid` | N/A[^2] | `WithExtensionAppID` | 3 (1) |
161+
| [§10.1.2 FIDO AppID Exclusion Extension](https://www.w3.org/TR/webauthn-3/#sctn-appid-exclude-extension) | `appidExclude` | `WithExtensionAppIDExclude` | N/A[^1] | 3 (1) |
162+
| [§10.1.3 Credential Properties Extension](https://www.w3.org/TR/webauthn-3/#sctn-authenticator-credential-properties-extension) | `credProps` | `WithExtensionCredProps` | N/A[^1] | 3 (2) |
163+
| [§10.1.4 Pseudo-Random Function Extension](https://www.w3.org/TR/webauthn-3/#prf-extension) | `prf` | `WithExtensionPRF`, `WithExtensionPRFSupport` | `WithExtensionPRF`, `WithExtensionPRFSupport` | 3 (2) |
164+
| [§10.1.5 Large Blob Storage Extension](https://www.w3.org/TR/webauthn-3/#sctn-large-blob-extension) | `largeBlob` | `WithExtensionLargeBlobSupport` | `WithExtensionLargeBlobRead`, `WithExtensionLargeBlobWrite` | 3 (2) |
165+
166+
CTAP 2.1 / CTAP 2.2 / CTAP 2.3 Extensions registered in the IANA ["WebAuthn Extension Identifiers"](https://www.iana.org/assignments/webauthn/webauthn.xhtml)
167+
registry:
168+
169+
| Extension | Identifier | Registration | Authentication |
170+
|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:------------------------------------:|:-----------------------------------------:|:----------------------------:|
171+
| [Credential Protection Extension](https://fidoalliance.org/specs/fido-v2.3-ps-20260226/fido-client-to-authenticator-protocol-v2.3-ps-20260226.html#sctn-credProtect-extension) | `credProtect` | `WithExtensionCredentialProtectionPolicy` | N/A[^1] |
172+
| [Credential Blob Extension](https://fidoalliance.org/specs/fido-v2.3-ps-20260226/fido-client-to-authenticator-protocol-v2.3-ps-20260226.html#sctn-credBlob-extension) | `credBlob` / `getCredBlob` | `WithExtensionCredBlob` | `WithExtensionGetCredBlob` |
173+
| [Minimum PIN Length Extension](https://fidoalliance.org/specs/fido-v2.3-ps-20260226/fido-client-to-authenticator-protocol-v2.3-ps-20260226.html#sctn-minpinlength-extension) | `minPinLength` | `WithExtensionMinPinLength` | N/A[^1] |
174+
| [HMAC Secret Extension](https://fidoalliance.org/specs/fido-v2.3-ps-20260226/fido-client-to-authenticator-protocol-v2.3-ps-20260226.html#sctn-hmac-secret-extension) | `hmacCreateSecret` / `hmacGetSecret` | `WithExtensionHMACCreateSecret` | `WithExtensionHMACGetSecret` |
175+
| [User Verification Method Extension](https://www.iana.org/assignments/webauthn/webauthn.xhtml)[^5] | `uvm` | `WithExtensionUVM` | `WithExtensionUVM` |
176+
| [Large Blob Key Extension](https://fidoalliance.org/specs/fido-v2.3-ps-20260226/fido-client-to-authenticator-protocol-v2.3-ps-20260226.html#sctn-largeBlobKey-extension) | `largeBlobKey` | Not modelled[^3] | Not modelled[^3] |
177+
| [PIN Complexity Extension](https://fidoalliance.org/specs/fido-v2.3-ps-20260226/fido-client-to-authenticator-protocol-v2.3-ps-20260226.html#sctn-pincomplexitypolicy-extension) | `pinComplexityPolicy` | Not modelled[^4] | N/A[^1] |
178+
| [HMAC Secret MakeCredential Extension](https://fidoalliance.org/specs/fido-v2.3-ps-20260226/fido-client-to-authenticator-protocol-v2.3-ps-20260226.html#sctn-hmac-secret-make-cred-extension) | `hmac-secret-mc` | Not modelled[^4] | N/A[^1] |
179+
| [Third-Party Payment Authentication Extension](https://fidoalliance.org/specs/fido-v2.3-ps-20260226/fido-client-to-authenticator-protocol-v2.3-ps-20260226.html#sctn-thirdPartyPayment-extension) | `thirdPartyPayment` | Not modelled[^4] | Not modelled[^4] |
180+
181+
The CTAP authenticator data also carries a `hmac-secret` extension output identifier (`protocol.ExtensionHMACSecret`),
182+
distinct from the `hmacCreateSecret` / `hmacGetSecret` client-facing identifiers above; it is decoded automatically
183+
into `AuthenticatorExtensionOutputs.HMACSecret` / `HMACSecretOutput` and is not something a Relying Party requests.
170184

171185
[^1]: This extension is only applicable during Registration.
172186
[^2]: This extension is only applicable during Authentication.
187+
[^3]: Deliberately not modelled. CTAP 2.3 §12.3 defines no client extension input, output, or processing for
188+
`largeBlobKey`: it is a CTAP response member returned directly to the platform, not to the Relying Party.
189+
`protocol.ExtensionLargeBlobKey` is kept as a documented identifier and remains reachable verbatim via
190+
`webauthn.WithExtension`.
191+
[^4]: Not modelled. Set on input with `webauthn.WithExtension`; an output returned under this identifier is
192+
preserved in the outputs' `Extra` map.
193+
[^5]: `uvm` was deprecated from the core WebAuthn spec text at Level 3 (see the deprecated extensions table
194+
below) but remains registered as a CTAP 2.1/2.2/2.3 extension in the IANA registry and is still forwarded by
195+
several clients, hence a dedicated option.
173196

174197
Extensions that have been deprecated and removed from the spec. The deprecated level is the first spec level that did
175-
not include the extension. These are all technically supported by the extensions map, but have no official support from
176-
this library, and are most likely not supported by either browsers or authenticators.
198+
not include the extension. These are all technically reachable as untyped values via `webauthn.WithExtension`, but
199+
have no official (typed) support from this library, and are most likely not supported by either browsers or
200+
authenticators. The one exception is `uvm`: it remains registered as a CTAP extension and is modelled, so
201+
`webauthn.WithExtension` rejects it and `webauthn.WithExtensionUVM` is used instead.
177202

178203
These extensions often either were excluded due to privacy or security concerns, were introduced into the core of the
179204
spec as legitimate inputs outside of extensions, or never received support from browsers or authenticators.

protocol/assertion.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,13 +160,19 @@ func (p *ParsedCredentialAssertionData) Verify(storedChallenge string, relyingPa
160160
// Begin Step 11. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the RP.
161161
rpIDHash := sha256.Sum256([]byte(relyingPartyID))
162162

163-
var appIDHash [32]byte
163+
// The appid is only non-empty when the Relying Party requested the FIDO AppID Extension and the client reported
164+
// having acted on it; see [ParsedPublicKeyCredential.GetAppID], which derives it from the session data. In that
165+
// case §10.1.1 requires the AppID hash to be the expected rpIdHash in place of the RP ID hash, so the hash is
166+
// left nil rather than zeroed when the extension does not apply.
167+
var appIDHash []byte
168+
164169
if appID != "" {
165-
appIDHash = sha256.Sum256([]byte(appID))
170+
sum := sha256.Sum256([]byte(appID))
171+
appIDHash = sum[:]
166172
}
167173

168174
// Handle steps 11 through 14, verifying the authenticator data.
169-
validError = p.Response.AuthenticatorData.Verify(rpIDHash[:], appIDHash[:], verifyUser, verifyUserPresence)
175+
validError = p.Response.AuthenticatorData.Verify(rpIDHash[:], appIDHash, verifyUser, verifyUserPresence)
170176
if validError != nil {
171177
return validError
172178
}

protocol/assertion_test.go

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ func TestParseCredentialRequestResponse(t *testing.T) {
5050
Type: string(PublicKeyCredentialType),
5151
},
5252
RawID: byteID,
53-
ClientExtensionResults: map[string]any{
54-
"appID": "example.com",
53+
ClientExtensionResults: AuthenticationExtensionsClientOutputs{
54+
Extra: map[string]any{"vendorAppID": "example.com"},
5555
},
5656
},
5757
Response: ParsedAssertionResponse{
@@ -81,8 +81,8 @@ func TestParseCredentialRequestResponse(t *testing.T) {
8181
ID: "AI7D5q2P0LS-Fal9ZT7CHM2N5BLbUunF92T8b6iYC199bO2kagSuU05-5dZGqb1SP0A0lyTWng",
8282
},
8383
RawID: byteID,
84-
ClientExtensionResults: map[string]any{
85-
"appID": "example.com",
84+
ClientExtensionResults: AuthenticationExtensionsClientOutputs{
85+
Extra: map[string]any{"vendorAppID": "example.com"},
8686
},
8787
},
8888
AssertionResponse: AuthenticatorAssertionResponse{
@@ -325,12 +325,26 @@ func TestParsedCredentialAssertionData_Verify(t *testing.T) {
325325
err: "Error validating the assertion signature: <nil>",
326326
},
327327
{
328-
name: "ShouldFailWithAppID",
328+
// The authenticator data of this vector is scoped to the RP ID, so an appid in play must be rejected
329+
// rather than silently falling back to the RP ID hash. §10.1.1 requires the Relying Party to expect the
330+
// hash of the AppID and not the hash of the RP ID once the client reports the extension was acted upon.
331+
name: "ShouldFailWithAppIDNotMatchingAuthenticatorData",
329332
challenge: challenge,
330333
relyingPartyID: "example.org",
331334
rpOrigins: []string{"https://example.org"},
332335
appID: "https://example.org",
333336
credentialBytes: credPubKey,
337+
err: "Error validating the authenticator response",
338+
},
339+
{
340+
// An appid whose hash does match the authenticator data reaches the FIDO U2F public key parser, which
341+
// this CTAP2 formatted spec vector key is not valid for.
342+
name: "ShouldFailWithAppIDMatchingAuthenticatorData",
343+
challenge: challenge,
344+
relyingPartyID: "example.org",
345+
rpOrigins: []string{"https://example.org"},
346+
appID: "example.org",
347+
credentialBytes: credPubKey,
334348
err: "Error parsing the assertion public key: failed to parse FIDO public key: crypto/ecdh: invalid public key",
335349
},
336350
}
@@ -512,7 +526,7 @@ var testAssertionResponses = map[string]string{
512526
`success`: `{
513527
"id":"AI7D5q2P0LS-Fal9ZT7CHM2N5BLbUunF92T8b6iYC199bO2kagSuU05-5dZGqb1SP0A0lyTWng",
514528
"rawId":"AI7D5q2P0LS-Fal9ZT7CHM2N5BLbUunF92T8b6iYC199bO2kagSuU05-5dZGqb1SP0A0lyTWng",
515-
"clientExtensionResults":{"appID":"example.com"},
529+
"clientExtensionResults":{"vendorAppID":"example.com"},
516530
"type":"public-key",
517531
"response":{
518532
"authenticatorData":"dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBFXJJiGa3OAAI1vMYKZIsLJfHwVQMANwCOw-atj9C0vhWpfWU-whzNjeQS21Lpxfdk_G-omAtffWztpGoErlNOfuXWRqm9Uj9ANJck1p6lAQIDJiABIVggKAhfsdHcBIc0KPgAcRyAIK_-Vi-nCXHkRHPNaCMBZ-4iWCBxB8fGYQSBONi9uvq0gv95dGWlhJrBwCsj_a4LJQKVHQ",
@@ -524,7 +538,7 @@ var testAssertionResponses = map[string]string{
524538
`trailingData`: `{
525539
"id":"AI7D5q2P0LS-Fal9ZT7CHM2N5BLbUunF92T8b6iYC199bO2kagSuU05-5dZGqb1SP0A0lyTWng",
526540
"rawId":"AI7D5q2P0LS-Fal9ZT7CHM2N5BLbUunF92T8b6iYC199bO2kagSuU05-5dZGqb1SP0A0lyTWng",
527-
"clientExtensionResults":{"appID":"example.com"},
541+
"clientExtensionResults":{"vendorAppID":"example.com"},
528542
"type":"public-key",
529543
"response":{
530544
"authenticatorData":"dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBFXJJiGa3OAAI1vMYKZIsLJfHwVQMANwCOw-atj9C0vhWpfWU-whzNjeQS21Lpxfdk_G-omAtffWztpGoErlNOfuXWRqm9Uj9ANJck1p6lAQIDJiABIVggKAhfsdHcBIc0KPgAcRyAIK_-Vi-nCXHkRHPNaCMBZ-4iWCBxB8fGYQSBONi9uvq0gv95dGWlhJrBwCsj_a4LJQKVHQ",

0 commit comments

Comments
 (0)