diff options
3 files changed, 216 insertions, 27 deletions
diff --git a/services/core/java/com/android/server/security/rkp/RemoteProvisioningService.java b/services/core/java/com/android/server/security/rkp/RemoteProvisioningService.java index 2bd7383ddde0..1c5838c165a4 100644 --- a/services/core/java/com/android/server/security/rkp/RemoteProvisioningService.java +++ b/services/core/java/com/android/server/security/rkp/RemoteProvisioningService.java @@ -105,14 +105,27 @@ public class RemoteProvisioningService extends SystemService { @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) return; - new RemoteProvisioningShellCommand().dump(pw); + final int callerUid = Binder.getCallingUidOrThrow(); + final long callingIdentity = Binder.clearCallingIdentity(); + try { + new RemoteProvisioningShellCommand(getContext(), callerUid).dump(pw); + } finally { + Binder.restoreCallingIdentity(callingIdentity); + } } @Override public int handleShellCommand(ParcelFileDescriptor in, ParcelFileDescriptor out, ParcelFileDescriptor err, String[] args) { - return new RemoteProvisioningShellCommand().exec(this, in.getFileDescriptor(), - out.getFileDescriptor(), err.getFileDescriptor(), args); + final int callerUid = Binder.getCallingUidOrThrow(); + final long callingIdentity = Binder.clearCallingIdentity(); + try { + return new RemoteProvisioningShellCommand(getContext(), callerUid).exec(this, + in.getFileDescriptor(), out.getFileDescriptor(), err.getFileDescriptor(), + args); + } finally { + Binder.restoreCallingIdentity(callingIdentity); + } } } } diff --git a/services/core/java/com/android/server/security/rkp/RemoteProvisioningShellCommand.java b/services/core/java/com/android/server/security/rkp/RemoteProvisioningShellCommand.java index 187b93931f0b..4a6d74658754 100644 --- a/services/core/java/com/android/server/security/rkp/RemoteProvisioningShellCommand.java +++ b/services/core/java/com/android/server/security/rkp/RemoteProvisioningShellCommand.java @@ -16,22 +16,30 @@ package com.android.server.security.rkp; +import android.content.Context; import android.hardware.security.keymint.DeviceInfo; import android.hardware.security.keymint.IRemotelyProvisionedComponent; import android.hardware.security.keymint.MacedPublicKey; import android.hardware.security.keymint.ProtectedData; import android.hardware.security.keymint.RpcHardwareInfo; +import android.os.CancellationSignal; +import android.os.OutcomeReceiver; import android.os.RemoteException; import android.os.ServiceManager; import android.os.ShellCommand; +import android.security.rkp.service.RegistrationProxy; +import android.security.rkp.service.RemotelyProvisionedKey; import android.util.IndentingPrintWriter; -import com.android.internal.annotations.VisibleForTesting; - import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.PrintWriter; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.time.Duration; import java.util.Base64; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; import co.nstant.in.cbor.CborDecoder; import co.nstant.in.cbor.CborEncoder; @@ -54,16 +62,17 @@ class RemoteProvisioningShellCommand extends ShellCommand { + "csr [--challenge CHALLENGE] NAME\n" + " Generate and print a base64-encoded CSR from the named\n" + " IRemotelyProvisionedComponent. A base64-encoded challenge can be provided,\n" - + " or else it defaults to an empty challenge.\n"; + + " or else it defaults to an empty challenge.\n" + + "certify NAME\n" + + " Output the PEM-encoded certificate chain provisioned for the named\n" + + " IRemotelyProvisionedComponent.\n"; - @VisibleForTesting static final String EEK_ED25519_BASE64 = "goRDoQEnoFgqpAEBAycgBiFYIJm57t1e5FL2hcZMYtw+YatXSH11N" + "ymtdoAy0rPLY1jZWEAeIghLpLekyNdOAw7+uK8UTKc7b6XN3Np5xitk/pk5r3bngPpmAIUNB5gqrJFcpyUUS" + "QY0dcqKJ3rZ41pJ6wIDhEOhASegWE6lAQECWCDQrsEVyirPc65rzMvRlh1l6LHd10oaN7lDOpfVmd+YCAM4G" + "CAEIVggvoXnRsSjQlpA2TY6phXQLFh+PdwzAjLS/F4ehyVfcmBYQJvPkOIuS6vRGLEOjl0gJ0uEWP78MpB+c" + "gWDvNeCvvpkeC1UEEvAMb9r6B414vAtzmwvT/L1T6XUg62WovGHWAQ="; - @VisibleForTesting static final String EEK_P256_BASE64 = "goRDoQEmoFhNpQECAyYgASFYIPcUituX9MxT79JkEcTjdR9mH6RxDGzP" + "+glGgHSHVPKtIlggXn9b9uzk9hnM/xM3/Q+hyJPbGAZ2xF3m12p3hsMtr49YQC+XjkL7vgctlUeFR5NAsB/U" + "m0ekxESp8qEHhxDHn8sR9L+f6Dvg5zRMFfx7w34zBfTRNDztAgRgehXgedOK/ySEQ6EBJqBYcaYBAgJYIDVz" @@ -74,14 +83,20 @@ class RemoteProvisioningShellCommand extends ShellCommand { private static final int ERROR = -1; private static final int SUCCESS = 0; + private static final Duration BIND_TIMEOUT = Duration.ofSeconds(10); + private static final int KEY_ID = 452436; + + private final Context mContext; + private final int mCallerUid; private final Injector mInjector; - RemoteProvisioningShellCommand() { - this(new Injector()); + RemoteProvisioningShellCommand(Context context, int callerUid) { + this(context, callerUid, new Injector()); } - @VisibleForTesting - RemoteProvisioningShellCommand(Injector injector) { + RemoteProvisioningShellCommand(Context context, int callerUid, Injector injector) { + mContext = context; + mCallerUid = callerUid; mInjector = injector; } @@ -102,6 +117,8 @@ class RemoteProvisioningShellCommand extends ShellCommand { return list(); case "csr": return csr(); + case "certify": + return certify(); default: return handleDefaultCommands(cmd); } @@ -232,7 +249,45 @@ class RemoteProvisioningShellCommand extends ShellCommand { return new CborDecoder(bais).decodeNext(); } - @VisibleForTesting + private int certify() throws Exception { + String name = getNextArgRequired(); + + Executor executor = mContext.getMainExecutor(); + CancellationSignal cancellationSignal = new CancellationSignal(); + OutcomeFuture<RemotelyProvisionedKey> key = new OutcomeFuture<>(); + mInjector.getRegistrationProxy(mContext, mCallerUid, name, executor) + .getKeyAsync(KEY_ID, cancellationSignal, executor, key); + byte[] encodedCertChain = key.join().getEncodedCertChain(); + ByteArrayInputStream is = new ByteArrayInputStream(encodedCertChain); + PrintWriter pw = getOutPrintWriter(); + for (Certificate cert : CertificateFactory.getInstance("X.509").generateCertificates(is)) { + String encoded = Base64.getEncoder().encodeToString(cert.getEncoded()); + pw.println("-----BEGIN CERTIFICATE-----"); + pw.println(encoded.replaceAll("(.{64})", "$1\n").stripTrailing()); + pw.println("-----END CERTIFICATE-----"); + } + return SUCCESS; + } + + /** Treat an OutcomeReceiver as a future for use in synchronous code. */ + private static class OutcomeFuture<T> implements OutcomeReceiver<T, Exception> { + private CompletableFuture<T> mFuture = new CompletableFuture<>(); + + @Override + public void onResult(T result) { + mFuture.complete(result); + } + + @Override + public void onError(Exception e) { + mFuture.completeExceptionally(e); + } + + public T join() { + return mFuture.join(); + } + } + static class Injector { String[] getIrpcNames() { return ServiceManager.getDeclaredInstances(IRemotelyProvisionedComponent.DESCRIPTOR); @@ -248,5 +303,14 @@ class RemoteProvisioningShellCommand extends ShellCommand { } return binder; } + + RegistrationProxy getRegistrationProxy( + Context context, int callerUid, String name, Executor executor) { + String irpc = IRemotelyProvisionedComponent.DESCRIPTOR + "/" + name; + OutcomeFuture<RegistrationProxy> registration = new OutcomeFuture<>(); + RegistrationProxy.createAsync( + context, callerUid, irpc, BIND_TIMEOUT, executor, registration); + return registration.join(); + } } } diff --git a/services/tests/RemoteProvisioningServiceTests/src/com/android/server/security/rkp/RemoteProvisioningShellCommandTest.java b/services/tests/RemoteProvisioningServiceTests/src/com/android/server/security/rkp/RemoteProvisioningShellCommandTest.java index 2d93120681ec..007c0db1b731 100644 --- a/services/tests/RemoteProvisioningServiceTests/src/com/android/server/security/rkp/RemoteProvisioningShellCommandTest.java +++ b/services/tests/RemoteProvisioningServiceTests/src/com/android/server/security/rkp/RemoteProvisioningShellCommandTest.java @@ -21,12 +21,14 @@ import static com.google.common.truth.Truth8.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.content.Context; import android.hardware.security.keymint.DeviceInfo; import android.hardware.security.keymint.IRemotelyProvisionedComponent; import android.hardware.security.keymint.MacedPublicKey; @@ -34,28 +36,35 @@ import android.hardware.security.keymint.ProtectedData; import android.hardware.security.keymint.RpcHardwareInfo; import android.os.Binder; import android.os.FileUtils; +import android.os.OutcomeReceiver; +import android.os.Process; +import android.security.rkp.service.RegistrationProxy; +import android.security.rkp.service.RemotelyProvisionedKey; +import androidx.test.core.app.ApplicationProvider; import androidx.test.runner.AndroidJUnit4; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; +import java.util.Arrays; import java.util.Base64; import java.util.Map; +import java.util.concurrent.Executor; @RunWith(AndroidJUnit4.class) public class RemoteProvisioningShellCommandTest { - private static class Injector extends RemoteProvisioningShellCommand.Injector { + private Context mContext; - private final Map<String, IRemotelyProvisionedComponent> mIrpcs; + private static class Injector extends RemoteProvisioningShellCommand.Injector { - Injector(Map irpcs) { - mIrpcs = irpcs; - } + Map<String, IRemotelyProvisionedComponent> mIrpcs; + Map<String, RegistrationProxy> mRegistrationProxies; @Override String[] getIrpcNames() { @@ -70,6 +79,12 @@ public class RemoteProvisioningShellCommandTest { } return irpc; } + + @Override + RegistrationProxy getRegistrationProxy( + Context context, int callerUid, String name, Executor executor) { + return mRegistrationProxies.get(name); + } } private static class CommandResult { @@ -111,10 +126,17 @@ public class RemoteProvisioningShellCommandTest { code, FileUtils.readTextFile(out, 0, null), FileUtils.readTextFile(err, 0, null)); } + @Before + public void setUp() { + mContext = ApplicationProvider.getApplicationContext(); + } + @Test public void list_zeroInstances() throws Exception { + Injector injector = new Injector(); + injector.mIrpcs = Map.of(); RemoteProvisioningShellCommand cmd = new RemoteProvisioningShellCommand( - new Injector(Map.of())); + mContext, Process.SHELL_UID, injector); CommandResult res = exec(cmd, new String[] {"list"}); assertThat(res.getErr()).isEmpty(); assertThat(res.getCode()).isEqualTo(0); @@ -124,8 +146,10 @@ public class RemoteProvisioningShellCommandTest { @Test public void list_oneInstances() throws Exception { + Injector injector = new Injector(); + injector.mIrpcs = Map.of("default", mock(IRemotelyProvisionedComponent.class)); RemoteProvisioningShellCommand cmd = new RemoteProvisioningShellCommand( - new Injector(Map.of("default", mock(IRemotelyProvisionedComponent.class)))); + mContext, Process.SHELL_UID, injector); CommandResult res = exec(cmd, new String[] {"list"}); assertThat(res.getErr()).isEmpty(); assertThat(res.getCode()).isEqualTo(0); @@ -134,10 +158,12 @@ public class RemoteProvisioningShellCommandTest { @Test public void list_twoInstances() throws Exception { + Injector injector = new Injector(); + injector.mIrpcs = Map.of( + "default", mock(IRemotelyProvisionedComponent.class), + "strongbox", mock(IRemotelyProvisionedComponent.class)); RemoteProvisioningShellCommand cmd = new RemoteProvisioningShellCommand( - new Injector(Map.of( - "default", mock(IRemotelyProvisionedComponent.class), - "strongbox", mock(IRemotelyProvisionedComponent.class)))); + mContext, Process.SHELL_UID, injector); CommandResult res = exec(cmd, new String[] {"list"}); assertThat(res.getErr()).isEmpty(); assertThat(res.getCode()).isEqualTo(0); @@ -158,8 +184,10 @@ public class RemoteProvisioningShellCommandTest { }).when(defaultMock).generateCertificateRequest( anyBoolean(), any(), any(), any(), any(), any()); + Injector injector = new Injector(); + injector.mIrpcs = Map.of("default", defaultMock); RemoteProvisioningShellCommand cmd = new RemoteProvisioningShellCommand( - new Injector(Map.of("default", defaultMock))); + mContext, Process.SHELL_UID, injector); CommandResult res = exec(cmd, new String[] { "csr", "--challenge", "dGVzdHRlc3R0ZXN0dGVzdA==", "default"}); verify(defaultMock).generateCertificateRequest( @@ -189,8 +217,10 @@ public class RemoteProvisioningShellCommandTest { }).when(defaultMock).generateCertificateRequest( anyBoolean(), any(), any(), any(), any(), any()); + Injector injector = new Injector(); + injector.mIrpcs = Map.of("default", defaultMock); RemoteProvisioningShellCommand cmd = new RemoteProvisioningShellCommand( - new Injector(Map.of("default", defaultMock))); + mContext, Process.SHELL_UID, injector); CommandResult res = exec(cmd, new String[] { "csr", "--challenge", "dGVzdHRlc3R0ZXN0dGVzdA==", "default"}); verify(defaultMock).generateCertificateRequest( @@ -215,8 +245,10 @@ public class RemoteProvisioningShellCommandTest { when(defaultMock.generateCertificateRequestV2(any(), any())) .thenReturn(new byte[] {0x68, 0x65, 0x6c, 0x6c, 0x6f}); + Injector injector = new Injector(); + injector.mIrpcs = Map.of("default", defaultMock); RemoteProvisioningShellCommand cmd = new RemoteProvisioningShellCommand( - new Injector(Map.of("default", defaultMock))); + mContext, Process.SHELL_UID, injector); CommandResult res = exec(cmd, new String[] {"csr", "default"}); verify(defaultMock).generateCertificateRequestV2(new MacedPublicKey[0], new byte[0]); assertThat(res.getErr()).isEmpty(); @@ -233,8 +265,10 @@ public class RemoteProvisioningShellCommandTest { when(defaultMock.generateCertificateRequestV2(any(), any())) .thenReturn(new byte[] {0x68, 0x69}); + Injector injector = new Injector(); + injector.mIrpcs = Map.of("default", defaultMock); RemoteProvisioningShellCommand cmd = new RemoteProvisioningShellCommand( - new Injector(Map.of("default", defaultMock))); + mContext, Process.SHELL_UID, injector); CommandResult res = exec(cmd, new String[] {"csr", "--challenge", "dHJpYWw=", "default"}); verify(defaultMock).generateCertificateRequestV2( new MacedPublicKey[0], new byte[] {0x74, 0x72, 0x69, 0x61, 0x6c}); @@ -242,4 +276,82 @@ public class RemoteProvisioningShellCommandTest { assertThat(res.getCode()).isEqualTo(0); assertThat(res.getOut()).isEqualTo("aGk=\n"); } + + @Test + public void certify_sameOrderAsReceived() throws Exception { + String cert1 = "MIIBqDCCAU2gAwIBAgIUI3FFU7xZno/2Xf/wZzKKquP0ov0wCgYIKoZIzj0EAwIw\n" + + "KTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQ0wCwYDVQQKDARUZXN0MB4XDTIz\n" + + "MDgyMjE5MzgxMFoXDTMzMDgxOTE5MzgxMFowKTELMAkGA1UEBhMCVVMxCzAJBgNV\n" + + "BAgMAkNBMQ0wCwYDVQQKDARUZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE\n" + + "czOpG6NKOdDjV/yrKjuy0q0jEJvsVLGgTeY+vyKRBJS59OhyRWG6n3aza21bNg5d\n" + + "WE9ruz+bcT0IP4kDbiS0y6NTMFEwHQYDVR0OBBYEFHYfJxCUipNI7qRqvczcWsOb\n" + + "FIDPMB8GA1UdIwQYMBaAFHYfJxCUipNI7qRqvczcWsObFIDPMA8GA1UdEwEB/wQF\n" + + "MAMBAf8wCgYIKoZIzj0EAwIDSQAwRgIhAKm/kpJwlnWkjoLCAddBiSnxbT4EfJIK\n" + + "H0j58tg5VazHAiEAnS/kRzU9AbstOZyD7el/ws3gLXkbUNey3pLFutBWsSU=\n"; + String cert2 = "MIIBpjCCAU2gAwIBAgIUdSzfZzeGr+h70JPO7Sxwdkw99iMwCgYIKoZIzj0EAwIw\n" + + "KTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQ0wCwYDVQQKDARUZXN0MB4XDTIz\n" + + "MDgyMjIwMTcyMFoXDTMzMDgxOTIwMTcyMFowKTELMAkGA1UEBhMCVVMxCzAJBgNV\n" + + "BAgMAkNBMQ0wCwYDVQQKDARUZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE\n" + + "voGJi4DxuqH8rzPV6Eq0OVULc0xFzaM0500VBqiQEB7Qt0Ktk2d+3bUrFAb3SZV4\n" + + "6TIdb7SkynvaDtr0x45Ng6NTMFEwHQYDVR0OBBYEFMeGjvGV0ADPBJk5/FPoW9HQ\n" + + "uTc6MB8GA1UdIwQYMBaAFMeGjvGV0ADPBJk5/FPoW9HQuTc6MA8GA1UdEwEB/wQF\n" + + "MAMBAf8wCgYIKoZIzj0EAwIDRwAwRAIgd1gu7iiNOQXaQUn5BT3WwWR0Yk78ndWt\n" + + "ew7tRiTOhFcCIFURi6WcNH0oWa6IbwBSMC9aZlo98Fbg+dTwhLAAw+PW\n"; + byte[] cert1Bytes = Base64.getDecoder().decode(cert1.replaceAll("\\s+", "")); + byte[] cert2Bytes = Base64.getDecoder().decode(cert2.replaceAll("\\s+", "")); + byte[] certChain = Arrays.copyOf(cert1Bytes, cert1Bytes.length + cert2Bytes.length); + System.arraycopy(cert2Bytes, 0, certChain, cert1Bytes.length, cert2Bytes.length); + RemotelyProvisionedKey keyMock = mock(RemotelyProvisionedKey.class); + when(keyMock.getEncodedCertChain()).thenReturn(certChain); + RegistrationProxy defaultMock = mock(RegistrationProxy.class); + doAnswer(invocation -> { + ((OutcomeReceiver<RemotelyProvisionedKey, Exception>) invocation.getArgument(3)) + .onResult(keyMock); + return null; + }).when(defaultMock).getKeyAsync(anyInt(), any(), any(), any()); + + Injector injector = new Injector(); + injector.mRegistrationProxies = Map.of("default", defaultMock); + RemoteProvisioningShellCommand cmd = new RemoteProvisioningShellCommand( + mContext, Process.SHELL_UID, injector); + CommandResult res = exec(cmd, new String[] {"certify", "default"}); + assertThat(res.getErr()).isEmpty(); + assertThat(res.getCode()).isEqualTo(0); + assertThat(res.getOut()).isEqualTo( + "-----BEGIN CERTIFICATE-----\n" + cert1 + "-----END CERTIFICATE-----\n" + + "-----BEGIN CERTIFICATE-----\n" + cert2 + "-----END CERTIFICATE-----\n"); + } + + @Test + public void certify_noBlankLineBeforeTrailer() throws Exception { + String cert = "MIIB2zCCAYGgAwIBAgIRAOpN7Em1k7gaqLAB2dzXUTYwCgYIKoZIzj0EAwIwKTET\n" + + "MBEGA1UEChMKR29vZ2xlIExMQzESMBAGA1UEAxMJRHJvaWQgQ0EzMB4XDTIzMDgx\n" + + "ODIzMzI1MloXDTIzMDkyMTIzMzI1MlowOTEMMAoGA1UEChMDVEVFMSkwJwYDVQQD\n" + + "EyBlYTRkZWM0OWI1OTNiODFhYThiMDAxZDlkY2Q3NTEzNjBZMBMGByqGSM49AgEG\n" + + "CCqGSM49AwEHA0IABHM/cKZblmlw8bdGbDXnX+ZiLiGjSjaLHXYOoHDrVArAMXUi\n" + + "L6brhcUPaqSGcVLcfFZbaFMOxXW6TsGdQiwJ0iyjejB4MB0GA1UdDgQWBBTYzft+\n" + + "X32TH/Hh+ngwQF6aPhnfXDAfBgNVHSMEGDAWgBQT4JObI9mzNNW2FRsHRcw4zVn2\n" + + "8jAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwICBDAVBgorBgEEAdZ5AgEe\n" + + "BAehARoABAAAMAoGCCqGSM49BAMCA0gAMEUCIDc0OR7CzIYw0myTr0y/Brl1nZyk\n" + + "eGSQp615WpTwYhwxAiEApM10gSIKBIo7Z4/FNzkuiz1zZwW9+Dcqisqxkfe6icQ=\n"; + byte[] certBytes = Base64.getDecoder().decode(cert.replaceAll("\\s+", "")); + RemotelyProvisionedKey keyMock = mock(RemotelyProvisionedKey.class); + when(keyMock.getEncodedCertChain()).thenReturn(certBytes); + RegistrationProxy defaultMock = mock(RegistrationProxy.class); + doAnswer(invocation -> { + ((OutcomeReceiver<RemotelyProvisionedKey, Exception>) invocation.getArgument(3)) + .onResult(keyMock); + return null; + }).when(defaultMock).getKeyAsync(anyInt(), any(), any(), any()); + + Injector injector = new Injector(); + injector.mRegistrationProxies = Map.of("strongbox", defaultMock); + RemoteProvisioningShellCommand cmd = new RemoteProvisioningShellCommand( + mContext, Process.SHELL_UID, injector); + CommandResult res = exec(cmd, new String[] {"certify", "strongbox"}); + assertThat(res.getErr()).isEmpty(); + assertThat(res.getCode()).isEqualTo(0); + assertThat(res.getOut()).isEqualTo( + "-----BEGIN CERTIFICATE-----\n" + cert + "-----END CERTIFICATE-----\n"); + } } |