0

I am using SafetyNet to verify the integrity of the android app.

This is the flow as of now.

  1. I generate a nonce value in the server and send it to the SafetyNet service to get the response.
  2. I get the response from the server. Now I want to verify the result on the server.

I get a base64 string. I decode it and get the response as below.

{
    "evaluationType": "BASIC",
    "ctsProfileMatch": false,
    "apkPackageName": "com.test.safetynetproject",
    "apkDigestSha256": "CbU9JzwRzQneYqnEXewB56ZzPm1DgQ4LGUK0eGlWmyM=",
    "nonce": "U2FnYXI=",
    "apkCertificateDigestSha256": [
        "AJRBzWCfJIY7QD2cp4sv9t0cCGMRGdxuID9VdPLV1H4="
    ],
    "timestampMs": 1624099377557,
    "basicIntegrity": false
}

Now i want to verify the apkCertificateDigestSha256. The sha256 created from my system using cmd is -

C:\Program Files\Java\jdk-11.0.11\bin>keytool -list -v -alias androiddebugkey -keystore C:\Users\.android\debug.keystore
Enter keystore password:
Alias name: androiddebugkey
Creation date: May 25, 2021
Entry type: PrivateKeyEntry
Certificate chain length: 1
Certificate[1]:
Owner: C=US, O=Android, CN=Android Debug
Issuer: C=US, O=Android, CN=Android Debug
Serial number: 1
Valid from: Tue May 25 11:48:00 IST 2021 until: Thu May 18 11:48:00 IST 2051
Certificate fingerprints:
         SHA1: 43:16:E2:63:DB:2A:53:7C:7D:BB:E9:80:7B:05:1C:74:7C:84:66:A2
         SHA256: 00:94:41:CD:60:9F:24:86:3B:40:3D:9C:A7:8B:2F:F6:DD:1C:08:63:11:19:DC:6E:20:3F:55:74:F2:D5:D4:7E
Signature algorithm name: SHA1withRSA (weak)
Subject Public Key Algorithm: 2048-bit RSA key
Version: 1

Warning:
The certificate uses the SHA1withRSA signature algorithm which is considered a security risk. This algorithm will be disabled in a future update.

The SHA256

00:94:41:CD:60:9F:24:86:3B:40:3D:9C:A7:8B:2F:F6:DD:1C:08:63:11:19:DC:6E:20:3F:55:74:F2:D5:D4:7E

Question - I want to verify if the apkCertificateDigestSha256 is the same as the app certificate. Bt unable to find any way to do it.

Tries- I tried to base64 decode the AJRBzWCfJIY7QD2cp4sv9t0cCGMRGdxuID9VdPLV1H4= and got a random byte array that does not match with the sha256 created in cmd.

Code -

val decode =
    String(
        Base64.decode(
            responseJws!!.apkCertificateDigestSha256!![0],
            Base64.DEFAULT
        ),
        StandardCharsets.UTF_8
    )

The output -

