Skip to content

Commit 26a4868

Browse files
fix(webauthn): include backup flag check in registration (#748)
This includes the check in §7.1 to the registration handler.
1 parent 0661c81 commit 26a4868

2 files changed

Lines changed: 54 additions & 1 deletion

File tree

webauthn/registration.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,11 @@ func (webauthn *WebAuthn) CreateCredential(user User, session SessionData, parse
168168
return nil, err
169169
}
170170

171+
// Specification: §7.1. Registering a New Credential, step 18 (https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential)
172+
if flags := parsedResponse.Response.AttestationObject.AuthData.Flags; !flags.HasBackupEligible() && flags.HasBackupState() {
173+
return nil, protocol.ErrBadRequest.WithDetails("Backup State Flag is true but Backup Eligible flag is false which is invalid")
174+
}
175+
171176
if err = parsedResponse.ClientExtensionResults.Verify(session.Extensions, protocol.CreateCeremony, webauthn.Config.ExtensionsUnsolicitedOutputPolicy); err != nil {
172177
return nil, err
173178
}

webauthn/registration_test.go

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"io"
1010
"net/http"
11+
"strings"
1112
"testing"
1213
"time"
1314

@@ -789,6 +790,35 @@ func TestCreateCredential_Full(t *testing.T) {
789790
}
790791
}
791792

793+
// TestCreateCredential_RejectsBackupStateWithoutBackupEligibility covers §7.1 step 18, which requires the BS bit to
794+
// be unset when the BE bit is unset. The equivalent assertion step is covered by the login validation.
795+
func TestCreateCredential_RejectsBackupStateWithoutBackupEligibility(t *testing.T) {
796+
body, challenge, _ := testRegistrationSpecVectorNoneES256Flags(t, protocol.FlagUserPresent|protocol.FlagBackupState|protocol.FlagAttestedCredentialData)
797+
798+
parsedResponse, err := protocol.ParseCredentialCreationResponseBytes(body)
799+
require.NoError(t, err)
800+
801+
userID := []byte(testUserID)
802+
803+
w := &WebAuthn{
804+
Config: &Config{
805+
RPID: "example.org",
806+
RPOrigins: []string{"https://example.org"},
807+
},
808+
}
809+
810+
session := SessionData{
811+
Challenge: challenge,
812+
UserID: userID,
813+
CredParams: []protocol.CredentialParameter{{Type: protocol.PublicKeyCredentialType, Algorithm: webauthncose.AlgES256}},
814+
}
815+
816+
credential, err := w.CreateCredential(&defaultUser{id: userID}, session, parsedResponse)
817+
818+
assert.Nil(t, credential)
819+
assert.EqualError(t, err, "Backup State Flag is true but Backup Eligible flag is false which is invalid")
820+
}
821+
792822
func TestFinishRegistration_Success(t *testing.T) {
793823
body, challenge, credentialID := testRegistrationSpecVectorNoneES256(t)
794824
credParams := []protocol.CredentialParameter{{Type: protocol.PublicKeyCredentialType, Algorithm: webauthncose.AlgES256}}
@@ -975,20 +1005,38 @@ func TestValidateFilteredCredential(t *testing.T) {
9751005
func testRegistrationSpecVectorNoneES256(t *testing.T) (body []byte, challenge string, credentialID []byte) {
9761006
t.Helper()
9771007

1008+
// The flags of the vector as published: UP, BE, BS, and AT.
1009+
return testRegistrationSpecVectorNoneES256Flags(t, protocol.FlagUserPresent|protocol.FlagBackupEligible|protocol.FlagBackupState|protocol.FlagAttestedCredentialData)
1010+
}
1011+
1012+
// testRegistrationSpecVectorNoneES256Flags returns the spec test vector data for NoneES256 registration with the
1013+
// authenticator data flags byte replaced by flags. The none attestation statement format conveys no signature over
1014+
// the authenticator data, so the flags can be varied without invalidating the vector.
1015+
func testRegistrationSpecVectorNoneES256Flags(t *testing.T, flags protocol.AuthenticatorFlags) (body []byte, challenge string, credentialID []byte) {
1016+
t.Helper()
1017+
9781018
const (
9791019
attestationObjectHex = "a363666d74646e6f6e656761747453746d74a068617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b559000000008446ccb9ab1db374750b2367ff6f3a1f0020f91f391db4c9b2fde0ea70189cba3fb63f579ba6122b33ad94ff3ec330084be4a5010203262001215820afefa16f97ca9b2d23eb86ccb64098d20db90856062eb249c33a9b672f26df61225820930a56b87a2fca66334b03458abf879717c12cc68ed73290af2e2664796b9220"
9801020
clientDataJSONHex = "7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a22414d4d507434557878475453746e63647134313759447742466938767049612d7077386f4f755657345441222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a20426b5165446a646354427258426941774a544c453551227d"
9811021
credentialIDHex = "f91f391db4c9b2fde0ea70189cba3fb63f579ba6122b33ad94ff3ec330084be4" //nolint:gosec
9821022
challengeHex = "00c30fb78531c464d2b6771dab8d7b603c01162f2fa486bea70f283ae556e130"
1023+
1024+
// The flags byte of the authenticator data immediately follows the RP ID hash, which is what makes this
1025+
// prefix sufficient to locate it unambiguously within the attestation object.
1026+
rpIDHashHex = "bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b5"
1027+
vectorFlagHex = "59"
9831028
)
9841029

9851030
credentialID, err := hex.DecodeString(credentialIDHex)
9861031
require.NoError(t, err)
9871032

9881033
challenge = base64.RawURLEncoding.EncodeToString(testRegDecodeHex(t, challengeHex))
9891034

1035+
attestationObjectFlagsHex := strings.Replace(attestationObjectHex, rpIDHashHex+vectorFlagHex, rpIDHashHex+hex.EncodeToString([]byte{byte(flags)}), 1)
1036+
require.Contains(t, attestationObjectFlagsHex, rpIDHashHex+hex.EncodeToString([]byte{byte(flags)}))
1037+
9901038
id := base64.RawURLEncoding.EncodeToString(credentialID)
991-
attObj := base64.RawURLEncoding.EncodeToString(testRegDecodeHex(t, attestationObjectHex))
1039+
attObj := base64.RawURLEncoding.EncodeToString(testRegDecodeHex(t, attestationObjectFlagsHex))
9921040
cdj := base64.RawURLEncoding.EncodeToString(testRegDecodeHex(t, clientDataJSONHex))
9931041

9941042
response := map[string]any{

0 commit comments

Comments
 (0)