Skip to content

Commit 025d897

Browse files
fix(protocol): compound returns incorrect type (#731)
This fixes an issue where compound attestation statement parsing returns "none" as the attestation type in all situations, instead we return the first validated type.
1 parent d161848 commit 025d897

2 files changed

Lines changed: 63 additions & 4 deletions

File tree

protocol/attestation_compound.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ func init() {
2525
//
2626
// nonCompoundAttStmt = { $$attStmtType } .within { fmt: text .ne "compound", * any => any }
2727
//
28+
// §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+
//
2832
// Specification: §8.9. Compound Attestation Statement Forma
2933
//
3034
// See: https://www.w3.org/TR/webauthn-3/#sctn-compound-attestation
@@ -87,7 +91,7 @@ func attestationFormatValidationHandlerCompound(att AttestationObject, clientDat
8791
}
8892
}
8993

90-
for _, attStmt := range attStmts {
94+
for i, attStmt := range attStmts {
9195
object := AttestationObject{
9296
Format: attStmt.Format,
9397
AttStatement: attStmt.AttStatement,
@@ -104,6 +108,12 @@ func attestationFormatValidationHandlerCompound(att AttestationObject, clientDat
104108
return "", nil, err
105109
}
106110

111+
// Every sub-statement attests the same credential, so the type conveyed by the first describes an attestation
112+
// which was obtained and is the value recorded against the credential.
113+
if i == 0 {
114+
attestationType = subAttType
115+
}
116+
107117
if mds == nil {
108118
continue
109119
}
@@ -113,5 +123,8 @@ func attestationFormatValidationHandlerCompound(att AttestationObject, clientDat
113123
}
114124
}
115125

116-
return stmtTypNone, nil, nil
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
117130
}

protocol/attestation_compound_test.go

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package protocol
22

33
import (
4+
"context"
45
"errors"
56
"reflect"
67
"testing"
@@ -248,7 +249,9 @@ func TestAttestationFormatValidationHandlerCompound(t *testing.T) {
248249
gotType, gotX5Cs, err := attestationFormatValidationHandlerCompound(att, []byte("hash"), nil)
249250
require.NoError(t, err)
250251

251-
assert.Equal(t, stmtTypNone, gotType)
252+
// §8.9 returns any combination of the outputs of the successful verification procedures. The type of the
253+
// first sub-statement is conveyed so the credential isn't recorded as carrying no attestation.
254+
assert.Equal(t, "packed-type", gotType)
252255
assert.Nil(t, gotX5Cs)
253256

254257
require.Len(t, calls, 2)
@@ -364,10 +367,53 @@ func TestAttestationFormatValidationHandlerCompound(t *testing.T) {
364367
gotType, gotX5Cs, err := attestationFormatValidationHandlerCompound(att, []byte("hash"), nil)
365368
require.NoError(t, err)
366369

367-
assert.Equal(t, stmtTypNone, gotType)
370+
assert.Equal(t, testAttTypeSome, gotType)
368371
assert.Nil(t, gotX5Cs)
369372
assert.Equal(t, 2, handlerCalls)
370373
})
374+
375+
// A compound attestation which conveys stmtTypNone suppresses the attestation type validation performed by
376+
// ValidateMetadata, which skips the check for that value, so the type of a sub-statement must reach it.
377+
t.Run("ShouldValidateMetadataAgainstTheConveyedAttestationType", func(t *testing.T) {
378+
withFreshAttestationRegistry(t)
379+
380+
attestationRegistry[AttestationFormatPacked] = func(att AttestationObject, clientDataHash []byte, mds metadata.Provider) (string, []any, error) {
381+
return string(metadata.BasicFull), nil, nil
382+
}
383+
384+
att := AttestationObject{
385+
Format: string(AttestationFormatCompound),
386+
AuthData: AuthenticatorData{
387+
AttData: AttestedCredentialData{AAGUID: make([]byte, 0)},
388+
},
389+
AttStatement: map[string]any{
390+
stmtAttStmt: []any{
391+
map[string]any{stmtFmt: string(AttestationFormatPacked), stmtAttStmt: map[string]any{}},
392+
map[string]any{stmtFmt: string(AttestationFormatPacked), stmtAttStmt: map[string]any{}},
393+
},
394+
},
395+
}
396+
397+
gotType, _, err := attestationFormatValidationHandlerCompound(att, []byte("hash"), nil)
398+
require.NoError(t, err)
399+
require.NotEqual(t, stmtTypNone, gotType)
400+
401+
ctrl := gomock.NewController(t)
402+
mds := mocks.NewMockMetadataProvider(ctrl)
403+
404+
entry := &metadata.Entry{
405+
MetadataStatement: metadata.Statement{
406+
AttestationTypes: metadata.AuthenticatorAttestationTypes{metadata.AttCA},
407+
},
408+
}
409+
410+
mds.EXPECT().GetEntry(gomock.Any(), gomock.Any()).Return(entry, nil)
411+
mds.EXPECT().GetValidateAttestationTypes(gomock.Any()).Return(true)
412+
413+
protoErr := ValidateMetadata(context.Background(), mds, uuid.Nil, gotType, string(AttestationFormatCompound), nil)
414+
require.NotNil(t, protoErr)
415+
assert.Contains(t, protoErr.DevInfo, "is not known to be used by this authenticator")
416+
})
371417
}
372418

373419
// Supporting functions.

0 commit comments

Comments
 (0)