This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
// Package memory handles a [metadata.Provider] implementation that solely exists in memory. It's intended as a basis
|
||||
// for other providers and generally not recommended to use directly unless you're implementing your own logic to handle
|
||||
// the download and potential caching of the MDS3 blob yourself.
|
||||
package memory
|
||||
@@ -0,0 +1,90 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/go-webauthn/webauthn/metadata"
|
||||
)
|
||||
|
||||
// Option describes an optional pattern for this provider.
|
||||
type Option func(provider *Provider) (err error)
|
||||
|
||||
// WithMetadata provides the required metadata for the memory provider.
|
||||
func WithMetadata(mds map[uuid.UUID]*metadata.Entry) Option {
|
||||
return func(provider *Provider) (err error) {
|
||||
provider.mds = mds
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 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. Default is true.
|
||||
func WithValidateEntry(require bool) Option {
|
||||
return func(provider *Provider) (err error) {
|
||||
provider.entry = require
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 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]. Default is false.
|
||||
func WithValidateEntryPermitZeroAAGUID(permit bool) Option {
|
||||
return func(provider *Provider) (err error) {
|
||||
provider.entryPermitZero = permit
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithValidateTrustAnchor when set to true enables the validation of the attestation statement against the trust anchor
|
||||
// from the metadata. Default is true.
|
||||
func WithValidateTrustAnchor(validate bool) Option {
|
||||
return func(provider *Provider) (err error) {
|
||||
provider.anchors = validate
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithValidateStatus when set to true enables the validation of the attestation statements AAGUID against the desired
|
||||
// and undesired [metadata.AuthenticatorStatus] lists. Default is true.
|
||||
func WithValidateStatus(validate bool) Option {
|
||||
return func(provider *Provider) (err error) {
|
||||
provider.status = validate
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithValidateAttestationTypes when set to true enables the validation of the attestation statements type against the
|
||||
// known types the authenticator can produce. Default is true.
|
||||
func WithValidateAttestationTypes(validate bool) Option {
|
||||
return func(provider *Provider) (err error) {
|
||||
provider.attestation = validate
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 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) (err error) {
|
||||
provider.undesired = statuses
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 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) (err error) {
|
||||
provider.desired = statuses
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/go-webauthn/webauthn/metadata"
|
||||
)
|
||||
|
||||
// New returns a new memory Provider given a set of functional Option's.
|
||||
func New(opts ...Option) (provider metadata.Provider, err error) {
|
||||
p := &Provider{
|
||||
undesired: metadata.DefaultUndesiredAuthenticatorStatuses(),
|
||||
entry: true,
|
||||
anchors: true,
|
||||
status: true,
|
||||
attestation: true,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
if err = opt(p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if p.mds == nil {
|
||||
return nil, fmt.Errorf("memory metadata provider has not been initialized with metadata")
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// 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.Entry
|
||||
desired []metadata.AuthenticatorStatus
|
||||
undesired []metadata.AuthenticatorStatus
|
||||
entry bool
|
||||
entryPermitZero bool
|
||||
anchors bool
|
||||
status bool
|
||||
attestation bool
|
||||
}
|
||||
|
||||
func (p *Provider) GetEntry(ctx context.Context, aaguid uuid.UUID) (entry *metadata.Entry, err error) {
|
||||
if p.mds == nil {
|
||||
return nil, metadata.ErrNotInitialized
|
||||
}
|
||||
|
||||
var ok bool
|
||||
|
||||
if entry, ok = p.mds[aaguid]; ok {
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (p *Provider) GetValidateEntry(ctx context.Context) (require bool) {
|
||||
return p.entry
|
||||
}
|
||||
|
||||
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) GetValidateStatus(ctx context.Context) (validate bool) {
|
||||
return p.status
|
||||
}
|
||||
|
||||
func (p *Provider) GetValidateAttestationTypes(ctx context.Context) (validate bool) {
|
||||
return p.attestation
|
||||
}
|
||||
|
||||
func (p *Provider) ValidateStatusReports(ctx context.Context, reports []metadata.StatusReport) (err error) {
|
||||
if !p.status {
|
||||
return nil
|
||||
}
|
||||
|
||||
return metadata.ValidateStatusReports(reports, p.desired, p.undesired)
|
||||
}
|
||||
|
||||
var (
|
||||
_ metadata.Provider = (*Provider)(nil)
|
||||
)
|
||||
@@ -0,0 +1,236 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/go-webauthn/webauthn/metadata"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
id := uuid.New()
|
||||
|
||||
entry := &metadata.Entry{
|
||||
AaGUID: id,
|
||||
MetadataStatement: metadata.Statement{
|
||||
Description: "Test Authenticator",
|
||||
},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
opts []Option
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "ShouldSucceedWithMetadata",
|
||||
opts: []Option{WithMetadata(map[uuid.UUID]*metadata.Entry{id: entry})},
|
||||
},
|
||||
{
|
||||
name: "ShouldFailWithoutMetadata",
|
||||
opts: nil,
|
||||
err: "memory metadata provider has not been initialized with metadata",
|
||||
},
|
||||
{
|
||||
name: "ShouldSucceedWithAllOptions",
|
||||
opts: []Option{
|
||||
WithMetadata(map[uuid.UUID]*metadata.Entry{id: entry}),
|
||||
WithValidateEntry(false),
|
||||
WithValidateEntryPermitZeroAAGUID(true),
|
||||
WithValidateTrustAnchor(false),
|
||||
WithValidateStatus(false),
|
||||
WithValidateAttestationTypes(false),
|
||||
WithStatusUndesired([]metadata.AuthenticatorStatus{metadata.Revoked}),
|
||||
WithStatusDesired([]metadata.AuthenticatorStatus{metadata.FidoCertified}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
provider, err := New(tc.opts...)
|
||||
|
||||
if tc.err != "" {
|
||||
assert.Nil(t, provider)
|
||||
require.EqualError(t, err, tc.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, provider)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_GetEntry(t *testing.T) {
|
||||
id := uuid.New()
|
||||
missing := uuid.New()
|
||||
|
||||
entry := &metadata.Entry{
|
||||
AaGUID: id,
|
||||
MetadataStatement: metadata.Statement{
|
||||
Description: "Test Authenticator",
|
||||
},
|
||||
}
|
||||
|
||||
provider, err := New(WithMetadata(map[uuid.UUID]*metadata.Entry{id: entry}))
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
aaguid uuid.UUID
|
||||
expected *metadata.Entry
|
||||
}{
|
||||
{
|
||||
name: "ShouldReturnEntryWhenExists",
|
||||
aaguid: id,
|
||||
expected: entry,
|
||||
},
|
||||
{
|
||||
name: "ShouldReturnNilWhenNotExists",
|
||||
aaguid: missing,
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "ShouldReturnNilForNilUUID",
|
||||
aaguid: uuid.Nil,
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result, err := provider.GetEntry(context.Background(), tc.aaguid)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_ConfigurationFlags(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
opts []Option
|
||||
expectedEntry bool
|
||||
expectedPermitZero bool
|
||||
expectedTrustAnchor bool
|
||||
expectedStatus bool
|
||||
expectedAttestationTypes bool
|
||||
}{
|
||||
{
|
||||
name: "ShouldReturnDefaultFlags",
|
||||
opts: []Option{
|
||||
WithMetadata(map[uuid.UUID]*metadata.Entry{}),
|
||||
},
|
||||
expectedEntry: true,
|
||||
expectedPermitZero: false,
|
||||
expectedTrustAnchor: true,
|
||||
expectedStatus: true,
|
||||
expectedAttestationTypes: true,
|
||||
},
|
||||
{
|
||||
name: "ShouldReturnCustomFlags",
|
||||
opts: []Option{
|
||||
WithMetadata(map[uuid.UUID]*metadata.Entry{}),
|
||||
WithValidateEntry(false),
|
||||
WithValidateEntryPermitZeroAAGUID(true),
|
||||
WithValidateTrustAnchor(false),
|
||||
WithValidateStatus(false),
|
||||
WithValidateAttestationTypes(false),
|
||||
},
|
||||
expectedEntry: false,
|
||||
expectedPermitZero: true,
|
||||
expectedTrustAnchor: false,
|
||||
expectedStatus: false,
|
||||
expectedAttestationTypes: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
provider, err := New(tc.opts...)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
assert.Equal(t, tc.expectedEntry, provider.GetValidateEntry(ctx))
|
||||
assert.Equal(t, tc.expectedPermitZero, provider.GetValidateEntryPermitZeroAAGUID(ctx))
|
||||
assert.Equal(t, tc.expectedTrustAnchor, provider.GetValidateTrustAnchor(ctx))
|
||||
assert.Equal(t, tc.expectedStatus, provider.GetValidateStatus(ctx))
|
||||
assert.Equal(t, tc.expectedAttestationTypes, provider.GetValidateAttestationTypes(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_ValidateStatusReports(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
opts []Option
|
||||
reports []metadata.StatusReport
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "ShouldPassWithNoUndesiredStatuses",
|
||||
opts: []Option{
|
||||
WithMetadata(map[uuid.UUID]*metadata.Entry{}),
|
||||
WithValidateStatus(true),
|
||||
},
|
||||
reports: []metadata.StatusReport{{Status: metadata.FidoCertified}},
|
||||
},
|
||||
{
|
||||
name: "ShouldFailWithUndesiredStatus",
|
||||
opts: []Option{
|
||||
WithMetadata(map[uuid.UUID]*metadata.Entry{}),
|
||||
WithValidateStatus(true),
|
||||
WithStatusUndesired([]metadata.AuthenticatorStatus{metadata.Revoked}),
|
||||
},
|
||||
reports: []metadata.StatusReport{{Status: metadata.Revoked}},
|
||||
err: "The following undesired status reports were present: REVOKED",
|
||||
},
|
||||
{
|
||||
name: "ShouldPassWhenStatusValidationDisabled",
|
||||
opts: []Option{
|
||||
WithMetadata(map[uuid.UUID]*metadata.Entry{}),
|
||||
WithValidateStatus(false),
|
||||
},
|
||||
reports: []metadata.StatusReport{{Status: metadata.Revoked}},
|
||||
},
|
||||
{
|
||||
name: "ShouldFailWithDesiredStatusAbsent",
|
||||
opts: []Option{
|
||||
WithMetadata(map[uuid.UUID]*metadata.Entry{}),
|
||||
WithValidateStatus(true),
|
||||
WithStatusDesired([]metadata.AuthenticatorStatus{metadata.FidoCertified}),
|
||||
WithStatusUndesired(nil),
|
||||
},
|
||||
reports: []metadata.StatusReport{{Status: metadata.NotFidoCertified}},
|
||||
err: "The following desired status reports were absent: FIDO_CERTIFIED",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
provider, err := New(tc.opts...)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = provider.ValidateStatusReports(context.Background(), tc.reports)
|
||||
|
||||
if tc.err != "" {
|
||||
require.EqualError(t, err, tc.err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_GetEntry_NilMDS(t *testing.T) {
|
||||
p := &Provider{}
|
||||
|
||||
entry, err := p.GetEntry(context.Background(), uuid.New())
|
||||
assert.Nil(t, entry)
|
||||
require.EqualError(t, err, "metadata: not initialized")
|
||||
}
|
||||
Reference in New Issue
Block a user