Repository: go-webauthn/webauthn (go)

Issue:
Support Hints
### Description

`hints` is an array of strings that is on the opt-level of `PublicKeyCredentialCreationOptions`.

### Use Case

We'd like to use the new hints field to hint to the platform which kind of credentials we support.

### Documentation

See https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-hints and https://www.w3.org/TR/webauthn-3/#enum-hints

Pull request description:
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.

Closes #208

Patch:
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) {


Test patch:
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