This post is just a collection of information about how to implement SafetyNet Attestation API in your application.
This whole thing started weeks ago after my team was researching a way to block suspicious requests that were occurring in our authentication API. There were lots of different users trying to log in, from different IP addresses/ranges. We already had a WAF set up, however, in this scenario, this was not effective. We think that there had been a data breach with lots of users and passwords, therefore, attackers were using these information to try a lucky guess in our API, almost like a brute-force attack.
Our first attempt was to use reCAPTCHA as our mechanism to block those requests. However, we've found that user experience using this mechanism was really bad, as reCAPTCHA is an intrusive approach (even though it has an invisible option). After some research, we decided to give a try to use SafetyNet Attestation API.
In our scenario, the problem we were trying to solve was to separate malicious attacks from real login attempts from our clients. In other words, we wanted to know if a login attempt was occurring from an automation script or from a real device.
Therefore, when we know we have a real device, we allow the user to log in, otherwise, we just block the request.
I'll not get into lots of details about the device implementation, however, it's pretty straightforward: https://developer.android.com/training/safetynet/attestation#request-attestation-step.
First and foremost, you need to have in mind that the token verification is up to you. There are some tutorials and even a code in google samples that verifies the token for you using an online API. However, this should not be your production approach.
In order to verify the token, we must first understand what this token
represents. The token is a
JWS
token. If you want, you'll be able to parse this token using simple
jwt
tools. For example, one may use https://jwt.io/ debugger to get
the token information, or use any standard jwt
implementation.
A valid SafetyNet token will have a header that looks like this:
{
"alg": "RS256",
"x5c": [
"signing certificate as base64 goes here",
"issuer certificate as base64 goes here"
]
}
And it will have a payload that looks like this:
{
"timestampMs": 9860437986543,
"nonce": "R2Rra24fVm5xa2Mg",
"apkPackageName": "com.package.name.of.requesting.app",
"apkCertificateDigestSha256": ["base64 encoded, SHA-256 hash of the
certificate used to sign requesting app"],
"ctsProfileMatch": true,
"basicIntegrity": true,
}
If you want, you can have the payload representation as follows (in Go):
// Attestation response from Google SafetyNet Attestation API
type Attestation struct {
TimestampMs int64 `json:"timestampMs"` // time when response was generated by Google's servers
Nonce string `json:"nonce"` // single use token
ApkPackageName string `json:"apkPackageName"` // calling app's package name
ApkCertificateDigestSha256 []string `json:"apkCertificateDigestSha256"` // base64 encoded representation of SHA-256 hash of the calling app signing certificate
CtsProfileMatch bool `json:"ctsProfileMatch"` // stricter veredict of device integrity
BasicIntegrity bool `json:"basicIntegrity"` // a more lenient veredict of device integrity
Error string `json:"error"` // error occured from API request
Advice string `json:"advice"` // suggestion on how to get device back into a good state
}
Now you're wondering what you should do with that information. And you're not alone. There are many pieces of information and you must decide what to use.
One thing that is really important to be done is to verify if the
token is valid. You can do that using standard jws
tools. If you
search for terms like jws and jose you might find some tools for
your preferred language. If you're using Go, you can use
go-jose
. Here goes an
example on how to check the validity of a JWS token:
func Validate(token string) {
signedAttestation, err := jose.ParseSigned(token)
if err != nil {
log.Fatalf("error on parse signed attestation: %s", err)
}
}
After that, you need to check if that message was signed by Google
itself. If you saw before, we have a
x5c
property on the header. This information says two things: first, who
signed the JWS token itself. Second, who signed the certificate that
signed the JWS token. In other words, it's a certificate
chain.
There are lots of solutions for validating a certificate chain. If
you're using Go and go-jose
, it's pretty straightforward to do that:
func ValidateSafetyNetAttesationToken(token string) {
// remaining code
// uses default system root CAs to validate
opts := x509.VerifyOptions{}
certs, err := signedAttestation.Signatures[0].Header.Certificates(opts)
if err != nil {
log.Fatalf("error on validating certificates: %s", err)
}
}
Now that you know that the message is really sent by Google, the next step is to get its payload. The payload can be used to check device integrity (for example, to check if the phone is not rooted). Here is an example on how to do that:
func ValidateSafetyNetAttesationToken(token string) {
// remaining code
attestationPayload, err := signedAttestation.Verify(certs[0][0].PublicKey)
if err != nil {
log.Fatalf("error on verifying attestation: %s", err)
}
attestation := &Attestation{}
json.Unmarshal(attestationPayload, &attestation)
fmt.Printf("%+v\n", attestation)
}
Now, it's up to you to do whatever you want. I recommend to check if all the fields match the values you expect. Some ideas:
apkCertificateDigestSha256
matches the fingerprint of the
certificate that was used to sign the app (only your team should have
the private key to do that)apkPackageName
matches the package that should call the
Attestation APIbasicIntegrity
and
ctsProfileMatch
nonce
, however, you
can use it to match if the information given by the client is what you
expected. This, combined with some cryptography strategy, makes it
even harder for an attacker to "guess" the nonceHope you find it helpful!