Skip to content

Commit c079c8b

Browse files
feat(protocol): att format updates (#485)
This updates the Android Keystore (andoird-key) and Apple Anonymous (apple) attestation format handlers. Specifically these methods now share a validator which checks the x5c leaf is the same as the credential public key, they validate the attestation x5c's against known roots, and were generally refactored to be in a spec-friendly order making them easier to validate. In addition it cleans up several areas and adds additional validations and documentation to attestation format handlers.
1 parent 9285e4c commit c079c8b

25 files changed

+1201
-368
lines changed

.codecov.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ coverage:
1414

1515
ignore:
1616
- "**/coverage.txt"
17+
- "testing"
1718

1819
parsers:
1920
gcov:

.github/workflows/dependabot.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ name: 'Dependabot'
22
on:
33
pull_request: {}
44
permissions:
5-
contents: 'write'
6-
pull-requests: 'write'
5+
contents: 'read'
76
jobs:
87
automerge:
98
name: 'Auto-Merge'
109
runs-on: 'ubuntu-latest'
10+
permissions:
11+
contents: 'read'
12+
pull-requests: 'write'
1113
if: github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'go-webauthn/webauthn'
1214
steps:
1315
- name: 'Harden Runner'
@@ -23,5 +25,5 @@ jobs:
2325
run: |
2426
gh pr merge --auto --squash "$PR_URL"
2527
env:
26-
PR_URL: ${{github.event.pull_request.html_url}}
27-
GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
28+
PR_URL: ${{ github.event.pull_request.html_url }}
29+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
github.com/google/uuid v1.6.0
1313
github.com/mitchellh/mapstructure v1.5.0
1414
github.com/stretchr/testify v1.11.0
15+
go.uber.org/mock v0.6.0
1516
)
1617

