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

Commit 0fb96bf2 authored by Andrew Scull's avatar Andrew Scull
Browse files

Add command line interface for remote_provisioning

Provide an ADB interface to interact with the remote_provisioning
service for diagnostic purposes. The service reports details of the IRPC
components in dumpsys and allows ADB to query the IRPC instances and
request a CSR from each of them.

Test: adb shell dumpsys remote_provisioning
Test: adb shell cmd remote_provisioning
Test: atest RemoteProvisioningShellCommandTest
Bug: 265747549
Change-Id: I593a4b599f4fc8e27d7f79d1d5f3955eabc9641d
parent 4a88a91a
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -161,6 +161,7 @@ java_library_static {
        "android.hardware.health-V2-java", // AIDL
        "android.hardware.health-translate-java",
        "android.hardware.light-V1-java",
        "android.hardware.security.rkp-V3-java",
        "android.hardware.tv.cec-V1.1-java",
        "android.hardware.tv.hdmi.cec-V1-java",
        "android.hardware.tv.hdmi.connection-V1-java",
@@ -177,6 +178,7 @@ java_library_static {
        "android.hardware.power.stats-V2-java",
        "android.hardware.power-V4-java",
        "android.hidl.manager-V1.2-java",
        "cbor-java",
        "icu4j_calendar_astronomer",
        "netd-client",
        "overlayable_policy_aidl-java",
+17 −0
Original line number Diff line number Diff line
@@ -19,14 +19,18 @@ package com.android.server.security.rkp;
import android.content.Context;
import android.os.Binder;
import android.os.OutcomeReceiver;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.security.rkp.IGetRegistrationCallback;
import android.security.rkp.IRemoteProvisioning;
import android.security.rkp.service.RegistrationProxy;
import android.util.Log;

import com.android.internal.util.DumpUtils;
import com.android.server.SystemService;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.time.Duration;
import java.util.concurrent.Executor;

@@ -97,5 +101,18 @@ public class RemoteProvisioningService extends SystemService {
                Binder.restoreCallingIdentity(callingIdentity);
            }
        }

        @Override
        protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
            if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) return;
            new RemoteProvisioningShellCommand().dump(pw);
        }

        @Override
        public int handleShellCommand(ParcelFileDescriptor in, ParcelFileDescriptor out,
                ParcelFileDescriptor err, String[] args) {
            return new RemoteProvisioningShellCommand().exec(this, in.getFileDescriptor(),
                    out.getFileDescriptor(), err.getFileDescriptor(), args);
        }
    }
}
+250 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * 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 com.android.server.security.rkp;

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.RemoteException;
import android.os.ServiceManager;
import android.os.ShellCommand;
import android.util.IndentingPrintWriter;

import com.android.internal.annotations.VisibleForTesting;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.PrintWriter;
import java.util.Base64;

import co.nstant.in.cbor.CborDecoder;
import co.nstant.in.cbor.CborEncoder;
import co.nstant.in.cbor.CborException;
import co.nstant.in.cbor.model.Array;
import co.nstant.in.cbor.model.ByteString;
import co.nstant.in.cbor.model.DataItem;
import co.nstant.in.cbor.model.Map;
import co.nstant.in.cbor.model.SimpleValue;
import co.nstant.in.cbor.model.UnsignedInteger;

class RemoteProvisioningShellCommand extends ShellCommand {
    private static final String USAGE = "usage: cmd remote_provisioning SUBCOMMAND [ARGS]\n"
            + "help\n"
            + "  Show this message.\n"
            + "dump\n"
            + "  Dump service diagnostics.\n"
            + "list [--min-version MIN_VERSION]\n"
            + "  List the names of the IRemotelyProvisionedComponent instances.\n"
            + "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";

