Skip to content

Commit 9c52e3d

Browse files
committed
binder: Introduce server authorization strategy v2
Adds support for android:isolatedProcess Services (fixing #12293) and moves all peer authorization checks to the handshake, allowing subsequent transactions to go unchecked.
1 parent 701f2dc commit 9c52e3d

File tree

8 files changed

+259
-16
lines changed

8 files changed

+259
-16
lines changed

binder/src/androidTest/java/io/grpc/binder/BinderChannelSmokeTest.java

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ public final class BinderChannelSmokeTest {
9797
.setType(MethodDescriptor.MethodType.BIDI_STREAMING)
9898
.build();
9999

100+
AndroidComponentAddress serverAddress;
100101
ManagedChannel channel;
101102
AtomicReference<Metadata> headersCapture = new AtomicReference<>();
102103
AtomicReference<PeerUid> clientUidCapture = new AtomicReference<>();
@@ -134,7 +135,7 @@ public void setUp() throws Exception {
134135
TestUtils.recordRequestHeadersInterceptor(headersCapture),
135136
PeerUids.newPeerIdentifyingServerInterceptor());
136137

137-
AndroidComponentAddress serverAddress = HostServices.allocateService(appContext);
138+
serverAddress = HostServices.allocateService(appContext);
138139
HostServices.configureService(
139140
serverAddress,
140141
HostServices.serviceParamsBuilder()
@@ -149,13 +150,15 @@ public void setUp() throws Exception {
149150
.build())
150151
.build());
151152

152-
channel =
153-
BinderChannelBuilder.forAddress(serverAddress, appContext)
153+
channel = newBinderChannelBuilder().build();
154+
}
155+
156+
BinderChannelBuilder newBinderChannelBuilder() {
157+
return BinderChannelBuilder.forAddress(serverAddress, appContext)
154158
.inboundParcelablePolicy(
155-
InboundParcelablePolicy.newBuilder()
156-
.setAcceptParcelableMetadataValues(true)
157-
.build())
158-
.build();
159+
InboundParcelablePolicy.newBuilder()
160+
.setAcceptParcelableMetadataValues(true)
161+
.build());
159162
}
160163

161164
@After
@@ -185,6 +188,18 @@ public void testBasicCall() throws Exception {
185188
assertThat(doCall("Hello").get()).isEqualTo("Hello");
186189
}
187190

