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 diff --git a/go.mod b/go.mod index 40a49cf..d820919 100644 --- a/go.mod +++ b/go.mod @@ -4,19 +4,19 @@ 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 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..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= @@ -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= 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 897a56c..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"` @@ -124,14 +127,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 } @@ -161,7 +164,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 335f9a1..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)) @@ -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.go b/protocol/attestation.go index 54716de..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. @@ -65,16 +71,16 @@ 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[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 +141,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 +149,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..b8551ca 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: @@ -31,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. @@ -50,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") @@ -165,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 935218f..0e7be7c 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: @@ -33,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 8b0940a..c33a904 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: @@ -36,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. @@ -45,17 +43,17 @@ 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. - 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) @@ -74,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 { @@ -201,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 8e94ad1..954541d 100644 --- a/protocol/attestation_safetynet.go +++ b/protocol/attestation_safetynet.go @@ -14,20 +14,18 @@ import ( "github.com/go-webauthn/webauthn/metadata" ) -var safetyNetAttestationKey = "android-safetynet" - func init() { - RegisterAttestationFormat(safetyNetAttestationKey, verifySafetyNetFormat) + RegisterAttestationFormat(AttestationFormatAndroidSafetyNet, verifySafetyNetFormat) } 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 @@ -42,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", @@ -75,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)))) @@ -110,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 f714429..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 @@ -36,7 +39,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/attestation_tpm.go b/protocol/attestation_tpm.go index 892bdd8..e86881c 100644 --- a/protocol/attestation_tpm.go +++ b/protocol/attestation_tpm.go @@ -15,13 +15,11 @@ 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) { +func verifyTPMFormat(att AttestationObject, clientDataHash []byte) (string, []any, error) { // Given the verification procedure inputs attStmt, authenticatorData // and clientDataHash, the verification procedure is as follows @@ -44,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 e203f07..0689d42 100644 --- a/protocol/attestation_u2f.go +++ b/protocol/attestation_u2f.go @@ -11,14 +11,12 @@ import ( "github.com/go-webauthn/webauthn/protocol/webauthncose" ) -var u2fAttestationKey = "fido-u2f" - func init() { - RegisterAttestationFormat(u2fAttestationKey, 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") } @@ -42,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 c98577a..ab9b6ab 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,omitempty"` + 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..d532e2b 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,17 +47,13 @@ type ParsedPublicKeyCredential struct { 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"` + AttestationResponse AuthenticatorAttestationResponse `json:"response"` } type ParsedCredentialCreationData struct { ParsedPublicKeyCredential + Response ParsedAttestationResponse Raw CredentialCreationResponse } @@ -112,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 { @@ -140,9 +131,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..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{ @@ -259,7 +135,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)) @@ -364,7 +240,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/protocol/entities.go b/protocol/entities.go index 1d2f6e8..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"` } @@ -51,5 +42,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 80a9e55..9085fcd 100644 --- a/protocol/options.go +++ b/protocol/options.go @@ -17,26 +17,27 @@ 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'. +// 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"` - 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 []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. // -// TODO: There are two fields missing from this for WebAuthn Level 3. A string type named 'attestation', and a string -// slice named 'attestationFormats'. +// WebAuthn Level 3: hints. // // Specification: §5.5. Options for Assertion Generation (https://www.w3.org/TR/webauthn/#dictionary-assertion-options) type PublicKeyCredentialRequestOptions struct { @@ -45,6 +46,7 @@ type PublicKeyCredentialRequestOptions struct { RelyingPartyID string `json:"rpId,omitempty"` AllowedCredentials []CredentialDescriptor `json:"allowCredentials,omitempty"` UserVerification UserVerificationRequirement `json:"userVerification,omitempty"` + Hints []PublicKeyCredentialHints `json:"hints,omitempty"` Extensions AuthenticationExtensions `json:"extensions,omitempty"` } @@ -98,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. // @@ -183,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)) @@ -193,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/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 c64a9e3..391f35b 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: @@ -115,6 +122,15 @@ func WithUserVerification(userVerification protocol.UserVerificationRequirement) } } +// 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.Hints = hints + } +} + // WithAssertionExtensions adjusts the requested extensions. func WithAssertionExtensions(extensions protocol.AuthenticationExtensions) LoginOption { return func(cco *protocol.PublicKeyCredentialRequestOptions) { @@ -129,7 +145,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 @@ -138,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) @@ -269,6 +292,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 { @@ -276,7 +300,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/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 9715246..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" @@ -29,7 +30,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()) @@ -42,7 +43,6 @@ func (webauthn *WebAuthn) BeginRegistration(user User, opts ...RegistrationOptio DisplayName: user.WebAuthnDisplayName(), CredentialEntity: protocol.CredentialEntity{ Name: user.WebAuthnName(), - Icon: user.WebAuthnIcon(), }, } @@ -50,7 +50,6 @@ func (webauthn *WebAuthn) BeginRegistration(user User, opts ...RegistrationOptio ID: webauthn.Config.RPID, CredentialEntity: protocol.CredentialEntity{ Name: webauthn.Config.RPDisplayName, - Icon: webauthn.Config.RPIcon, }, } @@ -71,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: @@ -93,6 +102,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,10 +124,26 @@ func WithAuthenticatorSelection(authenticatorSelection protocol.AuthenticatorSel } } -// WithExclusions adjusts the non-default parameters regarding credentials to exclude from registration. -func WithExclusions(excludeList []protocol.CredentialDescriptor) 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.CredentialExcludeList = excludeList + cco.AuthenticatorSelection.ResidentKey = requirement + + switch requirement { + case protocol.ResidentKeyRequirementRequired: + cco.AuthenticatorSelection.RequireResidentKey = protocol.ResidentKeyRequired() + default: + cco.AuthenticatorSelection.RequireResidentKey = protocol.ResidentKeyNotRequired() + } + } +} + +// WithPublicKeyCredentialHints adjusts the non-default hints for credential types to select during registration. +// +// WebAuthn Level 3. +func WithPublicKeyCredentialHints(hints []protocol.PublicKeyCredentialHints) RegistrationOption { + return func(cco *protocol.PublicKeyCredentialCreationOptions) { + cco.Hints = hints } } @@ -116,17 +155,19 @@ func WithConveyancePreference(preference protocol.ConveyancePreference) Registra } } -// WithExtensions adjusts the extension parameter in the registration options. -func WithExtensions(extension protocol.AuthenticationExtensions) 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.Extensions = extension + cco.AttestationFormats = formats } } -// WithCredentialParameters adjusts the credential parameters in the registration options. -func WithCredentialParameters(credentialParams []protocol.CredentialParameter) RegistrationOption { +// WithExtensions adjusts the extension parameter in the registration options. +func WithExtensions(extension protocol.AuthenticationExtensions) RegistrationOption { return func(cco *protocol.PublicKeyCredentialCreationOptions) { - cco.Parameters = credentialParams + cco.Extensions = extension } } @@ -137,7 +178,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 @@ -146,17 +187,17 @@ func WithAppIdExcludeExtension(appid string) RegistrationOption { } } -// WithResidentKeyRequirement sets both the resident key and require resident key protocol options. -func WithResidentKeyRequirement(requirement protocol.ResidentKeyRequirement) RegistrationOption { +// WithRegistrationRelyingPartyID sets the relying party id for the registration. +func WithRegistrationRelyingPartyID(id string) RegistrationOption { return func(cco *protocol.PublicKeyCredentialCreationOptions) { - cco.AuthenticatorSelection.ResidentKey = requirement + cco.RelyingParty.ID = id + } +} - switch requirement { - case protocol.ResidentKeyRequirementRequired: - cco.AuthenticatorSelection.RequireResidentKey = protocol.ResidentKeyRequired() - default: - cco.AuthenticatorSelection.RequireResidentKey = protocol.ResidentKeyNotRequired() - } +// WithRegistrationRelyingPartyName sets the relying party name for the registration. +func WithRegistrationRelyingPartyName(name string) RegistrationOption { + return func(cco *protocol.PublicKeyCredentialCreationOptions) { + cco.RelyingParty.Name = name } } @@ -183,7 +224,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/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 bb93f31..a0dcc67 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 @@ -54,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. @@ -97,34 +91,17 @@ 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 config.RPIcon != "" { - if _, err = url.Parse(config.RPIcon); err != nil { - return fmt.Errorf(errFmtFieldNotValidURI, "RPIcon", err) + if len(config.RPID) != 0 { + if _, err = url.Parse(config.RPID); err != nil { + return fmt.Errorf(errFmtFieldNotValidURI, "RPID", 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 } @@ -141,18 +118,19 @@ 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") } + 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() } @@ -196,10 +174,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 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 }