    @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"
            + "tz+gioCJsSZn6ct8daGvAmH8bmUDkTvTS30UlD5GAzgYIAEhWCDgQc8vDzQPHDMsQbDP1wwwVTXSHmpHE0su"
            + "0UiWfiScaCJYIB/ORcX7YbqBIfnlBZubOQ52hoZHuB4vRfHOr9o/gGjbWECMs7p+ID4ysGjfYNEdffCsOI5R"
            + "vP9s4Wc7Snm8Vnizmdh8igfY2rW1f3H02GvfMyc0e2XRKuuGmZirOrSAqr1Q";

    private static final int ERROR = -1;
    private static final int SUCCESS = 0;

    private final Injector mInjector;

    RemoteProvisioningShellCommand() {
        this(new Injector());
    }

    @VisibleForTesting
    RemoteProvisioningShellCommand(Injector injector) {
        mInjector = injector;
    }

    @Override
    public void onHelp() {
        getOutPrintWriter().print(USAGE);
    }

    @Override
    @SuppressWarnings("CatchAndPrintStackTrace")
    public int onCommand(String cmd) {
        if (cmd == null) {
            return handleDefaultCommands(cmd);
        }
        try {
            switch (cmd) {
                case "list":
                    return list();
                case "csr":
                    return csr();
                default:
                    return handleDefaultCommands(cmd);
            }
        } catch (Exception e) {
            e.printStackTrace(getErrPrintWriter());
            return ERROR;
        }
    }

    @SuppressWarnings("CatchAndPrintStackTrace")
    void dump(PrintWriter pw) {
        try {
            IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
            for (String name : mInjector.getIrpcNames()) {
                ipw.println(name + ":");
                ipw.increaseIndent();
                dumpRpcInstance(ipw, name);
                ipw.decreaseIndent();
            }
        } catch (Exception e) {
            e.printStackTrace(pw);
        }
    }

    private void dumpRpcInstance(PrintWriter pw, String name) throws RemoteException {
        RpcHardwareInfo info = mInjector.getIrpcBinder(name).getHardwareInfo();
        pw.println("hwVersion=" + info.versionNumber);
        pw.println("rpcAuthorName=" + info.rpcAuthorName);
        if (info.versionNumber < 3) {
            pw.println("supportedEekCurve=" + info.supportedEekCurve);
        }
        pw.println("uniqueId=" + info.uniqueId);
        pw.println("supportedNumKeysInCsr=" + info.supportedNumKeysInCsr);
    }

    private int list() throws RemoteException {
        for (String name : mInjector.getIrpcNames()) {
            getOutPrintWriter().println(name);
        }
        return SUCCESS;
    }

    private int csr() throws RemoteException, CborException {
        byte[] challenge = {};
        String opt;
        while ((opt = getNextOption()) != null) {
            switch (opt) {
                case "--challenge":
                    challenge = Base64.getDecoder().decode(getNextArgRequired());
                    break;
                default:
                    getErrPrintWriter().println("error: unknown option");
                    return ERROR;
            }
        }
        String name = getNextArgRequired();

        IRemotelyProvisionedComponent binder = mInjector.getIrpcBinder(name);
        RpcHardwareInfo info = binder.getHardwareInfo();
        MacedPublicKey[] emptyKeys = new MacedPublicKey[] {};
        byte[] csrBytes;
        switch (info.versionNumber) {
            case 1:
            case 2:
                DeviceInfo deviceInfo = new DeviceInfo();
                ProtectedData protectedData = new ProtectedData();
                byte[] eek = getEekChain(info.supportedEekCurve);
                byte[] keysToSignMac = binder.generateCertificateRequest(
                        /*testMode=*/false, emptyKeys, eek, challenge, deviceInfo, protectedData);
                csrBytes = composeCertificateRequestV1(
                        deviceInfo, challenge, protectedData, keysToSignMac);
                break;
            case 3:
                csrBytes = binder.generateCertificateRequestV2(emptyKeys, challenge);
                break;
            default:
                getErrPrintWriter().println("error: unsupported hwVersion: " + info.versionNumber);
                return ERROR;
        }
        getOutPrintWriter().println(Base64.getEncoder().encodeToString(csrBytes));
        return SUCCESS;
    }

