Skip to content

binder: Introduce server pre-authorization #12127

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

Draft
wants to merge 35 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
3d03902
Create jules-setup.sh
jdcormie Mar 22, 2025
d31d488
Update jules-setup.sh
jdcormie Mar 22, 2025
4bf794c
Create jules-run-tests.sh
jdcormie Mar 22, 2025
3996eda
Update jules-setup.sh
jdcormie Mar 22, 2025
5107080
Update jules-setup.sh
jdcormie Mar 22, 2025
17b3d84
Merge branch 'grpc:master' into master
jdcormie Apr 11, 2025
a684359
binder: Optionally pre-authorize servers using their PackageManager i…
jdcormie May 2, 2025
7cc704d
Cut the pre-auth-required attribute for now.
jdcormie May 2, 2025
80c9b90
undo java format
jdcormie May 2, 2025
71943c6
undo default enabled
jdcormie May 2, 2025
af3ff6c
move/update the comment
jdcormie May 6, 2025
a0e76af
remove jules
jdcormie Jun 3, 2025
0c49469
javadoc
jdcormie Jun 3, 2025
bde880f
Leave TARGET_SERVICE_INFO for a follow up
jdcormie Jun 3, 2025
9ec685b
resolve() throws
jdcormie Jun 3, 2025
454eebb
Merge branch 'keep-alive' of https://github.com/jdcormie/grpc-java in…
jdcormie Jun 3, 2025
b2b006b
Test with and without preauth
jdcormie Jun 3, 2025
741ef76
ServiceBinding docs/tests
jdcormie Jun 3, 2025
2eb26ec
unused imports
jdcormie Jun 3, 2025
148457d
doc tweaks
jdcormie Jun 3, 2025
56c6f90
doc tweaks
jdcormie Jun 3, 2025
49ee878
java format
jdcormie Jun 3, 2025
2632951
comment
jdcormie Jun 3, 2025
5928c45
format
jdcormie Jun 3, 2025
daddd3c
hide reflection mess in a static method
jdcormie Jun 3, 2025
c2a7596
experimental
jdcormie Jun 3, 2025
6658220
Add pre-auth override EAG attr and tests
jdcormie Jun 10, 2025
bac0877
Don't commit to a roll out plan
jdcormie Jun 10, 2025
9281c2b
Add testResolveNonExistentServiceWithTargetuserThrows
jdcormie Jun 10, 2025
c34c815
java format
jdcormie Jun 10, 2025
295d537
builder required fields
jdcormie Jun 10, 2025
df0b824
order test for readability
jdcormie Jun 10, 2025
67c2c93
Don't set the ready timeout if we fail to resolve.
jdcormie Jun 10, 2025
87f1c13
unnecessary this
jdcormie Jun 11, 2025
206570c
undo startInternal to minimize diff
jdcormie Jun 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions binder/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ dependencies {
androidTestImplementation testFixtures(project(':grpc-core'))

testFixturesImplementation libraries.guava.testlib
testFixturesImplementation testFixtures(project(':grpc-core'))
}

