@@ -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
4442func 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
184203type 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+
194221func init () {
195222 RegisterAttestationFormat (AttestationFormatAndroidSafetyNet , attestationFormatValidationHandlerAndroidSafetyNet )
196223}
0 commit comments