diff --git a/README.md b/README.md index 9565223..d53fa1b 100644 --- a/README.md +++ b/README.md @@ -285,6 +285,49 @@ func main() { } ``` +## Credential Record + +The WebAuthn Level 3 specification describes the Credential Record which includes several required and optional elements +that you should store for. See [§ 4 Terminology](https://www.w3.org/TR/webauthn-3/#credential-record) for details. + +This section describes this element. + +The fields listed in the specification have corresponding fields in the [webauthn.Credential] struct. See the below +table for more information. We also include JSON mappings for those that wish to just store these values as JSON. + +| Specification Field | Library Field | JSON Field | Notes | +|:-------------------------:|:--------------------------:|:--------------------------:|:-----------------------------------------------------------------------------------------:| +| type | N/A | N/A | This field is always `publicKey` for WebAuthn | +| id | ID | id | | +| publicKey | PublicKey | publicKey | | +| signCount | Authenticator.SignCount | authenticator.signCount | | +| transports | Transport | transport | | +| uvInitialized | Flags.UserVerified | flags.userVerified | | +| backupEligible | Flags.BackupEligible | flags.backupEligible | | +| backupState | Flags.BackupState | flags.backupState | | +| attestationObject | Attestation.Object | attestation.object | This field is a composite of the attestationObject and the relevant values to validate it | +| attestationClientDataJSON | Attestation.ClientDataJSON | attestation.clientDataJSON | | + +### Storage + +It is also important to note that restoring the [webauthn.Credential] with the correct values will likely affect the +validity of the [webauthn.Credential], i.e. if some values are not restored the [webauthn.Credential] may fail +validation in this scenario. + +### Verification + +As long as the [webauthn.Credential] struct has exactly the same values when restored the [Credential Verify] function +can be leveraged to verify the credential against the [metadata.Provider]. This can be either done during registration, +on every login, or with a audit schedule. + +In addition to using the [Credential Verify] function the +[webauthn.Config](https://pkg.go.dev/github.com/go-webauthn/webauthn/webauthn#Config) can contain a provider which will +process all registrations automatically. + +At this time no tooling exists to verify the credential automatically outside the registration flow. Implementation of +this is considered domain logic and beyond the scope of what we provide documentation for; we just provide the necessary +tooling to implement this yourself. + ## Acknowledgements We graciously acknowledge the original authors of this library [github.com/duo-labs/webauthn] for their amazing @@ -292,3 +335,6 @@ implementation. Without their amazing work this library could not exist. [github.com/duo-labs/webauthn]: https://github.com/duo-labs/webauthn +[webauthn.Credential]: https://pkg.go.dev/github.com/go-webauthn/webauthn/webauthn#Credential +[metadata.Provider]: https://pkg.go.dev/github.com/go-webauthn/webauthn/metadata#Provider +[Credential Verify]: https://pkg.go.dev/github.com/go-webauthn/webauthn/webauthn#Credential.Verify \ No newline at end of file diff --git a/protocol/attestation.go b/protocol/attestation.go index 4153c55..5cc045d 100644 --- a/protocol/attestation.go +++ b/protocol/attestation.go @@ -132,6 +132,12 @@ func (a *AttestationObject) Verify(relyingPartyID string, clientDataHash []byte, return err } + return a.VerifyAttestation(clientDataHash, mds) +} + +// VerifyAttestation only verifies the attestation object excluding the AuthData values. If you wish to also verify the +// AuthData values you should use Verify. +func (a *AttestationObject) VerifyAttestation(clientDataHash []byte, mds metadata.Provider) (err error) { // Step 13. Determine the attestation statement format by performing a // USASCII case-sensitive match on fmt against the set of supported // WebAuthn Attestation Statement Format Identifier values. The up-to-date diff --git a/webauthn/credential.go b/webauthn/credential.go index 1cd298a..19e45f9 100644 --- a/webauthn/credential.go +++ b/webauthn/credential.go @@ -1,19 +1,22 @@ package webauthn import ( + "crypto/sha256" + "fmt" + "github.com/go-webauthn/webauthn/metadata" "github.com/go-webauthn/webauthn/protocol" ) -// Credential contains all needed information about a WebAuthn credential for storage. +// Credential contains all needed information about a WebAuthn credential for storage. This struct is effectively the +// Credential Record as described in the specification. +// +// See: §4. Terminology: Credential Record (https://www.w3.org/TR/webauthn-3/#credential-record) type Credential struct { - // A probabilistically-unique byte sequence identifying a public key credential source and its authentication assertions. + // The Credential ID of the public key credential source. Described by the Credential Record 'id' field. ID []byte `json:"id"` - // The public key portion of a Relying Party-specific credential key pair, generated by an authenticator and returned to - // a Relying Party at registration time (see also public key credential). The private key portion of the credential key - // pair is known as the credential private key. Note that in the case of self attestation, the credential key pair is also - // used as the attestation key pair, see self attestation for details. + // The credential public key of the public key credential source. Described by the Credential Record 'publicKey field. PublicKey []byte `json:"publicKey"` // The attestation format used (if any) by the authenticator when creating the credential. @@ -29,7 +32,7 @@ type Credential struct { Authenticator Authenticator `json:"authenticator"` // The attestation values that can be used to validate this credential via the MDS3 at a later date. - Attestation VerifiableAttestation `json:"attestation"` + Attestation CredentialAttestation `json:"attestation"` } type CredentialFlags struct { @@ -47,6 +50,14 @@ type CredentialFlags struct { BackupState bool `json:"backupState"` } +type CredentialAttestation struct { + ClientDataJSON []byte `json:"clientDataJSON"` + ClientDataHash []byte `json:"clientDataHash"` + AuthenticatorData []byte `json:"authenticatorData"` + PublicKeyAlgorithm int64 `json:"publicKeyAlgorithm"` + Object []byte `json:"object"` +} + // Descriptor converts a Credential into a protocol.CredentialDescriptor. func (c Credential) Descriptor() (descriptor protocol.CredentialDescriptor) { return protocol.CredentialDescriptor{ @@ -57,8 +68,8 @@ func (c Credential) Descriptor() (descriptor protocol.CredentialDescriptor) { } } -// MakeNewCredential will return a credential pointer on successful validation of a registration response. -func MakeNewCredential(clientDataHash []byte, s *SessionData, c *protocol.ParsedCredentialCreationData) (credential *Credential, err error) { +// NewCredential will return a credential pointer on successful validation of a registration response. +func NewCredential(clientDataHash []byte, c *protocol.ParsedCredentialCreationData) (credential *Credential, err error) { credential = &Credential{ ID: c.Response.AttestationObject.AuthData.AttData.CredentialID, PublicKey: c.Response.AttestationObject.AuthData.AttData.CredentialPublicKey, @@ -75,39 +86,56 @@ func MakeNewCredential(clientDataHash []byte, s *SessionData, c *protocol.Parsed SignCount: c.Response.AttestationObject.AuthData.Counter, Attachment: c.AuthenticatorAttachment, }, - Attestation: VerifiableAttestation{ - RPID: s.RelyingPartyID, - ClientDataHash: clientDataHash, - UserVerificationRequired: s.UserVerification == protocol.VerificationRequired, - RawAuthData: c.Response.AttestationObject.RawAuthData, - AuthData: c.Response.AttestationObject.AuthData, - Format: c.Response.AttestationObject.Format, - AttStatement: c.Response.AttestationObject.AttStatement, + Attestation: CredentialAttestation{ + ClientDataJSON: c.Raw.AttestationResponse.ClientDataJSON, + ClientDataHash: clientDataHash, + AuthenticatorData: c.Raw.AttestationResponse.AuthenticatorData, + PublicKeyAlgorithm: c.Raw.AttestationResponse.PublicKeyAlgorithm, + Object: c.Raw.AttestationResponse.AttestationObject, }, } return credential, nil } -// VerifiableAttestation is a self-contained attestation from an authenticator that can later be validated against the -// MDS3. -type VerifiableAttestation struct { - RPID string `json:"rpId"` - ClientDataHash []byte `json:"clientDataHash"` - UserVerificationRequired bool `json:"uv"` - RawAuthData []byte `json:"rawAuthData,omitempty"` - AuthData protocol.AuthenticatorData `json:"authData"` - Format string `json:"fmt"` - AttStatement map[string]any `json:"attStmt,omitempty"` -} +// Verify this credentials against the metadata.Provider given. +func (c Credential) Verify(mds metadata.Provider) (err error) { + if mds == nil { + return fmt.Errorf("error verifying credential: the metadata provider must be provided but it's nil") + } + + raw := &protocol.AuthenticatorAttestationResponse{ + AuthenticatorResponse: protocol.AuthenticatorResponse{ + ClientDataJSON: c.Attestation.ClientDataJSON, + }, + Transports: make([]string, len(c.Transport)), + AuthenticatorData: c.Attestation.AuthenticatorData, + PublicKey: c.PublicKey, + PublicKeyAlgorithm: c.Attestation.PublicKeyAlgorithm, + AttestationObject: c.Attestation.Object, + } + + for i, transport := range c.Transport { + raw.Transports[i] = string(transport) + } + + var attestation *protocol.ParsedAttestationResponse + + if attestation, err = raw.Parse(); err != nil { + return fmt.Errorf("error verifying credential: error parsing attestation: %w", err) + } + + clientDataHash := c.Attestation.ClientDataHash + + if len(clientDataHash) == 0 { + sum := sha256.Sum256(c.Attestation.ClientDataJSON) + + clientDataHash = sum[:] + } -func (a *VerifiableAttestation) Verify(mds metadata.Provider) (err error) { - object := &protocol.AttestationObject{ - AuthData: a.AuthData, - RawAuthData: a.RawAuthData, - Format: a.Format, - AttStatement: a.AttStatement, + if err = attestation.AttestationObject.VerifyAttestation(clientDataHash, mds); err != nil { + return fmt.Errorf("error verifying credential: error verifying attestation: %w", err) } - return object.Verify(a.RPID, a.ClientDataHash, a.UserVerificationRequired, mds) + return nil } diff --git a/webauthn/credential_test.go b/webauthn/credential_test.go index 9a2ed7e..d70b705 100644 --- a/webauthn/credential_test.go +++ b/webauthn/credential_test.go @@ -1,9 +1,11 @@ package webauthn import ( - "reflect" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/go-webauthn/webauthn/protocol" ) @@ -12,22 +14,22 @@ func TestMakeNewCredential(t *testing.T) { c *protocol.ParsedCredentialCreationData } - var tests []struct { - name string - args args - want *Credential - wantErr bool + var testCases []struct { + name string + args args + expected *Credential + err string } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := MakeNewCredential(nil, nil, tt.args.c) - if (err != nil) != tt.wantErr { - t.Errorf("MakeNewCredential() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("MakeNewCredential() = %v, want %v", got, tt.want) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual, err := NewCredential(nil, tc.args.c) + if len(tc.err) > 0 { + assert.EqualError(t, err, tc.err) + } else { + require.NoError(t, err) + + assert.EqualValues(t, tc.expected, actual) } }) } diff --git a/webauthn/registration.go b/webauthn/registration.go index a088640..3352eb2 100644 --- a/webauthn/registration.go +++ b/webauthn/registration.go @@ -231,7 +231,7 @@ func (webauthn *WebAuthn) CreateCredential(user User, session SessionData, parse return nil, err } - return MakeNewCredential(clientDataHash, &session, parsedResponse) + return NewCredential(clientDataHash, parsedResponse) } func defaultRegistrationCredentialParameters() []protocol.CredentialParameter {