Skip to content

Commit d161848

Browse files
refactor(protocol): cleanup (#730)
This cleans up a lot of the elements within androidkey and tpm attestation implementations after lots of changes.
1 parent 3ed3e75 commit d161848

12 files changed

Lines changed: 635 additions & 261 deletions

metadata/decode.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ func validateChain(root string, chain []any) (bool, error) {
278278
opts := x509.VerifyOptions{
279279
Roots: roots,
280280
Intermediates: ints,
281-
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
281+
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
282282
}
283283

284284
_, err = leafcert.Verify(opts)

metadata/metadata.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,7 @@ func (s *Statement) Verifier(x5cis []*x509.Certificate) (opts x509.VerifyOptions
437437
return x509.VerifyOptions{
438438
Roots: roots,
439439
Intermediates: intermediates,
440-
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
440+
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
441441
}
442442
}
443443

protocol/attestation_androidkey.go

Lines changed: 105 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import (
44
"bytes"
55
"crypto/x509"
66
"encoding/asn1"
7+
"errors"
78
"fmt"
9+
"reflect"
10+
"strconv"
11+
"strings"
812
"time"
913

1014
"github.com/go-webauthn/webauthn/metadata"
@@ -111,12 +115,24 @@ func attestationFormatValidationHandlerAndroidKey(att AttestationObject, clientD
111115
return "", nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions missing 1.3.6.1.4.1.11129.2.1.17")
112116
}
113117

114-
decoded := keyDescription{}
118+
decoded := androidkeyDescription{}
115119

116120
if _, err = asn1.Unmarshal(attExtBytes, &decoded); err != nil {
117121
return "", nil, ErrAttestationFormat.WithDetails("Unable to parse Android key attestation certificate extensions").WithError(err)
118122
}
119123

124+
// The decode above silently abandons the remaining fields of an authorization list on meeting an element it can't
125+
// model, so the elements actually present are checked against the raw extension before any of it is relied upon.
126+
raw := androidkeyDescriptionRaw{}
127+
128+
if _, err = asn1.Unmarshal(attExtBytes, &raw); err != nil {
129+
return "", nil, ErrAttestationFormat.WithDetails("Unable to parse Android key attestation certificate extensions").WithError(err)
130+
}
131+
132+
if protoErr := androidKeyVerifyAuthorizationListTags(&raw); protoErr != nil {
133+
return "", nil, protoErr
134+
}
135+
120136
// Verify that the attestationChallenge field in the attestation certificate extension data is identical to clientDataHash.
121137
if !bytes.Equal(decoded.AttestationChallenge, clientDataHash) {
122138
return "", nil, ErrAttestationFormat.WithDetails("Attestation challenge not equal to clientDataHash")
@@ -131,7 +147,7 @@ func attestationFormatValidationHandlerAndroidKey(att AttestationObject, clientD
131147

132148
// androidKeyValidateAuthorizationLists performs the §8.4 verification steps which apply to the authorization lists of
133149
// the Android key attestation certificate extension.
134-
func androidKeyValidateAuthorizationLists(decoded *keyDescription) *Error {
150+
func androidKeyValidateAuthorizationLists(decoded *androidkeyDescription) *Error {
135151
// The AuthorizationList.allApplications field is not present on either authorization list (softwareEnforced nor teeEnforced), since PublicKeyCredential MUST be scoped to the RP ID.
136152
if len(decoded.SoftwareEnforced.AllApplications.FullBytes) != 0 || len(decoded.TeeEnforced.AllApplications.FullBytes) != 0 {
137153
return ErrAttestationFormat.WithDetails("Attestation certificate extensions contains all applications field")
@@ -172,7 +188,7 @@ func androidKeyValidateAuthorizationLists(decoded *keyDescription) *Error {
172188
// authorizationListOrigin returns the origin of an authorization list and reports whether the field was present. The
173189
// value is decoded from the raw element because encoding/asn1 leaves an absent optional integer at its zero value,
174190
// which is indistinguishable from a present origin of KM_ORIGIN_GENERATED.
175-
func authorizationListOrigin(list *authorizationList) (origin int, present bool, err error) {
191+
func authorizationListOrigin(list *androidkeyAuthorizationList) (origin int, present bool, err error) {
176192
// An explicit tag which is present always carries a child as encoding/asn1 rejects one which doesn't while
177193
// decoding the key description, so an empty raw value means the field was absent rather than empty.
178194
if len(list.Origin.FullBytes) == 0 {
@@ -202,111 +218,108 @@ func contains(s []int, e int) bool {
202218
return false
203219
}
204220

205-
type keyDescription struct {
206-
AttestationVersion int
207-
AttestationSecurityLevel asn1.Enumerated
208-
KeymasterVersion int
209-
KeymasterSecurityLevel asn1.Enumerated
210-
AttestationChallenge []byte
211-
UniqueID []byte
212-
SoftwareEnforced authorizationList
213-
TeeEnforced authorizationList
214-
}
221+
// authorizationListTags contains every context specific tag number modelled by [authorizationList]. It's derived from
222+
// the struct definition so that declaring a field is the only step needed to support a tag, and the two can't drift.
223+
var authorizationListTags = func() (tags map[int]bool) {
224+
t := reflect.TypeOf(androidkeyAuthorizationList{})
225+
226+
tags = make(map[int]bool, t.NumField())
215227

216-
type authorizationList struct {
217-
Purpose []int `asn1:"tag:1,explicit,set,optional"`
218-
Algorithm int `asn1:"tag:2,explicit,optional"`
219-
KeySize int `asn1:"tag:3,explicit,optional"`
220-
Digest []int `asn1:"tag:5,explicit,set,optional"`
221-
Padding []int `asn1:"tag:6,explicit,set,optional"`
222-
EcCurve int `asn1:"tag:10,explicit,optional"`
223-
RsaPublicExponent int `asn1:"tag:200,explicit,optional"`
224-
RollbackResistance asn1.RawValue `asn1:"tag:303,explicit,optional"`
225-
ActiveDateTime int `asn1:"tag:400,explicit,optional"`
226-
OriginationExpireDateTime int `asn1:"tag:401,explicit,optional"`
227-
UsageExpireDateTime int `asn1:"tag:402,explicit,optional"`
228-
NoAuthRequired asn1.RawValue `asn1:"tag:503,explicit,optional"`
229-
UserAuthType int `asn1:"tag:504,explicit,optional"`
230-
AuthTimeout int `asn1:"tag:505,explicit,optional"`
231-
AllowWhileOnBody asn1.RawValue `asn1:"tag:506,explicit,optional"`
232-
TrustedUserPresenceRequired asn1.RawValue `asn1:"tag:507,explicit,optional"`
233-
TrustedConfirmationRequired asn1.RawValue `asn1:"tag:508,explicit,optional"`
234-
UnlockedDeviceRequired asn1.RawValue `asn1:"tag:509,explicit,optional"`
235-
AllApplications asn1.RawValue `asn1:"tag:600,explicit,optional"`
236-
ApplicationID asn1.RawValue `asn1:"tag:601,explicit,optional"`
237-
CreationDateTime int `asn1:"tag:701,explicit,optional"`
238-
// Origin is decoded as a raw element rather than an integer. encoding/asn1 leaves an absent optional integer at
239-
// its zero value, which is indistinguishable from a present origin of KM_ORIGIN_GENERATED.
240-
Origin asn1.RawValue `asn1:"tag:702,explicit,optional"`
241-
242-
RootOfTrust rootOfTrust `asn1:"tag:704,explicit,optional"`
243-
244-
OsVersion int `asn1:"tag:705,explicit,optional"`
245-
OsPatchLevel int `asn1:"tag:706,explicit,optional"`
246-
AttestationApplicationID []byte `asn1:"tag:709,explicit,optional"`
247-
AttestationIDBrand []byte `asn1:"tag:710,explicit,optional"`
248-
AttestationIDDevice []byte `asn1:"tag:711,explicit,optional"`
249-
AttestationIDProduct []byte `asn1:"tag:712,explicit,optional"`
250-
AttestationIDSerial []byte `asn1:"tag:713,explicit,optional"`
251-
AttestationIDImei []byte `asn1:"tag:714,explicit,optional"`
252-
AttestationIDMeid []byte `asn1:"tag:715,explicit,optional"`
253-
AttestationIDManufacturer []byte `asn1:"tag:716,explicit,optional"`
254-
AttestationIDModel []byte `asn1:"tag:717,explicit,optional"`
255-
VendorPatchLevel int `asn1:"tag:718,explicit,optional"`
256-
BootPatchLevel int `asn1:"tag:719,explicit,optional"`
228+
for i := range t.NumField() {
229+
field := t.Field(i)
230+
231+
for _, option := range strings.Split(field.Tag.Get("asn1"), ",") {
232+
if !strings.HasPrefix(option, "tag:") {
233+
continue
234+
}
235+
236+
tag, err := strconv.Atoi(strings.TrimPrefix(option, "tag:"))
237+
if err != nil {
238+
panic(fmt.Sprintf("protocol: authorizationList field %s has a malformed asn1 tag: %v", field.Name, err))
239+
}
240+
241+
tags[tag] = true
242+
}
243+
}
244+
245+
return tags
246+
}()
247+
248+
// authorizationListValidatedTags contains the tags of the authorization list fields which the §8.4 verification steps
249+
// consult. An element the struct can't model only matters when it displaces one of these.
250+
var authorizationListValidatedTags = []int{
251+
1, // purpose.
252+
600, // allApplications.
253+
702, // origin.
257254
}
258255

259-
type rootOfTrust struct {
260-
VerifiedBootKey []byte
261-
DeviceLocked bool
262-
VerifiedBootState asn1.Enumerated
263-
VerifiedBootHash []byte `asn1:"optional"`
256+
// androidKeyVerifyAuthorizationListTags rejects an attestation whose authorization lists carry an element that
257+
// [authorizationList] can't model and which precedes a field the verification procedure depends on.
258+
//
259+
// An unmodelled tag otherwise defeats the §8.4 requirement that allApplications is absent, as the element is dropped
260+
// along with every field declared after it while the union permits the other list to supply the origin and purpose. A
261+
// list which can't be modelled in full is rejected explicitly rather than silently truncated.
262+
func androidKeyVerifyAuthorizationListTags(raw *androidkeyDescriptionRaw) *Error {
263+
for _, list := range []struct {
264+
name string
265+
raw asn1.RawValue
266+
}{
267+
{"teeEnforced", raw.TeeEnforced},
268+
{"softwareEnforced", raw.SoftwareEnforced},
269+
} {
270+
if err := authorizationListVerifyTags(list.raw); err != nil {
271+
return ErrAttestationFormat.WithDetails(fmt.Sprintf("Unable to validate the %s authorization list", list.name)).WithInfo(err.Error()).WithError(err)
272+
}
273+
}
274+
275+
return nil
264276
}
265277

266-
type verifiedBootState int
278+
// authorizationListVerifyTags reports an error when an element of the raw authorization list isn't modelled by
279+
// [authorizationList] and is positioned before an element the verification procedure consults. Anything after the last
280+
// consulted element can't influence the outcome as decoding reached them all, which keeps a tag appended to a later
281+
// revision of the schema from rejecting an otherwise sound attestation.
282+
func authorizationListVerifyTags(raw asn1.RawValue) (err error) {
283+
if raw.Class != asn1.ClassUniversal || raw.Tag != asn1.TagSequence || !raw.IsCompound {
284+
return errors.New("authorization list is not a sequence")
285+
}
267286

268-
const (
269-
Verified verifiedBootState = iota
270-
SelfSigned
271-
Unverified
272-
Failed
273-
)
287+
var tags []int
274288

275-
const (
276-
// KM_ORIGIN_GENERATED means generated in keymaster. Should not exist outside the TEE.
277-
KM_ORIGIN_GENERATED = iota
289+
rest := raw.Bytes
278290

279-
// KM_ORIGIN_DERIVED means derived inside keymaster. Likely exists off-device.
280-
KM_ORIGIN_DERIVED
291+
for len(rest) > 0 {
292+
var element asn1.RawValue
281293

282-
// KM_ORIGIN_IMPORTED means imported into keymaster. Existed as clear text in Android.
283-
KM_ORIGIN_IMPORTED
294+
if rest, err = asn1.Unmarshal(rest, &element); err != nil {
295+
return err
296+
}
284297

285-
// KM_ORIGIN_UNKNOWN means keymaster did not record origin. This value can only be seen on keys in a keymaster0
286-
// implementation. The keymaster0 adapter uses this value to document the fact that it is unknown whether the key
287-
// was generated inside or imported into keymaster.
288-
KM_ORIGIN_UNKNOWN
289-
)
298+
if element.Class != asn1.ClassContextSpecific {
299+
return fmt.Errorf("element %d has class %d where a context specific class was expected", len(tags), element.Class)
300+
}
290301

291-
const (
292-
// KM_PURPOSE_ENCRYPT is usable with RSA, EC and AES keys.
293-
KM_PURPOSE_ENCRYPT = iota
302+
tags = append(tags, element.Tag)
303+
}
294304

295-
// KM_PURPOSE_DECRYPT is usable with RSA, EC and AES keys.
296-
KM_PURPOSE_DECRYPT
305+
last := -1
297306

298-
// KM_PURPOSE_SIGN is usable with RSA, EC and HMAC keys.
299-
KM_PURPOSE_SIGN
307+
for i, tag := range tags {
308+
if contains(authorizationListValidatedTags, tag) {
309+
last = i
310+
}
311+
}
300312

301-
// KM_PURPOSE_VERIFY is usable with RSA, EC and HMAC keys.
302-
KM_PURPOSE_VERIFY
313+
for i, tag := range tags[:last+1] {
314+
if authorizationListTags[tag] {
315+
continue
316+
}
303317

304-
// KM_PURPOSE_DERIVE_KEY is usable with EC keys.
305-
KM_PURPOSE_DERIVE_KEY
318+
return fmt.Errorf("element %d has tag [%d] which is not supported and precedes a field required by the verification procedure", i, tag)
319+
}
306320

307-
// KM_PURPOSE_WRAP is usable with wrapped keys.
308-
KM_PURPOSE_WRAP
309-
)
321+
return nil
322+
}
310323

311324
var (
312325
attAndroidKeyHardwareRootsCertPool *x509.CertPool

0 commit comments

Comments
 (0)