binder: Expose client identity via a new abstract 'PeerUid' type (#9952)

The actual remote uid was kept private to prevent misuse.
This commit is contained in:
Jeff Davidson 2023-03-16 17:34:13 -07:00 committed by GitHub
parent b09473b0d3
commit b8444d563d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 363 additions and 6 deletions

View File

@ -66,6 +66,7 @@ dependencies {
exclude group: 'junit', module: 'junit'
}
testImplementation libraries.truth
testImplementation project(':grpc-testing')
androidTestAnnotationProcessor libraries.auto.value
androidTestImplementation project(':grpc-testing')

View File

@ -21,11 +21,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
import android.app.Application;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.io.ByteStreams;
@ -37,7 +33,6 @@ import io.grpc.ManagedChannel;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.NameResolverRegistry;
import io.grpc.Server;
import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptors;
import io.grpc.ServerServiceDefinition;
@ -91,12 +86,14 @@ public final class BinderChannelSmokeTest {
FakeNameResolverProvider fakeNameResolverProvider;
ManagedChannel channel;
AtomicReference<Metadata> headersCapture = new AtomicReference<>();
AtomicReference<PeerUid> clientUidCapture = new AtomicReference<>();
@Before
public void setUp() throws Exception {
ServerCallHandler<String, String> callHandler =
ServerCalls.asyncUnaryCall(
(req, respObserver) -> {
clientUidCapture.set(PeerUids.REMOTE_PEER.get());
respObserver.onNext(req);
respObserver.onCompleted();
});
@ -104,6 +101,7 @@ public final class BinderChannelSmokeTest {
ServerCallHandler<String, String> singleLargeResultCallHandler =
ServerCalls.asyncUnaryCall(
(req, respObserver) -> {
clientUidCapture.set(PeerUids.REMOTE_PEER.get());
respObserver.onNext(createLargeString(SLIGHTLY_MORE_THAN_ONE_BLOCK));
respObserver.onCompleted();
});
@ -118,7 +116,8 @@ public final class BinderChannelSmokeTest {
.addMethod(singleLargeResultMethod, singleLargeResultCallHandler)
.addMethod(bidiMethod, bidiCallHandler)
.build(),
TestUtils.recordRequestHeadersInterceptor(headersCapture));
TestUtils.recordRequestHeadersInterceptor(headersCapture),
PeerUids.newPeerIdentifyingServerInterceptor());
AndroidComponentAddress serverAddress = HostServices.allocateService(appContext);
fakeNameResolverProvider = new FakeNameResolverProvider(SERVER_TARGET_URI, serverAddress);
@ -162,6 +161,12 @@ public final class BinderChannelSmokeTest {
assertThat(doCall("Hello").get()).isEqualTo("Hello");
}
@Test
public void testPeerUidIsRecorded() throws Exception {
assertThat(doCall("Hello").get()).isEqualTo("Hello");
assertThat(clientUidCapture.get()).isEqualTo(PeerUid.forCurrentProcess());
}
@Test
public void testEmptyMessage() throws Exception {
assertThat(doCall("").get()).isEmpty();

View File

@ -0,0 +1,78 @@
/*
* Copyright 2023 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc.binder;
import io.grpc.ExperimentalApi;
/**
* Identifies a gRPC/binder client or server by Android/Linux UID
* (https://source.android.com/security/app-sandbox).
*
* <p>Use {@link PeerUids#REMOTE_PEER} to obtain the client's {@link PeerUid} from the server's
* {@link io.grpc.Context}
*
* <p>The actual integer uid is intentionally not exposed to prevent misuse. If you want the uid for
* access control, consider one of the existing {@link SecurityPolicies} instead (or propose a new
* one). If you want the uid to pass to some other Android API, consider one of the static wrapper
* methods of {@link PeerUids} instead (or propose a new one).
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/8022")
public final class PeerUid {
private final int uid;
/** Constructs a new instance. Intentionally non-public to prevent misuse. */
PeerUid(int uid) {
this.uid = uid;
}
/** Returns an identifier for the current process. */
public static PeerUid forCurrentProcess() {
return new PeerUid(android.os.Process.myUid());
}
/**
* Returns this peer's Android/Linux uid.
*
* <p>Intentionally non-public to prevent misuse.
*/
int getUid() {
return uid;
}
@Override
public boolean equals(Object otherObj) {
if (this == otherObj) {
return true;
}
if (otherObj == null || getClass() != otherObj.getClass()) {
return false;
}
PeerUid otherPeerUid = (PeerUid) otherObj;
return uid == otherPeerUid.uid;
}
@Override
public int hashCode() {
return Integer.valueOf(uid).hashCode();
}
@Override
public String toString() {
return "PeerUid{" + uid + '}';
}
}

View File

@ -0,0 +1,103 @@
/*
* Copyright 2023 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc.binder;
import static com.google.common.base.Preconditions.checkNotNull;
import android.content.pm.PackageManager;
import android.os.Build.VERSION_CODES;
import android.os.UserHandle;
import androidx.annotation.RequiresApi;
import io.grpc.Context;
import io.grpc.Contexts;
import io.grpc.ExperimentalApi;
import io.grpc.Metadata;
import io.grpc.ServerCall;
import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor;
import io.grpc.binder.internal.BinderTransport;
import javax.annotation.Nullable;
/** Static methods that operate on {@link PeerUid}. */
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/8022")
public final class PeerUids {
/**
* The client's authentic identity will be stored under this key in the server {@link Context}.
*
* <p>In order for this key to be populated, the interceptor returned by {@link
* #newPeerIdentifyingServerInterceptor} must be attached to the service. Note that the Context
* must be propagated correctly across threads for this key to be populated when read from other
* threads.
*/
public static final Context.Key<PeerUid> REMOTE_PEER = Context.key("remote-peer");
/**
* Returns package names associated with the given peer's uid according to {@link
* PackageManager#getPackagesForUid(int)}.
*
* <p><em>WARNING</em>: Apps installed from untrusted sources can set any package name they want.
* Don't depend on package names for security -- use {@link SecurityPolicies} instead.
*/
public static String[] getInsecurePackagesForUid(PackageManager packageManager, PeerUid who) {
return packageManager.getPackagesForUid(who.getUid());
}
/**
* Retrieves the "official name" associated with this uid, as specified by {@link
* PackageManager#getNameForUid(int)}.
*/
@Nullable
public static String getNameForUid(PackageManager packageManager, PeerUid who) {
return packageManager.getNameForUid(who.getUid());
}
/**
* Retrieves the {@link UserHandle} associated with this uid according to {@link
* UserHandle#getUserHandleForUid}.
*/
@RequiresApi(api = VERSION_CODES.N)
public static UserHandle getUserHandleForUid(PeerUid who) {
return UserHandle.getUserHandleForUid(who.getUid());
}
/**
* Creates an interceptor that exposes the client's identity in the {@link Context} under {@link
* #REMOTE_PEER}.
*
* <p>The returned interceptor only works with the Android Binder transport. If installed
* elsewhere, all intercepted requests will fail without ever reaching application-layer
* processing.
*/
public static ServerInterceptor newPeerIdentifyingServerInterceptor() {
return new ServerInterceptor() {
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) {
Context context = Context.current();
PeerUid client =
new PeerUid(
checkNotNull(
call.getAttributes().get(BinderTransport.REMOTE_UID),
"Expected BinderTransport attribute REMOTE_UID was missing. Is this "
+ "interceptor installed on an unsupported type of Server?"));
return Contexts.interceptCall(context.withValue(REMOTE_PEER, client), call, headers, next);
}
};
}
private PeerUids() {}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright 2023 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc.binder;
import com.google.common.testing.EqualsTester;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public class PeerUidTest {
@Test
public void shouldImplementEqualsAndHashCode() {
new EqualsTester()
.addEqualityGroup(new PeerUid(123), new PeerUid(123))
.addEqualityGroup(new PeerUid(456))
.testEquals();
}
}

View File

@ -0,0 +1,136 @@
/*
* Copyright 2023 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc.binder;
import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableList;
import com.google.common.io.ByteStreams;
import io.grpc.Attributes;
import io.grpc.CallOptions;
import io.grpc.ManagedChannel;
import io.grpc.MethodDescriptor;
import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor;
import io.grpc.ServerInterceptors;
import io.grpc.ServerServiceDefinition;
import io.grpc.ServerTransportFilter;
import io.grpc.StatusRuntimeException;
import io.grpc.binder.internal.BinderTransport;
import io.grpc.inprocess.InProcessChannelBuilder;
import io.grpc.inprocess.InProcessServerBuilder;
import io.grpc.stub.ClientCalls;
import io.grpc.stub.ServerCalls;
import io.grpc.testing.GrpcCleanupRule;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public class PeerUidsTest {
@Rule public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule();
private static final int FAKE_UID = 12345;
private final MethodDescriptor<String, String> method =
MethodDescriptor.newBuilder(StringMarshaller.INSTANCE, StringMarshaller.INSTANCE)
.setFullMethodName("test/method")
.setType(MethodDescriptor.MethodType.UNARY)
.build();
private final AtomicReference<PeerUid> clientUidCapture = new AtomicReference<>();
@Test
public void keyPopulatedWithInterceptor() throws Exception {
makeServiceCall(/* populateUid= */ true, /* includeInterceptor= */ true);
assertThat(clientUidCapture.get()).isEqualTo(new PeerUid(FAKE_UID));
}
@Test
public void keyNotPopulatedWithoutInterceptor() throws Exception {
makeServiceCall(/* populateUid= */ true, /* includeInterceptor= */ false);
assertThat(clientUidCapture.get()).isNull();
}
@Test
public void exceptionThrownWithoutUid() throws Exception {
assertThrows(
StatusRuntimeException.class,
() -> makeServiceCall(/* populateUid= */ false, /* includeInterceptor= */ true));
}
private void makeServiceCall(boolean populateUid, boolean includeInterceptor) throws Exception {
ServerCallHandler<String, String> callHandler =
ServerCalls.asyncUnaryCall(
(req, respObserver) -> {
clientUidCapture.set(PeerUids.REMOTE_PEER.get());
respObserver.onNext(req);
respObserver.onCompleted();
});
ImmutableList<ServerInterceptor> interceptors;
if (includeInterceptor) {
interceptors = ImmutableList.of(PeerUids.newPeerIdentifyingServerInterceptor());
} else {
interceptors = ImmutableList.of();
}
ServerServiceDefinition serviceDef =
ServerInterceptors.intercept(
ServerServiceDefinition.builder("test").addMethod(method, callHandler).build(),
interceptors);
InProcessServerBuilder server =
InProcessServerBuilder.forName("test").directExecutor().addService(serviceDef);
if (populateUid) {
server.addTransportFilter(
new ServerTransportFilter() {
@Override
public Attributes transportReady(Attributes attributes) {
return attributes.toBuilder().set(BinderTransport.REMOTE_UID, FAKE_UID).build();
}
});
}
grpcCleanup.register(server.build().start());
ManagedChannel channel = InProcessChannelBuilder.forName("test").directExecutor().build();
ClientCalls.blockingUnaryCall(channel, method, CallOptions.DEFAULT, "hello");
}
private static class StringMarshaller implements MethodDescriptor.Marshaller<String> {
public static final StringMarshaller INSTANCE = new StringMarshaller();
@Override
public InputStream stream(String value) {
return new ByteArrayInputStream(value.getBytes(UTF_8));
}
@Override
public String parse(InputStream stream) {
try {
return new String(ByteStreams.toByteArray(stream), UTF_8);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
}
}