Skip to content

Commit 746501d

Browse files
authored
binder: SecurityPolicy updates (take 2). (#8637)
The previous attempt at this CL relied on guava's Hashing class which is still in beta. This update compares Signature objects directly instead of SHA256 hashs, removing the need for the Hashing class. Add additional comments to the security policy class, to mention that implementing new policies requires significant care. With that in mind, add security policies to check the peer app's signature, so people can create cross-app communication without having to implement their own policy. Finally, add the UntrustedSecurityPolicies class, since that's inevitably a policy which is sometimes needed.
1 parent a46560e commit 746501d

File tree

4 files changed

+302
-0
lines changed

4 files changed

+302
-0
lines changed

binder/src/main/java/io/grpc/binder/SecurityPolicies.java

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,20 @@
1616

1717
package io.grpc.binder;
1818

19+
import android.annotation.SuppressLint;
20+
import android.content.pm.PackageInfo;
21+
import android.content.pm.PackageManager;
22+
import android.content.pm.PackageManager.NameNotFoundException;
23+
import android.content.pm.Signature;
24+
import android.os.Build;
1925
import android.os.Process;
26+
import com.google.common.base.Preconditions;
27+
import com.google.common.collect.ImmutableList;
2028
import io.grpc.ExperimentalApi;
2129
import io.grpc.Status;
30+
import java.util.Arrays;
31+
import java.util.Collection;
32+
import java.util.List;
2233
import javax.annotation.CheckReturnValue;
2334

2435
/** Static factory methods for creating standard security policies. */
@@ -55,4 +66,125 @@ public Status checkAuthorization(int uid) {
5566
}
5667
};
5768
}
69+
70+
/**
71+
* Creates a {@link SecurityPolicy} which checks if the package signature
72+
* matches {@code requiredSignature}.
73+
*
74+
* @param packageName the package name of the allowed package.
75+
* @param requiredSignature the allowed signature of the allowed package.
76+
* @throws NullPointerException if any of the inputs are {@code null}.
77+
*/
78+
public static SecurityPolicy hasSignature(
79+
PackageManager packageManager, String packageName, Signature requiredSignature) {
80+
return oneOfSignatures(
81+
packageManager, packageName, ImmutableList.of(requiredSignature));
82+
}
83+
84+
/**
85+
* Creates a {@link SecurityPolicy} which checks if the package signature
86+
* matches any of {@code requiredSignatures}.
87+
*
88+
* @param packageName the package name of the allowed package.
89+
* @param requiredSignatures the allowed signatures of the allowed package.
90+
* @throws NullPointerException if any of the inputs are {@code null}.
91+
* @throws IllegalArgumentException if {@code requiredSignatures} is empty.
92+
*/
93+
public static SecurityPolicy oneOfSignatures(
94+
PackageManager packageManager,
95+
String packageName,
96+
Collection<Signature> requiredSignatures) {
97+
Preconditions.checkNotNull(packageManager, "packageManager");
98+
Preconditions.checkNotNull(packageName, "packageName");
99+
Preconditions.checkNotNull(requiredSignatures, "requiredSignatures");
100+
Preconditions.checkArgument(!requiredSignatures.isEmpty(),
101+
"requiredSignatures");
102+
ImmutableList<Signature> requiredSignaturesImmutable = ImmutableList.copyOf(requiredSignatures);
103+
104+
for (Signature requiredSignature : requiredSignaturesImmutable) {
105+
Preconditions.checkNotNull(requiredSignature);
106+
}
107+
108+
return new SecurityPolicy() {
109+
@Override
110+
public Status checkAuthorization(int uid) {
111+
return checkUidSignature(
112+
packageManager, uid, packageName, requiredSignaturesImmutable);
113+
}
114+
};
115+
}
116+
117+
private static Status checkUidSignature(
118+
PackageManager packageManager,
119+
int uid,
120+
String packageName,
121+
ImmutableList<Signature> requiredSignatures) {
122+
String[] packages = packageManager.getPackagesForUid(uid);
123+
if (packages == null) {
124+
return Status.UNAUTHENTICATED.withDescription(
125+
"Rejected by signature check security policy");
126+
}
127+
boolean packageNameMatched = false;
128+
for (String pkg : packages) {
129+
if (!packageName.equals(pkg)) {
130+
continue;
131+
}
132+
packageNameMatched = true;
133+
if (checkPackageSignature(packageManager, pkg, requiredSignatures)) {
134+
return Status.OK;
135+
}
136+
}
137+
return Status.PERMISSION_DENIED.withDescription(
138+
"Rejected by signature check security policy. Package name matched: "
139+
+ packageNameMatched);
140+
}
141+
142+
/**
143+
* Checks if the signature of {@code packageName} matches one of the given signatures.
144+
*
145+
* @param packageName the package to be checked
146+
* @param requiredSignatures list of signatures.
147+
* @return {@code true} if {@code packageName} has a matching signature.
148+
*/
149+
@SuppressWarnings("deprecation") // For PackageInfo.signatures
150+
@SuppressLint("PackageManagerGetSignatures") // We only allow 1 signature.
151+
private static boolean checkPackageSignature(
152+
PackageManager packageManager,
153+
String packageName,
154+
ImmutableList<Signature> requiredSignatures) {
155+
PackageInfo packageInfo;
156+
try {
157+
if (Build.VERSION.SDK_INT >= 28) {
158+
packageInfo =
159+
packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES);
160+
if (packageInfo.signingInfo == null) {
161+
return false;
162+
}
163+
Signature[] signatures =
164+
packageInfo.signingInfo.hasMultipleSigners()
165+
? packageInfo.signingInfo.getApkContentsSigners()
166+
: packageInfo.signingInfo.getSigningCertificateHistory();
167+
168+
for (Signature signature : signatures) {
169+
if (requiredSignatures.contains(signature)) {
170+
return true;
171+
}
172+
}
173+
} else {
174+
packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
175+
if (packageInfo.signatures == null || packageInfo.signatures.length != 1) {
176+
// Reject multiply-signed apks because of b/13678484
177+
// (See PackageManagerGetSignatures supression above).
178+
return false;
179+
}
180+
181+
if (requiredSignatures.contains(packageInfo.signatures[0])) {
182+
return true;
183+
}
184+
}
185+
} catch (NameNotFoundException nnfe) {
186+
return false;
187+
}
188+
return false;
189+
}
58190
}

