@@ -3,62 +3,175 @@ package metadata
33import (
44 "fmt"
55 "strings"
6+ "time"
67)
78
8- // ValidateStatusReports checks a list of [StatusReport] structs against a list of desired and undesired [AuthenticatorStatus]
9- // values. If the reports contain all of the desired and none of the undesired status reports then no error is returned
10- // otherwise an error describing the issue is returned.
9+ // InEffectAt returns true if this [StatusReport] is in effect at the given time.
1110//
12- //nolint:gocyclo
11+ // A report which has an effective date in the future has not come into effect yet, and a report which has reached its
12+ // sunset date has expired. An absent effective date means the report is in effect while present, and an absent sunset
13+ // date means it has no scheduled expiry.
14+ //
15+ // See: https://fidoalliance.org/specs/mds/fido-metadata-service-v3.1.1-rd-20251016.html#sctn-stat-rep
16+ func (r StatusReport ) InEffectAt (at time.Time ) bool {
17+ if r .EffectiveDate != nil && r .EffectiveDate .After (at ) {
18+ return false
19+ }
20+
21+ if r .SunsetDate != nil && ! r .SunsetDate .After (at ) {
22+ return false
23+ }
24+
25+ return true
26+ }
27+
28+ // EffectiveStatusReports returns the subset of the given [StatusReport] values which are in effect at the given time,
29+ // preserving their original order. See [StatusReport.InEffectAt] for the rules applied.
30+ func EffectiveStatusReports (reports []StatusReport , at time.Time ) (effective []StatusReport ) {
31+ for _ , report := range reports {
32+ if report .InEffectAt (at ) {
33+ effective = append (effective , report )
34+ }
35+ }
36+
37+ return effective
38+ }
39+
40+ // CurrentStatusReportAt returns the [StatusReport] which reflects the current status of the authenticator at the given
41+ // time, or nil if no report is in effect. See [currentStatusReport] for how the current report is selected.
42+ func CurrentStatusReportAt (reports []StatusReport , at time.Time ) (current * StatusReport ) {
43+ return currentStatusReport (EffectiveStatusReports (reports , at ))
44+ }
45+
46+ // currentStatusReport returns the report reflecting the current status from a set of reports already known to be in
47+ // effect, or nil if there are none.
48+ //
49+ // MDS3 requires that the latest entry reflects the current status, so the last element wins by default. Where both
50+ // candidates carry an effective date that date is preferred over the document order, which keeps the selection correct
51+ // for a blob whose reports are not listed oldest first. Ties resolve to the later element.
52+ func currentStatusReport (effective []StatusReport ) (current * StatusReport ) {
53+ for i := range effective {
54+ report := & effective [i ]
55+
56+ if current == nil || current .EffectiveDate == nil || report .EffectiveDate == nil {
57+ current = report
58+
59+ continue
60+ }
61+
62+ if ! report .EffectiveDate .Before (* current .EffectiveDate ) {
63+ current = report
64+ }
65+ }
66+
67+ return current
68+ }
69+
70+ // ValidateStatusReports checks a list of [StatusReport] structs against a list of desired and undesired
71+ // [AuthenticatorStatus] values as at the current time. See [ValidateStatusReportsAt] for the validation performed.
1372func ValidateStatusReports (reports []StatusReport , desired , undesired []AuthenticatorStatus ) (err error ) {
14- if len (desired ) == 0 && (len (undesired ) == 0 || len (reports ) == 0 ) {
73+ return ValidateStatusReportsAt (reports , desired , undesired , time .Now ())
74+ }
75+
76+ // ValidateStatusReportsAt checks a list of [StatusReport] structs against a list of desired and undesired
77+ // [AuthenticatorStatus] values as at the given time. If the reports satisfy the desired statuses and contain none of
78+ // the undesired statuses then no error is returned, otherwise an error describing the issue is returned.
79+ //
80+ // Reports which are not in effect at the given time are excluded from consideration entirely, i.e. those with an
81+ // effective date in the future and those which have reached their sunset date.
82+ //
83+ // The desired statuses are matched against the current status only, and are treated as a set of acceptable statuses of
84+ // which one must match. MDS3 requires that the latest report reflects the current status, so an authenticator formerly
85+ // certified at some level but since revoked does not satisfy a desired status naming that certification level.
86+ //
87+ // The undesired statuses are matched against every report in effect rather than the current one alone. Statuses such as
88+ // [AttestationKeyCompromise] and [UserKeyPhysicalCompromise] describe a weakness discovered in the authenticator model
89+ // which a later report does not retract; the sunset date honored above is the mechanism by which such a report stops
90+ // applying.
91+ //
92+ // Note that a compromise reported against a specific attestation certificate, i.e. where [StatusReport.Certificate] or
93+ // [StatusReport.BatchCertificate] is set, applies only to that batch of authenticators. This function has no visibility
94+ // of the attestation statement being validated, so it conservatively treats such a report as applying to every
95+ // authenticator of the model.
96+ func ValidateStatusReportsAt (reports []StatusReport , desired , undesired []AuthenticatorStatus , at time.Time ) (err error ) {
97+ if len (desired ) == 0 && len (undesired ) == 0 {
1598 return nil
1699 }
17100
18- var present , absent []string
101+ effective := EffectiveStatusReports (reports , at )
102+
103+ var present []string
19104
20105 if len (undesired ) != 0 {
21- for _ , report := range reports {
22- for _ , status := range undesired {
23- if report .Status == status {
24- present = append (present , string (status ))
106+ seen := make (map [AuthenticatorStatus ]struct {}, len (undesired ))
25107
26- continue
27- }
108+ for _ , report := range effective {
109+ if _ , ok := seen [report .Status ]; ok {
110+ continue
111+ }
112+
113+ if hasStatus (report .Status , undesired ) {
114+ seen [report .Status ] = struct {}{}
115+
116+ present = append (present , string (report .Status ))
28117 }
29118 }
30119 }
31120
121+ var (
122+ current * StatusReport
123+ unsatisfied bool
124+ )
125+
32126 if len (desired ) != 0 {
33- desired:
34- for _ , status := range desired {
35- for _ , report := range reports {
36- if report .Status == status {
37- continue desired
38- }
39- }
127+ current = currentStatusReport (effective )
40128
41- absent = append (absent , string (status ))
42- }
129+ unsatisfied = current == nil || ! hasStatus (current .Status , desired )
43130 }
44131
45132 switch {
46- case len (present ) == 0 && len ( absent ) == 0 :
133+ case len (present ) == 0 && ! unsatisfied :
47134 return nil
48- case len (present ) != 0 && len ( absent ) == 0 :
135+ case len (present ) != 0 && ! unsatisfied :
49136 return & Error {
50137 Type : "invalid_status" ,
51138 Details : fmt .Sprintf ("The following undesired status reports were present: %s" , strings .Join (present , ", " )),
52139 }
53- case len (present ) == 0 && len ( absent ) != 0 :
140+ case len (present ) == 0 :
54141 return & Error {
55142 Type : "invalid_status" ,
56- Details : fmt . Sprintf ( "The following desired status reports were absent: %s" , strings . Join ( absent , ", " ) ),
143+ Details : describeDesiredUnsatisfied ( current , desired ),
57144 }
58145 default :
59146 return & Error {
60147 Type : "invalid_status" ,
61- Details : fmt .Sprintf ("The following undesired status reports were present: %s; the following desired status reports were absent: %s" , strings .Join (present , ", " ), strings . Join ( absent , ", " )),
148+ Details : fmt .Sprintf ("The following undesired status reports were present: %s; %s" , strings .Join (present , ", " ), describeDesiredUnsatisfied ( current , desired )),
62149 }
63150 }
64151}
152+
153+ // hasStatus returns true if the given status is a member of the given values.
154+ func hasStatus (status AuthenticatorStatus , values []AuthenticatorStatus ) bool {
155+ for _ , value := range values {
156+ if value == status {
157+ return true
158+ }
159+ }
160+
161+ return false
162+ }
163+
164+ // describeDesiredUnsatisfied renders the reason the desired statuses were not satisfied by the current status report.
165+ func describeDesiredUnsatisfied (current * StatusReport , desired []AuthenticatorStatus ) string {
166+ statuses := make ([]string , len (desired ))
167+
168+ for i , status := range desired {
169+ statuses [i ] = string (status )
170+ }
171+
172+ if current == nil {
173+ return fmt .Sprintf ("no status report was in effect so none of the desired statuses could be satisfied: %s" , strings .Join (statuses , ", " ))
174+ }
175+
176+ return fmt .Sprintf ("the current status report '%s' was not one of the desired statuses: %s" , current .Status , strings .Join (statuses , ", " ))
177+ }
0 commit comments