Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add security Policy for verifying signature using sha-256 hash #9305

Merged
merged 13 commits into from Jun 24, 2022
117 changes: 110 additions & 7 deletions binder/src/main/java/io/grpc/binder/SecurityPolicies.java
Expand Up @@ -24,12 +24,15 @@
import android.os.Build;
import android.os.Process;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.hash.Hashing;
import com.google.errorprone.annotations.CheckReturnValue;
import io.grpc.ExperimentalApi;
import io.grpc.Status;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
Expand All @@ -40,6 +43,7 @@
public final class SecurityPolicies {

private static final int MY_UID = Process.myUid();
private static final int SHA_256_BYTES_LENGTH = 32;

private SecurityPolicies() {}

Expand Down Expand Up @@ -83,6 +87,21 @@ public static SecurityPolicy hasSignature(
packageManager, packageName, ImmutableList.of(requiredSignature));
}

/**
* Creates {@link SecurityPolicy} which checks if the SHA-256 hash of the package signature
* matches {@code requiredSignatureSha256Hash}.
*
* @param packageName the package name of the allowed package.
* @param requiredSignatureSha256Hash the SHA-256 digest of the signature of the allowed package.
* @throws NullPointerException if any of the inputs are {@code null}.
* @throws IllegalArgumentException if {@code requiredSignatureSha256Hash} is not of length 32.
*/
public static SecurityPolicy hasSignatureSha256Hash(
PackageManager packageManager, String packageName, byte[] requiredSignatureSha256Hash) {
return oneOfSignatureSha256Hash(
packageManager, packageName, ImmutableList.of(requiredSignatureSha256Hash));
}

/**
* Creates a {@link SecurityPolicy} which checks if the package signature
* matches any of {@code requiredSignatures}.
Expand Down Expand Up @@ -116,6 +135,39 @@ public Status checkAuthorization(int uid) {
};
}

/**
* Creates {@link SecurityPolicy} which checks if the SHA-256 hash of the package signature
* matches any of {@code requiredSignatureSha256Hashes}.
*
* @param packageName the package name of the allowed package.
* @param requiredSignatureSha256Hashes the SHA-256 digests of the signatures of the allowed
* package.
* @throws NullPointerException if any of the inputs are {@code null}.
* @throws IllegalArgumentException if {@code requiredSignatureSha256Hashes} is empty, or if any
* of the {@code requiredSignatureSha256Hashes} are not of length 32.
*/
public static SecurityPolicy oneOfSignatureSha256Hash(
PackageManager packageManager,
String packageName,
ImmutableList<byte[]> requiredSignatureSha256Hashes) {
ejona86 marked this conversation as resolved.
Show resolved Hide resolved
Preconditions.checkNotNull(packageManager);
Preconditions.checkNotNull(packageName);
Preconditions.checkNotNull(requiredSignatureSha256Hashes);
Preconditions.checkArgument(!requiredSignatureSha256Hashes.isEmpty());
for (byte[] requiredSignatureSha256Hash : requiredSignatureSha256Hashes) {
ejona86 marked this conversation as resolved.
Show resolved Hide resolved
Preconditions.checkNotNull(requiredSignatureSha256Hash);
Preconditions.checkArgument(requiredSignatureSha256Hash.length == SHA_256_BYTES_LENGTH);
}

return new SecurityPolicy() {
@Override
public Status checkAuthorization(int uid) {
return checkUidSha256Signature(
packageManager, uid, packageName, requiredSignatureSha256Hashes);
}
};
}

private static Status checkUidSignature(
PackageManager packageManager,
int uid,
Expand All @@ -132,7 +184,7 @@ private static Status checkUidSignature(
continue;
}
packageNameMatched = true;
if (checkPackageSignature(packageManager, pkg, requiredSignatures)) {
if (checkPackageSignature(packageManager, pkg, requiredSignatures::contains)) {
return Status.OK;
}
}
Expand All @@ -141,19 +193,50 @@ private static Status checkUidSignature(
+ packageNameMatched);
}