    private byte[] getEekChain(int supportedEekCurve) {
        switch (supportedEekCurve) {
            case RpcHardwareInfo.CURVE_25519:
                return Base64.getDecoder().decode(EEK_ED25519_BASE64);
            case RpcHardwareInfo.CURVE_P256:
                return Base64.getDecoder().decode(EEK_P256_BASE64);
            default:
                throw new IllegalArgumentException("unsupported EEK curve: " + supportedEekCurve);
        }
    }

    private byte[] composeCertificateRequestV1(DeviceInfo deviceInfo, byte[] challenge,
            ProtectedData protectedData, byte[] keysToSignMac) throws CborException {
        Array info = new Array()
                .add(decode(deviceInfo.deviceInfo))
                .add(new Map());

        // COSE_Signature with the hmac-sha256 algorithm and without a payload.
        Array mac = new Array()
                .add(new ByteString(encode(
                            new Map().put(new UnsignedInteger(1), new UnsignedInteger(5)))))
                .add(new Map())
                .add(SimpleValue.NULL)
                .add(new ByteString(keysToSignMac));

        Array csr = new Array()
                .add(info)
                .add(new ByteString(challenge))
                .add(decode(protectedData.protectedData))
                .add(mac);

        return encode(csr);
    }

    private byte[] encode(DataItem item) throws CborException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        new CborEncoder(baos).encode(item);
        return baos.toByteArray();
    }

    private DataItem decode(byte[] data) throws CborException {
        ByteArrayInputStream bais = new ByteArrayInputStream(data);
        return new CborDecoder(bais).decodeNext();
    }

    @VisibleForTesting
    static class Injector {
        String[] getIrpcNames() {
            return ServiceManager.getDeclaredInstances(IRemotelyProvisionedComponent.DESCRIPTOR);
        }

        IRemotelyProvisionedComponent getIrpcBinder(String name) {
            String irpc = IRemotelyProvisionedComponent.DESCRIPTOR + "/" + name;
            IRemotelyProvisionedComponent binder =
                    IRemotelyProvisionedComponent.Stub.asInterface(
                            ServiceManager.waitForDeclaredService(irpc));
            if (binder == null) {
                throw new IllegalArgumentException("failed to find " + irpc);
            }
            return binder;
        }
    }
}
+244 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * 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 com.android.server.security.rkp;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
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.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.Binder;
import android.os.FileUtils;

import androidx.test.runner.AndroidJUnit4;

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;

@RunWith(AndroidJUnit4.class)
public class RemoteProvisioningShellCommandTest {

    private static class Injector extends RemoteProvisioningShellCommand.Injector {

        private final Map<String, IRemotelyProvisionedComponent> mIrpcs;

        Injector(Map irpcs) {
            mIrpcs = irpcs;
        }

        @Override
        String[] getIrpcNames() {
            return mIrpcs.keySet().toArray(new String[0]);
        }

        @Override
        IRemotelyProvisionedComponent getIrpcBinder(String name) {
            IRemotelyProvisionedComponent irpc = mIrpcs.get(name);
            if (irpc == null) {
                throw new IllegalArgumentException("failed to find " + irpc);
            }
            return irpc;
        }
    }

    private static class CommandResult {
        private int mCode;
        private String mOut;
        private String mErr;

        CommandResult(int code, String out, String err) {
            mCode = code;
            mOut = out;
            mErr = err;
        }

        int getCode() {
            return mCode;
        }

        String getOut() {
            return mOut;
        }

        String getErr() {
            return mErr;
        }
    }

