Merge "Add command line interface for remote_provisioning" into udc-dev

This commit is contained in:
Andrew Scull
2023-04-20 15:28:12 +00:00
committed by Android (Google) Code Review
4 changed files with 513 additions and 0 deletions

View File

@@ -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",

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,250 @@
/*
* 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;
}
}
}

View File

@@ -0,0 +1,244 @@
/*
* 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");
}
}