-
Notifications
You must be signed in to change notification settings - Fork 62
/
attestation_safetynet.go
150 lines (124 loc) · 6.56 KB
/
attestation_safetynet.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
package protocol
import (
"bytes"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"fmt"
"time"
"github.com/go-webauthn/webauthn/metadata"
"github.com/golang-jwt/jwt/v4"
"github.com/mitchellh/mapstructure"
)
var safetyNetAttestationKey = "android-safetynet"
func init() {
RegisterAttestationFormat(safetyNetAttestationKey, verifySafetyNetFormat)
}
type SafetyNetResponse struct {
Nonce string `json:"nonce"`
TimestampMs int64 `json:"timestampMs"`
ApkPackageName string `json:"apkPackageName"`
ApkDigestSha256 string `json:"apkDigestSha256"`
CtsProfileMatch bool `json:"ctsProfileMatch"`
ApkCertificateDigestSha256 []interface{} `json:"apkCertificateDigestSha256"`
BasicIntegrity bool `json:"basicIntegrity"`
}
// Thanks to @koesie10 and @herrjemand for outlining how to support this type really well
// §8.5. Android SafetyNet Attestation Statement Format https://w3c.github.io/webauthn/#android-safetynet-attestation
// When the authenticator in question is a platform-provided Authenticator on certain Android platforms, the attestation
// statement is based on the SafetyNet API. In this case the authenticator data is completely controlled by the caller of
// the SafetyNet API (typically an application running on the Android platform) and the attestation statement only provides
// some statements about the health of the platform and the identity of the calling application. This attestation does not
// provide information regarding provenance of the authenticator and its associated data. Therefore platform-provided
// authenticators SHOULD make use of the Android Key Attestation when available, even if the SafetyNet API is also present.
func verifySafetyNetFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) {
// The syntax of an Android Attestation statement is defined as follows:
// $$attStmtType //= (
// fmt: "android-safetynet",
// attStmt: safetynetStmtFormat
// )
// safetynetStmtFormat = {
// ver: text,
// response: bytes
// }
// §8.5.1 Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract
// the contained fields.
// We have done this
// §8.5.2 Verify that response is a valid SafetyNet response of version ver.
version, present := att.AttStatement["ver"].(string)
if !present {
return safetyNetAttestationKey, nil, ErrAttestationFormat.WithDetails("Unable to find the version of SafetyNet")
}
if version == "" {
return safetyNetAttestationKey, nil, ErrAttestationFormat.WithDetails("Not a proper version for SafetyNet")
}
// TODO: provide user the ability to designate their supported versions
response, present := att.AttStatement["response"].([]byte)
if !present {
return safetyNetAttestationKey, nil, ErrAttestationFormat.WithDetails("Unable to find the SafetyNet response")
}
token, err := jwt.Parse(string(response), func(token *jwt.Token) (interface{}, error) {
chain := token.Header["x5c"].([]interface{})
o := make([]byte, base64.StdEncoding.DecodedLen(len(chain[0].(string))))
n, err := base64.StdEncoding.Decode(o, []byte(chain[0].(string)))
if err != nil {
return nil, err
}
cert, err := x509.ParseCertificate(o[:n])
return cert.PublicKey, err
})
if err != nil {
return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err))
}
// marshall the JWT payload into the safetynet response json
var safetyNetResponse SafetyNetResponse
err = mapstructure.Decode(token.Claims, &safetyNetResponse)
if err != nil {
return safetyNetAttestationKey, nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing the SafetyNet response: %+v", err))
}
// §8.5.3 Verify that the nonce in the response is identical to the Base64 encoding of the SHA-256 hash of the concatenation
// of authenticatorData and clientDataHash.
nonceBuffer := sha256.Sum256(append(att.RawAuthData, clientDataHash...))
nonceBytes, err := base64.StdEncoding.DecodeString(safetyNetResponse.Nonce)
if !bytes.Equal(nonceBuffer[:], nonceBytes) || err != nil {
return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails("Invalid nonce for in SafetyNet response")
}
// §8.5.4 Let attestationCert be the attestation certificate (https://www.w3.org/TR/webauthn/#attestation-certificate)
certChain := token.Header["x5c"].([]interface{})
l := make([]byte, base64.StdEncoding.DecodedLen(len(certChain[0].(string))))
n, err := base64.StdEncoding.Decode(l, []byte(certChain[0].(string)))
if err != nil {
return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err))
}
attestationCert, err := x509.ParseCertificate(l[:n])
if err != nil {
return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err))
}
// §8.5.5 Verify that attestationCert is issued to the hostname "attest.android.com"
err = attestationCert.VerifyHostname("attest.android.com")
if err != nil {
return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err))
}
// §8.5.6 Verify that the ctsProfileMatch attribute in the payload of response is true.
if !safetyNetResponse.CtsProfileMatch {
return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails("ctsProfileMatch attribute of the JWT payload is false")
}
// Verify sanity of timestamp in the payload
now := time.Now()
oneMinuteAgo := now.Add(-time.Minute)
t := time.Unix(safetyNetResponse.TimestampMs/1000, 0)
if t.After(now) {
// zero tolerance for post-dated timestamps
return "Basic attestation with SafetyNet", nil, ErrInvalidAttestation.WithDetails("SafetyNet response with timestamp after current time")
} else if t.Before(oneMinuteAgo) {
// allow old timestamp for testing purposes
// TODO: Make this user configurable
msg := "SafetyNet response with timestamp before one minute ago"
if metadata.Conformance {
return "Basic attestation with SafetyNet", nil, ErrInvalidAttestation.WithDetails(msg)
}
}
// §8.5.7 If successful, return implementation-specific values representing attestation type Basic and attestation
// trust path attestationCert.
return "Basic attestation with SafetyNet", nil, nil
}