    private static CommandResult exec(
            RemoteProvisioningShellCommand cmd, String[] args) throws Exception {
        File in = File.createTempFile("rpsct_in_", null);
        File out = File.createTempFile("rpsct_out_", null);
        File err = File.createTempFile("rpsct_err_", null);
        int code = cmd.exec(
                new Binder(),
                new FileInputStream(in).getFD(),
                new FileOutputStream(out).getFD(),
                new FileOutputStream(err).getFD(),
                args);
        return new CommandResult(
                code, FileUtils.readTextFile(out, 0, null), FileUtils.readTextFile(err, 0, null));
    }

    @Test
    public void list_zeroInstances() throws Exception {
        RemoteProvisioningShellCommand cmd = new RemoteProvisioningShellCommand(
                new Injector(Map.of()));
        CommandResult res = exec(cmd, new String[] {"list"});
        assertThat(res.getErr()).isEmpty();
        assertThat(res.getCode()).isEqualTo(0);
        assertThat(res.getOut()).isEmpty();
    }

    @Test
    public void list_oneInstances() throws Exception {
        RemoteProvisioningShellCommand cmd = new RemoteProvisioningShellCommand(
                new Injector(Map.of("default", mock(IRemotelyProvisionedComponent.class))));
        CommandResult res = exec(cmd, new String[] {"list"});
        assertThat(res.getErr()).isEmpty();
        assertThat(res.getCode()).isEqualTo(0);
        assertThat(Arrays.asList(res.getOut().split("\n"))).containsExactly("default");
    }

    @Test
    public void list_twoInstances() throws Exception {
        RemoteProvisioningShellCommand cmd = new RemoteProvisioningShellCommand(
                new Injector(Map.of(
                       "default", mock(IRemotelyProvisionedComponent.class),
                       "strongbox", mock(IRemotelyProvisionedComponent.class))));
        CommandResult res = exec(cmd, new String[] {"list"});
        assertThat(res.getErr()).isEmpty();
        assertThat(res.getCode()).isEqualTo(0);
        assertThat(Arrays.asList(res.getOut().split("\n"))).containsExactly("default", "strongbox");
    }

    @Test
    public void csr_hwVersion1_withChallenge() throws Exception {
        IRemotelyProvisionedComponent defaultMock = mock(IRemotelyProvisionedComponent.class);
        RpcHardwareInfo defaultInfo = new RpcHardwareInfo();
        defaultInfo.versionNumber = 1;
        defaultInfo.supportedEekCurve = RpcHardwareInfo.CURVE_25519;
        when(defaultMock.getHardwareInfo()).thenReturn(defaultInfo);
        doAnswer(invocation -> {
            ((DeviceInfo) invocation.getArgument(4)).deviceInfo = new byte[] {0x00};
            ((ProtectedData) invocation.getArgument(5)).protectedData = new byte[] {0x00};
            return new byte[] {0x77, 0x77, 0x77, 0x77};
        }).when(defaultMock).generateCertificateRequest(
                anyBoolean(), any(), any(), any(), any(), any());

        RemoteProvisioningShellCommand cmd = new RemoteProvisioningShellCommand(
                new Injector(Map.of("default", defaultMock)));
        CommandResult res = exec(cmd, new String[] {
                "csr", "--challenge", "dGVzdHRlc3R0ZXN0dGVzdA==", "default"});
        verify(defaultMock).generateCertificateRequest(
                /*test_mode=*/eq(false),
                eq(new MacedPublicKey[0]),
                eq(Base64.getDecoder().decode(RemoteProvisioningShellCommand.EEK_ED25519_BASE64)),
                eq(new byte[] {
                        0x74, 0x65, 0x73, 0x74, 0x74, 0x65, 0x73, 0x74,
                        0x74, 0x65, 0x73, 0x74, 0x74, 0x65, 0x73, 0x74}),
                any(DeviceInfo.class),
                any(ProtectedData.class));
        assertThat(res.getErr()).isEmpty();
        assertThat(res.getCode()).isEqualTo(0);
    }

