From 3ed1f0db5e2e82ea6d0ff5fca6c250fb51ab2c25 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Fri, 9 Feb 2024 05:34:36 +1100 Subject: [PATCH] feat: support hints and attestation formats 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) {