Loading services/core/Android.bp +2 −0 Original line number Diff line number Diff line Loading @@ -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", Loading @@ -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", Loading services/core/java/com/android/server/security/rkp/RemoteProvisioningService.java +17 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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); } } } services/core/java/com/android/server/security/rkp/RemoteProvisioningShellCommand.java 0 → 100644 +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; } } } services/tests/RemoteProvisioningServiceTests/src/com/android/server/security/rkp/RemoteProvisioningShellCommandTest.java 0 → 100644 +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"); } } Loading
services/core/Android.bp +2 −0 Original line number Diff line number Diff line Loading @@ -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", Loading @@ -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", Loading
services/core/java/com/android/server/security/rkp/RemoteProvisioningService.java +17 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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); } } }
services/core/java/com/android/server/security/rkp/RemoteProvisioningShellCommand.java 0 → 100644 +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; } } }
services/tests/RemoteProvisioningServiceTests/src/com/android/server/security/rkp/RemoteProvisioningShellCommandTest.java 0 → 100644 +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"); } }