Skip to content

Commit a684359

Browse files
committed
binder: Optionally pre-authorize servers using their PackageManager identity before binding.
1 parent 17b3d84 commit a684359

10 files changed

Lines changed: 304 additions & 42 deletions

File tree

binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java

Lines changed: 73 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,8 @@
5858
import java.io.InputStream;
5959
import java.util.ArrayDeque;
6060
import java.util.Deque;
61-
import java.util.concurrent.BlockingQueue;
6261
import java.util.concurrent.ExecutorService;
6362
import java.util.concurrent.Executors;
64-
import java.util.concurrent.LinkedBlockingQueue;
6563
import java.util.concurrent.ScheduledExecutorService;
6664
import java.util.concurrent.TimeUnit;
6765
import javax.annotation.Nullable;
@@ -102,7 +100,7 @@ public final class BinderClientTransportTest {
102100

103101
AndroidComponentAddress serverAddress;
104102
BinderTransport.BinderClientTransport transport;
105-
BlockingSecurityPolicy blockingSecurityPolicy = new BlockingSecurityPolicy();
103+
SettableAsyncSecurityPolicy blockingSecurityPolicy = new SettableAsyncSecurityPolicy();
106104

107105
private final ObjectPool<ScheduledExecutorService> executorServicePool =
108106
new FixedObjectPool<>(Executors.newScheduledThreadPool(1));
@@ -170,6 +168,11 @@ public BinderClientTransportBuilder setReadyTimeoutMillis(int timeoutMillis) {
170168
return this;
171169
}
172170

171+
public BinderClientTransportBuilder setPreAuthorizeServer(boolean preAuthorizeServer) {
172+
factoryBuilder.setPreAuthorizeServers(preAuthorizeServer);
173+
return this;
174+
}
175+
173176
public BinderTransport.BinderClientTransport build() {
174177
return factoryBuilder
175178
.buildClientTransportFactory()
@@ -179,7 +182,7 @@ public BinderTransport.BinderClientTransport build() {
179182

180183
@After
181184
public void tearDown() throws Exception {
182-
blockingSecurityPolicy.provideNextCheckAuthorizationResult(Status.ABORTED);
185+
blockingSecurityPolicy.setAuthorizationResult(Status.ABORTED);
183186
transport.shutdownNow(Status.OK);
184187
HostServices.awaitServiceShutdown();
185188
shutdownAndTerminate(executorServicePool.getObject());
@@ -285,16 +288,16 @@ public void testMessageProducerClosedAfterStream_b169313545() throws Exception {
285288
@Test
286289
public void testNewStreamBeforeTransportReadyFails() throws Exception {
287290
// Use a special SecurityPolicy that lets us act before the transport is setup/ready.
288-
transport =
289-
new BinderClientTransportBuilder().setSecurityPolicy(blockingSecurityPolicy).build();
291+
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
292+
transport = new BinderClientTransportBuilder().setSecurityPolicy(securityPolicy).build();
290293
transport.start(transportListener).run();
291294
ClientStream stream =
292295
transport.newStream(streamingMethodDesc, new Metadata(), CallOptions.DEFAULT, tracers);
293296
stream.start(streamListener);
294297
assertThat(streamListener.awaitClose().getCode()).isEqualTo(Code.INTERNAL);
295298

296299
// Unblock the SETUP_TRANSPORT handshake and make sure it becomes ready in the usual way.
297-
blockingSecurityPolicy.provideNextCheckAuthorizationResult(Status.OK);
300+
securityPolicy.setAuthorizationResult(Status.OK);
298301
transportListener.awaitReady();
299302
}
300303

@@ -370,24 +373,60 @@ public void testBlackHoleEndpointConnectTimeout() throws Exception {
370373
}
371374

372375
@Test
373-
public void testBlackHoleSecurityPolicyConnectTimeout() throws Exception {
376+
public void testBlackHoleSecurityPolicyAuthTimeout() throws Exception {
377+
transport =
378+
new BinderClientTransportBuilder()
379+
.setSecurityPolicy(blockingSecurityPolicy)
380+
.setPreAuthorizeServer(false)
381+
.setReadyTimeoutMillis(1_234)
382+
.build();
383+
transport.start(transportListener).run();
384+
Status transportStatus = transportListener.awaitShutdown();
385+
assertThat(transportStatus.getCode()).isEqualTo(Code.DEADLINE_EXCEEDED);
386+
assertThat(transportStatus.getDescription()).contains("1234");
387+
transportListener.awaitTermination();
388+
}
389+
390+
@Test
391+
public void testBlackHoleSecurityPolicyPreAuthTimeout() throws Exception {
374392
transport =
375393
new BinderClientTransportBuilder()
376394
.setSecurityPolicy(blockingSecurityPolicy)
395+
.setPreAuthorizeServer(true)
377396
.setReadyTimeoutMillis(1_234)
378397
.build();
379398
transport.start(transportListener).run();
380399
Status transportStatus = transportListener.awaitShutdown();
381400
assertThat(transportStatus.getCode()).isEqualTo(Code.DEADLINE_EXCEEDED);
382401
assertThat(transportStatus.getDescription()).contains("1234");
383402
transportListener.awaitTermination();
384-
blockingSecurityPolicy.provideNextCheckAuthorizationResult(Status.OK);
385403
}
386404

387405
@Test
388-
public void testAsyncSecurityPolicyFailure() throws Exception {
406+
public void testAsyncSecurityPolicyAuthFailure() throws Exception {
389407
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
390-
transport = new BinderClientTransportBuilder().setSecurityPolicy(securityPolicy).build();
408+
transport =
409+
new BinderClientTransportBuilder()
410+
.setPreAuthorizeServer(false)
411+
.setSecurityPolicy(securityPolicy)
412+
.build();
413+
RuntimeException exception = new NullPointerException();
414+
securityPolicy.setAuthorizationException(exception);
415+
transport.start(transportListener).run();
416+
Status transportStatus = transportListener.awaitShutdown();
417+
assertThat(transportStatus.getCode()).isEqualTo(Code.INTERNAL);
418+
assertThat(transportStatus.getCause()).isEqualTo(exception);
419+
transportListener.awaitTermination();
420+
}
421+
422+
@Test
423+
public void testAsyncSecurityPolicyPreAuthFailure() throws Exception {
424+
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
425+
transport =
426+
new BinderClientTransportBuilder()
427+
.setPreAuthorizeServer(true)
428+
.setSecurityPolicy(securityPolicy)
429+
.build();
391430
RuntimeException exception = new NullPointerException();
392431
securityPolicy.setAuthorizationException(exception);
393432
transport.start(transportListener).run();
@@ -400,11 +439,32 @@ public void testAsyncSecurityPolicyFailure() throws Exception {
400439
@Test
401440
public void testAsyncSecurityPolicySuccess() throws Exception {
402441
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
403-
transport = new BinderClientTransportBuilder().setSecurityPolicy(securityPolicy).build();
404-
securityPolicy.setAuthorizationResult(Status.PERMISSION_DENIED);
442+
transport =
443+
new BinderClientTransportBuilder()
444+
.setPreAuthorizeServer(false)
445+
.setSecurityPolicy(securityPolicy)
446+
.build();
447+
securityPolicy.setAuthorizationResult(Status.PERMISSION_DENIED.withDescription("xyzzy"));
448+
transport.start(transportListener).run();
449+
Status transportStatus = transportListener.awaitShutdown();
450+
assertThat(transportStatus.getCode()).isEqualTo(Code.PERMISSION_DENIED);
451+
assertThat(transportStatus.getDescription()).contains("xyzzy");
452+
transportListener.awaitTermination();
453+
}
454+
455+
@Test
456+
public void testAsyncSecurityPolicyPreAuthSuccess() throws Exception {
457+
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
458+
transport =
459+
new BinderClientTransportBuilder()
460+
.setPreAuthorizeServer(true)
461+
.setSecurityPolicy(securityPolicy)
462+
.build();
463+
securityPolicy.setAuthorizationResult(Status.PERMISSION_DENIED.withDescription("xyzzy"));
405464
transport.start(transportListener).run();
406465
Status transportStatus = transportListener.awaitShutdown();
407466
assertThat(transportStatus.getCode()).isEqualTo(Code.PERMISSION_DENIED);
467+
assertThat(transportStatus.getDescription()).contains("xyzzy");
408468
transportListener.awaitTermination();
409469
}
410470

@@ -548,26 +608,6 @@ public synchronized void closed(Status status, RpcProgress rpcProgress, Metadata
548608
}
549609
}
550610

551-
/**
552-
* A SecurityPolicy that blocks the transport authorization check until a test sets the outcome.
553-
*/
554-
static class BlockingSecurityPolicy extends SecurityPolicy {
555-
private final BlockingQueue<Status> results = new LinkedBlockingQueue<>();
556-
557-
public void provideNextCheckAuthorizationResult(Status status) {
558-
results.add(status);
559-
}
560-
561-
@Override
562-
public Status checkAuthorization(int uid) {
563-
try {
564-
return results.take();
565-
} catch (InterruptedException e) {
566-
return Status.fromThrowable(e);
567-
}
568-
}
569-
}
570-
571611
/** An AsyncSecurityPolicy that lets a test specify the outcome of checkAuthorizationAsync(). */
572612
static class SettableAsyncSecurityPolicy extends AsyncSecurityPolicy {
573613
private SettableFuture<Status> result = SettableFuture.create();

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

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

1919
import android.content.Intent;
20+
import android.content.pm.ServiceInfo;
2021
import android.os.UserHandle;
22+
import io.grpc.Attributes;
2123
import io.grpc.ExperimentalApi;
2224
import io.grpc.NameResolver;
2325

@@ -43,4 +45,22 @@ private ApiConstants() {}
4345
*/
4446
public static final NameResolver.Args.Key<UserHandle> TARGET_ANDROID_USER =
4547
NameResolver.Args.Key.create("target-android-user");
48+
49+
/**
50+
* Marks an {@link io.grpc.EquivalentAddressGroup} as needing pre-authorization.
51+
*
52+
* <p>Clients should authorize servers before connecting to them, but older versions of the binder
53+
* transport didn't do so. While this important extra security check is now possible (see {@link
54+
* BinderChannelBuilder#preAuthorizeServers(boolean)}, it remains optional, because it's a slight
55+
* behavior change and has a small performance cost and we don't want to break existing apps.
56+
*/
57+
public static final Attributes.Key<Void> PRE_AUTH_REQUIRED =
58+
Attributes.Key.create("pre-auth-required");
59+
60+
/**
61+
* The authentic ServiceInfo for an {@link io.grpc.EquivalentAddressGroup} of {@link
62+
* AndroidComponentAddress}es, in case a {@link NameResolver} has already looked it up.
63+
*/
64+
public static final Attributes.Key<ServiceInfo> TARGET_SERVICE_INFO =
65+
Attributes.Key.create("target-service-info");
4666
}

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ public static BinderChannelBuilder forTarget(String target) {
158158
}
159159

160160
private final ManagedChannelImplBuilder managedChannelImplBuilder;
161-
private final BinderClientTransportFactory.Builder transportFactoryBuilder;
161+
final BinderClientTransportFactory.Builder transportFactoryBuilder;
162162

163163
private boolean strictLifecycleManagement;
164164

@@ -179,6 +179,8 @@ private BinderChannelBuilder(
179179
} else {
180180
managedChannelImplBuilder =
181181
new ManagedChannelImplBuilder(target, transportFactoryBuilder, null);
182+
183+
preAuthorizeServers(true); // Pre-authorize resolved addresses by default.
182184
}
183185
idleTimeout(60, TimeUnit.SECONDS);
184186
}
@@ -279,6 +281,29 @@ public BinderChannelBuilder strictLifecycleManagement() {
279281
return this;
280282
}
281283

284+
/**
285+
* Checks server addresses against this channel's {@link SecurityPolicy} *before* connecting.
286+
*
287+
* <p>Android users can be tricked into installing a malicious app with the same package name as a
288+
* legitimate server. That's why we don't send calls to a server until it has been authorized by
289+
* an appropriate {@link SecurityPolicy}. But merely connecting to a malicious server can enable
290+
* "keep-alive" and "background activity launch" attacks, even if that server is ultimately
291+
* disallowed by the channel's security policy. Pre-authorization is even more important for
292+
* security when the server's address isn't known in advance but rather resolved via target URI or
293+
* discovered by other means.
294+
*
295+
* <p>Pre-authorization is strongly recommended but it remains optional for now because of the
296+
* small performance cost and because it precludes servers that proxy their "endpoint binder"
297+
* through another (unauthorized) app (not recommended, but technically a behavior change).
298+
*
299+
* <p>The default value of this property is false but it will become true in a future release.
300+
* Clients that require a particular behavior should configure it explicitly using this method.
301+
*/
302+
public BinderChannelBuilder preAuthorizeServers(boolean preAuthorize) {
303+
transportFactoryBuilder.setPreAuthorizeServers(preAuthorize);
304+
return this;
305+
}
306+
282307
@Override
283308
public BinderChannelBuilder idleTimeout(long value, TimeUnit unit) {
284309
checkState(

binder/src/main/java/io/grpc/binder/internal/Bindable.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package io.grpc.binder.internal;
1818

19+
import android.content.pm.ServiceInfo;
1920
import android.os.IBinder;
2021
import androidx.annotation.AnyThread;
2122
import androidx.annotation.MainThread;
@@ -45,6 +46,10 @@ interface Observer {
4546
void onUnbound(Status reason);
4647
}
4748

49+
/** Fetches details about the remote service from PackageManager *before* binding to it. */
50+
@AnyThread
51+
ServiceInfo resolve();
52+
4853
/**
4954
* Attempt to bind with the remote service.
5055
*

binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public final class BinderClientTransportFactory implements ClientTransportFactor
5555
final InboundParcelablePolicy inboundParcelablePolicy;
5656
final OneWayBinderProxy.Decorator binderDecorator;
5757
final long readyTimeoutMillis;
58+
final boolean preAuthorizeServers;
5859

5960
ScheduledExecutorService executorService;
6061
Executor offloadExecutor;
@@ -75,6 +76,7 @@ private BinderClientTransportFactory(Builder builder) {
7576
inboundParcelablePolicy = checkNotNull(builder.inboundParcelablePolicy);
7677
binderDecorator = checkNotNull(builder.binderDecorator);
7778
readyTimeoutMillis = builder.readyTimeoutMillis;
79+
preAuthorizeServers = builder.preAuthorizeServers;
7880

7981
executorService = scheduledExecutorPool.getObject();
8082
offloadExecutor = offloadExecutorPool.getObject();
@@ -128,6 +130,7 @@ public static final class Builder implements ClientTransportFactoryBuilder {
128130
InboundParcelablePolicy inboundParcelablePolicy = InboundParcelablePolicy.DEFAULT;
129131
OneWayBinderProxy.Decorator binderDecorator = OneWayBinderProxy.IDENTITY_DECORATOR;
130132
long readyTimeoutMillis = 60_000;
133+
boolean preAuthorizeServers;
131134

132135
@Override
133136
public BinderClientTransportFactory buildClientTransportFactory() {
@@ -216,5 +219,16 @@ public Builder setReadyTimeoutMillis(long readyTimeoutMillis) {
216219
this.readyTimeoutMillis = readyTimeoutMillis;
217220
return this;
218221
}
222+
223+
/** Whether to check server addresses against the SecurityPolicy before binding to them. */
224+
public Builder setPreAuthorizeServers(boolean preAuthorizeServers) {
225+
this.preAuthorizeServers = preAuthorizeServers;
226+
return this;
227+
}
228+
229+
/** Whether to check server addresses against the SecurityPolicy before binding to them. */
230+
public boolean getPreAuthorizeServers() {
231+
return this.preAuthorizeServers;
232+
}
219233
}
220234
}

0 commit comments

Comments
 (0)