binder/src/main/java/io/grpc/binder/SecurityPolicy.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@
2323
/**
2424
* Decides whether a given Android UID is authorized to access some resource.
2525
*
26+
* While it's possible to extend this class to define your own policy, it's strongly
27+
* recommended that you only use the policies provided by the {@link SecurityPolicies} or
28+
* {@link UntrustedSecurityPolicies} classes. Implementing your own security policy requires
29+
* significant care, and an understanding of the details and pitfalls of Android security.
30+
*
2631
* <p><b>IMPORTANT</b> For any concrete extensions of this class, it's assumed that the
2732
* authorization status of a given UID will <b>not</b> change as long as a process with that UID is
2833
* alive.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2021 The gRPC Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.grpc.binder;
18+
19+
import io.grpc.ExperimentalApi;
20+
import io.grpc.Status;
21+
import javax.annotation.CheckReturnValue;
22+
23+
/**
24+
* Static factory methods for creating untrusted security policies.
25+
*/
26+
@CheckReturnValue
27+
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/8022")
28+
public final class UntrustedSecurityPolicies {
29+
30+
private UntrustedSecurityPolicies() {}
31+
32+
/**
33+
* Return a security policy which allows any peer on device.
34+
* Servers should only use this policy if they intend to expose
35+
* a service to all applications on device.
36+
* Clients should only use this policy if they don't need to trust the
37+
* application they're connecting to.
38+
*/
39+
public static SecurityPolicy untrustedPublic() {
40+
return new SecurityPolicy() {
41+
@Override
42+
public Status checkAuthorization(int uid) {
43+
return Status.OK;
44+
}
45+
};
46+
}
47+
}

binder/src/test/java/io/grpc/binder/SecurityPoliciesTest.java

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,64 @@
1717
package io.grpc.binder;
1818

1919
import static com.google.common.truth.Truth.assertThat;
20+
import static org.robolectric.Shadows.shadowOf;
2021

22+
import android.content.Context;
23+
import android.content.pm.PackageInfo;
24+
import android.content.pm.PackageManager;
25+
import android.content.pm.Signature;
2126
import android.os.Process;
27+
import androidx.test.core.app.ApplicationProvider;
28+
import com.google.common.collect.ImmutableList;
2229
import io.grpc.Status;
30+
import io.grpc.binder.SecurityPolicy;
31+
import org.junit.Before;
2332
import org.junit.Test;
2433
import org.junit.runner.RunWith;
2534
import org.robolectric.RobolectricTestRunner;
2635