import net.ltgt.gradle.errorprone.CheckSeverity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,8 @@
import java.io.InputStream;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
Expand Down Expand Up @@ -102,7 +100,7 @@ public final class BinderClientTransportTest {

AndroidComponentAddress serverAddress;
BinderTransport.BinderClientTransport transport;
BlockingSecurityPolicy blockingSecurityPolicy = new BlockingSecurityPolicy();
SettableAsyncSecurityPolicy blockingSecurityPolicy = new SettableAsyncSecurityPolicy();

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

public BinderClientTransportBuilder setPreAuthorizeServer(boolean preAuthorizeServer) {
factoryBuilder.setPreAuthorizeServers(preAuthorizeServer);
return this;
}

public BinderTransport.BinderClientTransport build() {
return factoryBuilder
.buildClientTransportFactory()
Expand All @@ -179,7 +182,7 @@ public BinderTransport.BinderClientTransport build() {

@After
public void tearDown() throws Exception {
blockingSecurityPolicy.provideNextCheckAuthorizationResult(Status.ABORTED);
blockingSecurityPolicy.setAuthorizationResult(Status.ABORTED);
transport.shutdownNow(Status.OK);
HostServices.awaitServiceShutdown();
shutdownAndTerminate(executorServicePool.getObject());
Expand Down Expand Up @@ -285,16 +288,16 @@ public void testMessageProducerClosedAfterStream_b169313545() throws Exception {
@Test
public void testNewStreamBeforeTransportReadyFails() throws Exception {
// Use a special SecurityPolicy that lets us act before the transport is setup/ready.
transport =
new BinderClientTransportBuilder().setSecurityPolicy(blockingSecurityPolicy).build();
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
transport = new BinderClientTransportBuilder().setSecurityPolicy(securityPolicy).build();
transport.start(transportListener).run();
ClientStream stream =
transport.newStream(streamingMethodDesc, new Metadata(), CallOptions.DEFAULT, tracers);
stream.start(streamListener);
assertThat(streamListener.awaitClose().getCode()).isEqualTo(Code.INTERNAL);

// Unblock the SETUP_TRANSPORT handshake and make sure it becomes ready in the usual way.
blockingSecurityPolicy.provideNextCheckAuthorizationResult(Status.OK);
securityPolicy.setAuthorizationResult(Status.OK);
transportListener.awaitReady();
}

Expand Down Expand Up @@ -370,24 +373,60 @@ public void testBlackHoleEndpointConnectTimeout() throws Exception {
}

@Test
public void testBlackHoleSecurityPolicyConnectTimeout() throws Exception {
public void testBlackHoleSecurityPolicyAuthTimeout() throws Exception {
transport =
new BinderClientTransportBuilder()
.setSecurityPolicy(blockingSecurityPolicy)
.setPreAuthorizeServer(false)
.setReadyTimeoutMillis(1_234)
.build();
transport.start(transportListener).run();
Status transportStatus = transportListener.awaitShutdown();
assertThat(transportStatus.getCode()).isEqualTo(Code.DEADLINE_EXCEEDED);
assertThat(transportStatus.getDescription()).contains("1234");
transportListener.awaitTermination();
}

@Test
public void testBlackHoleSecurityPolicyPreAuthTimeout() throws Exception {
transport =
new BinderClientTransportBuilder()
.setSecurityPolicy(blockingSecurityPolicy)
.setPreAuthorizeServer(true)
.setReadyTimeoutMillis(1_234)
.build();
transport.start(transportListener).run();
Status transportStatus = transportListener.awaitShutdown();
assertThat(transportStatus.getCode()).isEqualTo(Code.DEADLINE_EXCEEDED);
assertThat(transportStatus.getDescription()).contains("1234");
transportListener.awaitTermination();
blockingSecurityPolicy.provideNextCheckAuthorizationResult(Status.OK);
}

@Test
public void testAsyncSecurityPolicyFailure() throws Exception {
public void testAsyncSecurityPolicyAuthFailure() throws Exception {
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
transport = new BinderClientTransportBuilder().setSecurityPolicy(securityPolicy).build();
transport =
new BinderClientTransportBuilder()
.setPreAuthorizeServer(false)
.setSecurityPolicy(securityPolicy)
.build();
RuntimeException exception = new NullPointerException();
securityPolicy.setAuthorizationException(exception);
transport.start(transportListener).run();
Status transportStatus = transportListener.awaitShutdown();
assertThat(transportStatus.getCode()).isEqualTo(Code.INTERNAL);
assertThat(transportStatus.getCause()).isEqualTo(exception);
transportListener.awaitTermination();
}

@Test
public void testAsyncSecurityPolicyPreAuthFailure() throws Exception {
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
transport =
new BinderClientTransportBuilder()
.setPreAuthorizeServer(true)
.setSecurityPolicy(securityPolicy)
.build();
RuntimeException exception = new NullPointerException();
securityPolicy.setAuthorizationException(exception);
transport.start(transportListener).run();
Expand All @@ -400,11 +439,32 @@ public void testAsyncSecurityPolicyFailure() throws Exception {
@Test
public void testAsyncSecurityPolicySuccess() throws Exception {
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
transport = new BinderClientTransportBuilder().setSecurityPolicy(securityPolicy).build();
securityPolicy.setAuthorizationResult(Status.PERMISSION_DENIED);
transport =
new BinderClientTransportBuilder()
.setPreAuthorizeServer(false)
.setSecurityPolicy(securityPolicy)
.build();
securityPolicy.setAuthorizationResult(Status.PERMISSION_DENIED.withDescription("xyzzy"));
transport.start(transportListener).run();
Status transportStatus = transportListener.awaitShutdown();
assertThat(transportStatus.getCode()).isEqualTo(Code.PERMISSION_DENIED);
assertThat(transportStatus.getDescription()).contains("xyzzy");
transportListener.awaitTermination();
}

@Test
public void testAsyncSecurityPolicyPreAuthSuccess() throws Exception {
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
transport =
new BinderClientTransportBuilder()
.setPreAuthorizeServer(true)
.setSecurityPolicy(securityPolicy)
.build();
securityPolicy.setAuthorizationResult(Status.PERMISSION_DENIED.withDescription("xyzzy"));
transport.start(transportListener).run();
Status transportStatus = transportListener.awaitShutdown();
assertThat(transportStatus.getCode()).isEqualTo(Code.PERMISSION_DENIED);
assertThat(transportStatus.getDescription()).contains("xyzzy");
transportListener.awaitTermination();
}

Expand Down Expand Up @@ -548,26 +608,6 @@ public synchronized void closed(Status status, RpcProgress rpcProgress, Metadata
}
}

/**
* A SecurityPolicy that blocks the transport authorization check until a test sets the outcome.
*/
static class BlockingSecurityPolicy extends SecurityPolicy {
private final BlockingQueue<Status> results = new LinkedBlockingQueue<>();

public void provideNextCheckAuthorizationResult(Status status) {
results.add(status);
}

@Override
public Status checkAuthorization(int uid) {
try {
return results.take();
} catch (InterruptedException e) {
return Status.fromThrowable(e);
}
}
}

/** An AsyncSecurityPolicy that lets a test specify the outcome of checkAuthorizationAsync(). */
static class SettableAsyncSecurityPolicy extends AsyncSecurityPolicy {
private SettableFuture<Status> result = SettableFuture.create();
Expand Down
13 changes: 13 additions & 0 deletions binder/src/main/java/io/grpc/binder/ApiConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

import android.content.Intent;
import android.os.UserHandle;
import io.grpc.Attributes;
import io.grpc.EquivalentAddressGroup;
import io.grpc.ExperimentalApi;
import io.grpc.NameResolver;

Expand All @@ -43,4 +45,15 @@ private ApiConstants() {}
*/
public static final NameResolver.Args.Key<UserHandle> TARGET_ANDROID_USER =
NameResolver.Args.Key.create("target-android-user");

/**
* Lets you override a Channel's pre-auth configuration (see {@link
* BinderChannelBuilder#preAuthorizeServers(boolean)} for a given {@link EquivalentAddressGroup}.
*
* <p>A {@link NameResolver} that discovers servers from an untrusted source like PackageManager
* can use this to force server pre-auth and prevent abuse.
*/
@EquivalentAddressGroup.Attr
public static final Attributes.Key<Boolean> PRE_AUTH_SERVER_OVERRIDE =
Attributes.Key.create("pre-auth-server-override");
}
28 changes: 28 additions & 0 deletions binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,34 @@ public BinderChannelBuilder strictLifecycleManagement() {
return this;
}

/**
* Checks servers against this Channel's {@link SecurityPolicy} *before* binding.
*
* <p>Android users can be tricked into installing a malicious app with the same package name as a
* legitimate server. That's why we don't send calls to a server until it has been authorized by
* an appropriate {@link SecurityPolicy}. But merely binding to a malicious server can enable
* "keep-alive" and "background activity launch" abuse, even if security policy ultimately causes
* the grpc connection to fail. Pre-authorization is especially important for security when the
* server's address isn't known in advance but rather resolved via target URI or discovered by
* other means.
*
* <p>Note that, unlike ordinary authorization, pre-authorization is performed against the server
* app's UID, not the UID of the process hosting the bound Service. These can be different, most
* commonly due to services that set `android:isolatedProcess=true`.
*
* <p>Pre-authorization is strongly recommended but it remains optional for now because of this
* behavior change and the small performance cost.
*
* <p>The default value of this property is false but it will become true in a future release.
* Clients that require a particular behavior should configure it explicitly using this method
* rather than relying on the default.
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/00000")
public BinderChannelBuilder preAuthorizeServers(boolean preAuthorize) {
transportFactoryBuilder.setPreAuthorizeServers(preAuthorize);
return this;
}

@Override
public BinderChannelBuilder idleTimeout(long value, TimeUnit unit) {
checkState(
Expand Down
15 changes: 15 additions & 0 deletions binder/src/main/java/io/grpc/binder/internal/Bindable.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@

package io.grpc.binder.internal;

import android.content.pm.ServiceInfo;
import android.os.IBinder;
import androidx.annotation.AnyThread;
import androidx.annotation.MainThread;
import io.grpc.Status;
import io.grpc.StatusException;

/** An interface for managing a {@code Binder} connection. */
interface Bindable {
Expand All @@ -45,6 +47,19 @@ interface Observer {
void onUnbound(Status reason);
}

/**
* Fetches details about the remote Service from PackageManager without binding to it.
*
* <p>Resolving an untrusted address before binding to it lets you screen out problematic servers
* before giving them a chance to run. However, note that the identity/existence of the resolved
* Service can change between the time this method returns and the time you actually bind/connect
* to it. For example, suppose the target package gets uninstalled right after this method
* returns. In {@link Observer#onBound}, you should verify that the server you resolved is the
* same one you connected to.
*/
@AnyThread
ServiceInfo resolve() throws StatusException;

/**
* Attempt to bind with the remote service.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public final class BinderClientTransportFactory implements ClientTransportFactor
final InboundParcelablePolicy inboundParcelablePolicy;
final OneWayBinderProxy.Decorator binderDecorator;
final long readyTimeoutMillis;
final boolean preAuthorizeServers; // TODO(jdcormie): Default to true.

ScheduledExecutorService executorService;
Executor offloadExecutor;
Expand All @@ -75,6 +76,7 @@ private BinderClientTransportFactory(Builder builder) {
inboundParcelablePolicy = checkNotNull(builder.inboundParcelablePolicy);
binderDecorator = checkNotNull(builder.binderDecorator);
readyTimeoutMillis = builder.readyTimeoutMillis;
preAuthorizeServers = builder.preAuthorizeServers;

executorService = scheduledExecutorPool.getObject();
offloadExecutor = offloadExecutorPool.getObject();
Expand Down Expand Up @@ -128,6 +130,7 @@ public static final class Builder implements ClientTransportFactoryBuilder {
InboundParcelablePolicy inboundParcelablePolicy = InboundParcelablePolicy.DEFAULT;
OneWayBinderProxy.Decorator binderDecorator = OneWayBinderProxy.IDENTITY_DECORATOR;
long readyTimeoutMillis = 60_000;
boolean preAuthorizeServers;

@Override
public BinderClientTransportFactory buildClientTransportFactory() {
Expand Down Expand Up @@ -216,5 +219,11 @@ public Builder setReadyTimeoutMillis(long readyTimeoutMillis) {
this.readyTimeoutMillis = readyTimeoutMillis;
return this;
}

/** Whether to check server addresses against the SecurityPolicy *before* binding to them. */
public Builder setPreAuthorizeServers(boolean preAuthorizeServers) {
this.preAuthorizeServers = preAuthorizeServers;
return this;
}
}
}
Loading