How to Implement SafetyNet Attestation in Your Application

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.

How SafetyNet Attestation API replaces reCAPTCHA?

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.

SafetyNet Attestation API implementation: device

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.

SafetyNet Attestation API implementation: token verification

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:

  • Verify if 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)
  • Verify if apkPackageName matches the package that should call the Attestation API
  • Verify the value from basicIntegrity and ctsProfileMatch
  • Verify if the provided timestamp is in a specific range/time limit (for example, consider it expired if generated 10 minutes ago)
  • I couldn't find a practical case for using the 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 nonce

Hope you find it helpful!


Tags:AndroidSafetyNetAttestationReCATPCHA AlternativeSecurity