private static Status checkUidSha256Signature(
PackageManager packageManager,
int uid,
String packageName,
ImmutableList<byte[]> requiredSignatureSha256Hashes) {
String[] packages = packageManager.getPackagesForUid(uid);
if (packages == null) {
return Status.UNAUTHENTICATED.withDescription(
"Rejected by (SHA-256 hash signature check) security policy");
}
boolean packageNameMatched = false;
for (String pkg : packages) {
if (!packageName.equals(pkg)) {
continue;
}
packageNameMatched = true;
if (checkPackageSignature(
packageManager,
pkg,
(signature) ->
checkSignatureSha256HashesMatch(signature, requiredSignatureSha256Hashes))) {
return Status.OK;
}
}
return Status.PERMISSION_DENIED.withDescription(
"Rejected by (SHA-256 hash signature check) security policy. Package name matched: "
+ packageNameMatched);
}

/**
* Checks if the signature of {@code packageName} matches one of the given signatures.
*
* @param packageName the package to be checked
* @param requiredSignatures list of signatures.
* @return {@code true} if {@code packageName} has a matching signature.
* @param signatureCheckFunction {@link Function} that takes a signature and verifies if it
ejona86 marked this conversation as resolved.
Show resolved Hide resolved
* satisfies any signature constraints
* return {@code true} if {@code packageName} has a signature that satisfies {@code
* signatureCheckFunction}.
*/
@SuppressWarnings("deprecation") // For PackageInfo.signatures
@SuppressLint("PackageManagerGetSignatures") // We only allow 1 signature.
private static boolean checkPackageSignature(
PackageManager packageManager,
String packageName,
ImmutableList<Signature> requiredSignatures) {
Predicate<Signature> signatureCheckFunction) {
PackageInfo packageInfo;
try {
if (Build.VERSION.SDK_INT >= 28) {
Expand All @@ -168,7 +251,7 @@ private static boolean checkPackageSignature(
: packageInfo.signingInfo.getSigningCertificateHistory();

for (Signature signature : signatures) {
if (requiredSignatures.contains(signature)) {
if (signatureCheckFunction.apply(signature)) {
return true;
}
}
Expand All @@ -180,7 +263,7 @@ private static boolean checkPackageSignature(
return false;
}

if (requiredSignatures.contains(packageInfo.signatures[0])) {
if (signatureCheckFunction.apply(packageInfo.signatures[0])) {
return true;
}
}
Expand Down Expand Up @@ -317,4 +400,24 @@ private static Status checkPermissions(

return Status.OK;
}
}

/**
* Checks if the SHA-256 hash of the {@code signature} matches one of the {@code
* expectedSignatureSha256Hashes}.
*/
private static boolean checkSignatureSha256HashesMatch(
Signature signature, List<byte[]> expectedSignatureSha256Hashes) {
byte[] signatureHash = getSha256Hash(signature);
for (byte[] hash : expectedSignatureSha256Hashes) {
if (Arrays.equals(hash, signatureHash)) {
return true;
}
}
return false;
}

/** Returns SHA-256 hash of the provided signature. */
private static byte[] getSha256Hash(Signature signature) {
return Hashing.sha256().hashBytes(signature.toByteArray()).asBytes();
}
}
129 changes: 129 additions & 0 deletions binder/src/test/java/io/grpc/binder/SecurityPoliciesTest.java
Expand Up @@ -32,6 +32,7 @@
import androidx.test.core.app.ApplicationProvider;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.hash.Hashing;
import io.grpc.Status;
import java.util.HashMap;
import java.util.concurrent.atomic.AtomicInteger;
Expand Down Expand Up @@ -435,4 +436,132 @@ public Status checkAuthorization(int uid) {
return Status.OK;
}
}

@Test
public void testHasSignatureSha256Hash_succeedsIfPackageNameAndSignatureHashMatch()
throws Exception {
PackageInfo info =
newBuilder().setPackageName(OTHER_UID_PACKAGE_NAME).setSignatures(SIG2).build();
installPackages(OTHER_UID, info);

policy =
SecurityPolicies.hasSignatureSha256Hash(
packageManager, OTHER_UID_PACKAGE_NAME, getSha256Hash(SIG2));

// THEN UID for package that has SIG2 will be authorized
assertThat(policy.checkAuthorization(OTHER_UID).getCode()).isEqualTo(Status.OK.getCode());
}

@Test
public void testHasSignatureSha256Hash_failsIfPackageNameDoesNotMatch() throws Exception {
PackageInfo info1 =
newBuilder().setPackageName(appContext.getPackageName()).setSignatures(SIG1).build();
installPackages(MY_UID, info1);

PackageInfo info2 =
newBuilder()
.setPackageName(OTHER_UID_SAME_SIGNATURE_PACKAGE_NAME)
.setSignatures(SIG1)
.build();
installPackages(OTHER_UID_SAME_SIGNATURE, info2);

policy =
SecurityPolicies.hasSignatureSha256Hash(
packageManager, appContext.getPackageName(), getSha256Hash(SIG1));

// THEN UID for package that has SIG1 but different package name will not be authorized
assertThat(policy.checkAuthorization(OTHER_UID_SAME_SIGNATURE).getCode())
.isEqualTo(Status.PERMISSION_DENIED.getCode());
}

@Test
public void testHasSignatureSha256Hash_failsIfSignatureHashDoesNotMatch() throws Exception {
PackageInfo info =
newBuilder().setPackageName(OTHER_UID_PACKAGE_NAME).setSignatures(SIG2).build();
installPackages(OTHER_UID, info);

policy =
SecurityPolicies.hasSignatureSha256Hash(
packageManager, OTHER_UID_PACKAGE_NAME, getSha256Hash(SIG1));

// THEN UID for package that doesn't have SIG1 will not be authorized
assertThat(policy.checkAuthorization(OTHER_UID).getCode())
.isEqualTo(Status.PERMISSION_DENIED.getCode());
}

@Test
public void testOneOfSignatureSha256Hash_succeedsIfPackageNameAndSignatureHashMatch()
throws Exception {
PackageInfo info =
newBuilder().setPackageName(OTHER_UID_PACKAGE_NAME).setSignatures(SIG2).build();
installPackages(OTHER_UID, info);

policy =
SecurityPolicies.oneOfSignatureSha256Hash(
packageManager, OTHER_UID_PACKAGE_NAME, ImmutableList.of(getSha256Hash(SIG2)));

// THEN UID for package that has SIG2 will be authorized
assertThat(policy.checkAuthorization(OTHER_UID).getCode()).isEqualTo(Status.OK.getCode());
}

@Test
public void testOneOfSignatureSha256Hash_succeedsIfPackageNameAndOneOfSignatureHashesMatch()
throws Exception {
PackageInfo info =
newBuilder().setPackageName(OTHER_UID_PACKAGE_NAME).setSignatures(SIG2).build();
installPackages(OTHER_UID, info);

policy =
SecurityPolicies.oneOfSignatureSha256Hash(
packageManager,
OTHER_UID_PACKAGE_NAME,
ImmutableList.of(getSha256Hash(SIG1), getSha256Hash(SIG2)));

// THEN UID for package that has SIG2 will be authorized
assertThat(policy.checkAuthorization(OTHER_UID).getCode()).isEqualTo(Status.OK.getCode());
}

@Test
public void
testOneOfSignatureSha256Hash_failsIfPackageNameDoNotMatchAndOneOfSignatureHashesMatch()
throws Exception {
PackageInfo info =
newBuilder().setPackageName(OTHER_UID_PACKAGE_NAME).setSignatures(SIG2).build();
installPackages(OTHER_UID, info);

policy =
SecurityPolicies.oneOfSignatureSha256Hash(
packageManager,
appContext.getPackageName(),
ImmutableList.of(getSha256Hash(SIG1), getSha256Hash(SIG2)));

// THEN UID for package that has SIG2 but different package name will not be authorized
assertThat(policy.checkAuthorization(OTHER_UID).getCode())
.isEqualTo(Status.PERMISSION_DENIED.getCode());
}

@Test
public void testOneOfSignatureSha256Hash_failsIfPackageNameMatchAndOneOfSignatureHashesNotMatch()
throws Exception {
PackageInfo info =
newBuilder()
.setPackageName(OTHER_UID_PACKAGE_NAME)
.setSignatures(new Signature("1234"))
.build();
installPackages(OTHER_UID, info);

policy =
SecurityPolicies.oneOfSignatureSha256Hash(
packageManager,
appContext.getPackageName(),
ImmutableList.of(getSha256Hash(SIG1), getSha256Hash(SIG2)));

// THEN UID for package that doesn't have SIG1 or SIG2 will not be authorized
assertThat(policy.checkAuthorization(OTHER_UID).getCode())
.isEqualTo(Status.PERMISSION_DENIED.getCode());
}

private static byte[] getSha256Hash(Signature signature) {
return Hashing.sha256().hashBytes(signature.toByteArray()).asBytes();
}
}