diff --git a/binder/src/main/java/io/grpc/binder/SecurityPolicies.java b/binder/src/main/java/io/grpc/binder/SecurityPolicies.java index fc65fb11428..c9fc4d83f29 100644 --- a/binder/src/main/java/io/grpc/binder/SecurityPolicies.java +++ b/binder/src/main/java/io/grpc/binder/SecurityPolicies.java @@ -16,6 +16,9 @@ package io.grpc.binder; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + import android.annotation.SuppressLint; import android.app.admin.DevicePolicyManager; import android.content.Context; @@ -23,6 +26,7 @@ import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.Signature; +import android.content.pm.SigningInfo; import android.os.Build; import android.os.Build.VERSION; import android.os.Process; @@ -32,6 +36,8 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.hash.Hashing; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; import com.google.errorprone.annotations.CheckReturnValue; import io.grpc.ExperimentalApi; import io.grpc.Status; @@ -40,6 +46,7 @@ import java.util.Collection; import java.util.Iterator; import java.util.List; +import java.util.concurrent.Executor; /** Static factory methods for creating standard security policies. */ @CheckReturnValue @@ -97,6 +104,54 @@ public static SecurityPolicy hasSignature( packageManager, packageName, ImmutableList.of(requiredSignature)); } + /** + * Creates a {@link SecurityPolicy} which checks if the given package is signed with the same + * certificate that the current Android platform was signed with. + * + * @param packageName the package name of the allowed package. + * @param backgroundExecutor an executor suitable for blocking I/O. + * @throws NullPointerException if any of the inputs are {@code null}. + */ + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/11238") + public static AsyncSecurityPolicy hasSameSignatureAsPlatform( + PackageManager packageManager, String packageName, Executor backgroundExecutor) { + return new AsyncSecurityPolicy() { + @Override + public ListenableFuture checkAuthorizationAsync(int uid) { + return Futures.submit( + () -> { + Signature[] platformSignatures = getPlatformSignatures(packageManager); + + return oneOfSignatures( + packageManager, + packageName, + ImmutableList.copyOf(platformSignatures)) + .checkAuthorization(uid); + }, + backgroundExecutor); + } + }; + } + + private static Signature[] getPlatformSignatures(PackageManager packageManager) { + try { + PackageInfo packageInfo; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + packageInfo = + packageManager.getPackageInfo("android", PackageManager.GET_SIGNING_CERTIFICATES); + SigningInfo signingInfo = checkNotNull(packageInfo.signingInfo); + return signingInfo.hasMultipleSigners() + ? signingInfo.getApkContentsSigners() + : signingInfo.getSigningCertificateHistory(); + } else { + packageInfo = packageManager.getPackageInfo("android", PackageManager.GET_SIGNATURES); + return checkNotNull(packageInfo.signatures); + } + } catch (NameNotFoundException nnfe) { + throw new AssertionError(nnfe); // impossible: the "android" package is always present + } + } + /** * Creates {@link SecurityPolicy} which checks if the SHA-256 hash of the package signature * matches {@code requiredSignatureSha256Hash}. diff --git a/binder/src/test/java/io/grpc/binder/SecurityPoliciesTest.java b/binder/src/test/java/io/grpc/binder/SecurityPoliciesTest.java index 02ff5e059e1..c606eab2fc5 100644 --- a/binder/src/test/java/io/grpc/binder/SecurityPoliciesTest.java +++ b/binder/src/test/java/io/grpc/binder/SecurityPoliciesTest.java @@ -30,11 +30,14 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.Signature; +import android.content.pm.SigningInfo; +import android.os.Build; import android.os.Process; 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 com.google.common.util.concurrent.MoreExecutors; import io.grpc.Status; import java.util.HashMap; import java.util.concurrent.atomic.AtomicInteger; @@ -42,6 +45,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; +import org.robolectric.shadows.ShadowSigningInfo; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @@ -84,6 +88,12 @@ private void installPackages(int uid, PackageInfo... packageInfo) { shadowOf(packageManager).setPackagesForUid(uid, packageNames); } + private void setupPlatformSignature(Signature... signatures) { + PackageInfo platformPackageInfo = + newBuilder().setPackageName("android").setSignatures(signatures).build(); + installPackages(Process.SYSTEM_UID, platformPackageInfo); + } + @Test public void testInternalOnly() throws Exception { policy = SecurityPolicies.internalOnly(); @@ -151,6 +161,68 @@ public void testHasSignature_failsIfSignatureDoesNotMatch() throws Exception { .isEqualTo(Status.PERMISSION_DENIED.getCode()); } + @Test + @Config(sdk = {Build.VERSION_CODES.O_MR1, Build.VERSION_CODES.P}) + public void testHasSameSignatureAsPlatform_succeedsIfSignaturesMatch() { + setupPlatformSignature(SIG2, SIG1); + PackageInfo info = + newBuilder().setPackageName(OTHER_UID_PACKAGE_NAME).setSignatures(SIG1).build(); + installPackages(OTHER_UID, info); + + policy = + SecurityPolicies.hasSameSignatureAsPlatform( + packageManager, OTHER_UID_PACKAGE_NAME, MoreExecutors.newDirectExecutorService()); + + assertThat(policy.checkAuthorization(OTHER_UID).getCode()).isEqualTo(Status.OK.getCode()); + } + + @Test + @Config(sdk = {Build.VERSION_CODES.O_MR1, Build.VERSION_CODES.P}) + public void testHasSameSignatureAsPlatform_failsIfPackageNameDoesNotMatch() { + setupPlatformSignature(SIG1); + PackageInfo info = + newBuilder() + .setPackageName(OTHER_UID_SAME_SIGNATURE_PACKAGE_NAME) + .setSignatures(SIG1) + .build(); + installPackages(OTHER_UID_SAME_SIGNATURE, info); + + policy = + SecurityPolicies.hasSameSignatureAsPlatform( + packageManager, appContext.getPackageName(), MoreExecutors.newDirectExecutorService()); + + assertThat(policy.checkAuthorization(OTHER_UID_SAME_SIGNATURE).getCode()) + .isEqualTo(Status.PERMISSION_DENIED.getCode()); + } + + @Test + @Config(sdk = {Build.VERSION_CODES.O_MR1, Build.VERSION_CODES.P}) + public void testHasSameSignatureAsPlatform_failsIfSignatureDoesNotMatch() { + setupPlatformSignature(SIG1); + PackageInfo info = + newBuilder().setPackageName(OTHER_UID_PACKAGE_NAME).setSignatures(SIG2).build(); + installPackages(OTHER_UID, info); + + policy = + SecurityPolicies.hasSameSignatureAsPlatform( + packageManager, OTHER_UID_PACKAGE_NAME, MoreExecutors.newDirectExecutorService()); + + assertThat(policy.checkAuthorization(OTHER_UID).getCode()) + .isEqualTo(Status.PERMISSION_DENIED.getCode()); + } + + @Test + @Config(sdk = {Build.VERSION_CODES.O_MR1, Build.VERSION_CODES.P}) + public void testHasSameSignatureAsPlatform_failsIfUidUnknown() { + setupPlatformSignature(SIG1); + policy = + SecurityPolicies.hasSameSignatureAsPlatform( + packageManager, appContext.getPackageName(), MoreExecutors.newDirectExecutorService()); + + assertThat(policy.checkAuthorization(OTHER_UID_UNKNOWN).getCode()) + .isEqualTo(Status.UNAUTHENTICATED.getCode()); + } + @Test public void testOneOfSignatures_succeedsIfPackageNameAndSignaturesMatch() throws Exception { @@ -500,6 +572,13 @@ public PackageInfo build() { if (this.signatures != null) { packageInfo.signatures = this.signatures; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + SigningInfo signingInfo = new SigningInfo(); + ShadowSigningInfo shadowSigningInfo = shadowOf(signingInfo); + shadowSigningInfo.setSignatures(this.signatures); + packageInfo.signingInfo = signingInfo; + } } if (!this.permissions.isEmpty()) {