diff --git a/metadata/memory.go b/metadata/memory.go deleted file mode 100644 index 2673a9f..0000000 --- a/metadata/memory.go +++ /dev/null @@ -1,74 +0,0 @@ -package metadata - -import ( - "context" - - "github.com/google/uuid" -) - -// NewMemoryProvider returns a new memory provider given a map, list of undesired AuthenticatorStatus types, a -// required boolean which if true will cause registrations to fail if no metadata entry is found for the attestation -// statement, and a validate boolean which determines if trust anchors should be validated by this provider during -// registration. -// -// If the undesired status slice is nil it will use a default value. You must explicitly use an empty slice to disable -// this functionality. -func NewMemoryProvider(mds map[uuid.UUID]*MetadataBLOBPayloadEntry, undesired []AuthenticatorStatus, required, validate bool) *MemoryProvider { - if undesired == nil { - undesired = make([]AuthenticatorStatus, len(defaultUndesiredAuthenticatorStatus)) - - for i := range defaultUndesiredAuthenticatorStatus { - undesired[i] = defaultUndesiredAuthenticatorStatus[i] - } - } - - return &MemoryProvider{ - mds: mds, - undesired: undesired, - require: required, - validate: validate, - } -} - -type MemoryProvider struct { - mds map[uuid.UUID]*MetadataBLOBPayloadEntry - undesired []AuthenticatorStatus - require bool - validate bool -} - -func (p *MemoryProvider) GetTrustAnchorValidation(ctx context.Context) (validate bool) { - return p.validate -} - -func (p *MemoryProvider) GetAuthenticatorStatusValidation(ctx context.Context) (validate bool) { - return len(p.undesired) > 0 -} - -func (p *MemoryProvider) GetRequireEntry(ctx context.Context) (require bool) { - return p.require -} - -func (p *MemoryProvider) GetEntry(ctx context.Context, aaguid uuid.UUID) (entry *MetadataBLOBPayloadEntry, err error) { - if p.mds == nil { - return nil, ErrNotInitialized - } - - var ok bool - - if entry, ok = p.mds[aaguid]; ok { - return entry, nil - } - - return nil, nil -} - -func (p *MemoryProvider) GetAuthenticatorStatusIsUndesired(ctx context.Context, status AuthenticatorStatus) (undesired bool) { - for _, s := range p.undesired { - if s == status { - return true - } - } - - return false -} diff --git a/metadata/metadata.go b/metadata/metadata.go index 9cbc9f6..2907e8a 100644 --- a/metadata/metadata.go +++ b/metadata/metadata.go @@ -19,7 +19,7 @@ func (m *Metadata) ToMap() (metadata map[uuid.UUID]*MetadataBLOBPayloadEntry) { metadata = make(map[uuid.UUID]*MetadataBLOBPayloadEntry) for _, entry := range m.Parsed.Entries { - if entry.AaGUID.ID() != 0 { + if entry.AaGUID != uuid.Nil { metadata[entry.AaGUID] = &entry } } @@ -875,3 +875,14 @@ type MDSGetEndpointsResponse struct { // An array of urls, each pointing to a MetadataTOCPayload. Result []string `json:"result"` } + +// DefaultUndesiredAuthenticatorStatuses returns a copy of the defaultUndesiredAuthenticatorStatus slice. +func DefaultUndesiredAuthenticatorStatuses() []AuthenticatorStatus { + undesired := make([]AuthenticatorStatus, len(defaultUndesiredAuthenticatorStatus)) + + for i := range defaultUndesiredAuthenticatorStatus { + undesired[i] = defaultUndesiredAuthenticatorStatus[i] + } + + return undesired +} diff --git a/metadata/passkey_authenticator.go b/metadata/passkey_authenticator.go index b057600..bd993d0 100644 --- a/metadata/passkey_authenticator.go +++ b/metadata/passkey_authenticator.go @@ -1,9 +1,16 @@ package metadata +// PasskeyAuthenticator is a type that represents the schema from the Passkey Developer AAGUID listing. +// +// See: https://github.com/passkeydeveloper/passkey-authenticator-aaguids type PasskeyAuthenticator map[string]PassKeyAuthenticatorAAGUID +// PassKeyAuthenticatorAAGUID is a type that represents the indivudal schema entry from the Passkey Developer AAGUID +// listing. Used with PasskeyAuthenticator. +// +// See: https://github.com/passkeydeveloper/passkey-authenticator-aaguids type PassKeyAuthenticatorAAGUID struct { Name string `json:"name"` - IconDark string `json:"icon_dark"` - IconLight string `json:"icon_light"` + IconDark string `json:"icon_dark,omitempty"` + IconLight string `json:"icon_light,omitempty"` } diff --git a/metadata/providers/memory/const.go b/metadata/providers/memory/const.go deleted file mode 100644 index d86630e..0000000 --- a/metadata/providers/memory/const.go +++ /dev/null @@ -1,12 +0,0 @@ -package memory - -import "github.com/go-webauthn/webauthn/metadata" - -// defaultUndesiredAuthenticatorStatus is an array of undesirable authenticator statuses -var defaultUndesiredAuthenticatorStatus = [...]metadata.AuthenticatorStatus{ - metadata.AttestationKeyCompromise, - metadata.UserVerificationBypass, - metadata.UserKeyRemoteCompromise, - metadata.UserKeyPhysicalCompromise, - metadata.Revoked, -} diff --git a/metadata/providers/memory/opts.go b/metadata/providers/memory/opts.go index 509e381..4d9f8fc 100644 --- a/metadata/providers/memory/opts.go +++ b/metadata/providers/memory/opts.go @@ -6,10 +6,61 @@ import ( "github.com/go-webauthn/webauthn/metadata" ) -type Opt func(*Provider) +// Option describes an optional pattern for this provider. +type Option func(*Provider) -func WithMetadata(metadata map[uuid.UUID]*metadata.MetadataBLOBPayloadEntry) Opt { +// WithMetadata provides the required metadata for the memory provider. +func WithMetadata(metadata map[uuid.UUID]*metadata.MetadataBLOBPayloadEntry) Option { return func(provider *Provider) { provider.mds = metadata } } + +// WithValidateEntry requires that the provided metadata has an entry for the given authenticator to be considered +// valid. By default an AAGUID which has a zero value should fail validation if WithValidateEntryPermitZeroAAGUID is not +// provided with the value of true. +func WithValidateEntry(require bool) Option { + return func(provider *Provider) { + provider.entry = require + } +} + +// WithValidateEntryPermitZeroAAGUID is an option that permits a zero'd AAGUID from an attestation statement to +// automatically pass metadata validations. Generally helpful to use with WithValidateEntry. +func WithValidateEntryPermitZeroAAGUID(permit bool) Option { + return func(provider *Provider) { + provider.entryPermitZero = permit + } +} + +// WithValidateTrustAnchor when set to true enables the validation of the attestation statement against the trust anchor +// from the metadata. +func WithValidateTrustAnchor(validate bool) Option { + return func(provider *Provider) { + provider.anchors = validate + } +} + +// WithValidateStatus when set to true enables the validation of the attestation statments AAGUID against the desired +// and undesired metadata.AuthenticatorStatus lists. +func WithValidateStatus(validate bool) Option { + return func(provider *Provider) { + provider.status = validate + } +} + +// WithStatusUndesired provides the list of statuses which are considered undesirable for status report validation +// purposes. Should be used with WithValidateStatus set to true. +func WithStatusUndesired(statuses []metadata.AuthenticatorStatus) Option { + return func(provider *Provider) { + provider.undesired = statuses + } +} + +// WithStatusDesired provides the list of statuses which are considered desired and will be required for status report +// validation purposes. Should be used with WithValidateStatus set to true. +func WithStatusDesired(statuses []metadata.AuthenticatorStatus) Option { + return func(provider *Provider) { + provider.desired = statuses + } +} diff --git a/metadata/providers/memory/provider.go b/metadata/providers/memory/provider.go index 7cb8caa..775e600 100644 --- a/metadata/providers/memory/provider.go +++ b/metadata/providers/memory/provider.go @@ -8,49 +8,31 @@ import ( "github.com/go-webauthn/webauthn/metadata" ) -// New returns a new memory provider given a map, list of undesired AuthenticatorStatus types, a -// required boolean which if true will cause registrations to fail if no metadata entry is found for the attestation -// statement, and a validate boolean which determines if trust anchors should be validated by this provider during -// registration. -// -// If the undesired status slice is nil it will use a default value. You must explicitly use an empty slice to disable -// this functionality. -func New(mds map[uuid.UUID]*metadata.MetadataBLOBPayloadEntry, undesired []metadata.AuthenticatorStatus, required, validate bool) *Provider { - if undesired == nil { - undesired = make([]metadata.AuthenticatorStatus, len(defaultUndesiredAuthenticatorStatus)) - - for i := range defaultUndesiredAuthenticatorStatus { - undesired[i] = defaultUndesiredAuthenticatorStatus[i] - } - } +// New returns a new memory provider given a set of functional Option's. +func New(opts ...Option) (provider *Provider) { - return &Provider{ - mds: mds, - undesired: undesired, - require: required, - validate: validate, + provider = &Provider{ + undesired: metadata.DefaultUndesiredAuthenticatorStatuses(), } -} - -type Provider struct { - mds map[uuid.UUID]*metadata.MetadataBLOBPayloadEntry - desired []metadata.AuthenticatorStatus - undesired []metadata.AuthenticatorStatus - require bool - validate bool - status bool -} -func (p *Provider) GetTrustAnchorValidation(ctx context.Context) (validate bool) { - return p.validate -} + for _, opt := range opts { + opt(provider) + } -func (p *Provider) GetAuthenticatorStatusValidation(ctx context.Context) (validate bool) { - return len(p.undesired) > 0 + return provider } -func (p *Provider) GetRequireEntry(ctx context.Context) (require bool) { - return p.require +// Provider is a concrete implementation of the metadata.Provider that utilizes memory for validation. This provider is +// a simple one-shot that doesn't perform any locking, provide dynamic functionality, or download the metadata at any +// stage (it expects it's provided via one of the Option's). +type Provider struct { + mds map[uuid.UUID]*metadata.MetadataBLOBPayloadEntry + desired []metadata.AuthenticatorStatus + undesired []metadata.AuthenticatorStatus + entry bool + entryPermitZero bool + anchors bool + status bool } func (p *Provider) GetEntry(ctx context.Context, aaguid uuid.UUID) (entry *metadata.MetadataBLOBPayloadEntry, err error) { @@ -67,20 +49,26 @@ func (p *Provider) GetEntry(ctx context.Context, aaguid uuid.UUID) (entry *metad return nil, nil } -func (p *Provider) ValidateAuthenticatorStatusReports(ctx context.Context, reports []metadata.StatusReport) (err error) { - if !p.status { - return nil - } +func (p *Provider) GetValidateEntry(ctx context.Context) (require bool) { + return p.entry +} - return metadata.ValidateStatusReports(reports, p.desired, p.undesired) +func (p *Provider) GetValidateEntryPermitZeroAAGUID(ctx context.Context) (skip bool) { + return p.entryPermitZero +} + +func (p *Provider) GetValidateTrustAnchor(ctx context.Context) (validate bool) { + return p.anchors } -func (p *Provider) GetAuthenticatorStatusIsUndesired(ctx context.Context, status metadata.AuthenticatorStatus) (undesired bool) { - for _, s := range p.undesired { - if s == status { - return true - } +func (p *Provider) GetValidateStatus(ctx context.Context) (validate bool) { + return p.status +} + +func (p *Provider) ValidateStatusReports(ctx context.Context, reports []metadata.StatusReport) (err error) { + if !p.status { + return nil } - return false + return metadata.ValidateStatusReports(reports, p.desired, p.undesired) } diff --git a/metadata/types.go b/metadata/types.go index 71d204c..fbcf9eb 100644 --- a/metadata/types.go +++ b/metadata/types.go @@ -14,16 +14,25 @@ type Provider interface { // GetEntry returns a MDS3 payload entry given a AAGUID. This GetEntry(ctx context.Context, aaguid uuid.UUID) (entry *MetadataBLOBPayloadEntry, err error) - // GetRequireEntry returns true if this provider requires an entry to exist with a AAGUID matching the attestation + // GetValidateEntry returns true if this provider requires an entry to exist with a AAGUID matching the attestation // statement during registration. - GetRequireEntry(ctx context.Context) (require bool) + GetValidateEntry(ctx context.Context) (validate bool) - // GetTrustAnchorValidation returns true if trust anchor validation of attestation statements is enforced during + // GetValidateEntryPermitZeroAAGUID returns true if attestation statements with zerod AAGUID should be permitted + // when considering the result from GetValidateEntry. i.e. if the AAGUID is zeroed, and GetValidateEntry returns + // true, and this implementation returns true, the attestation statement will pass validation. + GetValidateEntryPermitZeroAAGUID(ctx context.Context) (skip bool) + + // GetValidateTrustAnchor returns true if trust anchor validation of attestation statements is enforced during // registration. - GetTrustAnchorValidation(ctx context.Context) (validate bool) + GetValidateTrustAnchor(ctx context.Context) (validate bool) + + // GetValidateStatus returns true if the status reports for an authenticator should be validated against desired and + // undesired statuses. + GetValidateStatus(ctx context.Context) (validate bool) - // ValidateAuthenticatorStatusReports returns nil if the provided authenticator status reports are desired. - ValidateAuthenticatorStatusReports(ctx context.Context, reports []StatusReport) (err error) + // ValidateStatusReports returns nil if the provided authenticator status reports are desired. + ValidateStatusReports(ctx context.Context, reports []StatusReport) (err error) } var ( diff --git a/protocol/attestation.go b/protocol/attestation.go index 9f14fbb..90979e3 100644 --- a/protocol/attestation.go +++ b/protocol/attestation.go @@ -176,8 +176,10 @@ func (a *AttestationObject) VerifyAttestation(clientDataHash []byte, mds metadat entry *metadata.MetadataBLOBPayloadEntry ) - if aaguid, err = uuid.FromBytes(a.AuthData.AttData.AAGUID); err != nil { - return err + if len(a.AuthData.AttData.AAGUID) != 0 { + if aaguid, err = uuid.FromBytes(a.AuthData.AttData.AAGUID); err != nil { + return ErrInvalidAttestation.WithInfo("Error occurred parsing AAGUID during attestation validation").WithDetails(err.Error()) + } } if mds == nil { @@ -187,22 +189,28 @@ func (a *AttestationObject) VerifyAttestation(clientDataHash []byte, mds metadat ctx := context.Background() if entry, err = mds.GetEntry(ctx, aaguid); err != nil { - return ErrInvalidAttestation.WithInfo(fmt.Sprintf("Error occurred: %+v", err)).WithDetails(fmt.Sprintf("Error occurred looking up entry for AAGUID %s", aaguid.String())) + return ErrInvalidAttestation.WithInfo(fmt.Sprintf("Error occurred retrieving metadata entry during attestation validation: %+v", err)).WithDetails(fmt.Sprintf("Error occurred looking up entry for AAGUID %s", aaguid.String())) } if entry == nil { - if mds.GetRequireEntry(ctx) { - return ErrInvalidAttestation.WithDetails(fmt.Sprintf("AAGUID %s not found in metadata during conformance testing", aaguid.String())) + if aaguid == uuid.Nil && mds.GetValidateEntryPermitZeroAAGUID(ctx) { + return nil + } + + if mds.GetValidateEntry(ctx) { + return ErrInvalidAttestation.WithDetails(fmt.Sprintf("AAGUID %s not found in metadata during attestation validation", aaguid.String())) } return nil } - if err = mds.ValidateAuthenticatorStatusReports(ctx, entry.StatusReports); err != nil { - return ErrInvalidAttestation.WithDetails(fmt.Sprintf("Authenticator with invalid status encountered. %s", err.Error())) + if mds.GetValidateStatus(ctx) { + if err = mds.ValidateStatusReports(ctx, entry.StatusReports); err != nil { + return ErrInvalidAttestation.WithDetails(fmt.Sprintf("Authenticator with invalid status encountered during attestation validation. %s", err.Error())) + } } - if mds.GetTrustAnchorValidation(ctx) { + if mds.GetValidateTrustAnchor(ctx) { if x5cs == nil { return nil } @@ -214,24 +222,24 @@ func (a *AttestationObject) VerifyAttestation(clientDataHash []byte, mds metadat ) if len(x5cs) == 0 { - return ErrInvalidAttestation.WithDetails("Unable to parse attestation certificate from x5c").WithInfo("The attestation had no certificates") + return ErrInvalidAttestation.WithDetails("Unable to parse attestation certificate from x5c during attestation validation").WithInfo("The attestation had no certificates") } if raw, ok = x5cs[0].([]byte); !ok { - return ErrInvalidAttestation.WithDetails("Unable to parse attestation certificate from x5c").WithInfo(fmt.Sprintf("The first certificate in the attestation was type '%T' but '[]byte' was expected", x5cs[0])) + return ErrInvalidAttestation.WithDetails("Unable to parse attestation certificate from x5c during attestation validation").WithInfo(fmt.Sprintf("The first certificate in the attestation was type '%T' but '[]byte' was expected", x5cs[0])) } if x5c, err = x509.ParseCertificate(raw); err != nil { - return ErrInvalidAttestation.WithDetails("Unable to parse attestation certificate from x5c").WithInfo(fmt.Sprintf("Error returned from x509.ParseCertificate: %+v", err)) + return ErrInvalidAttestation.WithDetails("Unable to parse attestation certificate from x5c during attestation validation").WithInfo(fmt.Sprintf("Error returned from x509.ParseCertificate: %+v", err)) } if x5c.Subject.CommonName != x5c.Issuer.CommonName { if !entry.MetadataStatement.AttestationTypes.HasBasicFull() { - return ErrInvalidAttestation.WithDetails("Attestation with full attestation from authenticator that does not support full attestation") + return ErrInvalidAttestation.WithDetails("Unable to validate attestation statement signature during attestation validation: attestation with full attestation from authenticator that does not support full attestation") } if _, err = x5c.Verify(entry.MetadataStatement.Verifier()); err != nil { - return ErrInvalidAttestation.WithDetails(fmt.Sprintf("Invalid certificate chain from MDS: %v", err)) + return ErrInvalidAttestation.WithDetails(fmt.Sprintf("Unable to validate attestation signature statement during attestation validation: invalid certificate chain from MDS: %v", err)) } } } diff --git a/protocol/attestation_safetynet.go b/protocol/attestation_safetynet.go index 5406c0f..849cda1 100644 --- a/protocol/attestation_safetynet.go +++ b/protocol/attestation_safetynet.go @@ -138,7 +138,7 @@ func verifySafetyNetFormat(att AttestationObject, clientDataHash []byte, mds met return "", nil, ErrInvalidAttestation.WithDetails("SafetyNet response with timestamp after current time") } else if t.Before(time.Now().Add(-time.Minute)) { // Small tolerance for pre-dated timestamps. - if mds != nil && mds.GetRequireEntry(context.Background()) { + if mds != nil && mds.GetValidateEntry(context.Background()) { return "", nil, ErrInvalidAttestation.WithDetails("SafetyNet response with timestamp before one minute ago") } }