diff --git a/protocol/assertion.go b/protocol/assertion.go index 897a56cd..48dfb62c 100644 --- a/protocol/assertion.go +++ b/protocol/assertion.go @@ -124,14 +124,14 @@ func (car CredentialAssertionResponse) Parse() (par *ParsedCredentialAssertionDa // documentation. // // Specification: §7.2 Verifying an Authentication Assertion (https://www.w3.org/TR/webauthn/#sctn-verifying-assertion) -func (p *ParsedCredentialAssertionData) Verify(storedChallenge string, relyingPartyID string, relyingPartyOrigins []string, appID string, verifyUser bool, credentialBytes []byte) error { +func (p *ParsedCredentialAssertionData) Verify(storedChallenge string, relyingPartyID string, rpOrigins, rpTopOrigins []string, rpTopOriginsVerify TopOriginVerificationMode, appID string, verifyUser bool, credentialBytes []byte) error { // Steps 4 through 6 in verifying the assertion data (https://www.w3.org/TR/webauthn/#verifying-assertion) are // "assertive" steps, i.e "Let JSONtext be the result of running UTF-8 decode on the value of cData." // We handle these steps in part as we verify but also beforehand // Handle steps 7 through 10 of assertion by verifying stored data against the Collected Client Data // returned by the authenticator - validError := p.Response.CollectedClientData.Verify(storedChallenge, AssertCeremony, relyingPartyOrigins) + validError := p.Response.CollectedClientData.Verify(storedChallenge, AssertCeremony, rpOrigins, rpTopOrigins, rpTopOriginsVerify) if validError != nil { return validError } diff --git a/protocol/assertion_test.go b/protocol/assertion_test.go index 335f9a13..ab622399 100644 --- a/protocol/assertion_test.go +++ b/protocol/assertion_test.go @@ -180,7 +180,7 @@ func TestParsedCredentialAssertionData_Verify(t *testing.T) { Raw: tt.fields.Raw, } - if err := p.Verify(tt.args.storedChallenge.String(), tt.args.relyingPartyID, tt.args.relyingPartyOrigin, "", tt.args.verifyUser, tt.args.credentialBytes); (err != nil) != tt.wantErr { + if err := p.Verify(tt.args.storedChallenge.String(), tt.args.relyingPartyID, tt.args.relyingPartyOrigin, nil, TopOriginIgnoreVerificationMode, "", tt.args.verifyUser, tt.args.credentialBytes); (err != nil) != tt.wantErr { t.Errorf("ParsedCredentialAssertionData.Verify() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/protocol/attestation_test.go b/protocol/attestation_test.go index f7144294..60a6bf63 100644 --- a/protocol/attestation_test.go +++ b/protocol/attestation_test.go @@ -36,7 +36,7 @@ func TestAttestationVerify(t *testing.T) { pcc.Response = *parsedAttestationResponse // Test Base Verification - err = pcc.Verify(options.Response.Challenge.String(), false, options.Response.RelyingParty.ID, []string{options.Response.RelyingParty.Name}) + err = pcc.Verify(options.Response.Challenge.String(), false, options.Response.RelyingParty.ID, []string{options.Response.RelyingParty.Name}, nil, TopOriginIgnoreVerificationMode) if err != nil { t.Fatalf("Not valid: %+v (%s)", err, err.(*Error).DevInfo) } diff --git a/protocol/client.go b/protocol/client.go index c98577aa..02402f77 100644 --- a/protocol/client.go +++ b/protocol/client.go @@ -20,6 +20,8 @@ type CollectedClientData struct { Type CeremonyType `json:"type"` Challenge string `json:"challenge"` Origin string `json:"origin"` + TopOrigin string `json:"topOrigin"` + CrossOrigin bool `json:"crossOrigin,omitempty"` TokenBinding *TokenBinding `json:"tokenBinding,omitempty"` // Chromium (Chrome) returns a hint sometimes about how to handle clientDataJSON in a safe manner. @@ -77,7 +79,10 @@ func FullyQualifiedOrigin(rawOrigin string) (fqOrigin string, err error) { // new credential and steps 7 through 10 of verifying an authentication assertion // See https://www.w3.org/TR/webauthn/#registering-a-new-credential // and https://www.w3.org/TR/webauthn/#verifying-assertion -func (c *CollectedClientData) Verify(storedChallenge string, ceremony CeremonyType, rpOrigins []string) error { +// +// Note: the rpTopOriginsVerify parameter does not accept the TopOriginVerificationMode value of +// TopOriginDefaultVerificationMode as it's expected this value is updated by the config validation process. +func (c *CollectedClientData) Verify(storedChallenge string, ceremony CeremonyType, rpOrigins, rpTopOrigins []string, rpTopOriginsVerify TopOriginVerificationMode) (err error) { // Registration Step 3. Verify that the value of C.type is webauthn.create. // Assertion Step 7. Verify that the value of C.type is the string webauthn.get. @@ -101,8 +106,9 @@ func (c *CollectedClientData) Verify(storedChallenge string, ceremony CeremonyTy // Registration Step 5 & Assertion Step 9. Verify that the value of C.origin matches // the Relying Party's origin. - fqOrigin, err := FullyQualifiedOrigin(c.Origin) - if err != nil { + var fqOrigin string + + if fqOrigin, err = FullyQualifiedOrigin(c.Origin); err != nil { return ErrParsingData.WithDetails("Error decoding clientData origin as URL") } @@ -121,6 +127,54 @@ func (c *CollectedClientData) Verify(storedChallenge string, ceremony CeremonyTy WithInfo(fmt.Sprintf("Expected Values: %s, Received: %s", rpOrigins, fqOrigin)) } + if rpTopOriginsVerify != TopOriginIgnoreVerificationMode { + switch len(c.TopOrigin) { + case 0: + break + default: + if !c.CrossOrigin { + return ErrVerification. + WithDetails("Error validating topOrigin"). + WithInfo("The topOrigin can't have values unless crossOrigin is true.") + } + + var ( + fqTopOrigin string + possibleTopOrigins []string + ) + + if fqTopOrigin, err = FullyQualifiedOrigin(c.TopOrigin); err != nil { + return ErrParsingData.WithDetails("Error decoding clientData topOrigin as URL") + } + + switch rpTopOriginsVerify { + case TopOriginExplicitVerificationMode: + possibleTopOrigins = rpTopOrigins + case TopOriginAutoVerificationMode: + possibleTopOrigins = append(rpTopOrigins, rpOrigins...) + case TopOriginImplicitVerificationMode: + possibleTopOrigins = rpOrigins + default: + return ErrNotImplemented.WithDetails("Error handling unknown Top Origin verification mode") + } + + found = false + + for _, origin := range possibleTopOrigins { + if strings.EqualFold(fqTopOrigin, origin) { + found = true + break + } + } + + if !found { + return ErrVerification. + WithDetails("Error validating top origin"). + WithInfo(fmt.Sprintf("Expected Values: %s, Received: %s", possibleTopOrigins, fqTopOrigin)) + } + } + } + // Registration Step 6 and Assertion Step 10. Verify that the value of C.tokenBinding.status // matches the state of Token Binding for the TLS connection over which the assertion was // obtained. If Token Binding was used on that TLS connection, also verify that C.tokenBinding.id @@ -140,3 +194,28 @@ func (c *CollectedClientData) Verify(storedChallenge string, ceremony CeremonyTy return nil } + +type TopOriginVerificationMode int + +const ( + // TopOriginDefaultVerificationMode represents the default verification mode for the Top Origin. At this time this + // mode is the same as TopOriginIgnoreVerificationMode until such a time as the specification becomes stable. This + // value is intended as a fallback value and implementers should very intentionally pick another option if they want + // stability. + TopOriginDefaultVerificationMode TopOriginVerificationMode = iota + + // TopOriginIgnoreVerificationMode ignores verification entirely. + TopOriginIgnoreVerificationMode + + // TopOriginAutoVerificationMode represents the automatic verification mode for the Top Origin. In this mode the + // If the Top Origins parameter has values it checks against this, otherwise it checks against the Origins parameter. + TopOriginAutoVerificationMode + + // TopOriginImplicitVerificationMode represents the implicit verification mode for the Top Origin. In this mode the + // Top Origin is verified against the allowed Origins values. + TopOriginImplicitVerificationMode + + // TopOriginExplicitVerificationMode represents the explicit verification mode for the Top Origin. In this mode the + // Top Origin is verified against the allowed Top Origins values. + TopOriginExplicitVerificationMode +) diff --git a/protocol/client_test.go b/protocol/client_test.go index d83a5984..1a2df9e5 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 bb9782b0..7067402e 100644 --- a/protocol/credential.go +++ b/protocol/credential.go @@ -140,9 +140,9 @@ func (ccr CredentialCreationResponse) Parse() (pcc *ParsedCredentialCreationData // Verify the Client and Attestation data. // // Specification: §7.1. Registering a New Credential (https://www.w3.org/TR/webauthn/#sctn-registering-a-new-credential) -func (pcc *ParsedCredentialCreationData) Verify(storedChallenge string, verifyUser bool, relyingPartyID string, relyingPartyOrigins []string) error { +func (pcc *ParsedCredentialCreationData) Verify(storedChallenge string, verifyUser bool, relyingPartyID string, rpOrigins, rpTopOrigins []string, rpTopOriginsVerify TopOriginVerificationMode) error { // Handles steps 3 through 6 - Verifying the Client Data against the Relying Party's stored data - verifyError := pcc.Response.CollectedClientData.Verify(storedChallenge, CreateCeremony, relyingPartyOrigins) + verifyError := pcc.Response.CollectedClientData.Verify(storedChallenge, CreateCeremony, rpOrigins, rpTopOrigins, rpTopOriginsVerify) if verifyError != nil { return verifyError } diff --git a/protocol/credential_test.go b/protocol/credential_test.go index 7c0b819d..201c45fc 100644 --- a/protocol/credential_test.go +++ b/protocol/credential_test.go @@ -364,7 +364,7 @@ func TestParsedCredentialCreationData_Verify(t *testing.T) { Response: tt.fields.Response, Raw: tt.fields.Raw, } - if err := pcc.Verify(tt.args.storedChallenge.String(), tt.args.verifyUser, tt.args.relyingPartyID, tt.args.relyingPartyOrigin); (err != nil) != tt.wantErr { + if err := pcc.Verify(tt.args.storedChallenge.String(), tt.args.verifyUser, tt.args.relyingPartyID, tt.args.relyingPartyOrigin, nil, TopOriginIgnoreVerificationMode); (err != nil) != tt.wantErr { t.Errorf("ParsedCredentialCreationData.Verify() error = %+v, wantErr %v", err, tt.wantErr) } }) diff --git a/webauthn/login.go b/webauthn/login.go index 73e69af4..dc9447dd 100644 --- a/webauthn/login.go +++ b/webauthn/login.go @@ -269,6 +269,7 @@ func (webauthn *WebAuthn) validateLogin(user User, session SessionData, parsedRe rpID := webauthn.Config.RPID rpOrigins := webauthn.Config.RPOrigins + rpTopOrigins := webauthn.Config.RPOrigins appID, err := parsedResponse.GetAppID(session.Extensions, loginCredential.AttestationType) if err != nil { @@ -276,7 +277,7 @@ func (webauthn *WebAuthn) validateLogin(user User, session SessionData, parsedRe } // Handle steps 4 through 16. - validError := parsedResponse.Verify(session.Challenge, rpID, rpOrigins, appID, shouldVerifyUser, loginCredential.PublicKey) + validError := parsedResponse.Verify(session.Challenge, rpID, rpOrigins, rpTopOrigins, webauthn.Config.RPTopOriginVerificationMode, appID, shouldVerifyUser, loginCredential.PublicKey) if validError != nil { return nil, validError } diff --git a/webauthn/registration.go b/webauthn/registration.go index 9715246f..769ab800 100644 --- a/webauthn/registration.go +++ b/webauthn/registration.go @@ -183,7 +183,7 @@ func (webauthn *WebAuthn) CreateCredential(user User, session SessionData, parse shouldVerifyUser := session.UserVerification == protocol.VerificationRequired - invalidErr := parsedResponse.Verify(session.Challenge, shouldVerifyUser, webauthn.Config.RPID, webauthn.Config.RPOrigins) + invalidErr := parsedResponse.Verify(session.Challenge, shouldVerifyUser, webauthn.Config.RPID, webauthn.Config.RPOrigins, webauthn.Config.RPTopOrigins, webauthn.Config.RPTopOriginVerificationMode) if invalidErr != nil { return nil, invalidErr } diff --git a/webauthn/types.go b/webauthn/types.go index bb93f31a..79b6db16 100644 --- a/webauthn/types.go +++ b/webauthn/types.go @@ -36,6 +36,15 @@ type Config struct { // qualified origins. RPOrigins []string + // RPTopOrigins configures the list of Relying Party Server Top Origins that are permitted. These should be fully + // qualified origins. + RPTopOrigins []string + + // RPTopOriginVerificationMode determines the verification mode for the Top Origin value. By default the + // TopOriginIgnoreVerificationMode is used however this is going to change at such a time as WebAuthn Level 3 + // becomes recommended, implementers should explicitly set this value if they want stability. + RPTopOriginVerificationMode protocol.TopOriginVerificationMode + // AttestationPreference sets the default attestation conveyance preferences. AttestationPreference protocol.ConveyancePreference @@ -153,6 +162,15 @@ func (config *Config) validate() error { return fmt.Errorf("must provide at least one value to the 'RPOrigins' field") } + switch config.RPTopOriginVerificationMode { + case protocol.TopOriginDefaultVerificationMode: + config.RPTopOriginVerificationMode = protocol.TopOriginIgnoreVerificationMode + case protocol.TopOriginImplicitVerificationMode: + if len(config.RPTopOrigins) == 0 { + return fmt.Errorf("must provide at least one value to the 'RPTopOrigins' field when 'RPTopOriginVerificationMode' field is set to protocol.TopOriginImplicitVerificationMode") + } + } + if config.AuthenticatorSelection.RequireResidentKey == nil { config.AuthenticatorSelection.RequireResidentKey = protocol.ResidentKeyNotRequired() }