auth: publish passkey foundations preview
verify / verify (push) Successful in 3m40s

This commit is contained in:
2026-08-21 17:33:00 -04:00
parent fb6bbd0dad
commit bfe6cfd29e
230 changed files with 44547 additions and 17 deletions
@@ -0,0 +1,8 @@
// Package cached handles a [metadata.Provider] implementation that both downloads and caches the MDS3 blob. This
// effectively is the recommended provider in most instances as it's fairly robust. Alternatively we suggest
// implementing a similar provider that leverages the [memory.Provider] as an underlying element.
//
// This provider only specifically performs updates at the time it's initialized. It has no automatic update
// functionality. This may change in the future however if you want this functionality at this time we recommend making
// your own implementation.
package cached
@@ -0,0 +1,92 @@
package cached
import (
"net/http"
"net/url"
"github.com/go-webauthn/webauthn/metadata"
)
// Option describes an optional pattern for this provider.
type Option func(provider *Provider) (err error)
// NewFunc describes the type used to create the underlying provider.
type NewFunc func(mds *metadata.Metadata) (provider metadata.Provider, err error)
// WithPath sets the path name for the cached file. This option is REQUIRED.
func WithPath(name string) Option {
return func(provider *Provider) (err error) {
provider.name = name
return nil
}
}
// WithUpdate is used to enable or disable the update. By default it's set to true.
func WithUpdate(update bool) Option {
return func(provider *Provider) (err error) {
provider.update = update
return nil
}
}
// WithForceUpdate is used to force an update on creation. This will forcibly overwrite the file if possible.
func WithForceUpdate(force bool) Option {
return func(provider *Provider) (err error) {
provider.force = force
return nil
}
}
// WithNew customizes the NewFunc. By default we just create a fairly standard [memory.Provider] with strict defaults.
func WithNew(newup NewFunc) Option {
return func(provider *Provider) (err error) {
provider.newup = newup
return nil
}
}
// WithDecoder sets the decoder to be used for this provider. By default this is a decoder with the entry parsing errors
// configured to skip that entry.
func WithDecoder(decoder *metadata.Decoder) Option {
return func(provider *Provider) (err error) {
provider.decoder = decoder
return nil
}
}
// WithMetadataURL configures the URL to get the metadata from. This shouldn't be modified unless you know what you're
// doing as we use the [metadata.ProductionMDSURL] which is safe in most instances.
func WithMetadataURL(uri string) Option {
return func(provider *Provider) (err error) {
if _, err = url.ParseRequestURI(uri); err != nil {
return err
}
provider.uri = uri
return nil
}
}
// WithClient configures the [*http.Client] used to get the MDS3 blob.
func WithClient(client *http.Client) Option {
return func(provider *Provider) (err error) {
provider.client = client
return nil
}
}
// WithClock allows injection of a [metadata.Clock] to check the up-to-date status of a blob.
func WithClock(clock metadata.Clock) Option {
return func(provider *Provider) (err error) {
provider.clock = clock
return nil
}
}
@@ -0,0 +1,146 @@
package cached
import (
"fmt"
"io"
"net/http"
"os"
"github.com/go-webauthn/webauthn/metadata"
)
// New returns a new cached Provider given a set of functional [Option]'s. This provider will download a new version and
// save it to the configured file path if it doesn't exist or if it's out of date by default.
func New(opts ...Option) (provider metadata.Provider, err error) {
p := &Provider{
update: true,
uri: metadata.ProductionMDSURL,
}
for _, opt := range opts {
if err = opt(p); err != nil {
return nil, err
}
}
if p.name == "" {
return nil, fmt.Errorf("provider configured without setting a path for the cached file blob")
}
if p.newup == nil {
p.newup = defaultNew
}
if p.decoder == nil {
if p.decoder, err = metadata.NewDecoder(metadata.WithIgnoreEntryParsingErrors()); err != nil {
return nil, err
}
}
if p.clock == nil {
p.clock = &metadata.RealClock{}
}
if err = p.init(); err != nil {
return nil, err
}
return p, nil
}
// Provider implements a [metadata.Provider] with a file-based cache.
type Provider struct {
metadata.Provider
name string
uri string
update bool
force bool
clock metadata.Clock
client *http.Client
decoder *metadata.Decoder
newup NewFunc
}
func (p *Provider) init() (err error) {
var (
f *os.File
rc io.ReadCloser
created bool
mds *metadata.Metadata
)
if f, created, err = doOpenOrCreate(p.name); err != nil {
return err
}
defer f.Close()
if created || p.force {
if rc, err = p.get(); err != nil {
return err
}
} else {
if mds, err = p.parse(f); err != nil {
return err
}
if p.outdated(mds) {
if rc, err = p.get(); err != nil {
return err
}
}
}
if rc != nil {
if err = doTruncateCopyAndSeekStart(f, rc); err != nil {
return err
}
if mds, err = p.parse(f); err != nil {
return err
}
}
var provider metadata.Provider
if provider, err = p.newup(mds); err != nil {
return err
}
p.Provider = provider
return nil
}
func (p *Provider) parse(rc io.ReadCloser) (data *metadata.Metadata, err error) {
var payload *metadata.PayloadJSON
if payload, err = p.decoder.Decode(rc); err != nil {
return nil, err
}
if data, err = p.decoder.Parse(payload); err != nil {
return nil, err
}
return data, nil
}
func (p *Provider) outdated(mds *metadata.Metadata) bool {
return p.update && p.clock.Now().After(mds.Parsed.NextUpdate)
}
func (p *Provider) get() (f io.ReadCloser, err error) {
if p.client == nil {
p.client = &http.Client{}
}
var res *http.Response
if res, err = p.client.Get(p.uri); err != nil {
return nil, err
}
return res.Body, nil
}
@@ -0,0 +1,164 @@
package cached
import (
"net/http"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/go-webauthn/webauthn/metadata"
)
func TestNew_Errors(t *testing.T) {
testCases := []struct {
name string
opts []Option
err string
}{
{
name: "ShouldFailWithoutPath",
opts: nil,
err: "provider configured without setting a path for the cached file blob",
},
{
name: "ShouldFailWithEmptyPath",
opts: []Option{WithPath("")},
err: "provider configured without setting a path for the cached file blob",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
provider, err := New(tc.opts...)
assert.Nil(t, provider)
require.EqualError(t, err, tc.err)
})
}
}
func TestOptions(t *testing.T) {
testCases := []struct {
name string
opt Option
verify func(t *testing.T, p *Provider)
}{
{
name: "ShouldSetPath",
opt: WithPath("/tmp/test.json"),
verify: func(t *testing.T, p *Provider) {
assert.Equal(t, "/tmp/test.json", p.name)
},
},
{
name: "ShouldSetUpdate",
opt: WithUpdate(false),
verify: func(t *testing.T, p *Provider) {
assert.False(t, p.update)
},
},
{
name: "ShouldSetUpdateTrue",
opt: WithUpdate(true),
verify: func(t *testing.T, p *Provider) {
assert.True(t, p.update)
},
},
{
name: "ShouldSetForceUpdate",
opt: WithForceUpdate(true),
verify: func(t *testing.T, p *Provider) {
assert.True(t, p.force)
},
},
{
name: "ShouldSetClient",
opt: WithClient(&http.Client{Timeout: 5 * time.Second}),
verify: func(t *testing.T, p *Provider) {
require.NotNil(t, p.client)
assert.Equal(t, 5*time.Second, p.client.Timeout)
},
},
{
name: "ShouldSetMetadataURL",
opt: WithMetadataURL("https://example.com/mds"),
verify: func(t *testing.T, p *Provider) {
assert.Equal(t, "https://example.com/mds", p.uri)
},
},
{
name: "ShouldSetDecoder",
opt: func() Option {
d, _ := metadata.NewDecoder()
return WithDecoder(d)
}(),
verify: func(t *testing.T, p *Provider) {
assert.NotNil(t, p.decoder)
},
},
{
name: "ShouldSetClock",
opt: WithClock(&metadata.RealClock{}),
verify: func(t *testing.T, p *Provider) {
assert.NotNil(t, p.clock)
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
p := &Provider{}
err := tc.opt(p)
require.NoError(t, err)
tc.verify(t, p)
})
}
}
func TestWithMetadataURL_Invalid(t *testing.T) {
testCases := []struct {
name string
uri string
err string
}{
{
name: "ShouldRejectInvalidURL",
uri: "not a valid url",
err: `parse "not a valid url": invalid URI for request`,
},
{
name: "ShouldRejectEmptyURL",
uri: "",
err: `parse "": empty url`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
p := &Provider{}
err := WithMetadataURL(tc.uri)(p)
require.EqualError(t, err, tc.err)
})
}
}
func TestWithNew(t *testing.T) {
called := false
fn := func(mds *metadata.Metadata) (metadata.Provider, error) {
called = true
return nil, nil
}
p := &Provider{}
require.NoError(t, WithNew(fn)(p))
require.NotNil(t, p.newup)
_, _ = p.newup(nil)
assert.True(t, called)
}
@@ -0,0 +1,51 @@
package cached
import (
"io"
"os"
"github.com/go-webauthn/webauthn/metadata"
"github.com/go-webauthn/webauthn/metadata/providers/memory"
)
func doTruncateCopyAndSeekStart(f *os.File, rc io.ReadCloser) (err error) {
if err = f.Truncate(0); err != nil {
return err
}
if _, err = io.Copy(f, rc); err != nil {
return err
}
if _, err = f.Seek(0, io.SeekStart); err != nil {
return err
}
return rc.Close()
}
func doOpenOrCreate(name string) (f *os.File, created bool, err error) {
if f, err = os.OpenFile(name, os.O_RDWR, 0); err == nil {
return f, false, nil
}
if os.IsNotExist(err) {
if f, err = os.Create(name); err != nil {
return nil, false, err
}
return f, true, nil
}
return nil, false, err
}
func defaultNew(mds *metadata.Metadata) (provider metadata.Provider, err error) {
return memory.New(
memory.WithMetadata(mds.ToMap()),
memory.WithValidateEntry(true),
memory.WithValidateEntryPermitZeroAAGUID(false),
memory.WithValidateTrustAnchor(true),
memory.WithValidateStatus(true),
)
}
@@ -0,0 +1,253 @@
package cached
import (
"bytes"
"io"
"os"
"path/filepath"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/go-webauthn/webauthn/metadata"
)
func TestDoOpenOrCreate(t *testing.T) {
testCases := []struct {
name string
setup func(t *testing.T) string
expectedCreated bool
err string
}{
{
name: "ShouldCreateNewFile",
setup: func(t *testing.T) string {
t.Helper()
return filepath.Join(t.TempDir(), "new-file.json")
},
expectedCreated: true,
},
{
name: "ShouldOpenExistingFile",
setup: func(t *testing.T) string {
t.Helper()
path := filepath.Join(t.TempDir(), "existing-file.json")
f, err := os.Create(path)
require.NoError(t, err)
require.NoError(t, f.Close())
return path
},
expectedCreated: false,
},
{
name: "ShouldFailInvalidPath",
setup: func(t *testing.T) string {
t.Helper()
return filepath.Join(t.TempDir(), "nonexistent-dir", "subdir", "file.json")
},
err: "no such file or directory",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
path := tc.setup(t)
f, created, err := doOpenOrCreate(path)
if tc.err != "" {
assert.Nil(t, f)
require.Error(t, err)
assert.Contains(t, err.Error(), tc.err)
} else {
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, tc.expectedCreated, created)
require.NoError(t, f.Close())
}
})
}
}
func TestDoTruncateCopyAndSeekStart(t *testing.T) {
testCases := []struct {
name string
initialContent string
copyContent string
expectedContent string
err string
}{
{
name: "ShouldTruncateAndCopy",
initialContent: "old content that should be replaced",
copyContent: "new data",
expectedContent: "new data",
},
{
name: "ShouldHandleEmptyInitialContent",
initialContent: "",
copyContent: "fresh content",
expectedContent: "fresh content",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
path := filepath.Join(t.TempDir(), "test-file.json")
f, err := os.Create(path)
require.NoError(t, err)
_, err = f.WriteString(tc.initialContent)
require.NoError(t, err)
_, err = f.Seek(0, io.SeekStart)
require.NoError(t, err)
rc := io.NopCloser(bytes.NewReader([]byte(tc.copyContent)))
err = doTruncateCopyAndSeekStart(f, rc)
if tc.err != "" {
assert.EqualError(t, err, tc.err)
} else {
require.NoError(t, err)
content, err := io.ReadAll(f)
require.NoError(t, err)
assert.Equal(t, tc.expectedContent, string(content))
}
require.NoError(t, f.Close())
})
}
}
func TestDefaultNew(t *testing.T) {
testCases := []struct {
name string
have *metadata.Metadata
err string
}{
{
name: "ShouldSucceedWithEmptyMetadata",
have: &metadata.Metadata{
Parsed: metadata.Parsed{
NextUpdate: time.Now().Add(time.Hour * 24),
},
},
},
{
name: "ShouldSucceedWithEntries",
have: &metadata.Metadata{
Parsed: metadata.Parsed{
NextUpdate: time.Now().Add(time.Hour * 24),
Entries: []metadata.Entry{
{
AaGUID: uuid.MustParse("2369d4d0-13ce-48cb-9f26-f7ed8c9a6068"),
},
},
},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
provider, err := defaultNew(tc.have)
if tc.err == "" {
assert.NoError(t, err)
assert.NotNil(t, provider)
} else {
assert.EqualError(t, err, tc.err)
}
})
}
}
func TestProviderOutdated(t *testing.T) {
testCases := []struct {
name string
have struct {
update bool
clockAt time.Time
nextUpd time.Time
}
expected bool
}{
{
name: "ShouldBeOutdatedWhenPastNextUpdate",
have: struct {
update bool
clockAt time.Time
nextUpd time.Time
}{
update: true,
clockAt: time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC),
nextUpd: time.Date(2025, 5, 1, 0, 0, 0, 0, time.UTC),
},
expected: true,
},
{
name: "ShouldNotBeOutdatedWhenBeforeNextUpdate",
have: struct {
update bool
clockAt time.Time
nextUpd time.Time
}{
update: true,
clockAt: time.Date(2025, 4, 1, 0, 0, 0, 0, time.UTC),
nextUpd: time.Date(2025, 5, 1, 0, 0, 0, 0, time.UTC),
},
expected: false,
},
{
name: "ShouldNotBeOutdatedWhenUpdateDisabled",
have: struct {
update bool
clockAt time.Time
nextUpd time.Time
}{
update: false,
clockAt: time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC),
nextUpd: time.Date(2025, 5, 1, 0, 0, 0, 0, time.UTC),
},
expected: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
p := &Provider{
update: tc.have.update,
clock: &mockClock{now: tc.have.clockAt},
}
mds := &metadata.Metadata{
Parsed: metadata.Parsed{
NextUpdate: tc.have.nextUpd,
},
}
assert.Equal(t, tc.expected, p.outdated(mds))
})
}
}
type mockClock struct {
now time.Time
}
func (c *mockClock) Now() time.Time {
return c.now
}
@@ -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")
}