Skip to content

Commit e12f6e8

Browse files
fix(protocol): safetynet validation steps (#726)
This implements the full suite of safetynet validations which were missed in early development of this library. This is unlikely still being used as it's been shut down, but it's worth including it just in case.
1 parent f9a63f9 commit e12f6e8

2 files changed

Lines changed: 206 additions & 64 deletions

File tree

protocol/attestation_safetynet.go

Lines changed: 72 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,6 @@ import (
3939
// Specification: §8.5. Android SafetyNet Attestation Statement Format
4040
//
4141
// See: https://www.w3.org/TR/webauthn/#sctn-android-safetynet-attestation
42-
//
43-
//nolint:gocyclo
4442
func attestationFormatValidationHandlerAndroidSafetyNet(att AttestationObject, clientDataHash []byte, mds metadata.Provider) (attestationType string, x5cs []any, err error) {
4543
// The syntax of an Android Attestation statement is defined as follows:
4644
// $$attStmtType //= (
@@ -76,8 +74,14 @@ func attestationFormatValidationHandlerAndroidSafetyNet(att AttestationObject, c
7674

7775
var token *jwt.Token
7876

79-
if token, err = jwt.Parse(string(response), keyFuncSafetyNetJWT, jwt.WithValidMethods([]string{jwt.SigningMethodRS256.Alg()})); err != nil {
80-
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err)).WithError(err)
77+
// §8.5.2 and §8.5.4 Verify that response is a valid SafetyNet response of version ver, and that it actually came
78+
// from the SafetyNet service, by following the steps indicated by the SafetyNet online documentation. Those steps
79+
// require the certificate chain in the JWS header be validated and the leaf matched to the SafetyNet hostname
80+
// before the signature is verified with it, which the verifier below performs as part of supplying the key.
81+
verifier := &safetyNetJWTVerifier{}
82+
83+
if token, err = jwt.Parse(string(response), verifier.keyFunc, jwt.WithValidMethods([]string{jwt.SigningMethodRS256.Alg()})); err != nil {
84+
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error verifying the SafetyNet response signature: %+v", err)).WithError(err)
8185
}
8286

8387
// marshall the JWT payload into the safetynet response json.
@@ -96,33 +100,8 @@ func attestationFormatValidationHandlerAndroidSafetyNet(att AttestationObject, c
96100
return "", nil, ErrInvalidAttestation.WithDetails("Invalid nonce for in SafetyNet response").WithError(err)
97101
}
98102

99-
// §8.5.4 Let attestationCert be the attestation certificate (https://www.w3.org/TR/webauthn/#attestation-certificate)
100-
certChain, ok := token.Header[stmtX5C].([]any)
101-
if !ok || len(certChain) == 0 {
102-
return "", nil, ErrInvalidAttestation.WithDetails("Error getting certificate from JWT header x5c")
103-
}
104-
105-
first, ok := certChain[0].(string)
106-
if !ok || first == "" {
107-
return "", nil, ErrInvalidAttestation.WithDetails("Error getting first certificate from JWT header x5c")
108-
}
109-
110-
l := make([]byte, base64.StdEncoding.DecodedLen(len(first)))
111-
112-
n, err := base64.StdEncoding.Decode(l, []byte(first))
113-
if err != nil {
114-
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err)).WithError(err)
115-
}
116-
117-
attestationCert, err := x509.ParseCertificate(l[:n])
118-
if err != nil {
119-
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err)).WithError(err)
120-
}
121-
122-
// §8.5.5 Verify that attestationCert is issued to the hostname "attest.android.com".
123-
if err = attestationCert.VerifyHostname(attStatementAndroidSafetyNetHostname); err != nil {
124-
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err)).WithError(err)
125-
}
103+
// §8.5.5 Verify that attestationCert is issued to the hostname "attest.android.com". This is performed by the
104+
// verifier above as part of the chain validation rather than against a certificate nothing has vouched for.
126105