2736
@RunWith(RobolectricTestRunner.class)
2837
public final class SecurityPoliciesTest {
38+
2939
private static final int MY_UID = Process.myUid();
3040
private static final int OTHER_UID = MY_UID + 1;
41+
private static final int OTHER_UID_SAME_SIGNATURE = MY_UID + 2;
42+
private static final int OTHER_UID_NO_SIGNATURE = MY_UID + 3;
43+
private static final int OTHER_UID_UNKNOWN = MY_UID + 4;
3144

3245
private static final String PERMISSION_DENIED_REASONS = "some reasons";
3346

47+
private static final Signature SIG1 = new Signature("1234");
48+
private static final Signature SIG2 = new Signature("4321");
49+
50+
private static final String OTHER_UID_PACKAGE_NAME = "other.package";
51+
private static final String OTHER_UID_SAME_SIGNATURE_PACKAGE_NAME = "other.package.samesignature";
52+
private static final String OTHER_UID_NO_SIGNATURE_PACKAGE_NAME = "other.package.nosignature";
53+
54+
private Context appContext;
55+
private PackageManager packageManager;
56+
3457
private SecurityPolicy policy;
3558

59+
@Before
60+
public void setUp() {
61+
appContext = ApplicationProvider.getApplicationContext();
62+
packageManager = appContext.getPackageManager();
63+
installPackage(MY_UID, appContext.getPackageName(), SIG1);
64+
installPackage(OTHER_UID, OTHER_UID_PACKAGE_NAME, SIG2);
65+
installPackage(OTHER_UID_SAME_SIGNATURE, OTHER_UID_SAME_SIGNATURE_PACKAGE_NAME, SIG1);
66+
installPackage(OTHER_UID_NO_SIGNATURE, OTHER_UID_NO_SIGNATURE_PACKAGE_NAME);
67+
}
68+
69+
@SuppressWarnings("deprecation")
70+
private void installPackage(int uid, String packageName, Signature... signatures) {
71+
PackageInfo info = new PackageInfo();
72+
info.packageName = packageName;
73+
info.signatures = signatures;
74+
shadowOf(packageManager).installPackage(info);
75+
shadowOf(packageManager).setPackagesForUid(uid, packageName);
76+
}
77+
3678
@Test
3779
public void testInternalOnly() throws Exception {
3880
policy = SecurityPolicies.internalOnly();
@@ -53,4 +95,80 @@ public void testPermissionDenied() throws Exception {
5395
assertThat(policy.checkAuthorization(OTHER_UID).getDescription())
5496
.isEqualTo(PERMISSION_DENIED_REASONS);
5597
}
98+
99+
@Test
100+
public void testHasSignature_succeedsIfPackageNameAndSignaturesMatch()
101+
throws Exception {
102+
policy = SecurityPolicies.hasSignature(packageManager, OTHER_UID_PACKAGE_NAME, SIG2);
103+
104+
// THEN UID for package that has SIG2 will be authorized
105+
assertThat(policy.checkAuthorization(OTHER_UID).getCode()).isEqualTo(Status.OK.getCode());
106+
}
107+
108+
@Test
109+
public void testHasSignature_failsIfPackageNameDoesNotMatch() throws Exception {
110+
policy = SecurityPolicies.hasSignature(packageManager, appContext.getPackageName(), SIG1);
111+
112+
// THEN UID for package that has SIG1 but different package name will not be authorized
113+
assertThat(policy.checkAuthorization(OTHER_UID_SAME_SIGNATURE).getCode())
114+
.isEqualTo(Status.PERMISSION_DENIED.getCode());
115+
}
116+
117+
@Test
118+
public void testHasSignature_failsIfSignatureDoesNotMatch() throws Exception {
119+
policy = SecurityPolicies.hasSignature(packageManager, OTHER_UID_PACKAGE_NAME, SIG1);
120+
121+
// THEN UID for package that doesn't have SIG1 will not be authorized
122+
assertThat(policy.checkAuthorization(OTHER_UID).getCode())
123+
.isEqualTo(Status.PERMISSION_DENIED.getCode());
124+
}
125+
126+
@Test
127+
public void testOneOfSignatures_succeedsIfPackageNameAndSignaturesMatch()
128+
throws Exception {
129+
policy =
130+
SecurityPolicies.oneOfSignatures(
131+
packageManager, OTHER_UID_PACKAGE_NAME, ImmutableList.of(SIG2));
132+
133+
// THEN UID for package that has SIG2 will be authorized
134+
assertThat(policy.checkAuthorization(OTHER_UID).getCode()).isEqualTo(Status.OK.getCode());
135+
}
136+
137+
@Test
138+
public void testOneOfSignature_failsIfAllSignaturesDoNotMatch() throws Exception {
139+
policy =
140+
SecurityPolicies.oneOfSignatures(
141+
packageManager,
142+
appContext.getPackageName(),
143+
ImmutableList.of(SIG1, new Signature("1314")));
144+
145+
// THEN UID for package that has SIG1 but different package name will not be authorized
146+
assertThat(policy.checkAuthorization(OTHER_UID_SAME_SIGNATURE).getCode())
147+
.isEqualTo(Status.PERMISSION_DENIED.getCode());
148+
}
149+
150+
@Test
151+
public void testOneOfSignature_succeedsIfPackageNameAndOneOfSignaturesMatch()
152+
throws Exception {
153+
policy =
154+
SecurityPolicies.oneOfSignatures(
155+
packageManager,
156+
OTHER_UID_PACKAGE_NAME,
157+
ImmutableList.of(SIG1, SIG2));
158+
159+
// THEN UID for package that has SIG2 will be authorized
160+
assertThat(policy.checkAuthorization(OTHER_UID).getCode()).isEqualTo(Status.OK.getCode());
161+
}
162+
163+
@Test
164+
public void testHasSignature_failsIfUidUnknown() throws Exception {
165+
policy =
166+
SecurityPolicies.hasSignature(
167+
packageManager,
168+
appContext.getPackageName(),
169+
SIG1);
170+
171+
assertThat(policy.checkAuthorization(OTHER_UID_UNKNOWN).getCode())
172+
.isEqualTo(Status.UNAUTHENTICATED.getCode());
173+
}
56174
}

0 commit comments

Comments
 (0)