1718
require (

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQ
1818
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
1919
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
2020
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
21+
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
22+
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
2123
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
2224
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
2325
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=

protocol/attestation_androidkey.go

Lines changed: 94 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -5,93 +5,101 @@ import (
55
"crypto/x509"
66
"encoding/asn1"
77
"fmt"
8+
"time"
89

910
"github.com/go-webauthn/webauthn/metadata"
1011
"github.com/go-webauthn/webauthn/protocol/webauthncose"
1112
)
1213

13-
func init() {
14-
RegisterAttestationFormat(AttestationFormatAndroidKey, verifyAndroidKeyFormat)
15-
}
16-
17-
// The android-key attestation statement looks like:
18-
// $$attStmtType //= (
14+
// attestationFormatValidationHandlerAndroidKey is the handler for the Android Key Attestation Statement Format.
1915
//
20-
// fmt: "android-key",
21-
// attStmt: androidStmtFormat
16+
// An Android key attestation statement consists simply of the Android attestation statement, which is a series of DER
17+
// encoded X.509 certificates. See the Android developer documentation. Its syntax is defined as follows:
2218
//
23-
// )
19+
// $$attStmtType //= (
20+
// fmt: "android-key",
21+
// attStmt: androidStmtFormat
22+
// )
23+
//
24+
// androidStmtFormat = {
25+
// alg: COSEAlgorithmIdentifier,
26+
// sig: bytes,
27+
// x5c: [ credCert: bytes, * (caCert: bytes) ]
28+
// }
2429
//
25-
// androidStmtFormat = {
26-
// alg: COSEAlgorithmIdentifier,
27-
// sig: bytes,
28-
// x5c: [ credCert: bytes, * (caCert: bytes) ]
29-
// }
30+
// Specification: §8.4. Android Key Attestation Statement Format
3031
//
31-
// Specification: §8.4. Android Key Attestation Statement Format (https://www.w3.org/TR/webauthn/#sctn-android-key-attestation)
32-
func verifyAndroidKeyFormat(att AttestationObject, clientDataHash []byte, _ metadata.Provider) (attestationType string, x5cs []any, err error) {
32+
// See: https://www.w3.org/TR/webauthn/#sctn-android-key-attestation
33+
func attestationFormatValidationHandlerAndroidKey(att AttestationObject, clientDataHash []byte, _ metadata.Provider) (attestationType string, x5cs []any, err error) {
34+
var (
35+
alg int64
36+
sig []byte
37+
ok bool
38+
)
39+
3340
// Given the verification procedure inputs attStmt, authenticatorData and clientDataHash, the verification procedure is as follows:
3441
// §8.4.1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract
3542
// the contained fields.
36-
3743
// Get the alg value - A COSEAlgorithmIdentifier containing the identifier of the algorithm
3844
// used to generate the attestation signature.
39-
alg, present := att.AttStatement[stmtAlgorithm].(int64)
40-
if !present {
45+
if alg, ok = att.AttStatement[stmtAlgorithm].(int64); !ok {
4146
return "", nil, ErrAttestationFormat.WithDetails("Error retrieving alg value")
4247
}
4348

4449
// Get the sig value - A byte string containing the attestation signature.
45-
sig, present := att.AttStatement[stmtSignature].([]byte)
46-
if !present {
50+
if sig, ok = att.AttStatement[stmtSignature].([]byte); !ok {
4751
return "", nil, ErrAttestationFormat.WithDetails("Error retrieving sig value")
4852
}
4953

50-
// If x5c is not present, return an error.
51-
x5c, x509present := att.AttStatement[stmtX5C].([]any)
52-
if !x509present {
53-
// Handle Basic Attestation steps for the x509 Certificate.
54-
return "", nil, ErrAttestationFormat.WithDetails("Error retrieving x5c value")
55-
}
56-
5754
// §8.4.2. Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash
5855
// using the public key in the first certificate in x5c with the algorithm specified in alg.
59-
attCertBytes, valid := x5c[0].([]byte)
60-
if !valid {
61-
return "", nil, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain")
56+
var (
57+
x5c []any
58+
certs []*x509.Certificate
59+
)
60+
61+
if x5c, certs, err = attStatementParseX5CS(att.AttStatement, stmtX5C); err != nil {
62+
return "", nil, err
6263
}
6364

64-
attCert, err := x509.ParseCertificate(attCertBytes)
65-
if err != nil {
66-
return "", nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing certificate from ASN.1 data: %+v", err)).WithError(err)
65+
if len(certs) == 0 {
66+
return "", nil, ErrInvalidAttestation.WithDetails("No certificates in x5c")
67+
}
68+
69+
credCert := certs[0]
70+
71+
if _, err = attStatementCertChainVerify(certs, attAndroidKeyHardwareRootsCertPool, true, time.Now().Add(time.Hour*8760).UTC()); err != nil {
72+
return "", nil, ErrInvalidAttestation.WithDetails("Error validating x5c cert chain").WithError(err)
6773
}
6874

6975
signatureData := append(att.RawAuthData, clientDataHash...) //nolint:gocritic // This is intentional.
7076

71-
coseAlg := webauthncose.COSEAlgorithmIdentifier(alg)
72-
if err = attCert.CheckSignature(webauthncose.SigAlgFromCOSEAlg(coseAlg), signatureData, sig); err != nil {
73-
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Signature validation error: %+v\n", err)).WithError(err)
77+
if sigAlg := webauthncose.SigAlgFromCOSEAlg(webauthncose.COSEAlgorithmIdentifier(alg)); sigAlg == x509.UnknownSignatureAlgorithm {
78+
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Unsupported COSE alg: %d", alg))
79+
} else if err = credCert.CheckSignature(sigAlg, signatureData, sig); err != nil {
80+
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Signature validation error: %+v", err)).WithError(err)
7481
}
7582

7683
// Verify that the public key in the first certificate in x5c matches the credentialPublicKey in the attestedCredentialData in authenticatorData.
77-
pubKey, err := webauthncose.ParsePublicKey(att.AuthData.AttData.CredentialPublicKey)
78-
if err != nil {
79-
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error parsing public key: %+v\n", err)).WithError(err)
84+
var attPublicKeyData webauthncose.EC2PublicKeyData
85+
if attPublicKeyData, err = verifyAttestationECDSAPublicKeyMatch(att, credCert); err != nil {
86+
return "", nil, err
8087
}
8188

82-
e := pubKey.(webauthncose.EC2PublicKeyData)
83-
84-
valid, err = e.Verify(signatureData, sig)
85-
if err != nil || !valid {
86-
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error parsing public key: %+v\n", err)).WithError(err)
89+
var valid bool
90+
if valid, err = attPublicKeyData.Verify(signatureData, sig); err != nil || !valid {
91+
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error parsing public key: %+v", err)).WithError(err)
8792
}
8893

8994
// §8.4.3. Verify that the attestationChallenge field in the attestation certificate extension data is identical to clientDataHash.
9095
// attCert.Extensions.
96+
// As noted in §8.4.1 (https://www.w3.org/TR/webauthn/#key-attstn-cert-requirements) the Android Key Attestation
97+
// certificate's android key attestation certificate extension data is identified by the OID
98+
// "1.3.6.1.4.1.11129.2.1.17".
9199
var attExtBytes []byte
92100

93-
for _, ext := range attCert.Extensions {
94-
if ext.Id.Equal([]int{1, 3, 6, 1, 4, 1, 11129, 2, 1, 17}) {
101+
for _, ext := range credCert.Extensions {
102+
if ext.Id.Equal(oidExtensionAndroidKeystore) {
95103
attExtBytes = ext.Value
96104
}
97105
}
@@ -100,8 +108,6 @@ func verifyAndroidKeyFormat(att AttestationObject, clientDataHash []byte, _ meta
100108
return "", nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions missing 1.3.6.1.4.1.11129.2.1.17")
101109
}
102110

103-
// As noted in §8.4.1 (https://www.w3.org/TR/webauthn/#key-attstn-cert-requirements) the Android Key Attestation attestation certificate's
104-
// android key attestation certificate extension data is identified by the OID "1.3.6.1.4.1.11129.2.1.17".
105111
decoded := keyDescription{}
106112

107113
if _, err = asn1.Unmarshal(attExtBytes, &decoded); err != nil {
@@ -114,7 +120,7 @@ func verifyAndroidKeyFormat(att AttestationObject, clientDataHash []byte, _ meta
114120
}
115121

116122
// The AuthorizationList.allApplications field is not present on either authorization list (softwareEnforced nor teeEnforced), since PublicKeyCredential MUST be scoped to the RP ID.
117-
if nil != decoded.SoftwareEnforced.AllApplications || nil != decoded.TeeEnforced.AllApplications {
123+
if decoded.SoftwareEnforced.AllApplications != nil || decoded.TeeEnforced.AllApplications != nil {
118124
return "", nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions contains all applications field")
119125
}
120126

@@ -208,34 +214,47 @@ const (
208214
Failed
209215
)
210216

211-
/**
212-
* The origin of a key (or pair), i.e. where it was generated. Note that KM_TAG_ORIGIN can be found
213-
* in either the hardware-enforced or software-enforced list for a key, indicating whether the key
214-
* is hardware or software-based. Specifically, a key with KM_ORIGIN_GENERATED in the
215-
* hardware-enforced list is guaranteed never to have existed outide the secure hardware.
216-
*/
217-
type KM_KEY_ORIGIN int
218-
219217
const (
220-
KM_ORIGIN_GENERATED = iota /* Generated in keymaster. Should not exist outside the TEE. */
221-
KM_ORIGIN_DERIVED /* Derived inside keymaster. Likely exists off-device. */
222-
KM_ORIGIN_IMPORTED /* Imported into keymaster. Existed as clear text in Android. */
223-
KM_ORIGIN_UNKNOWN /* Keymaster did not record origin. This value can only be seen on
224-
* keys in a keymaster0 implementation. The keymaster0 adapter uses
225-
* this value to document the fact that it is unknown whether the key
226-
* was generated inside or imported into keymaster. */
218+
// KM_ORIGIN_GENERATED means generated in keymaster. Should not exist outside the TEE.
219+
KM_ORIGIN_GENERATED = iota
220+
221+
// KM_ORIGIN_DERIVED means derived inside keymaster. Likely exists off-device.
222+
KM_ORIGIN_DERIVED
223+
224+
// KM_ORIGIN_IMPORTED means imported into keymaster. Existed as clear text in Android.
225+
KM_ORIGIN_IMPORTED
226+
227+
// KM_ORIGIN_UNKNOWN means keymaster did not record origin. This value can only be seen on keys in a keymaster0
228+
// implementation. The keymaster0 adapter uses this value to document the fact that it is unknown whether the key
229+
// was generated inside or imported into keymaster.
230+
KM_ORIGIN_UNKNOWN
227231
)
228232

229-
/**
230-
* Possible purposes of a key (or pair).
231-
*/
232-
type KM_PURPOSE int
233233

234234
const (
235-
KM_PURPOSE_ENCRYPT = iota /* Usable with RSA, EC and AES keys. */
236-
KM_PURPOSE_DECRYPT /* Usable with RSA, EC and AES keys. */
237-
KM_PURPOSE_SIGN /* Usable with RSA, EC and HMAC keys. */
238-
KM_PURPOSE_VERIFY /* Usable with RSA, EC and HMAC keys. */
239-
KM_PURPOSE_DERIVE_KEY /* Usable with EC keys. */
240-
KM_PURPOSE_WRAP /* Usable with wrapped keys. */
235+
// KM_PURPOSE_ENCRYPT is usable with RSA, EC and AES keys.
236+
KM_PURPOSE_ENCRYPT = iota
237+
238+
// KM_PURPOSE_DECRYPT is usable with RSA, EC and AES keys.
239+
KM_PURPOSE_DECRYPT
240+
241+
// KM_PURPOSE_SIGN is usable with RSA, EC and HMAC keys.
242+
KM_PURPOSE_SIGN
243+
244+
// KM_PURPOSE_VERIFY is usable with RSA, EC and HMAC keys.
245+
KM_PURPOSE_VERIFY
246+
247+
// KM_PURPOSE_DERIVE_KEY is usable with EC keys.
248+
KM_PURPOSE_DERIVE_KEY
249+
250+
// KM_PURPOSE_WRAP is usable with wrapped keys.
251+
KM_PURPOSE_WRAP
252+
)
253+
254+
var (
255+
attAndroidKeyHardwareRootsCertPool *x509.CertPool
241256
)
257+
258+
func init() {
259+
RegisterAttestationFormat(AttestationFormatAndroidKey, attestationFormatValidationHandlerAndroidKey)
260+
}

0 commit comments

Comments
 (0)