How to use PackageInfo.GET_SIGNING_CERTIFICATES in API 28?

Get package signatures:

private static List<String> getSignatures(@NonNull PackageManager pm, @NonNull String packageName) {
            try {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                    PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES);
                    if (packageInfo == null
                            || packageInfo.signingInfo == null) {
                        return null;
                    }
                    if(packageInfo.signingInfo.hasMultipleSigners()){
                        return signatureDigest(packageInfo.signingInfo.getApkContentsSigners());
                    }
                    else{
                        return signatureDigest(packageInfo.signingInfo.getSigningCertificateHistory());
                    }
                }
                else {
                    @SuppressLint("PackageManagerGetSignatures")
                    PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
                    if (packageInfo == null
                            || packageInfo.signatures == null
                            || packageInfo.signatures.length == 0
                            || packageInfo.signatures[0] == null) {
                        return null;
                    }
                    return signatureDigest(packageInfo.signatures);
                }
            } catch (PackageManager.NameNotFoundException e) {
                return null;
            }
        }

Convert signatures to list of hex strings:

        private static String signatureDigest(Signature sig) {
            byte[] signature = sig.toByteArray();
            try {
                MessageDigest md = MessageDigest.getInstance("SHA1");
                byte[] digest = md.digest(signature);
                return BaseEncoding.base16().lowerCase().encode(digest);
            } catch (NoSuchAlgorithmException e) {
                return null;
            }
        }
        private static List<String> signatureDigest(Signature[] sigList) {
            List<String> signaturesList= new ArrayList<>();
            for (Signature signature: sigList) {
                if(signature!=null) {
                    signaturesList.add(signatureDigest(signature));
                }
            }
           return signturesList;
        }

Compare package signatures with your whitelist:

    private static boolean verifyAppSignature(Context context) {
        //you should load approvedSignatures from a secure place not plain text
        List<String> approvedSignatures = new ArrayList<>();
        approvedSignatures.add("Your whitelist #1");
        approvedSignatures.add("Your whitelist #2");

        List<String> currentSignatures = getSignatures(context.getPackageManager(), context.getPackageName());
        if(currentSignatures!=null && currentSignatures.size()>0) {
            //first checking if no unapproved signatures exist
            for (String signatureHex : currentSignatures) {
                if (!approvedSignatures.contains(signatureHex)) {
                    return false;
                }
            }
            //now checking if any of approved signatures exist
            for (String signatureHex : currentSignatures) {
                if (approvedSignatures.contains(signatureHex)) {
                    return true;
                }
            }
        }
        return false;
    }

My solution is:

In the gradle build set "compileSdkVersion 28" and "targetSdkVersion 28", now you can use this sample code:

try {
    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
        @SuppressLint("WrongConstant") final PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_SIGNING_CERTIFICATES);
        final Signature[] signatures = packageInfo.signingInfo.getApkContentsSigners();
        final MessageDigest md = MessageDigest.getInstance("SHA");
        for (Signature signature : signatures) {
            md.update(signature.toByteArray());
            final String signatureBase64 = new String(Base64.encode(md.digest(), Base64.DEFAULT));
            Log.d("Signature Base64", signatureBase64);
        }
    }
} catch (PackageManager.NameNotFoundException | NoSuchAlgorithmException e) {
    e.printStackTrace();
}

If strangely Android Studio does not recognize the constant GET_SIGNING_CERTIFICATES you can use the @SuppressLint ("WrongConstant") annotation.


In API28 or higher you should check for multipleSigners as well.

This function will do the job:
(Works for Android 9.0 and lower)

fun getApplicationSignature(packageName: String = context.packageName): List<String> {
    val signatureList: List<String>
    try {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            // New signature
            val sig = context.packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES).signingInfo
            signatureList = if (sig.hasMultipleSigners()) {
                // Send all with apkContentsSigners
                sig.apkContentsSigners.map {
                    val digest = MessageDigest.getInstance("SHA")
                    digest.update(it.toByteArray())
                    bytesToHex(digest.digest())
                }
            } else {
                // Send one with signingCertificateHistory
                sig.signingCertificateHistory.map {
                    val digest = MessageDigest.getInstance("SHA")
                    digest.update(it.toByteArray())
                    bytesToHex(digest.digest())
                }
            }
        } else {
            val sig = context.packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES).signatures
            signatureList = sig.map {
                val digest = MessageDigest.getInstance("SHA")
                digest.update(it.toByteArray())
                bytesToHex(digest.digest())
            }
        }

        return signatureList
    } catch (e: Exception) {
        // Handle error
    }
    return emptyList()
}

And byteToHex is:

fun bytesToHex(bytes: ByteArray): String {
    val hexArray = charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F')
    val hexChars = CharArray(bytes.size * 2)
    var v: Int
    for (j in bytes.indices) {
        v = bytes[j].toInt() and 0xFF
        hexChars[j * 2] = hexArray[v.ushr(4)]
        hexChars[j * 2 + 1] = hexArray[v and 0x0F]
    }
    return String(hexChars)
}

TL;DR if your use case is that you're validating the calling package's signatures, you can still use GET_SIGNATURES in pre-api 28 securely as long as you validate all signers returned from the package manager (instead of stopping early when you find one that you trust). In fact, google patched it in lollipop (https://android.googlesource.com/platform/libcore/+/f8986a989759c43c155ae64f9a3b36f670602521).

Details: I believe your comment about GET_SIGNATURES being easily hacked is based on this vulnerability (https://www.blackhat.com/docs/us-14/materials/us-14-Forristal-Android-FakeID-Vulnerability-Walkthrough.pdf). Whereby android doesn't validate the trust chain prior to returning apk signers.

This is only a problem if you have code like this:

    private boolean validateCallingPackage(String: packageName) {
        PackageInfo packageInfo;
        try {
            packageInfo = context.getPackageManager().getPackageInfo(
                packageName,
                PackageManager.GET_SIGNATURES);
        } catch (PackageManager.NameNotFoundException e) {
            return false;
        }


        for (Signature signature : packageInfo.signatures) {
            String hashedSignature = Utility.sha256hash(signature.toByteArray());
            if (validAppSignatureHashes.contains(hashedSignature)) {
              return true;  //THIS is the problematic code
            }
        }
        return false
    }

The code is returning true if it finds any certificate that matches one from your whitelist. With the android vulnerability, if the signatures contained a signature from a malicious signer, your code still returns true.

The mitigation for this vulnerability is to instead check ALL signatures returned from the package manager and return false if any of them aren't in your whitelist. i.e.

    private boolean validateCallingPackage(String: packageName) {
        ...

        for (Signature signature : packageInfo.signatures) {
            String hashedSignature = Utility.sha256hash(signature.toByteArray());
            if (!validAppSignatureHashes.contains(hashedSignature)) {
              return false; //FIXED
            }
        }
        return true
    }