���A�`�$�;@=���/��c�n ?Ut���~

This is not matching 43:16:E2:63:DB:2A:53:7C:7D:BB:E9:80:7B:05:1C:74:7C:84:66:A2.

Update-

Found some ref but dont really know how to achieve this. Ref1

How do I do the matching?

Sagar Nayak
  • 2,138
  • 2
  • 19
  • 52

5 Answers5

1

I have used SafetyNet API for accessing device's runtime env. I have kept signing certificate of app on server to verify its sha256 against what we get in the SafetyNet response. Below are the steps you can refer if applies to you too.

  1. Get SHA256 fingerprint of signing X509Certificate

    MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] der = cert.getEncoded(); md.update(der); byte[] sha256 = md.digest();

  2. Encode sha256 to base64 string

    String checksum = Base64.getEncoder().encodeToString(sha256)

  3. Match checksum with apkCertificateDigestSha256 of SafetyNet response

As requested, below is the method i am using for getting SHA256 of the certificate. Here cert is the app signing certificate

    public static byte[] getSHA256Fingerprint(X509Certificate 
    cert) throws NoSuchAlgorithmException, 
    CertificateEncodingException {
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        byte[] der = cert.getEncoded();
        md.update(der);
        return md.digest();
     }

Consuming method above for calculating hash appSigningKeystoreCertHash and converting it to base64 string to compare with SafetyNet response value

    byte[] shaFingerprint = 
    CertificateUtility.getSHA256Fingerprint(certificate);
                String appSigningKeystoreCertHash = 
    java.util.Base64.getEncoder().encodeToString(shaFingerprint);
    //Base64 encoded SHA256 certificate fingerprint;
Dipali_P
  • 21
  • 2
0

I think this can help you

1.Find AttestationStatement file in GG example. and add this function:

public  String bytesToHex(byte[] bytes) {
    StringBuffer result = new StringBuffer();
    for (byte b : bytes) result.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1));
    return result.toString();
}

2.Find getApkCertificateDigestSha256 function and edit like this:

public byte[][] getApkCertificateDigestSha256() {
    byte[][] certs = new byte[apkCertificateDigestSha256.length][];
    for (int i = 0; i < apkCertificateDigestSha256.length; i++) {
        certs[i] = Base64.decodeBase64(apkCertificateDigestSha256[i]);
        System.out.println(bytesToHex(certs[i]));
    }
    return certs;
}

3.Find process() function in OnlineVerrify and add like this:

if (stmt.getApkPackageName() != null && stmt.getApkDigestSha256() != null) {
        System.out.println("APK package name: " + stmt.getApkPackageName());
        System.out.println("APK digest SHA256: " + Arrays.toString(stmt.getApkDigestSha256()));
        System.out.println("APK certificate digest SHA256: " +
                Arrays.deepToString(stmt.getApkCertificateDigestSha256()));
    }
  1. Now, run and you'll see the SHA-256 and let compare.

Not: there is no ":" charactor bettwen sha-256 generated cause i'm lazy. ^^.

NVD
  • 1
  • 2
0

Check the code here as reference on how to do the validations: https://github.com/Gralls/SafetyNetSample/blob/master/Server/src/main/java/pl/patryk/springer/safetynet/Main.kt

I just found it while searching for the same thing, and all credit goes to the person that owns the repo.

Harsh Shah
  • 368
  • 3
  • 17
0
public class Starter {

static String keystore_location = "C:\\Users\\<your_user>\\.android\\debug.keystore";

private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
public static String bytesToHex(byte[] bytes) {
    char[] hexChars = new char[bytes.length * 2];
    for (int j = 0; j < bytes.length; j++) {
        int v = bytes[j] & 0xFF;
        hexChars[j * 2] = HEX_ARRAY[v >>> 4];
        hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
    }
    return new String(hexChars);
}

public static void main(String[] args) {
    
    try {
        File file = new File(keystore_location);
        InputStream is = new FileInputStream(file);
        KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
        String password = "android"; // This is the default password
        keystore.load(is, password.toCharArray());


        Enumeration<String> enumeration = keystore.aliases();
        while(enumeration.hasMoreElements()) {
            String alias = enumeration.nextElement();
            System.out.println("alias name: " + alias);
            Certificate certificate = keystore.getCertificate(alias);
            System.out.println(certificate.toString());
            System.out.println(certificate.getEncoded());
            
            final MessageDigest md = MessageDigest.getInstance("SHA-1");
            final MessageDigest md2 = MessageDigest.getInstance("SHA-256");
            final byte[] der = certificate.getEncoded();
            md.update(der);
            md2.update(der);
            final byte[] digest = md.digest();
            final byte[] digest2 = md2.digest();
            System.out.println(bytesToHex(digest));
            System.out.println(bytesToHex(digest2));
            
            byte[] encoded = Base64.getEncoder().encode(digest2);
            System.out.println(encoded);
            
            String checksum = Base64.getEncoder().encodeToString(digest2);
            System.out.println(checksum); // This should match apkCertificateDigestSha256
        }
    } catch (java.security.cert.CertificateException e) {
        e.printStackTrace();
    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (KeyStoreException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
Evgeny Erlihman
  • 458
  • 4
  • 12
0

Now Google depreciated SafetyAPI and Introduced PlayIntegrity API for attestation. PlayIntegrity Service provides the response as follows.

{
"tokenPayloadExternal": {
    "accountDetails": {
        "appLicensingVerdict": "LICENSED"
    },
    "appIntegrity": {
        "appRecognitionVerdict": "PLAY_RECOGNIZED",
        "certificateSha256Digest": ["pnpa8e8eCArtvmaf49bJE1f5iG5-XLSU6w1U9ZvI96g"],
        "packageName": "com.test.android.safetynetsample",
        "versionCode": "4"
    },
    "deviceIntegrity": {
        "deviceRecognitionVerdict": ["MEETS_DEVICE_INTEGRITY"]
    },
    "requestDetails": {
        "nonce": "SafetyNetSample1654058651834",
        "requestPackageName": "com.test.android.safetynetsample",
        "timestampMillis": "1654058657132"
    }
}}

Response contains only certificateSha256Digest of the app (The sha256 digest of app certificates) instead of having apkDigestSha256 and apkCertificateDigestSha256.

How do we validate the received certificateSha256Digest at server?

If the app is deployed in Google PlayStore then follow the below steps

Download the App signing key certificate from Google Play Console (If you are using managed signing key) otherwise download Upload key certificate and then find checksum of the certificate.

public static Certificate getCertificate(String certificatePath)throws Exception {
  CertificateFactory certificateFactory = CertificateFactory.getInstance("X509");
  FileInputStream in = new FileInputStream(certificatePath);
  Certificate certificate = certificateFactory.generateCertificate(in);
  in.close();
 return certificate;
}

Generate checksum of the certificate

Certificate x509Cert = getCertificate("<Path of file>/deployment_cert.der");
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] x509Der = x509Cert.getEncoded();
md.update(x509Der);
byte[] sha256 = md.digest();
String checksum = Base64.getEncoder().encodeToString(sha256);

Then compare checksum with received certificateSha256Digest

String digest = jwsResponse.tokenPayloadExternal.appIntegrity.certificateSha256Digest;
if(checksum.contains(digest)){
  //
}
John_S
  • 532
  • 4
  • 10