Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3653134e authored by Treehugger Robot's avatar Treehugger Robot Committed by Gerrit Code Review
Browse files

Merge "Add shell command to dump certificate chains" into main

parents 03c5f64e 996c5701
Loading
Loading
Loading
Loading
+16 −3
Original line number Diff line number Diff line
@@ -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);
            }
        }
    }
}
+74 −10
Original line number Diff line number Diff line
@@ -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();
        }
    }
}
+126 −14
Original line number Diff line number Diff line
@@ -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 {
        RemoteProvisioningShellCommand cmd = new RemoteProvisioningShellCommand(
                new Injector(Map.of(
        Injector injector = new Injector();
        injector.mIrpcs = Map.of(
                "default", mock(IRemotelyProvisionedComponent.class),
                       "strongbox", mock(IRemotelyProvisionedComponent.class))));
                "strongbox", mock(IRemotelyProvisionedComponent.class));
        RemoteProvisioningShellCommand cmd = new RemoteProvisioningShellCommand(
                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");
    }
}