127106
// §8.5.6 Verify that the ctsProfileMatch attribute in the payload of response is true.
128107
if !safetyNetResponse.CtsProfileMatch {
@@ -144,14 +123,22 @@ func attestationFormatValidationHandlerAndroidSafetyNet(att AttestationObject, c
144123
return string(metadata.BasicFull), nil, nil
145124
}
146125

147-
func keyFuncSafetyNetJWT(token *jwt.Token) (key any, err error) {
126+
// safetyNetJWTVerifier verifies the certificate chain carried in the JWS x5c header before releasing the leaf public
127+
// key to the JWT parser, and retains the chain so it can be used as the attestation trust path.
128+
//
129+
// The SafetyNet documentation requires the chain be validated and the leaf matched to the SafetyNet hostname before
130+
// the signature is verified with it. Releasing the leaf public key without doing so allows any self-signed
131+
// certificate bearing that hostname to sign an entirely forged response.
132+
type safetyNetJWTVerifier struct {
133+
x5c []any
134+
certs []*x509.Certificate
135+
}
136+
137+
func (v *safetyNetJWTVerifier) keyFunc(token *jwt.Token) (key any, err error) {
148138
var (
149139
ok bool
150140
raw any
151141
chain []any
152-
first string
153-
der []byte
154-
cert *x509.Certificate
155142
)
156143

157144
if raw, ok = token.Header[stmtX5C]; !ok {
@@ -162,23 +149,55 @@ func keyFuncSafetyNetJWT(token *jwt.Token) (key any, err error) {
162149
return nil, fmt.Errorf("jwt header x5c is not a non-empty array")
163150
}
164151

165-
if first, ok = chain[0].(string); !ok || first == "" {
166-
return nil, fmt.Errorf("jwt header x5c[0] not a base64 string")
167-
}
152+
certs := make([]*x509.Certificate, len(chain))
153+
154+
for i, element := range chain {
155+
var (
156+
value string
157+
der []byte
158+
)
159+
160+
if value, ok = element.(string); !ok || value == "" {
161+
return nil, fmt.Errorf("jwt header x5c[%d] is not a base64 string", i)
162+
}
168163

169-
if der, err = base64.StdEncoding.DecodeString(first); err != nil {
170-
return nil, fmt.Errorf("decode x5c leaf: %w", err)
164+
if der, err = base64.StdEncoding.DecodeString(value); err != nil {
165+
return nil, fmt.Errorf("decode x5c[%d]: %w", i, err)
166+
}
167+
168+
if certs[i], err = x509.ParseCertificate(der); err != nil {
169+
return nil, fmt.Errorf("parse x5c[%d]: %w", i, err)
170+
}
171171
}
172172

173-
if cert, err = x509.ParseCertificate(der); err != nil {
174-
if cert != nil {
175-
return cert.PublicKey, fmt.Errorf("parse x5c leaf: %w", err)
173+
roots := attStatementAndroidSafetyNetRootsCertPool
174+
175+
if roots == nil {
176+
if roots, err = x509.SystemCertPool(); err != nil {
177+
return nil, fmt.Errorf("load system trust store: %w", err)
176178
}
179+
}
180+
181+
intermediates := x509.NewCertPool()
182+
183+
for _, cert := range certs[1:] {
184+
intermediates.AddCert(cert)
185+
}
177186

178-
return nil, fmt.Errorf("parse x5c leaf: %w", err)
187+
// The leaf is a TLS server certificate so the hostname match of §8.5.5 is performed here as part of the chain
188+
// verification, and ExtKeyUsageServerAuth is the applicable usage.
189+
if _, err = certs[0].Verify(x509.VerifyOptions{
190+
DNSName: attStatementAndroidSafetyNetHostname,
191+
Roots: roots,
192+
Intermediates: intermediates,
193+
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
194+
}); err != nil {
195+
return nil, fmt.Errorf("verify x5c chain: %w", err)
179196
}
180197

181-
return cert.PublicKey, nil
198+
v.x5c, v.certs = chain, certs
199+
200+
return certs[0].PublicKey, nil
182201
}
183202

184203
type SafetyNetResponse struct {
@@ -191,6 +210,14 @@ type SafetyNetResponse struct {
191210
BasicIntegrity bool `json:"basicIntegrity"`
192211
}
193212

213+
var (
214+
// attStatementAndroidSafetyNetRootsCertPool contains the trust anchors used to verify the certificate chain which
215+
// signs a SafetyNet response. A nil pool causes the host system trust store to be used, which is the correct
216+
// default as the leaf is an ordinary WebPKI TLS certificate issued to the SafetyNet hostname rather than an
217+
// attestation specific root.
218+
attStatementAndroidSafetyNetRootsCertPool *x509.CertPool
219+
)
220+
194221
func init() {
195222
RegisterAttestationFormat(AttestationFormatAndroidSafetyNet, attestationFormatValidationHandlerAndroidSafetyNet)
196223
}

0 commit comments

Comments
 (0)