@@ -2,7 +2,9 @@ package protocol
22
33import (
44 "context"
5+ "errors"
56 "fmt"
7+ "strings"
68
79 "github.com/google/uuid"
810
@@ -26,14 +28,12 @@ func init() {
2628// nonCompoundAttStmt = { $$attStmtType } .within { fmt: text .ne "compound", * any => any }
2729//
2830// §8.9 leaves the handling of a sub-statement which fails verification, and the number which must succeed, to Relying
29- // Party policy. The policy applied here is the strictest available: every sub-statement must verify, and the first
30- // failure rejects the attestation.
31+ // Party policy. The scope carries that decision and the zero value selects the strictest behavior available: every
32+ // sub-statement must verify, and the first failure rejects the attestation. See [CompoundSubStatementScope] .
3133//
3234// Specification: §8.9. Compound Attestation Statement Forma
3335//
3436// See: https://www.w3.org/TR/webauthn-3/#sctn-compound-attestation
35- //
36- //nolint:gocyclo
3737func attestationFormatValidationHandlerCompound (att AttestationObject , clientDataHash []byte , mds metadata.Provider , policy AttestationPolicy ) (attestationType string , x5cs []any , err error ) {
3838 var (
3939 aaguid uuid.UUID
@@ -91,20 +91,25 @@ func attestationFormatValidationHandlerCompound(att AttestationObject, clientDat
9191 }
9292 }
9393
94- for i , attStmt := range attStmts {
95- object := AttestationObject {
96- Format : attStmt .Format ,
97- AttStatement : attStmt .AttStatement ,
98- AuthData : att .AuthData ,
99- RawAuthData : att .RawAuthData ,
100- }
94+ // The trust paths are not conveyed to the caller. Each is validated against the Metadata Service by the
95+ // verification below alongside the format and attestation type it belongs to, which a single chain can't describe
96+ // for more than one sub-statement, and the paths of independent sub-statements joined together describe no real
97+ // chain.
98+ if policy .Compound .SubStatementScope .any () {
99+ return compoundVerifySubStatementsAny (att , attStmts , clientDataHash , mds , policy , aaguid )
100+ }
101+
102+ return compoundVerifySubStatementsAll (att , attStmts , clientDataHash , mds , policy , aaguid )
103+ }
101104
102- var (
103- cx5cs []any
104- subAttType string
105- )
105+ // compoundVerifySubStatementsAll verifies every sub-statement, rejecting the attestation on the first which fails.
106+ //
107+ // This is the behavior of [CompoundSubStatementScopeAll].
108+ func compoundVerifySubStatementsAll (att AttestationObject , attStmts []NonCompoundAttestationObject , clientDataHash []byte , mds metadata.Provider , policy AttestationPolicy , aaguid uuid.UUID ) (attestationType string , x5cs []any , err error ) {
109+ for i , attStmt := range attStmts {
110+ var subAttType string
106111
107- if subAttType , cx5cs , err = attestationRegistry [ AttestationFormat ( object . Format )]( object , clientDataHash , mds , policy ); err != nil {
112+ if subAttType , err = compoundVerifySubStatement ( att , attStmt , clientDataHash , mds , policy , aaguid ); err != nil {
108113 return "" , nil , err
109114 }
110115
@@ -113,18 +118,88 @@ func attestationFormatValidationHandlerCompound(att AttestationObject, clientDat
113118 if i == 0 {
114119 attestationType = subAttType
115120 }
121+ }
122+
123+ return attestationType , nil , nil
124+ }
125+
126+ // compoundVerifySubStatementsAny verifies sub-statements until one of them succeeds, rejecting the attestation only
127+ // when none can be verified. The sub-statements after the first success are not verified as the scope is satisfied
128+ // by it, and the failures of the sub-statements before it are conveyed together so the reason the accepted one was
129+ // reached is not lost.
130+ //
131+ // This is the behavior of [CompoundSubStatementScopeAny].
132+ func compoundVerifySubStatementsAny (att AttestationObject , attStmts []NonCompoundAttestationObject , clientDataHash []byte , mds metadata.Provider , policy AttestationPolicy , aaguid uuid.UUID ) (attestationType string , x5cs []any , err error ) {
133+ var (
134+ errs = make ([]error , 0 , len (attStmts ))
135+ reasons = make ([]string , 0 , len (attStmts ))
136+ )
137+
138+ for _ , attStmt := range attStmts {
139+ var subAttType string
140+
141+ if subAttType , err = compoundVerifySubStatement (att , attStmt , clientDataHash , mds , policy , aaguid ); err != nil {
142+ errs = append (errs , err )
143+ reasons = append (reasons , fmt .Sprintf ("%s: %s" , attStmt .Format , compoundSubStatementFailureReason (err )))
116144
117- if mds == nil {
118145 continue
119146 }
120147
121- if e := ValidateMetadata ( context . Background (), mds , aaguid , subAttType , object . Format , cx5cs ); e != nil {
122- return "" , nil , ErrInvalidAttestation . WithInfo ( fmt . Sprintf ( "Error occurred validating metadata during attestation validation: %+v" , e )). WithDetails ( e . DevInfo ). WithError ( e )
123- }
148+ // The sub-statement which was verified is the one which describes an attestation which was obtained, so its
149+ // type is the value recorded against the credential rather than that of a sub-statement which failed.
150+ return subAttType , nil , nil
124151 }
125152
126- // The trust paths are not conveyed to the caller. Each is validated against the Metadata Service above alongside
127- // the format and attestation type it belongs to, which a single chain can't describe for more than one
128- // sub-statement, and the paths of independent sub-statements joined together describe no real chain.
129- return attestationType , nil , nil
153+ return "" , nil , ErrInvalidAttestation .
154+ WithDetails (fmt .Sprintf ("Compound statement does not contain any sub-statement which could be verified (%s)" , strings .Join (reasons , "; " ))).
155+ WithError (errors .Join (errs ... ))
156+ }
157+
158+ // compoundSubStatementFailureReason describes the failure of a sub-statement for the aggregate error of the any
159+ // scope. The details of an [Error] are preferred as they name the specific failure, falling back to the debug
160+ // information and then the type so that a failed sub-statement is never described by an empty string.
161+ func compoundSubStatementFailureReason (err error ) string {
162+ var e * Error
163+
164+ if ! errors .As (err , & e ) {
165+ return err .Error ()
166+ }
167+
168+ switch {
169+ case e .Details != "" :
170+ return e .Details
171+ case e .DevInfo != "" :
172+ return e .DevInfo
173+ default :
174+ return e .Type
175+ }
176+ }
177+
178+ // compoundVerifySubStatement performs the verification procedure of a single sub-statement and validates the trust
179+ // path it produces against the Metadata Service. A sub-statement is verified in full or not at all, so a scope which
180+ // tolerates a failure treats a sub-statement whose trust path the Metadata Service rejects the same as one whose
181+ // verification procedure fails.
182+ func compoundVerifySubStatement (att AttestationObject , attStmt NonCompoundAttestationObject , clientDataHash []byte , mds metadata.Provider , policy AttestationPolicy , aaguid uuid.UUID ) (attestationType string , err error ) {
183+ object := AttestationObject {
184+ Format : attStmt .Format ,
185+ AttStatement : attStmt .AttStatement ,
186+ AuthData : att .AuthData ,
187+ RawAuthData : att .RawAuthData ,
188+ }
189+
190+ var cx5cs []any
191+
192+ if attestationType , cx5cs , err = attestationRegistry [AttestationFormat (object .Format )](object , clientDataHash , mds , policy ); err != nil {
193+ return "" , err
194+ }
195+
196+ if mds == nil {
197+ return attestationType , nil
198+ }
199+
200+ if e := ValidateMetadata (context .Background (), mds , aaguid , attestationType , object .Format , cx5cs ); e != nil {
201+ return "" , ErrInvalidAttestation .WithInfo (fmt .Sprintf ("Error occurred validating metadata during attestation validation: %+v" , e )).WithDetails (e .DevInfo ).WithError (e )
202+ }
203+
204+ return attestationType , nil
130205}
0 commit comments