191+
@Test
192+
public void testBasicCallWithLegacyAuthStrategy() throws Exception {
193+
channel = newBinderChannelBuilder().useLegacyAuthStrategy().build();
194+
assertThat(doCall("Hello").get()).isEqualTo("Hello");
195+
}
196+
197+
@Test
198+
public void testBasicCallWithV2AuthStrategy() throws Exception {
199+
channel = newBinderChannelBuilder().useV2AuthStrategy().build();
200+
assertThat(doCall("Hello").get()).isEqualTo("Hello");
201+
}
202+
188203
@Test
189204
public void testPeerUidIsRecorded() throws Exception {
190205
assertThat(doCall("Hello").get()).isEqualTo("Hello");

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,62 @@ public BinderChannelBuilder preAuthorizeServers(boolean preAuthorize) {
280280
return this;
281281
}
282282

283+
/**
284+
* Specifies how and when to authorize a server against this Channel's {@link SecurityPolicy}.
285+
*
286+
* <p>This method selects the original "legacy" authorization strategy, which is no longer
287+
* preferred for two reasons. First, the legacy strategy considers the UID of the server *process*
288+
* we connect to. This is problematic for services using the `android:isolatedProcess` attribute,
289+
* which runs them under a different UID and without any of the privileges of the hosting app.
290+
* Second, the legacy authorization strategy performs SecurityPolicy checks later in the
291+
* handshake, which means the calling UID must be rechecked on every subsequent transaction. For
292+
* these reasons, prefer {@link #useV2AuthStrategy()} instead.
293+
*
294+
* <p>The server does not know which authorization strategy a client is using. Both strategies
295+
* work with all versions of the grpc-binder server.
296+
*
297+
* <p>The default authorization strategy is unspecified. Clients that require the legacy strategy
298+
* should configure it explicitly using this method. Eventually support for the legacy strategy
299+
* will be removed.
300+
*
301+
* @return this
302+
*/
303+
public BinderChannelBuilder useLegacyAuthStrategy() {
304+
transportFactoryBuilder.setUseLegacyAuthStrategy(true);
305+
return this;
306+
}
307+
308+
/**
309+
* Specifies how and when to authorize a server against this Channel's {@link SecurityPolicy}.
310+
*
311+
* <p>This method selects the v2 authorization strategy. It improves on {@link
312+
* #useLegacyAuthStrategy()}, by considering the UID of the server *app* we connect to, rather
313+
* than the server *process*. This allows clients to connect to services using the
314+
* `android:isolatedProcess` attribute, which runs them under a different ephemeral UID and
315+
* without any of the privileges of the hosting app.
316+
*
317+
* <p>Furthermore, the v2 authorization strategy performs SecurityPolicy checks earlier the
318+
* handshake, which allows subsequent transactions over the connection to proceed securely without
319+
* further UID checks. For these reasons, clients should prefer the v2 strategy.
320+
*
321+
* <p>The server does not know which authorization strategy a client is using. Both strategies
322+
* work with all versions of the grpc-binder server.
323+
*
324+
* <p>The default authorization strategy is unspecified. Clients that require the v2 strategy
325+
* should configure it explicitly using this method. Eventually support for the legacy strategy
326+
* will be removed.
327+
*
328+
* <p>If moving to the new authorization strategy causes a robolectric test to fail, ensure your
329+
* fake Service component is registered with `ShadowPackageManager` using `addOrUpdateService()`.
330+
*
331+
* @return this
332+
*/
333+
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/12397")
334+
public BinderChannelBuilder useV2AuthStrategy() {
335+
transportFactoryBuilder.setUseLegacyAuthStrategy(false);
336+
return this;
337+
}
338+
283339
@Override
284340
public BinderChannelBuilder idleTimeout(long value, TimeUnit unit) {
285341
checkState(

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

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,11 @@ interface Observer {
5454
* before giving them a chance to run. However, note that the identity/existence of the resolved
5555
* Service can change between the time this method returns and the time you actually bind/connect
5656
* to it. For example, suppose the target package gets uninstalled or upgraded right after this
57-
* method returns. In {@link Observer#onBound}, you should verify that the server you resolved is
58-
* the same one you connected to.
57+
* method returns.
58+
*
59+
* <p>Compare with {@link #getConnectedServiceInfo()}, which can only be called after {@link
60+
* Observer#onBound(IBinder)} but can be used to learn about the service you actually connected
61+
* to.
5962
*/
6063
@AnyThread
6164
ServiceInfo resolve() throws StatusException;
@@ -68,6 +71,21 @@ interface Observer {
6871
@AnyThread
6972
void bind();
7073

74+
/**
75+
* Asks PackageManager for details about the remote Service we *actually* connected to.
76+
*
77+
* <p>Can only be called after {@link Observer#onBound}.
78+
*
79+
* <p>Compare with {@link #resolve()}, which reports which service would be selected as of now but
80+
* *without* connecting.
81+
*
82+
* @throws StatusException UNIMPLEMENTED if the connected service isn't found (an {@link
83+
* Observer#onUnbound} callback has likely already happened or is on its way!)
84+
* @throws IllegalStateException if {@link Observer#onBound} has not "happened-before" this call
85+
*/
86+
@AnyThread
87+
ServiceInfo getConnectedServiceInfo() throws StatusException;
88+
7189
/**
7290
* Unbind from the remote service if connected.
7391
*

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

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,10 @@ public BinderClientTransport(
120120
Boolean preAuthServerOverride = options.getEagAttributes().get(PRE_AUTH_SERVER_OVERRIDE);
121121
this.preAuthorizeServer =
122122
preAuthServerOverride != null ? preAuthServerOverride : factory.preAuthorizeServers;
123-
this.handshake = new LegacyClientHandshake();
123+
this.handshake =
124+
factory.useLegacyAuthStrategy ? new LegacyClientHandshake() : new NewClientHandshake();
124125
numInUseStreams = new AtomicInteger();
125126
pingTracker = new PingTracker(Ticker.systemTicker(), (id) -> sendPing(id));
126-
127127
serviceBinding =
128128
new ServiceBinding(
129129
factory.mainThreadExecutor,
@@ -398,6 +398,78 @@ private void handleAuthResult(OneWayBinderProxy binder, Status authorization) {
398398
}
399399
}
400400

401+
private class NewClientHandshake implements ClientHandshake {
402+
@Override
403+
@GuardedBy("BinderClientTransport.this") // By way of @GuardedBy("this") `handshake` member.
404+
public void onBound(OneWayBinderProxy endpointBinder) {
405+
Futures.addCallback(
406+
Futures.submit(serviceBinding::getConnectedServiceInfo, offloadExecutor),
407+
new FutureCallback<ServiceInfo>() {
408+
@Override
409+
public void onSuccess(ServiceInfo result) {
410+
synchronized (BinderClientTransport.this) {
411+
onConnectedServiceInfo(endpointBinder, result);
412+
}
413+
}
414+
415+
@Override
416+
public void onFailure(Throwable t) {
417+
synchronized (BinderClientTransport.this) {
418+
shutdownInternal(Status.fromThrowable(t), true);
419+
}
420+
}
421+
},
422+
offloadExecutor);
423+
}
424+
425+
@GuardedBy("BinderClientTransport.this")
426+
private void onConnectedServiceInfo(OneWayBinderProxy endpointBinder, ServiceInfo serviceInfo) {
427+
if (inState(TransportState.SETUP)) {
428+
attributes = setSecurityAttrs(attributes, serviceInfo.applicationInfo.uid);
429+
ListenableFuture<Status> authResultFuture =
430+
register(checkServerAuthorizationAsync(serviceInfo.applicationInfo.uid));
431+
Futures.addCallback(
432+
authResultFuture,
433+
new FutureCallback<Status>() {
434+
@Override
435+
public void onSuccess(Status result) {
436+
synchronized (BinderClientTransport.this) {
437+
handleAuthResult(endpointBinder, result);
438+
}
439+
}
440+
441+
@Override
442+
public void onFailure(Throwable t) {
443+
BinderClientTransport.this.handleAuthResult(t);
444+
}
445+
},
446+
offloadExecutor);
447+
}
448+
}
449+
450+
@GuardedBy("BinderClientTransport.this")
451+
private void handleAuthResult(OneWayBinderProxy endpointBinder, Status authResult) {
452+
if (inState(TransportState.SETUP)) {
453+
if (!authResult.isOk()) {
454+
shutdownInternal(authResult, true);
455+
} else {
456+
sendSetupTransaction(endpointBinder);
457+
}
458+
}
459+
}
460+
461+
@Override
462+
@GuardedBy("BinderClientTransport.this") // By way of @GuardedBy("this") `handshake` member.
463+
public void handleSetupTransport(OneWayBinderProxy serverBinder) {
464+
if (!setOutgoingBinder(serverBinder)) {
465+
shutdownInternal(
466+
Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true);
467+
} else {
468+
onHandshakeComplete();
469+
}
470+
}
471+
}
472+
401473
@GuardedBy("this")
402474
private void onHandshakeComplete() {
403475
setState(TransportState.READY);

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ public final class BinderClientTransportFactory implements ClientTransportFactor
5353
final OneWayBinderProxy.Decorator binderDecorator;
5454
final long readyTimeoutMillis;
5555
final boolean preAuthorizeServers; // TODO(jdcormie): Default to true.
56+
final boolean useLegacyAuthStrategy;
5657

5758
ScheduledExecutorService executorService;
5859
Executor offloadExecutor;
@@ -73,6 +74,7 @@ private BinderClientTransportFactory(Builder builder) {
7374
binderDecorator = checkNotNull(builder.binderDecorator);
7475
readyTimeoutMillis = builder.readyTimeoutMillis;
7576
preAuthorizeServers = builder.preAuthorizeServers;
77+
useLegacyAuthStrategy = builder.useLegacyAuthStrategy;
7678

7779
executorService = scheduledExecutorPool.getObject();
7880
offloadExecutor = offloadExecutorPool.getObject();
@@ -126,6 +128,7 @@ public static final class Builder implements ClientTransportFactoryBuilder {
126128
OneWayBinderProxy.Decorator binderDecorator = OneWayBinderProxy.IDENTITY_DECORATOR;
127129
long readyTimeoutMillis = 60_000;
128130
boolean preAuthorizeServers;
131+
boolean useLegacyAuthStrategy = true; // TODO(jdcormie): Default to false.
129132

130133
@Override
131134
public BinderClientTransportFactory buildClientTransportFactory() {
@@ -219,5 +222,11 @@ public Builder setPreAuthorizeServers(boolean preAuthorizeServers) {
219222
this.preAuthorizeServers = preAuthorizeServers;
220223
return this;
221224
}
225+
226+
/** Specifies which version of the client handshake to use. */
227+
public Builder setUseLegacyAuthStrategy(boolean useLegacyAuthStrategy) {
228+
this.useLegacyAuthStrategy = useLegacyAuthStrategy;
229+
return this;
230+
}
222231
}
223232
}

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ public String methodName() {
102102

103103
private State reportedState; // Only used on the main thread.
104104

105+
@GuardedBy("this")
106+
private ComponentName connectedServiceName;
107+
105108
@AnyThread
106109
ServiceBinding(
107110
Executor mainThreadExecutor,
@@ -305,13 +308,34 @@ private void clearReferences() {
305308
sourceContext = null;
306309
}
307310

311+
@AnyThread
312+
@Override
313+
public ServiceInfo getConnectedServiceInfo() throws StatusException {
314+
try {
315+
return getContextForTargetUser("cross-user v2 handshake")
316+
.getPackageManager()
317+
.getServiceInfo(getConnectedServiceName(), /* flags= */ 0);
318+
} catch (PackageManager.NameNotFoundException e) {
319+
throw Status.UNIMPLEMENTED
320+
.withCause(e)
321+
.withDescription("connected remote service was uninstalled/disabled during handshake")
322+
.asException();
323+
}
324+
}
325+
326+
private synchronized ComponentName getConnectedServiceName() {
327+
checkState(connectedServiceName != null, "onBound() not yet called!");
328+
return connectedServiceName;
329+
}
330+
308331
@Override
309332
@MainThread
310333
public void onServiceConnected(ComponentName className, IBinder binder) {
311334
boolean bound = false;
312335
synchronized (this) {
313336
if (state == State.BINDING) {
314337
state = State.BOUND;
338+
connectedServiceName = className;
315339
bound = true;
316340
}
317341
}

binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import static io.grpc.binder.internal.BinderTransport.SHUTDOWN_TRANSPORT;
2626
import static io.grpc.binder.internal.BinderTransport.WIRE_FORMAT_VERSION;
2727
import static java.util.concurrent.TimeUnit.MILLISECONDS;
28+
import static org.junit.Assume.assumeTrue;
2829
import static org.mockito.ArgumentMatchers.anyLong;
2930
import static org.mockito.Mockito.mock;
3031
import static org.mockito.Mockito.never;
@@ -119,11 +120,19 @@ public final class RobolectricBinderTransportTest extends AbstractTransportTest
119120

120121
private int nextServerAddress;
121122

122-
@Parameter public boolean preAuthServersParam;
123+
@Parameter(value = 0)
124+
public boolean preAuthServersParam;
123125

124-
@Parameters(name = "preAuthServersParam={0}")
125-
public static ImmutableList<Boolean> data() {
126-
return ImmutableList.of(true, false);
126+
@Parameter(value = 1)
127+
public boolean useLegacyAuthStrategy;
128+
129+
@Parameters(name = "preAuthServersParam={0};useLegacyAuthStrategy={1}")
130+
public static ImmutableList<Object[]> data() {
131+
return ImmutableList.of(
132+
new Object[] {false, false},
133+
new Object[] {false, true},
134+
new Object[] {true, false},
135+
new Object[] {true, true});
127136
}
128137

129138
@Override
@@ -189,6 +198,7 @@ protected InternalServer newServer(
189198
BinderClientTransportFactory.Builder newClientTransportFactoryBuilder() {
190199
return new BinderClientTransportFactory.Builder()
191200
.setPreAuthorizeServers(preAuthServersParam)
201+
.setUseLegacyAuthStrategy(useLegacyAuthStrategy)
192202
.setSourceContext(application)
193203
.setScheduledExecutorPool(executorServicePool)
194204
.setOffloadExecutorPool(offloadExecutorPool);
@@ -249,7 +259,11 @@ public void clientAuthorizesServerUidsInOrder() throws Exception {
249259
}
250260

251261
AuthRequest authRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_MS, MILLISECONDS);
252-
assertThat(authRequest.uid).isEqualTo(11111);
262+
if (useLegacyAuthStrategy) {
263+
assertThat(authRequest.uid).isEqualTo(11111);
264+
} else {
265+
assertThat(authRequest.uid).isEqualTo(22222);
266+
}
253267
verify(mockClientTransportListener, never()).transportReady();
254268
authRequest.setResult(Status.OK);
255269

@@ -320,6 +334,10 @@ public void clientIgnoresDuplicateSetupTransaction() throws Exception {
320334
@Test
321335
public void clientIgnoresTransactionFromNonServerUids() throws Exception {
322336
server.start(serverListener);
337+
338+
// This test is not applicable to the new auth strategy which keeps the client Binder a secret.
339+
assumeTrue(useLegacyAuthStrategy);
340+
323341
client = newClientTransport(server);
324342
startTransport(client, mockClientTransportListener);
325343

0 commit comments

Comments
 (0)