Skip to content

Commit

Permalink
feat: refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
james-d-elliott committed Jun 3, 2024
1 parent faeda90 commit 0bbb5fb
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 146 deletions.
74 changes: 0 additions & 74 deletions metadata/memory.go

This file was deleted.

2 changes: 1 addition & 1 deletion metadata/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
11 changes: 9 additions & 2 deletions metadata/passkey_authenticator.go
Original file line number Diff line number Diff line change
@@ -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"`
}
55 changes: 53 additions & 2 deletions metadata/providers/memory/opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
86 changes: 39 additions & 47 deletions metadata/providers/memory/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,49 +8,35 @@ 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: make([]metadata.AuthenticatorStatus, len(defaultUndesiredAuthenticatorStatus)),
}
}

type Provider struct {
mds map[uuid.UUID]*metadata.MetadataBLOBPayloadEntry
desired []metadata.AuthenticatorStatus
undesired []metadata.AuthenticatorStatus
require bool
validate bool
status bool
}
for i := range defaultUndesiredAuthenticatorStatus {
provider.undesired[i] = defaultUndesiredAuthenticatorStatus[i]
}

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) {
Expand All @@ -67,20 +53,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)
}
21 changes: 15 additions & 6 deletions metadata/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
34 changes: 21 additions & 13 deletions protocol/attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
Expand All @@ -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))
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion protocol/attestation_safetynet.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
Expand Down

0 comments on commit 0bbb5fb

Please sign in to comment.