    @Test
    public void csr_hwVersion2_withChallenge() throws Exception {
        IRemotelyProvisionedComponent defaultMock = mock(IRemotelyProvisionedComponent.class);
        RpcHardwareInfo defaultInfo = new RpcHardwareInfo();
        defaultInfo.versionNumber = 2;
        defaultInfo.supportedEekCurve = RpcHardwareInfo.CURVE_P256;
        when(defaultMock.getHardwareInfo()).thenReturn(defaultInfo);
        doAnswer(invocation -> {
            ((DeviceInfo) invocation.getArgument(4)).deviceInfo = new byte[] {0x00};
            ((ProtectedData) invocation.getArgument(5)).protectedData = new byte[] {0x00};
            return new byte[] {0x77, 0x77, 0x77, 0x77};
        }).when(defaultMock).generateCertificateRequest(
                anyBoolean(), any(), any(), any(), any(), any());

        RemoteProvisioningShellCommand cmd = new RemoteProvisioningShellCommand(
                new Injector(Map.of("default", defaultMock)));
        CommandResult res = exec(cmd, new String[] {
                "csr", "--challenge", "dGVzdHRlc3R0ZXN0dGVzdA==", "default"});
        verify(defaultMock).generateCertificateRequest(
                /*test_mode=*/eq(false),
                eq(new MacedPublicKey[0]),
                eq(Base64.getDecoder().decode(RemoteProvisioningShellCommand.EEK_P256_BASE64)),
                eq(new byte[] {
                        0x74, 0x65, 0x73, 0x74, 0x74, 0x65, 0x73, 0x74,
                        0x74, 0x65, 0x73, 0x74, 0x74, 0x65, 0x73, 0x74}),
                any(DeviceInfo.class),
                any(ProtectedData.class));
        assertThat(res.getErr()).isEmpty();
        assertThat(res.getCode()).isEqualTo(0);
    }

    @Test
    public void csr_hwVersion3_withoutChallenge() throws Exception {
        IRemotelyProvisionedComponent defaultMock = mock(IRemotelyProvisionedComponent.class);
        RpcHardwareInfo defaultInfo = new RpcHardwareInfo();
        defaultInfo.versionNumber = 3;
        when(defaultMock.getHardwareInfo()).thenReturn(defaultInfo);
        when(defaultMock.generateCertificateRequestV2(any(), any()))
            .thenReturn(new byte[] {0x68, 0x65, 0x6c, 0x6c, 0x6f});

        RemoteProvisioningShellCommand cmd = new RemoteProvisioningShellCommand(
                new Injector(Map.of("default", defaultMock)));
        CommandResult res = exec(cmd, new String[] {"csr", "default"});
        verify(defaultMock).generateCertificateRequestV2(new MacedPublicKey[0], new byte[0]);
        assertThat(res.getErr()).isEmpty();
        assertThat(res.getCode()).isEqualTo(0);
        assertThat(res.getOut()).isEqualTo("aGVsbG8=\n");
    }

    @Test
    public void csr_hwVersion3_withChallenge() throws Exception {
        IRemotelyProvisionedComponent defaultMock = mock(IRemotelyProvisionedComponent.class);
        RpcHardwareInfo defaultInfo = new RpcHardwareInfo();
        defaultInfo.versionNumber = 3;
        when(defaultMock.getHardwareInfo()).thenReturn(defaultInfo);
        when(defaultMock.generateCertificateRequestV2(any(), any()))
            .thenReturn(new byte[] {0x68, 0x69});

        RemoteProvisioningShellCommand cmd = new RemoteProvisioningShellCommand(
                new Injector(Map.of("default", defaultMock)));
        CommandResult res = exec(cmd, new String[] {"csr", "--challenge", "dHJpYWw=", "default"});
        verify(defaultMock).generateCertificateRequestV2(
                new MacedPublicKey[0], new byte[] {0x74, 0x72, 0x69, 0x61, 0x6c});
        assertThat(res.getErr()).isEmpty();
        assertThat(res.getCode()).isEqualTo(0);
        assertThat(res.getOut()).isEqualTo("aGk=\n");
    }
}