Monday, July 11, 2022

Android Device Attestation Client Side and Server Side using SafetyNet



Attestation Posts:


In this post we will review the steps to perform an android device attestation.

This includes the client side code to call google's safetynet and receive a token, and the server side to validate the token.

A good guide for this is included in the SafetryNet Attestation API documentation. The following class runs the entire flow for attestation client side.



package com.example.app;

import android.content.Context;
import android.os.AsyncTask;
import androidx.annotation.NonNull;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.android.gms.safetynet.SafetyNet;
import com.google.android.gms.safetynet.SafetyNetApi;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.OnSuccessListener;

public class DeviceAttestation extends AsyncTask
implements OnSuccessListener<SafetyNetApi.AttestationResponse>,
OnFailureListener
{

private String API_KEY = "AIzaSyCwl123456789012345678934567dsqIqY";

private byte[] nonce;

public Attestation(byte[] nonce) {
this.nonce = nonce;
}

public void start() {
Context context = Bouncer.getInstance().getContext();
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context)
!= ConnectionResult.SUCCESS) {
throw new RuntimeException("too old Google Play Services version");
}

SafetyNet.getClient(context).attest(this.nonce, API_KEY)
.addOnSuccessListener(this)
.addOnFailureListener(this);
}

@Override
public void onSuccess(SafetyNetApi.AttestationResponse response) {
String token = response.getJwsResult();
this.execute();
}

@Override
protected Object doInBackground(Object[] objects) {
sendTokenToServer(this.token);
return null;
}

@Override
public void onFailure(@NonNull Exception e) {
Logger.warn("attestation failed", e);
}

}



The attestation flow is a follows:

  • Call the Attestation constructor with a nonce. The nonce should be a pretty random value, even best to make it unique for the device, for example: the user name.
  • Call to start() method, which starts the communication with Google's attestation server.
  • Once completed the onSuccess() method is called, and the attestation token is received.
  • The onSuccess uses ASyncTask to start communication with the application backend, where the attestation token is delivered.
  • The server then validates the attestation token, and decides if to allow the client to proceed or not.

On the server side, we use Go to validate the attestation token.


package attestation

import (
"fmt"
"github.com/wongnai/safetynet/v2"
)

type Executor struct {
}


func (e *Executor) Execute(p web.Parser) (interface{}, error) {
request := AttestationRequest{}
err := p.GetBody(&request)
if err != nil {
return nil, fmt.Errorf("get body failed: %v", err)
}


attestation, err := safetynet.Validate(request.Token)
if err != nil {
return nil, fmt.Errorf("validate attestation failed: %v", err)
}

if !attestation.CTSProfileMatch {
return nil, fmt.Errorf("attestatation failed: %+v", attestation)
}


return AttestationValidResponse{}, nil
}



We use the CTSProfileMatch to decide if the device is a valid device, or a bot emulator, and allow or fail to client side to continue.


Final Notes


We have presented client side and server side for attestation on android devices.
When moving to production, we must consider the attestation quota, as well as whether we want to use not only CTSProfileMatch, but also basicIntegrity to allow access.





No comments:

Post a Comment