From 4bb577950aa0c51f48fe96f4461af4f8ae56549f Mon Sep 17 00:00:00 2001 From: printfn <1643883+printfn@users.noreply.github.com> Date: Thu, 25 Apr 2024 09:34:14 +1200 Subject: [PATCH 1/8] docs: adjust spelling (#231) Co-authored-by: printfn --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2c3c8d1..9565223 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ minor versions) with a brief transition time (usually 1 patch release of go, for will likely still support go 1.17 until go 1.21.1 is released). This library in our opinion handles a critical element of security in a dependent project and we aim to avoid backwards -compatability at the cost of security wherever possible. We also consider this especially important in a language like +compatibility at the cost of security wherever possible. We also consider this especially important in a language like go where their backwards compatibility when upgrading the compile tools is usually flawless. This policy means that users who wish to build this with older versions of go may find there are features being used From 824017d99111c90ebee22cc4b8b7d3a01e7802f4 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Fri, 26 Apr 2024 23:56:23 +1000 Subject: [PATCH 2/8] feat: support hints and attestation formats (#216) This adds support to the hints and attestation format options, both of which are new elements from Level 3 which are effectively optional. This allows users who wish to leverage this option to do so with no negative effects for those who do not. --- protocol/attestation.go | 8 +-- protocol/attestation_androidkey.go | 4 +- protocol/attestation_apple.go | 4 +- protocol/attestation_packed.go | 8 +-- protocol/attestation_safetynet.go | 4 +- protocol/attestation_tpm.go | 4 +- protocol/attestation_u2f.go | 4 +- protocol/options.go | 90 +++++++++++++++++++++++++----- webauthn/login.go | 16 ++++++ webauthn/registration.go | 26 ++++++++- 10 files changed, 128 insertions(+), 40 deletions(-) diff --git a/protocol/attestation.go b/protocol/attestation.go index 54716de..8376141 100644 --- a/protocol/attestation.go +++ b/protocol/attestation.go @@ -70,11 +70,11 @@ type AttestationObject struct { type attestationFormatValidationHandler func(AttestationObject, []byte) (string, []interface{}, error) -var attestationRegistry = make(map[string]attestationFormatValidationHandler) +var attestationRegistry = make(map[AttestationFormat]attestationFormatValidationHandler) // RegisterAttestationFormat is a method to register attestation formats with the library. Generally using one of the // locally registered attestation formats is sufficient. -func RegisterAttestationFormat(format string, handler attestationFormatValidationHandler) { +func RegisterAttestationFormat(format AttestationFormat, handler attestationFormatValidationHandler) { attestationRegistry[format] = handler } @@ -135,7 +135,7 @@ func (attestationObject *AttestationObject) Verify(relyingPartyID string, client // But first let's make sure attestation is present. If it isn't, we don't need to handle // any of the following steps - if attestationObject.Format == "none" { + if AttestationFormat(attestationObject.Format) == AttestationFormatNone { if len(attestationObject.AttStatement) != 0 { return ErrAttestationFormat.WithInfo("Attestation format none with attestation present") } @@ -143,7 +143,7 @@ func (attestationObject *AttestationObject) Verify(relyingPartyID string, client return nil } - formatHandler, valid := attestationRegistry[attestationObject.Format] + formatHandler, valid := attestationRegistry[AttestationFormat(attestationObject.Format)] if !valid { return ErrAttestationFormat.WithInfo(fmt.Sprintf("Attestation format %s is unsupported", attestationObject.Format)) } diff --git a/protocol/attestation_androidkey.go b/protocol/attestation_androidkey.go index de43783..0b733d3 100644 --- a/protocol/attestation_androidkey.go +++ b/protocol/attestation_androidkey.go @@ -10,10 +10,8 @@ import ( "github.com/go-webauthn/webauthn/protocol/webauthncose" ) -var androidAttestationKey = "android-key" - func init() { - RegisterAttestationFormat(androidAttestationKey, verifyAndroidKeyFormat) + RegisterAttestationFormat(AttestationFormatAndroidKey, verifyAndroidKeyFormat) } // The android-key attestation statement looks like: diff --git a/protocol/attestation_apple.go b/protocol/attestation_apple.go index 935218f..589e5d0 100644 --- a/protocol/attestation_apple.go +++ b/protocol/attestation_apple.go @@ -14,10 +14,8 @@ import ( "github.com/go-webauthn/webauthn/protocol/webauthncose" ) -var appleAttestationKey = "apple" - func init() { - RegisterAttestationFormat(appleAttestationKey, verifyAppleFormat) + RegisterAttestationFormat(AttestationFormatApple, verifyAppleFormat) } // The apple attestation statement looks like: diff --git a/protocol/attestation_packed.go b/protocol/attestation_packed.go index 8b0940a..8874d80 100644 --- a/protocol/attestation_packed.go +++ b/protocol/attestation_packed.go @@ -12,10 +12,8 @@ import ( "github.com/go-webauthn/webauthn/protocol/webauthncose" ) -var packedAttestationKey = "packed" - func init() { - RegisterAttestationFormat(packedAttestationKey, verifyPackedFormat) + RegisterAttestationFormat(AttestationFormatPacked, verifyPackedFormat) } // The packed attestation statement looks like: @@ -45,13 +43,13 @@ func verifyPackedFormat(att AttestationObject, clientDataHash []byte) (string, [ alg, present := att.AttStatement["alg"].(int64) if !present { - return packedAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retrieving alg value") + return string(AttestationFormatPacked), nil, ErrAttestationFormat.WithDetails("Error retrieving alg value") } // Get the sig value - A byte string containing the attestation signature. sig, present := att.AttStatement["sig"].([]byte) if !present { - return packedAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retrieving sig value") + return string(AttestationFormatPacked), nil, ErrAttestationFormat.WithDetails("Error retrieving sig value") } // Step 2. If x5c is present, this indicates that the attestation type is not ECDAA. diff --git a/protocol/attestation_safetynet.go b/protocol/attestation_safetynet.go index 8e94ad1..f88e804 100644 --- a/protocol/attestation_safetynet.go +++ b/protocol/attestation_safetynet.go @@ -14,10 +14,8 @@ import ( "github.com/go-webauthn/webauthn/metadata" ) -var safetyNetAttestationKey = "android-safetynet" - func init() { - RegisterAttestationFormat(safetyNetAttestationKey, verifySafetyNetFormat) + RegisterAttestationFormat(AttestationFormatAndroidSafetyNet, verifySafetyNetFormat) } type SafetyNetResponse struct { diff --git a/protocol/attestation_tpm.go b/protocol/attestation_tpm.go index 892bdd8..c1fbe39 100644 --- a/protocol/attestation_tpm.go +++ b/protocol/attestation_tpm.go @@ -15,10 +15,8 @@ import ( "github.com/go-webauthn/webauthn/protocol/webauthncose" ) -var tpmAttestationKey = "tpm" - func init() { - RegisterAttestationFormat(tpmAttestationKey, verifyTPMFormat) + RegisterAttestationFormat(AttestationFormatTPM, verifyTPMFormat) } func verifyTPMFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) { diff --git a/protocol/attestation_u2f.go b/protocol/attestation_u2f.go index e203f07..4b84962 100644 --- a/protocol/attestation_u2f.go +++ b/protocol/attestation_u2f.go @@ -11,10 +11,8 @@ import ( "github.com/go-webauthn/webauthn/protocol/webauthncose" ) -var u2fAttestationKey = "fido-u2f" - func init() { - RegisterAttestationFormat(u2fAttestationKey, verifyU2FFormat) + RegisterAttestationFormat(AttestationFormatFIDOU2F, verifyU2FFormat) } // verifyU2FFormat - Follows verification steps set out by https://www.w3.org/TR/webauthn/#fido-u2f-attestation diff --git a/protocol/options.go b/protocol/options.go index 80a9e55..f48577c 100644 --- a/protocol/options.go +++ b/protocol/options.go @@ -17,27 +17,24 @@ type CredentialAssertion struct { // In order to create a Credential via create(), the caller specifies a few parameters in a // PublicKeyCredentialCreationOptions object. // -// TODO: There is one field missing from this for WebAuthn Level 3. A string slice named 'attestationFormats'. -// // Specification: §5.4. Options for Credential Creation (https://www.w3.org/TR/webauthn/#dictionary-makecredentialoptions) type PublicKeyCredentialCreationOptions struct { - RelyingParty RelyingPartyEntity `json:"rp"` - User UserEntity `json:"user"` - Challenge URLEncodedBase64 `json:"challenge"` - Parameters []CredentialParameter `json:"pubKeyCredParams,omitempty"` - Timeout int `json:"timeout,omitempty"` - CredentialExcludeList []CredentialDescriptor `json:"excludeCredentials,omitempty"` - AuthenticatorSelection AuthenticatorSelection `json:"authenticatorSelection,omitempty"` - Attestation ConveyancePreference `json:"attestation,omitempty"` - Extensions AuthenticationExtensions `json:"extensions,omitempty"` + RelyingParty RelyingPartyEntity `json:"rp"` + User UserEntity `json:"user"` + Challenge URLEncodedBase64 `json:"challenge"` + Parameters []CredentialParameter `json:"pubKeyCredParams,omitempty"` + Timeout int `json:"timeout,omitempty"` + CredentialExcludeList []CredentialDescriptor `json:"excludeCredentials,omitempty"` + AuthenticatorSelection AuthenticatorSelection `json:"authenticatorSelection,omitempty"` + Hints []PublicKeyCredentialHint `json:"hints,omitempty"` + Attestation ConveyancePreference `json:"attestation,omitempty"` + AttestationFormats []AttestationFormat `json:"attestationFormats,omitempty"` + Extensions AuthenticationExtensions `json:"extensions,omitempty"` } // The PublicKeyCredentialRequestOptions dictionary supplies get() with the data it needs to generate an assertion. // Its challenge member MUST be present, while its other members are OPTIONAL. // -// TODO: There are two fields missing from this for WebAuthn Level 3. A string type named 'attestation', and a string -// slice named 'attestationFormats'. -// // Specification: §5.5. Options for Assertion Generation (https://www.w3.org/TR/webauthn/#dictionary-assertion-options) type PublicKeyCredentialRequestOptions struct { Challenge URLEncodedBase64 `json:"challenge"` @@ -45,6 +42,8 @@ type PublicKeyCredentialRequestOptions struct { RelyingPartyID string `json:"rpId,omitempty"` AllowedCredentials []CredentialDescriptor `json:"allowCredentials,omitempty"` UserVerification UserVerificationRequirement `json:"userVerification,omitempty"` + Attestation ConveyancePreference `json:"attestation,omitempty"` + AttestationFormats []AttestationFormat `json:"attestationFormats,omitempty"` Extensions AuthenticationExtensions `json:"extensions,omitempty"` } @@ -126,6 +125,69 @@ type AuthenticatorSelection struct { UserVerification UserVerificationRequirement `json:"userVerification,omitempty"` } +// PublicKeyCredentialHint is a type representing the enum PublicKeyCredentialHints from +// https://www.w3.org/TR/webauthn-3/#enum-hints. +type PublicKeyCredentialHint string + +const ( + // PublicKeyCredentialHintSecurityKey is a PublicKeyCredentialHint that indicates that the Relying Party believes + // that users will satisfy this request with a physical security key. For example, an enterprise Relying Party may + // set this hint if they have issued security keys to their employees and will only accept those authenticators for + // registration and authentication. + // + // For compatibility with older user agents, when this hint is used in PublicKeyCredentialCreationOptions, the + // authenticatorAttachment SHOULD be set to cross-platform. + PublicKeyCredentialHintSecurityKey PublicKeyCredentialHint = "security-key" + + // PublicKeyCredentialHintClientDevice is a PublicKeyCredentialHint that indicates that the Relying Party believes + // that users will satisfy this request with a platform authenticator attached to the client device. + // + // For compatibility with older user agents, when this hint is used in PublicKeyCredentialCreationOptions, the + // authenticatorAttachment SHOULD be set to platform. + PublicKeyCredentialHintClientDevice PublicKeyCredentialHint = "client-device" + + // PublicKeyCredentialHintHybrid is a PublicKeyCredentialHint that indicates that the Relying Party believes that + // users will satisfy this request with general-purpose authenticators such as smartphones. For example, a consumer + // Relying Party may believe that only a small fraction of their customers possesses dedicated security keys. This + // option also implies that the local platform authenticator should not be promoted in the UI. + // + // For compatibility with older user agents, when this hint is used in PublicKeyCredentialCreationOptions, the + // authenticatorAttachment SHOULD be set to cross-platform. + PublicKeyCredentialHintHybrid PublicKeyCredentialHint = "hybrid" +) + +type AttestationFormat string + +const ( + // AttestationFormatPacked is the "packed" attestation statement format is a WebAuthn-optimized format for + // attestation. It uses a very compact but still extensible encoding method. This format is implementable by + //authenticators with limited resources (e.g., secure elements). + AttestationFormatPacked AttestationFormat = "packed" + + // AttestationFormatTPM is the TPM attestation statement format returns an attestation statement in the same format + // as the packed attestation statement format, although the rawData and signature fields are computed differently. + AttestationFormatTPM AttestationFormat = "tpm" + + // AttestationFormatAndroidKey is the attestation statement format for platform authenticators on versions "N", and + // later, which may provide this proprietary "hardware attestation" statement. + AttestationFormatAndroidKey AttestationFormat = "android-key" + + // AttestationFormatAndroidSafetyNet is the attestation statement format that Android-based platform authenticators + // MAY produce an attestation statement based on the Android SafetyNet API. + AttestationFormatAndroidSafetyNet AttestationFormat = "android-safetynet" + + // AttestationFormatFIDOU2F is the attestation statement format that is used with FIDO U2F authenticators. + AttestationFormatFIDOU2F AttestationFormat = "fido-u2f" + + // AttestationFormatApple is the attestation statement format that is used with Apple devices' platform + // authenticators. + AttestationFormatApple AttestationFormat = "apple" + + // AttestationFormatNone is the attestation statement format that is used to replace any authenticator-provided + // attestation statement when a WebAuthn Relying Party indicates it does not wish to receive attestation information. + AttestationFormatNone AttestationFormat = "none" +) + // ConveyancePreference is the type representing the AttestationConveyancePreference IDL. // // WebAuthn Relying Parties may use AttestationConveyancePreference to specify their preference regarding attestation diff --git a/webauthn/login.go b/webauthn/login.go index 73e69af..ca981d5 100644 --- a/webauthn/login.go +++ b/webauthn/login.go @@ -115,6 +115,22 @@ func WithUserVerification(userVerification protocol.UserVerificationRequirement) } } +// WithLoginConveyancePreference adjusts the non-default parameters regarding whether the authenticator should attest to the +// credential. +func WithLoginConveyancePreference(preference protocol.ConveyancePreference) LoginOption { + return func(cco *protocol.PublicKeyCredentialRequestOptions) { + cco.Attestation = preference + } +} + +// WithLoginAttestationFormats adjusts the preferred attestation formats for this credential request in most to least +// preferable. Advisory only. +func WithLoginAttestationFormats(formats ...protocol.AttestationFormat) LoginOption { + return func(cco *protocol.PublicKeyCredentialRequestOptions) { + cco.AttestationFormats = formats + } +} + // WithAssertionExtensions adjusts the requested extensions. func WithAssertionExtensions(extensions protocol.AuthenticationExtensions) LoginOption { return func(cco *protocol.PublicKeyCredentialRequestOptions) { diff --git a/webauthn/registration.go b/webauthn/registration.go index 9715246..cb9d234 100644 --- a/webauthn/registration.go +++ b/webauthn/registration.go @@ -101,6 +101,12 @@ func WithAuthenticatorSelection(authenticatorSelection protocol.AuthenticatorSel } } +func WithHints(hints ...protocol.PublicKeyCredentialHint) RegistrationOption { + return func(cco *protocol.PublicKeyCredentialCreationOptions) { + cco.Hints = hints + } +} + // WithExclusions adjusts the non-default parameters regarding credentials to exclude from registration. func WithExclusions(excludeList []protocol.CredentialDescriptor) RegistrationOption { return func(cco *protocol.PublicKeyCredentialCreationOptions) { @@ -108,14 +114,30 @@ func WithExclusions(excludeList []protocol.CredentialDescriptor) RegistrationOpt } } -// WithConveyancePreference adjusts the non-default parameters regarding whether the authenticator should attest to the -// credential. +// WithConveyancePreference is a direct alias for WithRegistrationConveyancePreference. +// +// Deprecated: Use WithRegistrationConveyancePreference in favor of WithConveyancePreference as this function will be +// likely be removed in a future release. func WithConveyancePreference(preference protocol.ConveyancePreference) RegistrationOption { + return WithRegistrationConveyancePreference(preference) +} + +// WithRegistrationConveyancePreference adjusts the non-default parameters regarding whether the authenticator should attest to the +// credential. +func WithRegistrationConveyancePreference(preference protocol.ConveyancePreference) RegistrationOption { return func(cco *protocol.PublicKeyCredentialCreationOptions) { cco.Attestation = preference } } +// WithRegistrationAttestationFormats adjusts the preferred attestation formats for this credential creation in most to +// least preferable. Advisory only. +func WithRegistrationAttestationFormats(formats ...protocol.AttestationFormat) RegistrationOption { + return func(cco *protocol.PublicKeyCredentialCreationOptions) { + cco.AttestationFormats = formats + } +} + // WithExtensions adjusts the extension parameter in the registration options. func WithExtensions(extension protocol.AuthenticationExtensions) RegistrationOption { return func(cco *protocol.PublicKeyCredentialCreationOptions) { From 0c97761a14b4f9d6aa71eb0c3b0f30b365aa7eb9 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Sat, 27 Apr 2024 08:33:39 +1000 Subject: [PATCH 3/8] feat: top origin verification (#217) This adds top origin verification options to the library. Closes #205 --- protocol/assertion.go | 4 +- protocol/assertion_test.go | 2 +- protocol/attestation_test.go | 2 +- protocol/client.go | 85 ++++++++++++++++++++++++++++++++++-- protocol/client_test.go | 8 ++-- protocol/credential.go | 4 +- protocol/credential_test.go | 2 +- webauthn/login.go | 3 +- webauthn/registration.go | 2 +- webauthn/types.go | 18 ++++++++ 10 files changed, 114 insertions(+), 16 deletions(-) diff --git a/protocol/assertion.go b/protocol/assertion.go index 897a56c..48dfb62 100644 --- a/protocol/assertion.go +++ b/protocol/assertion.go @@ -124,14 +124,14 @@ func (car CredentialAssertionResponse) Parse() (par *ParsedCredentialAssertionDa // documentation. // // Specification: §7.2 Verifying an Authentication Assertion (https://www.w3.org/TR/webauthn/#sctn-verifying-assertion) -func (p *ParsedCredentialAssertionData) Verify(storedChallenge string, relyingPartyID string, relyingPartyOrigins []string, appID string, verifyUser bool, credentialBytes []byte) error { +func (p *ParsedCredentialAssertionData) Verify(storedChallenge string, relyingPartyID string, rpOrigins, rpTopOrigins []string, rpTopOriginsVerify TopOriginVerificationMode, appID string, verifyUser bool, credentialBytes []byte) error { // Steps 4 through 6 in verifying the assertion data (https://www.w3.org/TR/webauthn/#verifying-assertion) are // "assertive" steps, i.e "Let JSONtext be the result of running UTF-8 decode on the value of cData." // We handle these steps in part as we verify but also beforehand // Handle steps 7 through 10 of assertion by verifying stored data against the Collected Client Data // returned by the authenticator - validError := p.Response.CollectedClientData.Verify(storedChallenge, AssertCeremony, relyingPartyOrigins) + validError := p.Response.CollectedClientData.Verify(storedChallenge, AssertCeremony, rpOrigins, rpTopOrigins, rpTopOriginsVerify) if validError != nil { return validError } diff --git a/protocol/assertion_test.go b/protocol/assertion_test.go index 335f9a1..ab62239 100644 --- a/protocol/assertion_test.go +++ b/protocol/assertion_test.go @@ -180,7 +180,7 @@ func TestParsedCredentialAssertionData_Verify(t *testing.T) { Raw: tt.fields.Raw, } - if err := p.Verify(tt.args.storedChallenge.String(), tt.args.relyingPartyID, tt.args.relyingPartyOrigin, "", tt.args.verifyUser, tt.args.credentialBytes); (err != nil) != tt.wantErr { + if err := p.Verify(tt.args.storedChallenge.String(), tt.args.relyingPartyID, tt.args.relyingPartyOrigin, nil, TopOriginIgnoreVerificationMode, "", tt.args.verifyUser, tt.args.credentialBytes); (err != nil) != tt.wantErr { t.Errorf("ParsedCredentialAssertionData.Verify() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/protocol/attestation_test.go b/protocol/attestation_test.go index f714429..60a6bf6 100644 --- a/protocol/attestation_test.go +++ b/protocol/attestation_test.go @@ -36,7 +36,7 @@ func TestAttestationVerify(t *testing.T) { pcc.Response = *parsedAttestationResponse // Test Base Verification - err = pcc.Verify(options.Response.Challenge.String(), false, options.Response.RelyingParty.ID, []string{options.Response.RelyingParty.Name}) + err = pcc.Verify(options.Response.Challenge.String(), false, options.Response.RelyingParty.ID, []string{options.Response.RelyingParty.Name}, nil, TopOriginIgnoreVerificationMode) if err != nil { t.Fatalf("Not valid: %+v (%s)", err, err.(*Error).DevInfo) } diff --git a/protocol/client.go b/protocol/client.go index c98577a..02402f7 100644 --- a/protocol/client.go +++ b/protocol/client.go @@ -20,6 +20,8 @@ type CollectedClientData struct { Type CeremonyType `json:"type"` Challenge string `json:"challenge"` Origin string `json:"origin"` + TopOrigin string `json:"topOrigin"` + CrossOrigin bool `json:"crossOrigin,omitempty"` TokenBinding *TokenBinding `json:"tokenBinding,omitempty"` // Chromium (Chrome) returns a hint sometimes about how to handle clientDataJSON in a safe manner. @@ -77,7 +79,10 @@ func FullyQualifiedOrigin(rawOrigin string) (fqOrigin string, err error) { // new credential and steps 7 through 10 of verifying an authentication assertion // See https://www.w3.org/TR/webauthn/#registering-a-new-credential // and https://www.w3.org/TR/webauthn/#verifying-assertion -func (c *CollectedClientData) Verify(storedChallenge string, ceremony CeremonyType, rpOrigins []string) error { +// +// Note: the rpTopOriginsVerify parameter does not accept the TopOriginVerificationMode value of +// TopOriginDefaultVerificationMode as it's expected this value is updated by the config validation process. +func (c *CollectedClientData) Verify(storedChallenge string, ceremony CeremonyType, rpOrigins, rpTopOrigins []string, rpTopOriginsVerify TopOriginVerificationMode) (err error) { // Registration Step 3. Verify that the value of C.type is webauthn.create. // Assertion Step 7. Verify that the value of C.type is the string webauthn.get. @@ -101,8 +106,9 @@ func (c *CollectedClientData) Verify(storedChallenge string, ceremony CeremonyTy // Registration Step 5 & Assertion Step 9. Verify that the value of C.origin matches // the Relying Party's origin. - fqOrigin, err := FullyQualifiedOrigin(c.Origin) - if err != nil { + var fqOrigin string + + if fqOrigin, err = FullyQualifiedOrigin(c.Origin); err != nil { return ErrParsingData.WithDetails("Error decoding clientData origin as URL") } @@ -121,6 +127,54 @@ func (c *CollectedClientData) Verify(storedChallenge string, ceremony CeremonyTy WithInfo(fmt.Sprintf("Expected Values: %s, Received: %s", rpOrigins, fqOrigin)) } + if rpTopOriginsVerify != TopOriginIgnoreVerificationMode { + switch len(c.TopOrigin) { + case 0: + break + default: + if !c.CrossOrigin { + return ErrVerification. + WithDetails("Error validating topOrigin"). + WithInfo("The topOrigin can't have values unless crossOrigin is true.") + } + + var ( + fqTopOrigin string + possibleTopOrigins []string + ) + + if fqTopOrigin, err = FullyQualifiedOrigin(c.TopOrigin); err != nil { + return ErrParsingData.WithDetails("Error decoding clientData topOrigin as URL") + } + + switch rpTopOriginsVerify { + case TopOriginExplicitVerificationMode: + possibleTopOrigins = rpTopOrigins + case TopOriginAutoVerificationMode: + possibleTopOrigins = append(rpTopOrigins, rpOrigins...) + case TopOriginImplicitVerificationMode: + possibleTopOrigins = rpOrigins + default: + return ErrNotImplemented.WithDetails("Error handling unknown Top Origin verification mode") + } + + found = false + + for _, origin := range possibleTopOrigins { + if strings.EqualFold(fqTopOrigin, origin) { + found = true + break + } + } + + if !found { + return ErrVerification. + WithDetails("Error validating top origin"). + WithInfo(fmt.Sprintf("Expected Values: %s, Received: %s", possibleTopOrigins, fqTopOrigin)) + } + } + } + // Registration Step 6 and Assertion Step 10. Verify that the value of C.tokenBinding.status // matches the state of Token Binding for the TLS connection over which the assertion was // obtained. If Token Binding was used on that TLS connection, also verify that C.tokenBinding.id @@ -140,3 +194,28 @@ func (c *CollectedClientData) Verify(storedChallenge string, ceremony CeremonyTy return nil } + +type TopOriginVerificationMode int + +const ( + // TopOriginDefaultVerificationMode represents the default verification mode for the Top Origin. At this time this + // mode is the same as TopOriginIgnoreVerificationMode until such a time as the specification becomes stable. This + // value is intended as a fallback value and implementers should very intentionally pick another option if they want + // stability. + TopOriginDefaultVerificationMode TopOriginVerificationMode = iota + + // TopOriginIgnoreVerificationMode ignores verification entirely. + TopOriginIgnoreVerificationMode + + // TopOriginAutoVerificationMode represents the automatic verification mode for the Top Origin. In this mode the + // If the Top Origins parameter has values it checks against this, otherwise it checks against the Origins parameter. + TopOriginAutoVerificationMode + + // TopOriginImplicitVerificationMode represents the implicit verification mode for the Top Origin. In this mode the + // Top Origin is verified against the allowed Origins values. + TopOriginImplicitVerificationMode + + // TopOriginExplicitVerificationMode represents the explicit verification mode for the Top Origin. In this mode the + // Top Origin is verified against the allowed Top Origins values. + TopOriginExplicitVerificationMode +) diff --git a/protocol/client_test.go b/protocol/client_test.go index d83a598..1a2df9e 100644 --- a/protocol/client_test.go +++ b/protocol/client_test.go @@ -26,7 +26,7 @@ func TestVerifyCollectedClientData(t *testing.T) { var storedChallenge = newChallenge - if err = ccd.Verify(storedChallenge.String(), ccd.Type, []string{ccd.Origin}); err != nil { + if err = ccd.Verify(storedChallenge.String(), ccd.Type, []string{ccd.Origin}, nil, TopOriginIgnoreVerificationMode); err != nil { t.Fatalf("error verifying challenge: expected %#v got %#v", ccd.Challenge, storedChallenge) } } @@ -44,7 +44,7 @@ func TestVerifyCollectedClientDataIncorrectChallenge(t *testing.T) { t.Fatalf("error creating challenge: %s", err) } - if err = ccd.Verify(bogusChallenge.String(), ccd.Type, []string{ccd.Origin}); err == nil { + if err = ccd.Verify(bogusChallenge.String(), ccd.Type, []string{ccd.Origin}, nil, TopOriginIgnoreVerificationMode); err == nil { t.Fatalf("error expected but not received. expected %#v got %#v", ccd.Challenge, bogusChallenge) } } @@ -59,7 +59,7 @@ func TestVerifyCollectedClientDataUnexpectedOrigin(t *testing.T) { storedChallenge := newChallenge expectedOrigins := []string{"http://different.com"} - if err = ccd.Verify(storedChallenge.String(), ccd.Type, expectedOrigins); err == nil { + if err = ccd.Verify(storedChallenge.String(), ccd.Type, expectedOrigins, nil, TopOriginIgnoreVerificationMode); err == nil { t.Fatalf("error expected but not received. expected %#v got %#v", expectedOrigins, ccd.Origin) } } @@ -76,7 +76,7 @@ func TestVerifyCollectedClientDataWithMultipleExpectedOrigins(t *testing.T) { expectedOrigins := []string{"https://exmaple.com", "9C:B4:AE:EF:05:53:6E:73:0E:C4:B8:02:E7:67:F6:7D:A4:E7:BC:26:D7:42:B5:27:FF:01:7D:68:2A:EB:FA:1D", ccd.Origin} - if err = ccd.Verify(storedChallenge.String(), ccd.Type, expectedOrigins); err != nil { + if err = ccd.Verify(storedChallenge.String(), ccd.Type, expectedOrigins, nil, TopOriginIgnoreVerificationMode); err != nil { t.Fatalf("error verifying challenge: expected %#v got %#v", expectedOrigins, ccd.Origin) } } diff --git a/protocol/credential.go b/protocol/credential.go index bb9782b..7067402 100644 --- a/protocol/credential.go +++ b/protocol/credential.go @@ -140,9 +140,9 @@ func (ccr CredentialCreationResponse) Parse() (pcc *ParsedCredentialCreationData // Verify the Client and Attestation data. // // Specification: §7.1. Registering a New Credential (https://www.w3.org/TR/webauthn/#sctn-registering-a-new-credential) -func (pcc *ParsedCredentialCreationData) Verify(storedChallenge string, verifyUser bool, relyingPartyID string, relyingPartyOrigins []string) error { +func (pcc *ParsedCredentialCreationData) Verify(storedChallenge string, verifyUser bool, relyingPartyID string, rpOrigins, rpTopOrigins []string, rpTopOriginsVerify TopOriginVerificationMode) error { // Handles steps 3 through 6 - Verifying the Client Data against the Relying Party's stored data - verifyError := pcc.Response.CollectedClientData.Verify(storedChallenge, CreateCeremony, relyingPartyOrigins) + verifyError := pcc.Response.CollectedClientData.Verify(storedChallenge, CreateCeremony, rpOrigins, rpTopOrigins, rpTopOriginsVerify) if verifyError != nil { return verifyError } diff --git a/protocol/credential_test.go b/protocol/credential_test.go index 7c0b819..201c45f 100644 --- a/protocol/credential_test.go +++ b/protocol/credential_test.go @@ -364,7 +364,7 @@ func TestParsedCredentialCreationData_Verify(t *testing.T) { Response: tt.fields.Response, Raw: tt.fields.Raw, } - if err := pcc.Verify(tt.args.storedChallenge.String(), tt.args.verifyUser, tt.args.relyingPartyID, tt.args.relyingPartyOrigin); (err != nil) != tt.wantErr { + if err := pcc.Verify(tt.args.storedChallenge.String(), tt.args.verifyUser, tt.args.relyingPartyID, tt.args.relyingPartyOrigin, nil, TopOriginIgnoreVerificationMode); (err != nil) != tt.wantErr { t.Errorf("ParsedCredentialCreationData.Verify() error = %+v, wantErr %v", err, tt.wantErr) } }) diff --git a/webauthn/login.go b/webauthn/login.go index ca981d5..ff1c5c2 100644 --- a/webauthn/login.go +++ b/webauthn/login.go @@ -285,6 +285,7 @@ func (webauthn *WebAuthn) validateLogin(user User, session SessionData, parsedRe rpID := webauthn.Config.RPID rpOrigins := webauthn.Config.RPOrigins + rpTopOrigins := webauthn.Config.RPTopOrigins appID, err := parsedResponse.GetAppID(session.Extensions, loginCredential.AttestationType) if err != nil { @@ -292,7 +293,7 @@ func (webauthn *WebAuthn) validateLogin(user User, session SessionData, parsedRe } // Handle steps 4 through 16. - validError := parsedResponse.Verify(session.Challenge, rpID, rpOrigins, appID, shouldVerifyUser, loginCredential.PublicKey) + validError := parsedResponse.Verify(session.Challenge, rpID, rpOrigins, rpTopOrigins, webauthn.Config.RPTopOriginVerificationMode, appID, shouldVerifyUser, loginCredential.PublicKey) if validError != nil { return nil, validError } diff --git a/webauthn/registration.go b/webauthn/registration.go index cb9d234..e24e2b1 100644 --- a/webauthn/registration.go +++ b/webauthn/registration.go @@ -205,7 +205,7 @@ func (webauthn *WebAuthn) CreateCredential(user User, session SessionData, parse shouldVerifyUser := session.UserVerification == protocol.VerificationRequired - invalidErr := parsedResponse.Verify(session.Challenge, shouldVerifyUser, webauthn.Config.RPID, webauthn.Config.RPOrigins) + invalidErr := parsedResponse.Verify(session.Challenge, shouldVerifyUser, webauthn.Config.RPID, webauthn.Config.RPOrigins, webauthn.Config.RPTopOrigins, webauthn.Config.RPTopOriginVerificationMode) if invalidErr != nil { return nil, invalidErr } diff --git a/webauthn/types.go b/webauthn/types.go index bb93f31..79b6db1 100644 --- a/webauthn/types.go +++ b/webauthn/types.go @@ -36,6 +36,15 @@ type Config struct { // qualified origins. RPOrigins []string + // RPTopOrigins configures the list of Relying Party Server Top Origins that are permitted. These should be fully + // qualified origins. + RPTopOrigins []string + + // RPTopOriginVerificationMode determines the verification mode for the Top Origin value. By default the + // TopOriginIgnoreVerificationMode is used however this is going to change at such a time as WebAuthn Level 3 + // becomes recommended, implementers should explicitly set this value if they want stability. + RPTopOriginVerificationMode protocol.TopOriginVerificationMode + // AttestationPreference sets the default attestation conveyance preferences. AttestationPreference protocol.ConveyancePreference @@ -153,6 +162,15 @@ func (config *Config) validate() error { return fmt.Errorf("must provide at least one value to the 'RPOrigins' field") } + switch config.RPTopOriginVerificationMode { + case protocol.TopOriginDefaultVerificationMode: + config.RPTopOriginVerificationMode = protocol.TopOriginIgnoreVerificationMode + case protocol.TopOriginImplicitVerificationMode: + if len(config.RPTopOrigins) == 0 { + return fmt.Errorf("must provide at least one value to the 'RPTopOrigins' field when 'RPTopOriginVerificationMode' field is set to protocol.TopOriginImplicitVerificationMode") + } + } + if config.AuthenticatorSelection.RequireResidentKey == nil { config.AuthenticatorSelection.RequireResidentKey = protocol.ResidentKeyNotRequired() } From 482cf89b770bf7938afab1626d3a0fbb95eedd67 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Sat, 27 Apr 2024 16:10:07 +1000 Subject: [PATCH 4/8] feat: webauthn level 3 (#232) Adds some remaining elements from level 3. --- metadata/metadata.go | 10 +- protocol/assertion.go | 2 +- protocol/assertion_test.go | 6 +- protocol/attestation.go | 4 +- protocol/attestation_androidkey.go | 20 +-- protocol/attestation_androidkey_test.go | 2 +- protocol/attestation_apple.go | 4 +- protocol/attestation_apple_test.go | 6 +- protocol/attestation_packed.go | 10 +- protocol/attestation_packed_test.go | 2 +- protocol/attestation_safetynet.go | 22 ++-- protocol/attestation_safetynet_test.go | 4 +- protocol/attestation_test.go | 3 + protocol/attestation_tpm.go | 4 +- protocol/attestation_tpm_test.go | 33 ++--- protocol/attestation_u2f.go | 6 +- protocol/attestation_u2f_test.go | 6 +- protocol/authenticator.go | 12 +- protocol/challenge_test.go | 2 + protocol/client.go | 2 +- protocol/credential.go | 4 + protocol/credential_test.go | 2 +- protocol/entities.go | 2 +- protocol/extensions.go | 2 +- protocol/options.go | 162 ++++++++++++------------ protocol/webauthncbor/webauthncbor.go | 4 +- protocol/webauthncose/webauthncose.go | 4 +- webauthn/login.go | 19 +-- webauthn/registration.go | 78 ++++++------ 29 files changed, 220 insertions(+), 217 deletions(-) diff --git a/metadata/metadata.go b/metadata/metadata.go index af0a495..d97d809 100644 --- a/metadata/metadata.go +++ b/metadata/metadata.go @@ -548,16 +548,16 @@ type MDSGetEndpointsResponse struct { func unmarshalMDSBLOB(body []byte, c http.Client) (MetadataBLOBPayload, error) { var payload MetadataBLOBPayload - token, err := jwt.Parse(string(body), func(token *jwt.Token) (interface{}, error) { + token, err := jwt.Parse(string(body), func(token *jwt.Token) (any, error) { // 2. If the x5u attribute is present in the JWT Header, then - if _, ok := token.Header["x5u"].([]interface{}); ok { + if _, ok := token.Header["x5u"].(any); ok { // never seen an x5u here, although it is in the spec return nil, errors.New("x5u encountered in header of metadata TOC payload") } - var chain []interface{} + var chain []any // 3. If the x5u attribute is missing, the chain should be retrieved from the x5c attribute. - if x5c, ok := token.Header["x5c"].([]interface{}); !ok { + if x5c, ok := token.Header["x5c"].([]any); !ok { // If that attribute is missing as well, Metadata TOC signing trust anchor is considered the TOC signing certificate chain. chain[0] = MDSRoot } else { @@ -600,7 +600,7 @@ func unmarshalMDSBLOB(body []byte, c http.Client) (MetadataBLOBPayload, error) { return payload, err } -func validateChain(chain []interface{}, c http.Client) (bool, error) { +func validateChain(chain []any, c http.Client) (bool, error) { oRoot := make([]byte, base64.StdEncoding.DecodedLen(len(MDSRoot))) nRoot, err := base64.StdEncoding.Decode(oRoot, []byte(MDSRoot)) diff --git a/protocol/assertion.go b/protocol/assertion.go index 48dfb62..184eb42 100644 --- a/protocol/assertion.go +++ b/protocol/assertion.go @@ -161,7 +161,7 @@ func (p *ParsedCredentialAssertionData) Verify(storedChallenge string, relyingPa sigData := append(p.Raw.AssertionResponse.AuthenticatorData, clientDataHash[:]...) var ( - key interface{} + key any err error ) diff --git a/protocol/assertion_test.go b/protocol/assertion_test.go index ab62239..2731850 100644 --- a/protocol/assertion_test.go +++ b/protocol/assertion_test.go @@ -47,7 +47,7 @@ func TestParseCredentialRequestResponse(t *testing.T) { Type: "public-key", }, RawID: byteID, - ClientExtensionResults: map[string]interface{}{ + ClientExtensionResults: map[string]any{ "appID": "example.com", }, }, @@ -78,7 +78,7 @@ func TestParseCredentialRequestResponse(t *testing.T) { ID: "AI7D5q2P0LS-Fal9ZT7CHM2N5BLbUunF92T8b6iYC199bO2kagSuU05-5dZGqb1SP0A0lyTWng", }, RawID: byteID, - ClientExtensionResults: map[string]interface{}{ + ClientExtensionResults: map[string]any{ "appID": "example.com", }, }, @@ -135,7 +135,7 @@ func TestParseCredentialRequestResponse(t *testing.T) { assert.Equal(t, tc.expected.Response.CollectedClientData, actual.Response.CollectedClientData) var ( - pkExpected, pkActual interface{} + pkExpected, pkActual any ) assert.NoError(t, webauthncbor.Unmarshal(tc.expected.Response.AuthenticatorData.AttData.CredentialPublicKey, &pkExpected)) diff --git a/protocol/attestation.go b/protocol/attestation.go index 8376141..cdad1bf 100644 --- a/protocol/attestation.go +++ b/protocol/attestation.go @@ -65,10 +65,10 @@ type AttestationObject struct { // The format of the Attestation data. Format string `json:"fmt"` // The attestation statement data sent back if attestation is requested. - AttStatement map[string]interface{} `json:"attStmt,omitempty"` + AttStatement map[string]any `json:"attStmt,omitempty"` } -type attestationFormatValidationHandler func(AttestationObject, []byte) (string, []interface{}, error) +type attestationFormatValidationHandler func(AttestationObject, []byte) (string, []any, error) var attestationRegistry = make(map[AttestationFormat]attestationFormatValidationHandler) diff --git a/protocol/attestation_androidkey.go b/protocol/attestation_androidkey.go index 0b733d3..b8551ca 100644 --- a/protocol/attestation_androidkey.go +++ b/protocol/attestation_androidkey.go @@ -29,7 +29,7 @@ func init() { // } // // Specification: §8.4. Android Key Attestation Statement Format (https://www.w3.org/TR/webauthn/#sctn-android-key-attestation) -func verifyAndroidKeyFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) { +func verifyAndroidKeyFormat(att AttestationObject, clientDataHash []byte) (string, []any, error) { // Given the verification procedure inputs attStmt, authenticatorData and clientDataHash, the verification procedure is as follows: // §8.4.1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract // the contained fields. @@ -48,7 +48,7 @@ func verifyAndroidKeyFormat(att AttestationObject, clientDataHash []byte) (strin } // If x5c is not present, return an error - x5c, x509present := att.AttStatement["x5c"].([]interface{}) + x5c, x509present := att.AttStatement["x5c"].([]any) if !x509present { // Handle Basic Attestation steps for the x509 Certificate return "", nil, ErrAttestationFormat.WithDetails("Error retrieving x5c value") @@ -163,19 +163,19 @@ type authorizationList struct { Padding []int `asn1:"tag:6,explicit,set,optional"` EcCurve int `asn1:"tag:10,explicit,optional"` RsaPublicExponent int `asn1:"tag:200,explicit,optional"` - RollbackResistance interface{} `asn1:"tag:303,explicit,optional"` + RollbackResistance any `asn1:"tag:303,explicit,optional"` ActiveDateTime int `asn1:"tag:400,explicit,optional"` OriginationExpireDateTime int `asn1:"tag:401,explicit,optional"` UsageExpireDateTime int `asn1:"tag:402,explicit,optional"` - NoAuthRequired interface{} `asn1:"tag:503,explicit,optional"` + NoAuthRequired any `asn1:"tag:503,explicit,optional"` UserAuthType int `asn1:"tag:504,explicit,optional"` AuthTimeout int `asn1:"tag:505,explicit,optional"` - AllowWhileOnBody interface{} `asn1:"tag:506,explicit,optional"` - TrustedUserPresenceRequired interface{} `asn1:"tag:507,explicit,optional"` - TrustedConfirmationRequired interface{} `asn1:"tag:508,explicit,optional"` - UnlockedDeviceRequired interface{} `asn1:"tag:509,explicit,optional"` - AllApplications interface{} `asn1:"tag:600,explicit,optional"` - ApplicationID interface{} `asn1:"tag:601,explicit,optional"` + AllowWhileOnBody any `asn1:"tag:506,explicit,optional"` + TrustedUserPresenceRequired any `asn1:"tag:507,explicit,optional"` + TrustedConfirmationRequired any `asn1:"tag:508,explicit,optional"` + UnlockedDeviceRequired any `asn1:"tag:509,explicit,optional"` + AllApplications any `asn1:"tag:600,explicit,optional"` + ApplicationID any `asn1:"tag:601,explicit,optional"` CreationDateTime int `asn1:"tag:701,explicit,optional"` Origin int `asn1:"tag:702,explicit,optional"` RootOfTrust rootOfTrust `asn1:"tag:704,explicit,optional"` diff --git a/protocol/attestation_androidkey_test.go b/protocol/attestation_androidkey_test.go index cb35205..73266c8 100644 --- a/protocol/attestation_androidkey_test.go +++ b/protocol/attestation_androidkey_test.go @@ -22,7 +22,7 @@ func TestVerifyAndroidKeyFormat(t *testing.T) { name string args args want string - want1 []interface{} + want1 []any wantErr bool }{ { diff --git a/protocol/attestation_apple.go b/protocol/attestation_apple.go index 589e5d0..0e7be7c 100644 --- a/protocol/attestation_apple.go +++ b/protocol/attestation_apple.go @@ -31,12 +31,12 @@ func init() { // } // // Specification: §8.8. Apple Anonymous Attestation Statement Format (https://www.w3.org/TR/webauthn/#sctn-apple-anonymous-attestation) -func verifyAppleFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) { +func verifyAppleFormat(att AttestationObject, clientDataHash []byte) (string, []any, error) { // Step 1. Verify that attStmt is valid CBOR conforming to the syntax defined // above and perform CBOR decoding on it to extract the contained fields. // If x5c is not present, return an error - x5c, x509present := att.AttStatement["x5c"].([]interface{}) + x5c, x509present := att.AttStatement["x5c"].([]any) if !x509present { // Handle Basic Attestation steps for the x509 Certificate return "", nil, ErrAttestationFormat.WithDetails("Error retrieving x5c value") diff --git a/protocol/attestation_apple_test.go b/protocol/attestation_apple_test.go index bfb6ee6..fc42ff6 100644 --- a/protocol/attestation_apple_test.go +++ b/protocol/attestation_apple_test.go @@ -20,7 +20,7 @@ func Test_verifyAppleFormat(t *testing.T) { name string args args want string - want1 []interface{} + want1 []any wantErr bool }{ { @@ -42,12 +42,10 @@ func Test_verifyAppleFormat(t *testing.T) { t.Errorf("verifyAppleFormat() error = %v, wantErr %v", err, tt.wantErr) return } + if got != tt.want { t.Errorf("verifyAppleFormat() got = %v, want %v", got, tt.want) } - //if !reflect.DeepEqual(got1, tt.want1) { - // t.Errorf("verifyPackedFormat() got1 = %v, want %v", got1, tt.want1) - //} }) } } diff --git a/protocol/attestation_packed.go b/protocol/attestation_packed.go index 8874d80..c33a904 100644 --- a/protocol/attestation_packed.go +++ b/protocol/attestation_packed.go @@ -34,7 +34,7 @@ func init() { // } // // Specification: §8.2. Packed Attestation Statement Format (https://www.w3.org/TR/webauthn/#sctn-packed-attestation) -func verifyPackedFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) { +func verifyPackedFormat(att AttestationObject, clientDataHash []byte) (string, []any, error) { // Step 1. Verify that attStmt is valid CBOR conforming to the syntax defined // above and perform CBOR decoding on it to extract the contained fields. @@ -53,7 +53,7 @@ func verifyPackedFormat(att AttestationObject, clientDataHash []byte) (string, [ } // Step 2. If x5c is present, this indicates that the attestation type is not ECDAA. - x5c, x509present := att.AttStatement["x5c"].([]interface{}) + x5c, x509present := att.AttStatement["x5c"].([]any) if x509present { // Handle Basic Attestation steps for the x509 Certificate return handleBasicAttestation(sig, clientDataHash, att.RawAuthData, att.AuthData.AttData.AAGUID, alg, x5c) @@ -72,7 +72,7 @@ func verifyPackedFormat(att AttestationObject, clientDataHash []byte) (string, [ } // Handle the attestation steps laid out in -func handleBasicAttestation(signature, clientDataHash, authData, aaguid []byte, alg int64, x5c []interface{}) (string, []interface{}, error) { +func handleBasicAttestation(signature, clientDataHash, authData, aaguid []byte, alg int64, x5c []any) (string, []any, error) { // Step 2.1. Verify that sig is a valid signature over the concatenation of authenticatorData // and clientDataHash using the attestation public key in attestnCert with the algorithm specified in alg. for _, c := range x5c { @@ -199,11 +199,11 @@ func handleBasicAttestation(signature, clientDataHash, authData, aaguid []byte, return string(metadata.BasicFull), x5c, nil } -func handleECDAAAttestation(signature, clientDataHash, ecdaaKeyID []byte) (string, []interface{}, error) { +func handleECDAAAttestation(signature, clientDataHash, ecdaaKeyID []byte) (string, []any, error) { return "Packed (ECDAA)", nil, ErrNotSpecImplemented } -func handleSelfAttestation(alg int64, pubKey, authData, clientDataHash, signature []byte) (string, []interface{}, error) { +func handleSelfAttestation(alg int64, pubKey, authData, clientDataHash, signature []byte) (string, []any, error) { // §4.1 Validate that alg matches the algorithm of the credentialPublicKey in authenticatorData. // §4.2 Verify that sig is a valid signature over the concatenation of authenticatorData and diff --git a/protocol/attestation_packed_test.go b/protocol/attestation_packed_test.go index ea0552f..66e8b6b 100644 --- a/protocol/attestation_packed_test.go +++ b/protocol/attestation_packed_test.go @@ -24,7 +24,7 @@ func Test_verifyPackedFormat(t *testing.T) { name string args args want string - want1 []interface{} + want1 []any wantErr bool }{ { diff --git a/protocol/attestation_safetynet.go b/protocol/attestation_safetynet.go index f88e804..954541d 100644 --- a/protocol/attestation_safetynet.go +++ b/protocol/attestation_safetynet.go @@ -19,13 +19,13 @@ func init() { } type SafetyNetResponse struct { - Nonce string `json:"nonce"` - TimestampMs int64 `json:"timestampMs"` - ApkPackageName string `json:"apkPackageName"` - ApkDigestSha256 string `json:"apkDigestSha256"` - CtsProfileMatch bool `json:"ctsProfileMatch"` - ApkCertificateDigestSha256 []interface{} `json:"apkCertificateDigestSha256"` - BasicIntegrity bool `json:"basicIntegrity"` + Nonce string `json:"nonce"` + TimestampMs int64 `json:"timestampMs"` + ApkPackageName string `json:"apkPackageName"` + ApkDigestSha256 string `json:"apkDigestSha256"` + CtsProfileMatch bool `json:"ctsProfileMatch"` + ApkCertificateDigestSha256 []any `json:"apkCertificateDigestSha256"` + BasicIntegrity bool `json:"basicIntegrity"` } // Thanks to @koesie10 and @herrjemand for outlining how to support this type really well @@ -40,7 +40,7 @@ type SafetyNetResponse struct { // authenticators SHOULD make use of the Android Key Attestation when available, even if the SafetyNet API is also present. // // Specification: §8.5. Android SafetyNet Attestation Statement Format (https://www.w3.org/TR/webauthn/#sctn-android-safetynet-attestation) -func verifySafetyNetFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) { +func verifySafetyNetFormat(att AttestationObject, clientDataHash []byte) (string, []any, error) { // The syntax of an Android Attestation statement is defined as follows: // $$attStmtType //= ( // fmt: "android-safetynet", @@ -73,8 +73,8 @@ func verifySafetyNetFormat(att AttestationObject, clientDataHash []byte) (string return "", nil, ErrAttestationFormat.WithDetails("Unable to find the SafetyNet response") } - token, err := jwt.Parse(string(response), func(token *jwt.Token) (interface{}, error) { - chain := token.Header["x5c"].([]interface{}) + token, err := jwt.Parse(string(response), func(token *jwt.Token) (any, error) { + chain := token.Header["x5c"].([]any) o := make([]byte, base64.StdEncoding.DecodedLen(len(chain[0].(string)))) @@ -108,7 +108,7 @@ func verifySafetyNetFormat(att AttestationObject, clientDataHash []byte) (string } // §8.5.4 Let attestationCert be the attestation certificate (https://www.w3.org/TR/webauthn/#attestation-certificate) - certChain := token.Header["x5c"].([]interface{}) + certChain := token.Header["x5c"].([]any) l := make([]byte, base64.StdEncoding.DecodedLen(len(certChain[0].(string)))) n, err := base64.StdEncoding.Decode(l, []byte(certChain[0].(string))) diff --git a/protocol/attestation_safetynet_test.go b/protocol/attestation_safetynet_test.go index 379133a..d40ba30 100644 --- a/protocol/attestation_safetynet_test.go +++ b/protocol/attestation_safetynet_test.go @@ -21,7 +21,7 @@ func Test_verifySafetyNetFormat(t *testing.T) { name string args args want string - want1 []interface{} + want1 []any wantErr bool }{ { @@ -43,9 +43,11 @@ func Test_verifySafetyNetFormat(t *testing.T) { t.Errorf("verifySafetyNetFormat() error = %v, wantErr %v", err, tt.wantErr) return } + if got != tt.want { t.Errorf("verifySafetyNetFormat() got = %v, want %v", got, tt.want) } + if !reflect.DeepEqual(got1, tt.want1) { t.Errorf("verifySafetyNetFormat() got1 = %v, want %v", got1, tt.want1) } diff --git a/protocol/attestation_test.go b/protocol/attestation_test.go index 60a6bf6..15831c4 100644 --- a/protocol/attestation_test.go +++ b/protocol/attestation_test.go @@ -20,10 +20,13 @@ func TestAttestationVerify(t *testing.T) { if err := json.Unmarshal([]byte(testAttestationOptions[i]), &options); err != nil { t.Fatal(err) } + ccr := CredentialCreationResponse{} + if err := json.Unmarshal([]byte(testAttestationResponses[i]), &ccr); err != nil { t.Fatal(err) } + var pcc ParsedCredentialCreationData pcc.ID, pcc.RawID, pcc.Type, pcc.ClientExtensionResults = ccr.ID, ccr.RawID, ccr.Type, ccr.ClientExtensionResults pcc.Raw = ccr diff --git a/protocol/attestation_tpm.go b/protocol/attestation_tpm.go index c1fbe39..e86881c 100644 --- a/protocol/attestation_tpm.go +++ b/protocol/attestation_tpm.go @@ -19,7 +19,7 @@ func init() { RegisterAttestationFormat(AttestationFormatTPM, verifyTPMFormat) } -func verifyTPMFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) { +func verifyTPMFormat(att AttestationObject, clientDataHash []byte) (string, []any, error) { // Given the verification procedure inputs attStmt, authenticatorData // and clientDataHash, the verification procedure is as follows @@ -42,7 +42,7 @@ func verifyTPMFormat(att AttestationObject, clientDataHash []byte) (string, []in coseAlg := webauthncose.COSEAlgorithmIdentifier(alg) - x5c, x509present := att.AttStatement["x5c"].([]interface{}) + x5c, x509present := att.AttStatement["x5c"].([]any) if !x509present { // Handle Basic Attestation steps for the x509 Certificate return "", nil, ErrNotImplemented diff --git a/protocol/attestation_tpm_test.go b/protocol/attestation_tpm_test.go index f467f37..412a74f 100644 --- a/protocol/attestation_tpm_test.go +++ b/protocol/attestation_tpm_test.go @@ -31,6 +31,7 @@ func TestTPMAttestationVerificationSuccess(t *testing.T) { if err != nil { t.Fatalf("Not valid: %+v", err) } + assert.Equal(t, "attca", attestationType) }) } @@ -82,37 +83,37 @@ func TestTPMAttestationVerificationFailAttStatement(t *testing.T) { }, { "TPM Negative Test AttStatement Ver not 2.0", - AttestationObject{AttStatement: map[string]interface{}{"ver": "foo.bar"}}, + AttestationObject{AttStatement: map[string]any{"ver": "foo.bar"}}, "WebAuthn only supports TPM 2.0 currently", }, { "TPM Negative Test AttStatement Alg not present", - AttestationObject{AttStatement: map[string]interface{}{"ver": "2.0"}}, + AttestationObject{AttStatement: map[string]any{"ver": "2.0"}}, "Error retrieving alg value", }, { "TPM Negative Test AttStatement x5c not present", - AttestationObject{AttStatement: map[string]interface{}{"ver": "2.0", "alg": int64(0)}}, + AttestationObject{AttStatement: map[string]any{"ver": "2.0", "alg": int64(0)}}, ErrNotImplemented.Details, }, { "TPM Negative Test AttStatement ecdaaKeyId present", - AttestationObject{AttStatement: map[string]interface{}{"ver": "2.0", "alg": int64(0), "x5c": []interface{}{}, "ecdaaKeyId": []byte{}}}, + AttestationObject{AttStatement: map[string]any{"ver": "2.0", "alg": int64(0), "x5c": []any{}, "ecdaaKeyId": []byte{}}}, ErrNotImplemented.Details, }, { "TPM Negative Test AttStatement sig not present", - AttestationObject{AttStatement: map[string]interface{}{"ver": "2.0", "alg": int64(0), "x5c": []interface{}{}}}, + AttestationObject{AttStatement: map[string]any{"ver": "2.0", "alg": int64(0), "x5c": []any{}}}, "Error retrieving sig value", }, { "TPM Negative Test AttStatement certInfo not present", - AttestationObject{AttStatement: map[string]interface{}{"ver": "2.0", "alg": int64(0), "x5c": []interface{}{}, "sig": []byte{}}}, + AttestationObject{AttStatement: map[string]any{"ver": "2.0", "alg": int64(0), "x5c": []any{}, "sig": []byte{}}}, "Error retrieving certInfo value", }, { "TPM Negative Test AttStatement pubArea not present", - AttestationObject{AttStatement: map[string]interface{}{"ver": "2.0", "alg": int64(0), "x5c": []interface{}{}, "sig": []byte{}, "certInfo": []byte{}}}, + AttestationObject{AttStatement: map[string]any{"ver": "2.0", "alg": int64(0), "x5c": []any{}, "sig": []byte{}, "certInfo": []byte{}}}, "Error retrieving pubArea value", }, { @@ -122,7 +123,7 @@ func TestTPMAttestationVerificationFailAttStatement(t *testing.T) { }, { "TPM Negative Test Unsupported Public Key Type", - AttestationObject{AttStatement: map[string]interface{}{"ver": "2.0", "alg": int64(0), "x5c": []interface{}{}, "sig": []byte{}, "certInfo": []byte{}, "pubArea": makeDefaultRSAPublicBytes()}, AuthData: AuthenticatorData{AttData: AttestedCredentialData{CredentialPublicKey: []byte{}}}}, + AttestationObject{AttStatement: map[string]any{"ver": "2.0", "alg": int64(0), "x5c": []any{}, "sig": []byte{}, "certInfo": []byte{}, "pubArea": makeDefaultRSAPublicBytes()}, AuthData: AuthenticatorData{AttData: AttestedCredentialData{CredentialPublicKey: []byte{}}}}, "Unsupported Public Key Type", }, } @@ -169,7 +170,7 @@ var ( } ) -var defaultAttStatement = map[string]interface{}{"ver": "2.0", "alg": int64(-257), "x5c": []interface{}{}, "sig": []byte{}, "certInfo": []byte{}, "pubArea": []byte{}} +var defaultAttStatement = map[string]any{"ver": "2.0", "alg": int64(-257), "x5c": []any{}, "sig": []byte{}, "certInfo": []byte{}, "pubArea": []byte{}} type CredentialPublicKey struct { KeyType int64 `cbor:"1,keyasint" json:"kty"` @@ -332,7 +333,7 @@ func TestTPMAttestationVerificationFailPubArea(t *testing.T) { }, } for _, tt := range tests { - attStmt := make(map[string]interface{}, len(defaultAttStatement)) + attStmt := make(map[string]any, len(defaultAttStatement)) for id, v := range defaultAttStatement { attStmt[id] = v } @@ -373,7 +374,7 @@ func TestTPMAttestationVerificationFailPubArea(t *testing.T) { } func TestTPMAttestationVerificationFailCertInfo(t *testing.T) { - attStmt := make(map[string]interface{}, len(defaultAttStatement)) + attStmt := make(map[string]any, len(defaultAttStatement)) for id, v := range defaultAttStatement { attStmt[id] = v @@ -487,7 +488,7 @@ var ( ) func TestTPMAttestationVerificationFailX5c(t *testing.T) { - attStmt := make(map[string]interface{}, len(defaultAttStatement)) + attStmt := make(map[string]any, len(defaultAttStatement)) for id, v := range defaultAttStatement { attStmt[id] = v @@ -543,8 +544,8 @@ func TestTPMAttestationVerificationFailX5c(t *testing.T) { } attStmt["certInfo"], _ = certInfo.Encode() - makeX5c := func(b []byte) []interface{} { - q := make([]interface{}, 1) + makeX5c := func(b []byte) []any { + q := make([]any, 1) q[0] = b return q @@ -552,12 +553,12 @@ func TestTPMAttestationVerificationFailX5c(t *testing.T) { tests := []struct { name string - x5c []interface{} + x5c []any wantErr string }{ { "TPM Negative Test x5c empty", - make([]interface{}, 1), + make([]any, 1), "Error getting certificate from x5c cert chain", }, { diff --git a/protocol/attestation_u2f.go b/protocol/attestation_u2f.go index 4b84962..0689d42 100644 --- a/protocol/attestation_u2f.go +++ b/protocol/attestation_u2f.go @@ -12,11 +12,11 @@ import ( ) func init() { - RegisterAttestationFormat(AttestationFormatFIDOU2F, verifyU2FFormat) + RegisterAttestationFormat(AttestationFormatFIDOUniversalSecondFactor, verifyU2FFormat) } // verifyU2FFormat - Follows verification steps set out by https://www.w3.org/TR/webauthn/#fido-u2f-attestation -func verifyU2FFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) { +func verifyU2FFormat(att AttestationObject, clientDataHash []byte) (string, []any, error) { if !bytes.Equal(att.AuthData.AttData.AAGUID, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) { return "", nil, ErrUnsupportedAlgorithm.WithDetails("U2F attestation format AAGUID not set to 0x00") } @@ -40,7 +40,7 @@ func verifyU2FFormat(att AttestationObject, clientDataHash []byte) (string, []in // } // Check for "x5c" which is a single element array containing the attestation certificate in X.509 format. - x5c, present := att.AttStatement["x5c"].([]interface{}) + x5c, present := att.AttStatement["x5c"].([]any) if !present { return "", nil, ErrAttestationFormat.WithDetails("Missing properly formatted x5c data") } diff --git a/protocol/attestation_u2f_test.go b/protocol/attestation_u2f_test.go index 813ec9e..04dc618 100644 --- a/protocol/attestation_u2f_test.go +++ b/protocol/attestation_u2f_test.go @@ -20,7 +20,7 @@ func TestVerifyU2FFormat(t *testing.T) { name string args args want string - want1 []interface{} + want1 []any wantErr bool }{ { @@ -42,12 +42,10 @@ func TestVerifyU2FFormat(t *testing.T) { t.Errorf("verifyU2FFormat() error = %v, wantErr %v", err, tt.wantErr) return } + if got != tt.want { t.Errorf("verifyU2FFormat() got = %v, want %v", got, tt.want) } - //if !reflect.DeepEqual(got1, tt.want1) { - // t.Errorf("verifyU2FFormat() got1 = %v, want %v", got1, tt.want1) - //} }) } } diff --git a/protocol/authenticator.go b/protocol/authenticator.go index 8424fe5..b10b72d 100644 --- a/protocol/authenticator.go +++ b/protocol/authenticator.go @@ -135,6 +135,11 @@ const ( // BLE indicates the respective authenticator can be contacted over Bluetooth Smart (Bluetooth Low Energy / BLE). BLE AuthenticatorTransport = "ble" + // SmartCard indicates the respective authenticator can be contacted over ISO/IEC 7816 smart card with contacts. + // + // WebAuthn Level 3. + SmartCard AuthenticatorTransport = "smart-card" + // Hybrid indicates the respective authenticator can be contacted using a combination of (often separate) // data-transport and proximity mechanisms. This supports, for example, authentication on a desktop computer using // a smartphone. @@ -322,7 +327,7 @@ func (a *AuthenticatorData) unmarshalAttestedData(rawAuthData []byte) (err error // Unmarshall the credential's Public Key into CBOR encoding. func unmarshalCredentialPublicKey(keyBytes []byte) (rawBytes []byte, err error) { - var m interface{} + var m any if err = webauthncbor.Unmarshal(keyBytes, &m); err != nil { return nil, err @@ -348,11 +353,6 @@ func ResidentKeyNotRequired() *bool { return &required } -// Deprecated: ResidentKeyUnrequired is an alias for ResidentKeyNotRequired and will be completely removed in the future. -func ResidentKeyUnrequired() *bool { - return ResidentKeyNotRequired() -} - // Verify on AuthenticatorData handles Steps 9 through 12 for Registration // and Steps 11 through 14 for Assertion. func (a *AuthenticatorData) Verify(rpIdHash []byte, appIDHash []byte, userVerificationRequired bool) error { diff --git a/protocol/challenge_test.go b/protocol/challenge_test.go index f2e5d8d..6118d4f 100644 --- a/protocol/challenge_test.go +++ b/protocol/challenge_test.go @@ -26,7 +26,9 @@ func TestCreateChallenge(t *testing.T) { t.Errorf("CreateChallenge() error = %v, wantErr %v", err, tt.wantErr) return } + tt.want = got + if !reflect.DeepEqual(got, tt.want) { t.Errorf("CreateChallenge() = %v, want %v", got, tt.want) } diff --git a/protocol/client.go b/protocol/client.go index 02402f7..ab9b6ab 100644 --- a/protocol/client.go +++ b/protocol/client.go @@ -20,7 +20,7 @@ type CollectedClientData struct { Type CeremonyType `json:"type"` Challenge string `json:"challenge"` Origin string `json:"origin"` - TopOrigin string `json:"topOrigin"` + TopOrigin string `json:"topOrigin,omitempty"` CrossOrigin bool `json:"crossOrigin,omitempty"` TokenBinding *TokenBinding `json:"tokenBinding,omitempty"` diff --git a/protocol/credential.go b/protocol/credential.go index 7067402..e316ebb 100644 --- a/protocol/credential.go +++ b/protocol/credential.go @@ -31,6 +31,7 @@ type ParsedCredential struct { type PublicKeyCredential struct { Credential + RawID URLEncodedBase64 `json:"rawId"` ClientExtensionResults AuthenticationExtensionsClientOutputs `json:"clientExtensionResults,omitempty"` AuthenticatorAttachment string `json:"authenticatorAttachment,omitempty"` @@ -38,6 +39,7 @@ type PublicKeyCredential struct { type ParsedPublicKeyCredential struct { ParsedCredential + RawID []byte `json:"rawId"` ClientExtensionResults AuthenticationExtensionsClientOutputs `json:"clientExtensionResults,omitempty"` AuthenticatorAttachment AuthenticatorAttachment `json:"authenticatorAttachment,omitempty"` @@ -45,6 +47,7 @@ type ParsedPublicKeyCredential struct { type CredentialCreationResponse struct { PublicKeyCredential + AttestationResponse AuthenticatorAttestationResponse `json:"response"` // Deprecated: Transports is deprecated due to upstream changes to the API. @@ -56,6 +59,7 @@ type CredentialCreationResponse struct { type ParsedCredentialCreationData struct { ParsedPublicKeyCredential + Response ParsedAttestationResponse Raw CredentialCreationResponse } diff --git a/protocol/credential_test.go b/protocol/credential_test.go index 201c45f..70f1ba0 100644 --- a/protocol/credential_test.go +++ b/protocol/credential_test.go @@ -259,7 +259,7 @@ func TestParseCredentialCreationResponse(t *testing.T) { assert.Equal(t, tc.expected.Response.AttestationObject.Format, actual.Response.AttestationObject.Format) // Unmarshall CredentialPublicKey - var pkExpected, pkActual interface{} + var pkExpected, pkActual any pkBytesExpected := tc.expected.Response.AttestationObject.AuthData.AttData.CredentialPublicKey assert.NoError(t, webauthncbor.Unmarshal(pkBytesExpected, &pkExpected)) diff --git a/protocol/entities.go b/protocol/entities.go index 1d2f6e8..40f3c30 100644 --- a/protocol/entities.go +++ b/protocol/entities.go @@ -51,5 +51,5 @@ type UserEntity struct { // authentication and authorization decisions MUST be made on the basis of this id // member, not the displayName nor name members. See Section 6.1 of // [RFC8266](https://www.w3.org/TR/webauthn/#biblio-rfc8266). - ID interface{} `json:"id"` + ID any `json:"id"` } diff --git a/protocol/extensions.go b/protocol/extensions.go index f925eb2..e226f7c 100644 --- a/protocol/extensions.go +++ b/protocol/extensions.go @@ -5,7 +5,7 @@ package protocol // For a list of commonly supported extensions, see §10. Defined Extensions // (https://www.w3.org/TR/webauthn/#sctn-defined-extensions). -type AuthenticationExtensionsClientOutputs map[string]interface{} +type AuthenticationExtensionsClientOutputs map[string]any const ( ExtensionAppID = "appid" diff --git a/protocol/options.go b/protocol/options.go index f48577c..9085fcd 100644 --- a/protocol/options.go +++ b/protocol/options.go @@ -17,24 +17,28 @@ type CredentialAssertion struct { // In order to create a Credential via create(), the caller specifies a few parameters in a // PublicKeyCredentialCreationOptions object. // +// WebAuthn Level 3: hints,attestationFormats. +// // Specification: §5.4. Options for Credential Creation (https://www.w3.org/TR/webauthn/#dictionary-makecredentialoptions) type PublicKeyCredentialCreationOptions struct { - RelyingParty RelyingPartyEntity `json:"rp"` - User UserEntity `json:"user"` - Challenge URLEncodedBase64 `json:"challenge"` - Parameters []CredentialParameter `json:"pubKeyCredParams,omitempty"` - Timeout int `json:"timeout,omitempty"` - CredentialExcludeList []CredentialDescriptor `json:"excludeCredentials,omitempty"` - AuthenticatorSelection AuthenticatorSelection `json:"authenticatorSelection,omitempty"` - Hints []PublicKeyCredentialHint `json:"hints,omitempty"` - Attestation ConveyancePreference `json:"attestation,omitempty"` - AttestationFormats []AttestationFormat `json:"attestationFormats,omitempty"` - Extensions AuthenticationExtensions `json:"extensions,omitempty"` + RelyingParty RelyingPartyEntity `json:"rp"` + User UserEntity `json:"user"` + Challenge URLEncodedBase64 `json:"challenge"` + Parameters []CredentialParameter `json:"pubKeyCredParams,omitempty"` + Timeout int `json:"timeout,omitempty"` + CredentialExcludeList []CredentialDescriptor `json:"excludeCredentials,omitempty"` + AuthenticatorSelection AuthenticatorSelection `json:"authenticatorSelection,omitempty"` + Hints []PublicKeyCredentialHints `json:"hints,omitempty"` + Attestation ConveyancePreference `json:"attestation,omitempty"` + AttestationFormats []AttestationFormat `json:"attestationFormats,omitempty"` + Extensions AuthenticationExtensions `json:"extensions,omitempty"` } // The PublicKeyCredentialRequestOptions dictionary supplies get() with the data it needs to generate an assertion. // Its challenge member MUST be present, while its other members are OPTIONAL. // +// WebAuthn Level 3: hints. +// // Specification: §5.5. Options for Assertion Generation (https://www.w3.org/TR/webauthn/#dictionary-assertion-options) type PublicKeyCredentialRequestOptions struct { Challenge URLEncodedBase64 `json:"challenge"` @@ -42,8 +46,7 @@ type PublicKeyCredentialRequestOptions struct { RelyingPartyID string `json:"rpId,omitempty"` AllowedCredentials []CredentialDescriptor `json:"allowCredentials,omitempty"` UserVerification UserVerificationRequirement `json:"userVerification,omitempty"` - Attestation ConveyancePreference `json:"attestation,omitempty"` - AttestationFormats []AttestationFormat `json:"attestationFormats,omitempty"` + Hints []PublicKeyCredentialHints `json:"hints,omitempty"` Extensions AuthenticationExtensions `json:"extensions,omitempty"` } @@ -97,7 +100,7 @@ const ( // parameters requesting additional processing by the client and authenticator. // // Specification: §5.7.1. Authentication Extensions Client Inputs (https://www.w3.org/TR/webauthn/#iface-authentication-extensions-client-inputs) -type AuthenticationExtensions map[string]interface{} +type AuthenticationExtensions map[string]any // AuthenticatorSelection represents the AuthenticatorSelectionCriteria IDL. // @@ -125,69 +128,6 @@ type AuthenticatorSelection struct { UserVerification UserVerificationRequirement `json:"userVerification,omitempty"` } -// PublicKeyCredentialHint is a type representing the enum PublicKeyCredentialHints from -// https://www.w3.org/TR/webauthn-3/#enum-hints. -type PublicKeyCredentialHint string - -const ( - // PublicKeyCredentialHintSecurityKey is a PublicKeyCredentialHint that indicates that the Relying Party believes - // that users will satisfy this request with a physical security key. For example, an enterprise Relying Party may - // set this hint if they have issued security keys to their employees and will only accept those authenticators for - // registration and authentication. - // - // For compatibility with older user agents, when this hint is used in PublicKeyCredentialCreationOptions, the - // authenticatorAttachment SHOULD be set to cross-platform. - PublicKeyCredentialHintSecurityKey PublicKeyCredentialHint = "security-key" - - // PublicKeyCredentialHintClientDevice is a PublicKeyCredentialHint that indicates that the Relying Party believes - // that users will satisfy this request with a platform authenticator attached to the client device. - // - // For compatibility with older user agents, when this hint is used in PublicKeyCredentialCreationOptions, the - // authenticatorAttachment SHOULD be set to platform. - PublicKeyCredentialHintClientDevice PublicKeyCredentialHint = "client-device" - - // PublicKeyCredentialHintHybrid is a PublicKeyCredentialHint that indicates that the Relying Party believes that - // users will satisfy this request with general-purpose authenticators such as smartphones. For example, a consumer - // Relying Party may believe that only a small fraction of their customers possesses dedicated security keys. This - // option also implies that the local platform authenticator should not be promoted in the UI. - // - // For compatibility with older user agents, when this hint is used in PublicKeyCredentialCreationOptions, the - // authenticatorAttachment SHOULD be set to cross-platform. - PublicKeyCredentialHintHybrid PublicKeyCredentialHint = "hybrid" -) - -type AttestationFormat string - -const ( - // AttestationFormatPacked is the "packed" attestation statement format is a WebAuthn-optimized format for - // attestation. It uses a very compact but still extensible encoding method. This format is implementable by - //authenticators with limited resources (e.g., secure elements). - AttestationFormatPacked AttestationFormat = "packed" - - // AttestationFormatTPM is the TPM attestation statement format returns an attestation statement in the same format - // as the packed attestation statement format, although the rawData and signature fields are computed differently. - AttestationFormatTPM AttestationFormat = "tpm" - - // AttestationFormatAndroidKey is the attestation statement format for platform authenticators on versions "N", and - // later, which may provide this proprietary "hardware attestation" statement. - AttestationFormatAndroidKey AttestationFormat = "android-key" - - // AttestationFormatAndroidSafetyNet is the attestation statement format that Android-based platform authenticators - // MAY produce an attestation statement based on the Android SafetyNet API. - AttestationFormatAndroidSafetyNet AttestationFormat = "android-safetynet" - - // AttestationFormatFIDOU2F is the attestation statement format that is used with FIDO U2F authenticators. - AttestationFormatFIDOU2F AttestationFormat = "fido-u2f" - - // AttestationFormatApple is the attestation statement format that is used with Apple devices' platform - // authenticators. - AttestationFormatApple AttestationFormat = "apple" - - // AttestationFormatNone is the attestation statement format that is used to replace any authenticator-provided - // attestation statement when a WebAuthn Relying Party indicates it does not wish to receive attestation information. - AttestationFormatNone AttestationFormat = "none" -) - // ConveyancePreference is the type representing the AttestationConveyancePreference IDL. // // WebAuthn Relying Parties may use AttestationConveyancePreference to specify their preference regarding attestation @@ -245,6 +185,72 @@ const ( PreferEnterpriseAttestation ConveyancePreference = "enterprise" ) +// AttestationFormat is an internal representation of the relevant inputs for registration. +// +// Specification: §5.4 Options for Credential Creation (https://w3c.github.io/webauthn/#dom-publickeycredentialcreationoptions-attestationformats) +// Registry: https://www.iana.org/assignments/webauthn/webauthn.xhtml +type AttestationFormat string + +const ( + // AttestationFormatPacked is the "packed" attestation statement format is a WebAuthn-optimized format for + // attestation. It uses a very compact but still extensible encoding method. This format is implementable by + //authenticators with limited resources (e.g., secure elements). + AttestationFormatPacked AttestationFormat = "packed" + + // AttestationFormatTPM is the TPM attestation statement format returns an attestation statement in the same format + // as the packed attestation statement format, although the rawData and signature fields are computed differently. + AttestationFormatTPM AttestationFormat = "tpm" + + // AttestationFormatAndroidKey is the attestation statement format for platform authenticators on versions "N", and + // later, which may provide this proprietary "hardware attestation" statement. + AttestationFormatAndroidKey AttestationFormat = "android-key" + + // AttestationFormatAndroidSafetyNet is the attestation statement format that Android-based platform authenticators + // MAY produce an attestation statement based on the Android SafetyNet API. + AttestationFormatAndroidSafetyNet AttestationFormat = "android-safetynet" + + // AttestationFormatFIDOUniversalSecondFactor is the attestation statement format that is used with FIDO U2F + // authenticators. + AttestationFormatFIDOUniversalSecondFactor AttestationFormat = "fido-u2f" + + // AttestationFormatApple is the attestation statement format that is used with Apple devices' platform + // authenticators. + AttestationFormatApple AttestationFormat = "apple" + + // AttestationFormatNone is the attestation statement format that is used to replace any authenticator-provided + // attestation statement when a WebAuthn Relying Party indicates it does not wish to receive attestation information. + AttestationFormatNone AttestationFormat = "none" +) + +type PublicKeyCredentialHints string + +const ( + // PublicKeyCredentialHintSecurityKey is a PublicKeyCredentialHint that indicates that the Relying Party believes + // that users will satisfy this request with a physical security key. For example, an enterprise Relying Party may + // set this hint if they have issued security keys to their employees and will only accept those authenticators for + // registration and authentication. + // + // For compatibility with older user agents, when this hint is used in PublicKeyCredentialCreationOptions, the + // authenticatorAttachment SHOULD be set to cross-platform. + PublicKeyCredentialHintSecurityKey PublicKeyCredentialHints = "security-key" + + // PublicKeyCredentialHintClientDevice is a PublicKeyCredentialHint that indicates that the Relying Party believes + // that users will satisfy this request with a platform authenticator attached to the client device. + // + // For compatibility with older user agents, when this hint is used in PublicKeyCredentialCreationOptions, the + // authenticatorAttachment SHOULD be set to platform. + PublicKeyCredentialHintClientDevice PublicKeyCredentialHints = "client-device" + + // PublicKeyCredentialHintHybrid is a PublicKeyCredentialHint that indicates that the Relying Party believes that + // users will satisfy this request with general-purpose authenticators such as smartphones. For example, a consumer + // Relying Party may believe that only a small fraction of their customers possesses dedicated security keys. This + // option also implies that the local platform authenticator should not be promoted in the UI. + // + // For compatibility with older user agents, when this hint is used in PublicKeyCredentialCreationOptions, the + // authenticatorAttachment SHOULD be set to cross-platform. + PublicKeyCredentialHintHybrid PublicKeyCredentialHints = "hybrid" +) + func (a *PublicKeyCredentialRequestOptions) GetAllowedCredentialIDs() [][]byte { var allowedCredentialIDs = make([][]byte, len(a.AllowedCredentials)) @@ -255,7 +261,7 @@ func (a *PublicKeyCredentialRequestOptions) GetAllowedCredentialIDs() [][]byte { return allowedCredentialIDs } -type Extensions interface{} +type Extensions any type ServerResponse struct { Status ServerResponseStatus `json:"status"` diff --git a/protocol/webauthncbor/webauthncbor.go b/protocol/webauthncbor/webauthncbor.go index 2886d0f..aff1ac1 100644 --- a/protocol/webauthncbor/webauthncbor.go +++ b/protocol/webauthncbor/webauthncbor.go @@ -18,7 +18,7 @@ var ctap2CBOREncMode, _ = cbor.CTAP2EncOptions().EncMode() // Unmarshal parses the CBOR-encoded data into the value pointed to by v // following the CTAP2 canonical CBOR encoding form. // (https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#message-encoding) -func Unmarshal(data []byte, v interface{}) error { +func Unmarshal(data []byte, v any) error { // TODO (james-d-elliott): investigate the specific use case for Unmarshal vs UnmarshalFirst to determine the edge cases where this may be useful. _, err := ctap2CBORDecMode.UnmarshalFirst(data, v) @@ -28,6 +28,6 @@ func Unmarshal(data []byte, v interface{}) error { // Marshal encodes the value pointed to by v // following the CTAP2 canonical CBOR encoding form. // (https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#message-encoding) -func Marshal(v interface{}) ([]byte, error) { +func Marshal(v any) ([]byte, error) { return ctap2CBOREncMode.Marshal(v) } diff --git a/protocol/webauthncose/webauthncose.go b/protocol/webauthncose/webauthncose.go index 308adef..76a0aad 100644 --- a/protocol/webauthncose/webauthncose.go +++ b/protocol/webauthncose/webauthncose.go @@ -178,7 +178,7 @@ func HasherFromCOSEAlg(coseAlg COSEAlgorithmIdentifier) func() hash.Hash { } // ParsePublicKey figures out what kind of COSE material was provided and create the data for the new key. -func ParsePublicKey(keyBytes []byte) (interface{}, error) { +func ParsePublicKey(keyBytes []byte) (any, error) { pk := PublicKeyData{} // TODO (james-d-elliott): investigate the ignored errors. webauthncbor.Unmarshal(keyBytes, &pk) @@ -342,7 +342,7 @@ func (k *EC2PublicKeyData) TPMCurveID() tpm2.EllipticCurve { } } -func VerifySignature(key interface{}, data []byte, sig []byte) (bool, error) { +func VerifySignature(key any, data []byte, sig []byte) (bool, error) { switch k := key.(type) { case OKPPublicKeyData: return k.Verify(data, sig) diff --git a/webauthn/login.go b/webauthn/login.go index ff1c5c2..69ecb97 100644 --- a/webauthn/login.go +++ b/webauthn/login.go @@ -115,19 +115,12 @@ func WithUserVerification(userVerification protocol.UserVerificationRequirement) } } -// WithLoginConveyancePreference adjusts the non-default parameters regarding whether the authenticator should attest to the -// credential. -func WithLoginConveyancePreference(preference protocol.ConveyancePreference) LoginOption { - return func(cco *protocol.PublicKeyCredentialRequestOptions) { - cco.Attestation = preference - } -} - -// WithLoginAttestationFormats adjusts the preferred attestation formats for this credential request in most to least -// preferable. Advisory only. -func WithLoginAttestationFormats(formats ...protocol.AttestationFormat) LoginOption { +// WithAssertionPublicKeyCredentialHints adjusts the non-default hints for credential types to select during login. +// +// WebAuthn Level 3. +func WithAssertionPublicKeyCredentialHints(hints []protocol.PublicKeyCredentialHints) LoginOption { return func(cco *protocol.PublicKeyCredentialRequestOptions) { - cco.AttestationFormats = formats + cco.Hints = hints } } @@ -145,7 +138,7 @@ func WithAppIdExtension(appid string) LoginOption { for _, credential := range cco.AllowedCredentials { if credential.AttestationType == protocol.CredentialTypeFIDOU2F { if cco.Extensions == nil { - cco.Extensions = map[string]interface{}{} + cco.Extensions = map[string]any{} } cco.Extensions[protocol.ExtensionAppID] = appid diff --git a/webauthn/registration.go b/webauthn/registration.go index e24e2b1..c6009ea 100644 --- a/webauthn/registration.go +++ b/webauthn/registration.go @@ -29,7 +29,7 @@ func (webauthn *WebAuthn) BeginRegistration(user User, opts ...RegistrationOptio return nil, nil, err } - var entityUserID interface{} + var entityUserID any if webauthn.Config.EncodeUserIDAsString { entityUserID = string(user.WebAuthnID()) @@ -93,6 +93,20 @@ func (webauthn *WebAuthn) BeginRegistration(user User, opts ...RegistrationOptio return creation, session, nil } +// WithCredentialParameters adjusts the credential parameters in the registration options. +func WithCredentialParameters(credentialParams []protocol.CredentialParameter) RegistrationOption { + return func(cco *protocol.PublicKeyCredentialCreationOptions) { + cco.Parameters = credentialParams + } +} + +// WithExclusions adjusts the non-default parameters regarding credentials to exclude from registration. +func WithExclusions(excludeList []protocol.CredentialDescriptor) RegistrationOption { + return func(cco *protocol.PublicKeyCredentialCreationOptions) { + cco.CredentialExcludeList = excludeList + } +} + // WithAuthenticatorSelection adjusts the non-default parameters regarding the authenticator to select during // registration. func WithAuthenticatorSelection(authenticatorSelection protocol.AuthenticatorSelection) RegistrationOption { @@ -101,38 +115,41 @@ func WithAuthenticatorSelection(authenticatorSelection protocol.AuthenticatorSel } } -func WithHints(hints ...protocol.PublicKeyCredentialHint) RegistrationOption { +// WithResidentKeyRequirement sets both the resident key and require resident key protocol options. +func WithResidentKeyRequirement(requirement protocol.ResidentKeyRequirement) RegistrationOption { return func(cco *protocol.PublicKeyCredentialCreationOptions) { - cco.Hints = hints - } -} + cco.AuthenticatorSelection.ResidentKey = requirement -// WithExclusions adjusts the non-default parameters regarding credentials to exclude from registration. -func WithExclusions(excludeList []protocol.CredentialDescriptor) RegistrationOption { - return func(cco *protocol.PublicKeyCredentialCreationOptions) { - cco.CredentialExcludeList = excludeList + switch requirement { + case protocol.ResidentKeyRequirementRequired: + cco.AuthenticatorSelection.RequireResidentKey = protocol.ResidentKeyRequired() + default: + cco.AuthenticatorSelection.RequireResidentKey = protocol.ResidentKeyNotRequired() + } } } -// WithConveyancePreference is a direct alias for WithRegistrationConveyancePreference. +// WithPublicKeyCredentialHints adjusts the non-default hints for credential types to select during registration. // -// Deprecated: Use WithRegistrationConveyancePreference in favor of WithConveyancePreference as this function will be -// likely be removed in a future release. -func WithConveyancePreference(preference protocol.ConveyancePreference) RegistrationOption { - return WithRegistrationConveyancePreference(preference) +// WebAuthn Level 3. +func WithPublicKeyCredentialHints(hints []protocol.PublicKeyCredentialHints) RegistrationOption { + return func(cco *protocol.PublicKeyCredentialCreationOptions) { + cco.Hints = hints + } } -// WithRegistrationConveyancePreference adjusts the non-default parameters regarding whether the authenticator should attest to the +// WithConveyancePreference adjusts the non-default parameters regarding whether the authenticator should attest to the // credential. -func WithRegistrationConveyancePreference(preference protocol.ConveyancePreference) RegistrationOption { +func WithConveyancePreference(preference protocol.ConveyancePreference) RegistrationOption { return func(cco *protocol.PublicKeyCredentialCreationOptions) { cco.Attestation = preference } } -// WithRegistrationAttestationFormats adjusts the preferred attestation formats for this credential creation in most to -// least preferable. Advisory only. -func WithRegistrationAttestationFormats(formats ...protocol.AttestationFormat) RegistrationOption { +// WithAttestationFormats adjusts the non-default formats for credential types to select during registration. +// +// WebAuthn Level 3. +func WithAttestationFormats(formats []protocol.AttestationFormat) RegistrationOption { return func(cco *protocol.PublicKeyCredentialCreationOptions) { cco.AttestationFormats = formats } @@ -145,13 +162,6 @@ func WithExtensions(extension protocol.AuthenticationExtensions) RegistrationOpt } } -// WithCredentialParameters adjusts the credential parameters in the registration options. -func WithCredentialParameters(credentialParams []protocol.CredentialParameter) RegistrationOption { - return func(cco *protocol.PublicKeyCredentialCreationOptions) { - cco.Parameters = credentialParams - } -} - // WithAppIdExcludeExtension automatically includes the specified appid if the CredentialExcludeList contains a credential // with the type `fido-u2f`. func WithAppIdExcludeExtension(appid string) RegistrationOption { @@ -159,7 +169,7 @@ func WithAppIdExcludeExtension(appid string) RegistrationOption { for _, credential := range cco.CredentialExcludeList { if credential.AttestationType == protocol.CredentialTypeFIDOU2F { if cco.Extensions == nil { - cco.Extensions = map[string]interface{}{} + cco.Extensions = map[string]any{} } cco.Extensions[protocol.ExtensionAppIDExclude] = appid @@ -168,20 +178,6 @@ func WithAppIdExcludeExtension(appid string) RegistrationOption { } } -// WithResidentKeyRequirement sets both the resident key and require resident key protocol options. -func WithResidentKeyRequirement(requirement protocol.ResidentKeyRequirement) RegistrationOption { - return func(cco *protocol.PublicKeyCredentialCreationOptions) { - cco.AuthenticatorSelection.ResidentKey = requirement - - switch requirement { - case protocol.ResidentKeyRequirementRequired: - cco.AuthenticatorSelection.RequireResidentKey = protocol.ResidentKeyRequired() - default: - cco.AuthenticatorSelection.RequireResidentKey = protocol.ResidentKeyNotRequired() - } - } -} - // FinishRegistration takes the response from the authenticator and client and verify the credential against the user's // credentials and session data. func (webauthn *WebAuthn) FinishRegistration(user User, session SessionData, response *http.Request) (*Credential, error) { From f63fbc1c8eda0faa65bd5cfbbd997d64ac2305ba Mon Sep 17 00:00:00 2001 From: James Elliott Date: Sat, 27 Apr 2024 17:12:48 +1000 Subject: [PATCH 5/8] feat!: remove deprecated values (#233) This removes all deprecated fields and functions from the library. BREAKING CHANGE: the following fields and backwards compatible elements have been removed; Icon field from the CredentialEntity struct, WebAuthnIcon function from the User interface, RPIcon/RPOrigin/Timeout fields from the Config struct, Transports field from the CredentialCreationResponse (new field has existed in the AuthenticatorAttestationResponse struct for quite some time which matches the spec). Closes #221 --- protocol/assertion.go | 3 + protocol/attestation.go | 10 ++- protocol/credential.go | 13 ---- protocol/credential_test.go | 124 ------------------------------------ protocol/entities.go | 11 +--- webauthn/registration.go | 2 - webauthn/types.go | 38 ----------- 7 files changed, 12 insertions(+), 189 deletions(-) diff --git a/protocol/assertion.go b/protocol/assertion.go index 184eb42..0b5eaef 100644 --- a/protocol/assertion.go +++ b/protocol/assertion.go @@ -15,6 +15,7 @@ import ( // credential for login/assertion. type CredentialAssertionResponse struct { PublicKeyCredential + AssertionResponse AuthenticatorAssertionResponse `json:"response"` } @@ -22,6 +23,7 @@ type CredentialAssertionResponse struct { // that allows us to verify the client and authenticator data inside the response. type ParsedCredentialAssertionData struct { ParsedPublicKeyCredential + Response ParsedAssertionResponse Raw CredentialAssertionResponse } @@ -30,6 +32,7 @@ type ParsedCredentialAssertionData struct { // ParsedAssertionResponse. type AuthenticatorAssertionResponse struct { AuthenticatorResponse + AuthenticatorData URLEncodedBase64 `json:"authenticatorData"` Signature URLEncodedBase64 `json:"signature"` UserHandle URLEncodedBase64 `json:"userHandle,omitempty"` diff --git a/protocol/attestation.go b/protocol/attestation.go index cdad1bf..36a264a 100644 --- a/protocol/attestation.go +++ b/protocol/attestation.go @@ -22,6 +22,14 @@ type AuthenticatorAttestationResponse struct { // The byte slice of clientDataJSON, which becomes CollectedClientData AuthenticatorResponse + Transports []string `json:"transports,omitempty"` + + AuthenticatorData URLEncodedBase64 `json:"authenticatorData"` + + PublicKey URLEncodedBase64 `json:"publicKey"` + + PublicKeyAlgorithm int64 `json:"publicKeyAlgorithm"` + // AttestationObject is the byte slice version of attestationObject. // This attribute contains an attestation object, which is opaque to, and // cryptographically protected against tampering by, the client. The @@ -33,8 +41,6 @@ type AuthenticatorAttestationResponse struct { // requires to validate the attestation statement, as well as to decode and // validate the authenticator data along with the JSON-serialized client data. AttestationObject URLEncodedBase64 `json:"attestationObject"` - - Transports []string `json:"transports,omitempty"` } // ParsedAttestationResponse is the parsed version of AuthenticatorAttestationResponse. diff --git a/protocol/credential.go b/protocol/credential.go index e316ebb..d532e2b 100644 --- a/protocol/credential.go +++ b/protocol/credential.go @@ -49,12 +49,6 @@ type CredentialCreationResponse struct { PublicKeyCredential AttestationResponse AuthenticatorAttestationResponse `json:"response"` - - // Deprecated: Transports is deprecated due to upstream changes to the API. - // Use the Transports field of AuthenticatorAttestationResponse - // instead. Transports is kept for backward compatibility, and should not - // be used by new clients. - Transports []string `json:"transports,omitempty"` } type ParsedCredentialCreationData struct { @@ -116,13 +110,6 @@ func (ccr CredentialCreationResponse) Parse() (pcc *ParsedCredentialCreationData return nil, ErrParsingData.WithDetails("Error parsing attestation response") } - // TODO: Remove this as it's a backwards compatibility layer. - if len(response.Transports) == 0 && len(ccr.Transports) != 0 { - for _, t := range ccr.Transports { - response.Transports = append(response.Transports, AuthenticatorTransport(t)) - } - } - var attachment AuthenticatorAttachment switch ccr.AuthenticatorAttachment { diff --git a/protocol/credential_test.go b/protocol/credential_test.go index 70f1ba0..26d0256 100644 --- a/protocol/credential_test.go +++ b/protocol/credential_test.go @@ -94,130 +94,6 @@ func TestParseCredentialCreationResponse(t *testing.T) { }, errString: "", }, - { - name: "ShouldParseCredentialRequestDeprecatedTransports", - args: args{ - responseName: "successDeprecatedTransports", - }, - expected: &ParsedCredentialCreationData{ - ParsedPublicKeyCredential: ParsedPublicKeyCredential{ - ParsedCredential: ParsedCredential{ - ID: "6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g", - Type: "public-key", - }, - RawID: byteID, - ClientExtensionResults: AuthenticationExtensionsClientOutputs{ - "appid": true, - }, - }, - Response: ParsedAttestationResponse{ - CollectedClientData: CollectedClientData{ - Type: CeremonyType("webauthn.create"), - Challenge: "W8GzFU8pGjhoRbWrLDlamAfq_y4S1CZG1VuoeRLARrE", - Origin: "https://webauthn.io", - }, - AttestationObject: AttestationObject{ - Format: "none", - RawAuthData: byteAuthData, - AuthData: AuthenticatorData{ - RPIDHash: byteRPIDHash, - Counter: 0, - Flags: 0x041, - AttData: AttestedCredentialData{ - AAGUID: make([]byte, 16), - CredentialID: byteID, - CredentialPublicKey: byteCredentialPubKey, - }, - }, - }, - Transports: []AuthenticatorTransport{USB, NFC, "fake"}, - }, - Raw: CredentialCreationResponse{ - PublicKeyCredential: PublicKeyCredential{ - Credential: Credential{ - Type: "public-key", - ID: "6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g", - }, - RawID: byteID, - ClientExtensionResults: AuthenticationExtensionsClientOutputs{ - "appid": true, - }, - AuthenticatorAttachment: "not-valid", - }, - AttestationResponse: AuthenticatorAttestationResponse{ - AuthenticatorResponse: AuthenticatorResponse{ - ClientDataJSON: byteClientDataJSON, - }, - AttestationObject: byteAttObject, - }, - Transports: []string{"usb", "nfc", "fake"}, - }, - }, - errString: "", - }, - { - name: "ShouldParseCredentialRequestDeprecatedTransportsShouldNotOverride", - args: args{ - responseName: "successDeprecatedTransportsAndNew", - }, - expected: &ParsedCredentialCreationData{ - ParsedPublicKeyCredential: ParsedPublicKeyCredential{ - ParsedCredential: ParsedCredential{ - ID: "6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g", - Type: "public-key", - }, - RawID: byteID, - ClientExtensionResults: AuthenticationExtensionsClientOutputs{ - "appid": true, - }, - AuthenticatorAttachment: CrossPlatform, - }, - Response: ParsedAttestationResponse{ - CollectedClientData: CollectedClientData{ - Type: CeremonyType("webauthn.create"), - Challenge: "W8GzFU8pGjhoRbWrLDlamAfq_y4S1CZG1VuoeRLARrE", - Origin: "https://webauthn.io", - }, - AttestationObject: AttestationObject{ - Format: "none", - RawAuthData: byteAuthData, - AuthData: AuthenticatorData{ - RPIDHash: byteRPIDHash, - Counter: 0, - Flags: 0x041, - AttData: AttestedCredentialData{ - AAGUID: make([]byte, 16), - CredentialID: byteID, - CredentialPublicKey: byteCredentialPubKey, - }, - }, - }, - Transports: []AuthenticatorTransport{USB, NFC}, - }, - Raw: CredentialCreationResponse{ - PublicKeyCredential: PublicKeyCredential{ - Credential: Credential{ - Type: "public-key", - ID: "6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g", - }, - RawID: byteID, - ClientExtensionResults: AuthenticationExtensionsClientOutputs{ - "appid": true, - }, - AuthenticatorAttachment: "cross-platform", - }, - AttestationResponse: AuthenticatorAttestationResponse{ - AuthenticatorResponse: AuthenticatorResponse{ - ClientDataJSON: byteClientDataJSON, - }, - AttestationObject: byteAttObject, - Transports: []string{"usb", "nfc"}, - }, - Transports: []string{"usb", "nfc", "fake"}, - }, - }, - errString: "", - }, { name: "ShouldHandleTrailingData", args: args{ diff --git a/protocol/entities.go b/protocol/entities.go index 40f3c30..b0ba2ad 100644 --- a/protocol/entities.go +++ b/protocol/entities.go @@ -14,16 +14,6 @@ type CredentialEntity struct { // intended only for display, i.e., aiding the user in determining the difference between user accounts with similar // displayNames. For example, "alexm", "alex.p.mueller@example.com" or "+14255551234". Name string `json:"name"` - - // A serialized URL which resolves to an image associated with the entity. For example, - // this could be a user’s avatar or a Relying Party's logo. This URL MUST be an a priori - // authenticated URL. Authenticators MUST accept and store a 128-byte minimum length for - // an icon member’s value. Authenticators MAY ignore an icon member’s value if its length - // is greater than 128 bytes. The URL’s scheme MAY be "data" to avoid fetches of the URL, - // at the cost of needing more storage. - // - // Deprecated: this has been removed from the specification recommendations. - Icon string `json:"icon,omitempty"` } // The RelyingPartyEntity represents the PublicKeyCredentialRpEntity IDL and is used to supply additional Relying Party @@ -32,6 +22,7 @@ type CredentialEntity struct { // Specification: §5.4.2. Relying Party Parameters for Credential Generation (https://www.w3.org/TR/webauthn/#dictionary-rp-credential-params) type RelyingPartyEntity struct { CredentialEntity + // A unique identifier for the Relying Party entity, which sets the RP ID. ID string `json:"id"` } diff --git a/webauthn/registration.go b/webauthn/registration.go index c6009ea..92566bd 100644 --- a/webauthn/registration.go +++ b/webauthn/registration.go @@ -42,7 +42,6 @@ func (webauthn *WebAuthn) BeginRegistration(user User, opts ...RegistrationOptio DisplayName: user.WebAuthnDisplayName(), CredentialEntity: protocol.CredentialEntity{ Name: user.WebAuthnName(), - Icon: user.WebAuthnIcon(), }, } @@ -50,7 +49,6 @@ func (webauthn *WebAuthn) BeginRegistration(user User, opts ...RegistrationOptio ID: webauthn.Config.RPID, CredentialEntity: protocol.CredentialEntity{ Name: webauthn.Config.RPDisplayName, - Icon: webauthn.Config.RPIcon, }, } diff --git a/webauthn/types.go b/webauthn/types.go index 79b6db1..161c45b 100644 --- a/webauthn/types.go +++ b/webauthn/types.go @@ -63,21 +63,6 @@ type Config struct { Timeouts TimeoutsConfig validated bool - - // RPIcon sets the icon URL for the Relying Party Server. - // - // Deprecated: this option has been removed from newer specifications due to security considerations. - RPIcon string - - // RPOrigin configures the permitted Relying Party Server Origin. - // - // Deprecated: Use RPOrigins instead. - RPOrigin string - - // Timeout configures the default timeout in milliseconds. - // - // Deprecated: Use Timeouts instead. - Timeout int } // TimeoutsConfig represents the WebAuthn timeouts configuration. @@ -120,20 +105,9 @@ func (config *Config) validate() error { return fmt.Errorf(errFmtFieldNotValidURI, "RPID", err) } - if config.RPIcon != "" { - if _, err = url.Parse(config.RPIcon); err != nil { - return fmt.Errorf(errFmtFieldNotValidURI, "RPIcon", err) - } - } - defaultTimeoutConfig := defaultTimeout defaultTimeoutUVDConfig := defaultTimeoutUVD - if config.Timeout != 0 { - defaultTimeoutConfig = time.Millisecond * time.Duration(config.Timeout) - defaultTimeoutUVDConfig = defaultTimeoutConfig - } - if config.Timeouts.Login.Timeout.Milliseconds() == 0 { config.Timeouts.Login.Timeout = defaultTimeoutConfig } @@ -150,14 +124,6 @@ func (config *Config) validate() error { config.Timeouts.Registration.TimeoutUVD = defaultTimeoutUVDConfig } - if len(config.RPOrigin) > 0 { - if len(config.RPOrigins) != 0 { - return fmt.Errorf("deprecated field 'RPOrigin' can't be defined at the same tme as the replacement field 'RPOrigins'") - } - - config.RPOrigins = []string{config.RPOrigin} - } - if len(config.RPOrigins) == 0 { return fmt.Errorf("must provide at least one value to the 'RPOrigins' field") } @@ -214,10 +180,6 @@ type User interface { // WebAuthnCredentials provides the list of Credential objects owned by the user. WebAuthnCredentials() []Credential - - // WebAuthnIcon is a deprecated option. - // Deprecated: this has been removed from the specification recommendation. Suggest a blank string. - WebAuthnIcon() string } // SessionData is the data that should be stored by the Relying Party for the duration of the web authentication From c673c3df53aefa0ff054ea9d327353d42db1a93a Mon Sep 17 00:00:00 2001 From: James Elliott Date: Mon, 29 Apr 2024 22:52:03 +1000 Subject: [PATCH 6/8] feat(config): allow rpid to be defined at execution time (#234) This allows the Relying Party ID and Name to be configured at runtime rather than at configuration time. Closes #165 --- webauthn/const.go | 1 - webauthn/login.go | 14 +++++ webauthn/login_test.go | 82 +++++++++++++++++++++++++ webauthn/registration.go | 25 ++++++++ webauthn/registration_test.go | 92 ++++++++++++++++++++++++++++- webauthn/types.go | 14 ++--- webauthn/{user.go => types_test.go} | 10 +--- 7 files changed, 219 insertions(+), 19 deletions(-) rename webauthn/{user.go => types_test.go} (68%) diff --git a/webauthn/const.go b/webauthn/const.go index 9ab74f2..7f08c3d 100644 --- a/webauthn/const.go +++ b/webauthn/const.go @@ -5,7 +5,6 @@ import ( ) const ( - errFmtFieldEmpty = "the field '%s' must be configured but it is empty" errFmtFieldNotValidURI = "field '%s' is not a valid URI: %w" errFmtConfigValidate = "error occurred validating the configuration: %w" ) diff --git a/webauthn/login.go b/webauthn/login.go index 69ecb97..dd6bd7a 100644 --- a/webauthn/login.go +++ b/webauthn/login.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "net/http" + "net/url" "time" "github.com/go-webauthn/webauthn/protocol" @@ -70,6 +71,12 @@ func (webauthn *WebAuthn) beginLogin(userID []byte, allowedCredentials []protoco opt(&assertion.Response) } + if len(assertion.Response.RelyingPartyID) == 0 { + return nil, nil, fmt.Errorf("error generating assertion: the relying party id must be provided via the configuration or a functional option for a login") + } else if _, err = url.Parse(assertion.Response.RelyingPartyID); err != nil { + return nil, nil, fmt.Errorf("error generating assertion: the relying party id failed to validate as it's not a valid uri with error: %w", err) + } + if assertion.Response.Timeout == 0 { switch { case assertion.Response.UserVerification == protocol.VerificationDiscouraged: @@ -147,6 +154,13 @@ func WithAppIdExtension(appid string) LoginOption { } } +// WithLoginRelyingPartyID sets the Relying Party ID for this particular login. +func WithLoginRelyingPartyID(id string) LoginOption { + return func(cco *protocol.PublicKeyCredentialRequestOptions) { + cco.RelyingPartyID = id + } +} + // FinishLogin takes the response from the client and validate it against the user credentials and stored session data. func (webauthn *WebAuthn) FinishLogin(user User, session SessionData, response *http.Request) (*Credential, error) { parsedResponse, err := protocol.ParseCredentialRequestResponse(response) diff --git a/webauthn/login_test.go b/webauthn/login_test.go index 29fdbc5..a869cf9 100644 --- a/webauthn/login_test.go +++ b/webauthn/login_test.go @@ -3,6 +3,9 @@ package webauthn import ( "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/go-webauthn/webauthn/protocol" ) @@ -26,3 +29,82 @@ func TestLogin_FinishLoginFailure(t *testing.T) { t.Errorf("FinishLogin() credential = %v, want nil", credential) } } + +func TestWithLoginRelyingPartyID(t *testing.T) { + testCases := []struct { + name string + have *Config + opts []LoginOption + expectedID string + err string + }{ + { + name: "OptionDefinedInConfig", + have: &Config{ + RPID: "https://example.com", + RPDisplayName: "Test Display Name", + RPOrigins: []string{"https://example.com"}, + }, + opts: nil, + expectedID: "https://example.com", + }, + { + name: "OptionDefinedInConfigAndOpts", + have: &Config{ + RPID: "https://example.com", + RPDisplayName: "Test Display Name", + RPOrigins: []string{"https://example.com"}, + }, + opts: []LoginOption{WithLoginRelyingPartyID("https://a.example.com")}, + expectedID: "https://a.example.com", + }, + { + name: "OptionDefinedInConfigWithNoErrAndInOptsWithError", + have: &Config{ + RPID: "https://example.com", + RPDisplayName: "Test Display Name", + RPOrigins: []string{"https://example.com"}, + }, + opts: []LoginOption{WithLoginRelyingPartyID("---::~!!~@#M!@OIK#N!@IOK@@@@@@@@@@")}, + err: "error generating assertion: the relying party id failed to validate as it's not a valid uri with error: parse \"---::~!!~@\": first path segment in URL cannot contain colon", + }, + { + name: "OptionDefinedInOpts", + have: &Config{ + RPOrigins: []string{"https://example.com"}, + }, + opts: []LoginOption{WithLoginRelyingPartyID("https://example.com")}, + expectedID: "https://example.com", + }, + { + name: "OptionIDNotDefined", + have: &Config{ + RPOrigins: []string{"https://example.com"}, + }, + opts: nil, + err: "error generating assertion: the relying party id must be provided via the configuration or a functional option for a login", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + w, err := New(tc.have) + assert.NoError(t, err) + + user := &defaultUser{ + credentials: []Credential{ + {}, + }, + } + + creation, _, err := w.BeginLogin(user, tc.opts...) + if tc.err != "" { + assert.EqualError(t, err, tc.err) + } else { + assert.NoError(t, err) + require.NotNil(t, creation) + assert.Equal(t, tc.expectedID, creation.Response.RelyingPartyID) + } + }) + } +} diff --git a/webauthn/registration.go b/webauthn/registration.go index 92566bd..a0d6e3a 100644 --- a/webauthn/registration.go +++ b/webauthn/registration.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "net/http" + "net/url" "time" "github.com/go-webauthn/webauthn/protocol" @@ -69,6 +70,16 @@ func (webauthn *WebAuthn) BeginRegistration(user User, opts ...RegistrationOptio opt(&creation.Response) } + if len(creation.Response.RelyingParty.ID) == 0 { + return nil, nil, fmt.Errorf("error generating credential creation: the relying party id must be provided via the configuration or a functional option for a creation") + } else if _, err = url.Parse(creation.Response.RelyingParty.ID); err != nil { + return nil, nil, fmt.Errorf("error generating credential creation: the relying party id failed to validate as it's not a valid uri with error: %w", err) + } + + if len(creation.Response.RelyingParty.Name) == 0 { + return nil, nil, fmt.Errorf("error generating credential creation: the relying party display name must be provided via the configuration or a functional option for a creation") + } + if creation.Response.Timeout == 0 { switch { case creation.Response.AuthenticatorSelection.UserVerification == protocol.VerificationDiscouraged: @@ -176,6 +187,20 @@ func WithAppIdExcludeExtension(appid string) RegistrationOption { } } +// WithRegistrationRelyingPartyID sets the relying party id for the registration. +func WithRegistrationRelyingPartyID(id string) RegistrationOption { + return func(cco *protocol.PublicKeyCredentialCreationOptions) { + cco.RelyingParty.ID = id + } +} + +// WithRegistrationRelyingPartyName sets the relying party name for the registration. +func WithRegistrationRelyingPartyName(name string) RegistrationOption { + return func(cco *protocol.PublicKeyCredentialCreationOptions) { + cco.RelyingParty.Name = name + } +} + // FinishRegistration takes the response from the authenticator and client and verify the credential against the user's // credentials and session data. func (webauthn *WebAuthn) FinishRegistration(user User, session SessionData, response *http.Request) (*Credential, error) { diff --git a/webauthn/registration_test.go b/webauthn/registration_test.go index a2c597d..3aa7b96 100644 --- a/webauthn/registration_test.go +++ b/webauthn/registration_test.go @@ -4,10 +4,100 @@ import ( "encoding/json" "testing" - "github.com/go-webauthn/webauthn/protocol" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/go-webauthn/webauthn/protocol" ) +func TestWithRegistrationRelyingPartyID(t *testing.T) { + testCases := []struct { + name string + have *Config + opts []RegistrationOption + expectedID string + expectedName string + err string + }{ + { + name: "OptionDefinedInConfig", + have: &Config{ + RPID: "https://example.com", + RPDisplayName: "Test Display Name", + RPOrigins: []string{"https://example.com"}, + }, + opts: nil, + expectedID: "https://example.com", + expectedName: "Test Display Name", + }, + { + name: "OptionDefinedInConfigAndOpts", + have: &Config{ + RPID: "https://example.com", + RPDisplayName: "Test Display Name", + RPOrigins: []string{"https://example.com"}, + }, + opts: []RegistrationOption{WithRegistrationRelyingPartyID("https://a.example.com"), WithRegistrationRelyingPartyName("Test Display Name2")}, + expectedID: "https://a.example.com", + expectedName: "Test Display Name2", + }, + { + name: "OptionDefinedInConfigWithNoErrAndInOptsWithError", + have: &Config{ + RPID: "https://example.com", + RPDisplayName: "Test Display Name", + RPOrigins: []string{"https://example.com"}, + }, + opts: []RegistrationOption{WithRegistrationRelyingPartyID("---::~!!~@#M!@OIK#N!@IOK@@@@@@@@@@"), WithRegistrationRelyingPartyName("Test Display Name2")}, + err: "error generating credential creation: the relying party id failed to validate as it's not a valid uri with error: parse \"---::~!!~@\": first path segment in URL cannot contain colon", + }, + { + name: "OptionDefinedInOpts", + have: &Config{ + RPOrigins: []string{"https://example.com"}, + }, + opts: []RegistrationOption{WithRegistrationRelyingPartyID("https://example.com"), WithRegistrationRelyingPartyName("Test Display Name")}, + expectedID: "https://example.com", + expectedName: "Test Display Name", + }, + { + name: "OptionDisplayNameNotDefined", + have: &Config{ + RPOrigins: []string{"https://example.com"}, + }, + opts: []RegistrationOption{WithRegistrationRelyingPartyID("https://example.com")}, + err: "error generating credential creation: the relying party display name must be provided via the configuration or a functional option for a creation", + }, + { + name: "OptionIDNotDefined", + have: &Config{ + RPOrigins: []string{"https://example.com"}, + }, + opts: []RegistrationOption{WithRegistrationRelyingPartyName("Test Display Name")}, + err: "error generating credential creation: the relying party id must be provided via the configuration or a functional option for a creation", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + w, err := New(tc.have) + assert.NoError(t, err) + + user := &defaultUser{} + + creation, _, err := w.BeginRegistration(user, tc.opts...) + if tc.err != "" { + assert.EqualError(t, err, tc.err) + } else { + assert.NoError(t, err) + require.NotNil(t, creation) + assert.Equal(t, tc.expectedID, creation.Response.RelyingParty.ID) + assert.Equal(t, tc.expectedName, creation.Response.RelyingParty.Name) + } + }) + } +} + func TestRegistration_FinishRegistrationFailure(t *testing.T) { user := &defaultUser{ id: []byte("123"), diff --git a/webauthn/types.go b/webauthn/types.go index 161c45b..a0dcc67 100644 --- a/webauthn/types.go +++ b/webauthn/types.go @@ -91,18 +91,12 @@ func (config *Config) validate() error { return nil } - if len(config.RPDisplayName) == 0 { - return fmt.Errorf(errFmtFieldEmpty, "RPDisplayName") - } - - if len(config.RPID) == 0 { - return fmt.Errorf(errFmtFieldEmpty, "RPID") - } - var err error - if _, err = url.Parse(config.RPID); err != nil { - return fmt.Errorf(errFmtFieldNotValidURI, "RPID", err) + if len(config.RPID) != 0 { + if _, err = url.Parse(config.RPID); err != nil { + return fmt.Errorf(errFmtFieldNotValidURI, "RPID", err) + } } defaultTimeoutConfig := defaultTimeout diff --git a/webauthn/user.go b/webauthn/types_test.go similarity index 68% rename from webauthn/user.go rename to webauthn/types_test.go index 045ed8f..b154bd9 100644 --- a/webauthn/user.go +++ b/webauthn/types_test.go @@ -1,8 +1,8 @@ package webauthn -// TODO: move this to a _test.go file. type defaultUser struct { - id []byte + id []byte + credentials []Credential } var _ User = (*defaultUser)(nil) @@ -19,10 +19,6 @@ func (user *defaultUser) WebAuthnDisplayName() string { return "New User" } -func (user *defaultUser) WebAuthnIcon() string { - return "https://pics.com/avatar.png" -} - func (user *defaultUser) WebAuthnCredentials() []Credential { - return []Credential{} + return user.credentials } From 1297b9d8a0bdb916cadd6333e967384b75108afe Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 May 2024 08:29:42 +1000 Subject: [PATCH 7/8] build(deps): update module golang.org/x/crypto to v0.23.0 (#236) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 40a49cf..528bc45 100644 --- a/go.mod +++ b/go.mod @@ -10,13 +10,13 @@ require ( github.com/google/uuid v1.6.0 github.com/mitchellh/mapstructure v1.5.0 github.com/stretchr/testify v1.9.0 - golang.org/x/crypto v0.22.0 + golang.org/x/crypto v0.23.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect - golang.org/x/sys v0.19.0 // indirect + golang.org/x/sys v0.20.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index a7304dd..0a252cf 100644 --- a/go.sum +++ b/go.sum @@ -18,10 +18,10 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= From c99e7a7e840b722174b768087b151420b04d1d83 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Tue, 14 May 2024 05:56:20 +1000 Subject: [PATCH 8/8] build(deps): update module github.com/go-webauthn/x to v0.1.10 (#237) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 528bc45..d820919 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.21 require ( github.com/fxamacker/cbor/v2 v2.6.0 - github.com/go-webauthn/x v0.1.9 + github.com/go-webauthn/x v0.1.10 github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/go-tpm v0.9.0 github.com/google/uuid v1.6.0 diff --git a/go.sum b/go.sum index 0a252cf..256406a 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA= github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/go-webauthn/x v0.1.9 h1:v1oeLmoaa+gPOaZqUdDentu6Rl7HkSSsmOT6gxEQHhE= -github.com/go-webauthn/x v0.1.9/go.mod h1:pJNMlIMP1SU7cN8HNlKJpLEnFHCygLCvaLZ8a1xeoQA= +github.com/go-webauthn/x v0.1.10 h1:1JOJPQYJUssqOG3XcU0/Ifyl8TGk3iefZTrjv4oxTS8= +github.com/go-webauthn/x v0.1.10/go.mod h1:ZuuOJLvqa4+nYGY480aPXC4oY5jQUqzp8iKrRGYm64k= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk=