Skip to content

Commit

Permalink
feat: post registration verify
Browse files Browse the repository at this point in the history
  • Loading branch information
james-d-elliott committed May 22, 2024
1 parent 18d89d6 commit cc887ee
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 51 deletions.
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,10 +285,56 @@ 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
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
6 changes: 6 additions & 0 deletions protocol/attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
98 changes: 63 additions & 35 deletions webauthn/credential.go
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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 {
Expand All @@ -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{
Expand All @@ -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,
Expand All @@ -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
}
32 changes: 17 additions & 15 deletions webauthn/credential_test.go
Original file line number Diff line number Diff line change
@@ -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"
)

Expand All @@ -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)
}
})
}
Expand Down
2 changes: 1 addition & 1 deletion webauthn/registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit cc887ee

Please sign in to comment.