Skip to content

Commit 3ed3e75

Browse files
fix(protocol): androidkey union generated (#729)
This adds union generated validation on the androidkey attestation which is more permissive until we add TEE-enforcement.
1 parent 9b02b19 commit 3ed3e75

3 files changed

Lines changed: 253 additions & 20 deletions

File tree

protocol/attestation_androidkey.go

Lines changed: 61 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,25 @@ func androidKeyValidateAuthorizationLists(decoded *keyDescription) *Error {
139139

140140
// For the following, use only the teeEnforced authorization list if the RP wants to accept only keys from a trusted execution environment, otherwise use the union of teeEnforced and softwareEnforced.
141141
// The value in the AuthorizationList.origin field is equal to KM_ORIGIN_GENERATED (which == 0).
142-
if decoded.SoftwareEnforced.Origin != KM_ORIGIN_GENERATED || decoded.TeeEnforced.Origin != KM_ORIGIN_GENERATED {
142+
var (
143+
originTee, originSoftware int
144+
presentTee, presentSoftware bool
145+
err error
146+
)
147+
148+
if originTee, presentTee, err = authorizationListOrigin(&decoded.TeeEnforced); err != nil {
149+
return ErrAttestationFormat.WithDetails("Unable to parse the origin of the teeEnforced authorization list").WithError(err)
150+
}
151+
152+
if originSoftware, presentSoftware, err = authorizationListOrigin(&decoded.SoftwareEnforced); err != nil {
153+
return ErrAttestationFormat.WithDetails("Unable to parse the origin of the softwareEnforced authorization list").WithError(err)
154+
}
155+
156+
// The union is satisfied when either list carries an origin equal to KM_ORIGIN_GENERATED. An absent origin
157+
// satisfies nothing as there is no value to compare against, which mirrors the purpose check below.
158+
generated := (presentTee && originTee == KM_ORIGIN_GENERATED) || (presentSoftware && originSoftware == KM_ORIGIN_GENERATED)
159+
160+
if !generated {
143161
return ErrAttestationFormat.WithDetails("Attestation certificate extensions contains authorization list with origin not equal KM_ORIGIN_GENERATED")
144162
}
145163

@@ -151,6 +169,29 @@ func androidKeyValidateAuthorizationLists(decoded *keyDescription) *Error {
151169
return nil
152170
}
153171

172+
// authorizationListOrigin returns the origin of an authorization list and reports whether the field was present. The
173+
// value is decoded from the raw element because encoding/asn1 leaves an absent optional integer at its zero value,
174+
// which is indistinguishable from a present origin of KM_ORIGIN_GENERATED.
175+
func authorizationListOrigin(list *authorizationList) (origin int, present bool, err error) {
176+
// An explicit tag which is present always carries a child as encoding/asn1 rejects one which doesn't while
177+
// decoding the key description, so an empty raw value means the field was absent rather than empty.
178+
if len(list.Origin.FullBytes) == 0 {
179+
return 0, false, nil
180+
}
181+
182+
var rest []byte
183+
184+
if rest, err = asn1.Unmarshal(list.Origin.Bytes, &origin); err != nil {
185+
return 0, false, err
186+
}
187+
188+
if len(rest) != 0 {
189+
return 0, false, fmt.Errorf("origin has %d bytes of trailing data", len(rest))
190+
}
191+
192+
return origin, true, nil
193+
}
194+
154195
func contains(s []int, e int) bool {
155196
for _, a := range s {
156197
if a == e {
@@ -194,21 +235,25 @@ type authorizationList struct {
194235
AllApplications asn1.RawValue `asn1:"tag:600,explicit,optional"`
195236
ApplicationID asn1.RawValue `asn1:"tag:601,explicit,optional"`
196237
CreationDateTime int `asn1:"tag:701,explicit,optional"`
197-
Origin int `asn1:"tag:702,explicit,optional"`
198-
RootOfTrust rootOfTrust `asn1:"tag:704,explicit,optional"`
199-
OsVersion int `asn1:"tag:705,explicit,optional"`
200-
OsPatchLevel int `asn1:"tag:706,explicit,optional"`
201-
AttestationApplicationID []byte `asn1:"tag:709,explicit,optional"`
202-
AttestationIDBrand []byte `asn1:"tag:710,explicit,optional"`
203-
AttestationIDDevice []byte `asn1:"tag:711,explicit,optional"`
204-
AttestationIDProduct []byte `asn1:"tag:712,explicit,optional"`
205-
AttestationIDSerial []byte `asn1:"tag:713,explicit,optional"`
206-
AttestationIDImei []byte `asn1:"tag:714,explicit,optional"`
207-
AttestationIDMeid []byte `asn1:"tag:715,explicit,optional"`
208-
AttestationIDManufacturer []byte `asn1:"tag:716,explicit,optional"`
209-
AttestationIDModel []byte `asn1:"tag:717,explicit,optional"`
210-
VendorPatchLevel int `asn1:"tag:718,explicit,optional"`
211-
BootPatchLevel int `asn1:"tag:719,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"`
212257
}
213258

214259
type rootOfTrust struct {

protocol/attestation_androidkey_test.go

Lines changed: 189 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"crypto/x509"
99
"encoding/asn1"
1010
"encoding/hex"
11+
"fmt"
1112
"testing"
1213

1314
"github.com/stretchr/testify/assert"
@@ -355,12 +356,192 @@ func TestAndroidKeyAuthorizationListDecoding(t *testing.T) {
355356
assert.Len(t, decoded.TeeEnforced.RootOfTrust.VerifiedBootHash, 32)
356357

357358
assert.Equal(t, []int{KM_PURPOSE_SIGN}, decoded.TeeEnforced.Purpose)
358-
assert.Equal(t, KM_ORIGIN_GENERATED, decoded.TeeEnforced.Origin)
359+
360+
origin, present, err := authorizationListOrigin(&decoded.TeeEnforced)
361+
require.NoError(t, err)
362+
assert.True(t, present)
363+
assert.Equal(t, KM_ORIGIN_GENERATED, origin)
364+
365+
// A real attestation carries origin on one list only, which is why an absent origin decoding to the zero value
366+
// went unnoticed.
367+
_, present, err = authorizationListOrigin(&decoded.SoftwareEnforced)
368+
require.NoError(t, err)
369+
assert.False(t, present)
359370

360371
assert.Empty(t, decoded.TeeEnforced.AllApplications.FullBytes)
361372
assert.Empty(t, decoded.SoftwareEnforced.AllApplications.FullBytes)
362373
}
363374

375+
// TestAndroidKeyAuthorizationListOrigin asserts the §8.4 origin requirement is evaluated against the union of the two
376+
// authorization lists, matching the adjacent purpose requirement, and that an absent origin does not satisfy it. An
377+
// optional integer decodes to zero when absent, which is the value of KM_ORIGIN_GENERATED.
378+
func TestAndroidKeyAuthorizationListOrigin(t *testing.T) {
379+
// SEQUENCE { [1] EXPLICIT SET { INTEGER 2 } [, [702] EXPLICIT INTEGER n] } where the purpose is KM_PURPOSE_SIGN.
380+
list := func(t *testing.T, origin string) authorizationList {
381+
t.Helper()
382+
383+
body := "a1053103020102"
384+
385+
if origin != "" {
386+
body += "bf853e030201" + origin
387+
}
388+
389+
der, err := hex.DecodeString(fmt.Sprintf("30%02x", len(body)/2) + body)
390+
require.NoError(t, err)
391+
392+
var decoded authorizationList
393+
394+
rest, err := asn1.Unmarshal(der, &decoded)
395+
require.NoError(t, err)
396+
require.Empty(t, rest)
397+
398+
return decoded
399+
}
400+
401+
generated := list(t, "00") // KM_ORIGIN_GENERATED.
402+
imported := list(t, "02") // KM_ORIGIN_IMPORTED.
403+
missing := list(t, "")
404+
405+
testCases := []struct {
406+
name string
407+
tee authorizationList
408+
software authorizationList
409+
err string
410+
}{
411+
{
412+
name: "ShouldAcceptWhenGeneratedInTeeAndAbsentFromSoftware",
413+
tee: generated, software: missing,
414+
},
415+
{
416+
name: "ShouldAcceptWhenGeneratedInSoftwareAndAbsentFromTee",
417+
tee: missing, software: generated,
418+
},
419+
{
420+
name: "ShouldAcceptWhenGeneratedInBoth",
421+
tee: generated, software: generated,
422+
},
423+
{
424+
// The union is satisfied by the teeEnforced list. The previous implementation required both lists to
425+
// agree, which rejected this.
426+
name: "ShouldAcceptWhenGeneratedInTeeAndImportedInSoftware",
427+
tee: generated, software: imported,
428+
},
429+
{
430+
name: "ShouldRejectWhenImportedInBoth",
431+
tee: imported, software: imported,
432+
err: "Attestation certificate extensions contains authorization list with origin not equal KM_ORIGIN_GENERATED",
433+
},
434+
{
435+
// Neither list states an origin, so there is no value equal to KM_ORIGIN_GENERATED to verify.
436+
name: "ShouldRejectWhenAbsentFromBothLists",
437+
tee: missing, software: missing,
438+
err: "Attestation certificate extensions contains authorization list with origin not equal KM_ORIGIN_GENERATED",
439+
},
440+
{
441+
name: "ShouldRejectWhenImportedInTeeAndAbsentFromSoftware",
442+
tee: imported, software: missing,
443+
err: "Attestation certificate extensions contains authorization list with origin not equal KM_ORIGIN_GENERATED",
444+
},
445+
}
446+
447+
for _, tc := range testCases {
448+
t.Run(tc.name, func(t *testing.T) {
449+
decoded := keyDescription{TeeEnforced: tc.tee, SoftwareEnforced: tc.software}
450+
451+
protoErr := androidKeyValidateAuthorizationLists(&decoded)
452+
453+
if tc.err != "" {
454+
require.NotNil(t, protoErr)
455+
assert.EqualError(t, protoErr, tc.err)
456+
} else {
457+
assert.Nil(t, protoErr)
458+
}
459+
})
460+
}
461+
}
462+
463+
func TestAuthorizationListOriginDER(t *testing.T) {
464+
const purpose = "a1053103020102" // [1] EXPLICIT SET { INTEGER 2 }.
465+
466+
sequence := func(t *testing.T, body string) []byte {
467+
t.Helper()
468+
469+
der, err := hex.DecodeString(fmt.Sprintf("30%02x", len(body)/2) + body)
470+
require.NoError(t, err)
471+
472+
return der
473+
}
474+
475+
t.Run("ShouldRejectPresentButEmptyOriginWhileDecoding", func(t *testing.T) {
476+
var list authorizationList
477+
478+
_, err := asn1.Unmarshal(sequence(t, purpose+"bf853e00"), &list)
479+
require.EqualError(t, err, "asn1: structure error: explicit tag has no child")
480+
})
481+
482+
testCases := []struct {
483+
name string
484+
origin string
485+
origins int
486+
present bool
487+
err string
488+
}{
489+
{
490+
name: "ShouldReportAbsentWhenOriginMissing",
491+
origin: "",
492+
},
493+
{
494+
name: "ShouldDecodeGenerated",
495+
origin: "bf853e03020100",
496+
origins: KM_ORIGIN_GENERATED,
497+
present: true,
498+
},
499+
{
500+
name: "ShouldDecodeImported",
501+
origin: "bf853e03020102",
502+
origins: KM_ORIGIN_IMPORTED,
503+
present: true,
504+
},
505+
{
506+
// [702] EXPLICIT { INTEGER 0, NULL }. The value decodes but a NULL follows it inside the wrapper, which
507+
// was previously discarded along with the remainder returned by asn1.Unmarshal.
508+
name: "ShouldRejectTrailingDataAfterOrigin",
509+
origin: "bf853e05" + "020100" + "0500",
510+
err: "origin has 2 bytes of trailing data",
511+
},
512+
{
513+
// [702] EXPLICIT { INTEGER 0, INTEGER 2 }, so the trailing data is itself a valid origin value.
514+
name: "ShouldRejectTrailingOriginValue",
515+
origin: "bf853e06" + "020100" + "020102",
516+
err: "origin has 3 bytes of trailing data",
517+
},
518+
}
519+
520+
for _, tc := range testCases {
521+
t.Run(tc.name, func(t *testing.T) {
522+
var list authorizationList
523+
524+
rest, err := asn1.Unmarshal(sequence(t, purpose+tc.origin), &list)
525+
require.NoError(t, err)
526+
require.Empty(t, rest)
527+
528+
origin, present, err := authorizationListOrigin(&list)
529+
530+
if tc.err != "" {
531+
require.EqualError(t, err, tc.err)
532+
assert.False(t, present)
533+
assert.Equal(t, 0, origin)
534+
535+
return
536+
}
537+
538+
require.NoError(t, err)
539+
assert.Equal(t, tc.present, present)
540+
assert.Equal(t, tc.origins, origin)
541+
})
542+
}
543+
}
544+
364545
func TestAndroidKeyAuthorizationListAllApplicationsDER(t *testing.T) {
365546
// SEQUENCE { [1] EXPLICIT SET { INTEGER 2 }, [702] EXPLICIT INTEGER 0 }
366547
// purpose = KM_PURPOSE_SIGN, origin = KM_ORIGIN_GENERATED.
@@ -386,8 +567,13 @@ func TestAndroidKeyAuthorizationListAllApplicationsDER(t *testing.T) {
386567
// Both lists must otherwise be identical, so a failure below is attributable to allApplications alone.
387568
require.Equal(t, []int{KM_PURPOSE_SIGN}, absent.Purpose)
388569
require.Equal(t, []int{KM_PURPOSE_SIGN}, present.Purpose)
389-
require.Equal(t, KM_ORIGIN_GENERATED, absent.Origin)
390-
require.Equal(t, KM_ORIGIN_GENERATED, present.Origin)
570+
571+
for _, list := range []*authorizationList{&absent, &present} {
572+
origin, ok, err := authorizationListOrigin(list)
573+
require.NoError(t, err)
574+
require.True(t, ok)
575+
require.Equal(t, KM_ORIGIN_GENERATED, origin)
576+
}
391577

392578
assert.Empty(t, absent.AllApplications.FullBytes)
393579
assert.NotEmpty(t, present.AllApplications.FullBytes, "allApplications must be detectable in the decoded list")

protocol/specification_vectors_e2e_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,9 @@ func TestSpecVectors_Registration_E2E(t *testing.T) {
235235
format: "android-key",
236236
credParams: []CredentialParameter{{Type: PublicKeyCredentialType, Algorithm: webauthncose.AlgES256}},
237237
rpTopOriginVerificationMode: TopOriginExplicitVerificationMode,
238-
err: "Attestation certificate extensions contains authorization list with purpose not equal KM_PURPOSE_SIGN",
238+
// Both authorization lists of this vector are empty sequences, so it satisfies neither the origin nor the
239+
// purpose requirement of §8.4. The origin is reported as it is the first of the two in the specification.
240+
err: "Attestation certificate extensions contains authorization list with origin not equal KM_ORIGIN_GENERATED",
239241
},
240242
{
241243
// §16.15 Apple Anonymous Attestation - ES256 - Top Origin

0 commit comments

Comments
 (0)