Skip to content

Commit 34271da

Browse files
feat(protocol): compound sub-statement scope (#742)
Adds the compound attestation policy which selects the sub-statements §8.9 requires to be verified for the attestation to be accepted, either all of them or any one of them, with the zero value evaluating as all so an unset policy remains the strictest behavior available. A sub-statement counts as verified only when its format's verification procedure succeeds and the trust path it produces is accepted by the Metadata Service, so the any scope tolerates a failure of either provided another sub-statement satisfies both, and conveys the attestation type of the sub-statement which was verified rather than that of one which failed. When no sub-statement can be verified the failures are conveyed together, each named alongside its format, and joined so the underlying errors remain reachable. The scope applies only to the outcome of the verification procedures. A statement which does not satisfy the §8.9 CDDL, nests a compound sub-statement, or names an unsupported format is rejected under every scope.
1 parent 3239ed0 commit 34271da

6 files changed

Lines changed: 506 additions & 30 deletions

File tree

protocol/attestation_compound.go

Lines changed: 99 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package protocol
22

33
import (
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
3737
func 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

Comments
 (0)