1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.security.rkp;
18 
19 import android.content.Context;
20 import android.hardware.security.keymint.DeviceInfo;
21 import android.hardware.security.keymint.IRemotelyProvisionedComponent;
22 import android.hardware.security.keymint.MacedPublicKey;
23 import android.hardware.security.keymint.ProtectedData;
24 import android.hardware.security.keymint.RpcHardwareInfo;
25 import android.os.CancellationSignal;
26 import android.os.OutcomeReceiver;
27 import android.os.RemoteException;
28 import android.os.ServiceManager;
29 import android.os.ShellCommand;
30 import android.security.rkp.service.RegistrationProxy;
31 import android.security.rkp.service.RemotelyProvisionedKey;
32 import android.util.IndentingPrintWriter;
33 
34 import java.io.ByteArrayInputStream;
35 import java.io.ByteArrayOutputStream;
36 import java.io.PrintWriter;
37 import java.security.cert.Certificate;
38 import java.security.cert.CertificateFactory;
39 import java.time.Duration;
40 import java.util.Base64;
41 import java.util.concurrent.CompletableFuture;
42 import java.util.concurrent.Executor;
43 
44 import co.nstant.in.cbor.CborDecoder;
45 import co.nstant.in.cbor.CborEncoder;
46 import co.nstant.in.cbor.CborException;
47 import co.nstant.in.cbor.model.Array;
48 import co.nstant.in.cbor.model.ByteString;
49 import co.nstant.in.cbor.model.DataItem;
50 import co.nstant.in.cbor.model.Map;
51 import co.nstant.in.cbor.model.SimpleValue;
52 import co.nstant.in.cbor.model.UnsignedInteger;
53 
54 class RemoteProvisioningShellCommand extends ShellCommand {
55     private static final String USAGE = "usage: cmd remote_provisioning SUBCOMMAND [ARGS]\n"
56             + "help\n"
57             + "  Show this message.\n"
58             + "dump\n"
59             + "  Dump service diagnostics.\n"
60             + "list\n"
61             + "  List the names of the IRemotelyProvisionedComponent instances.\n"
62             + "csr [--challenge CHALLENGE] NAME\n"
63             + "  Generate and print a base64-encoded CSR from the named\n"
64             + "  IRemotelyProvisionedComponent. A base64-encoded challenge can be provided,\n"
65             + "  or else it defaults to an empty challenge.\n"
66             + "certify NAME\n"
67             + "  Output the PEM-encoded certificate chain provisioned for the named\n"
68             + "  IRemotelyProvisionedComponent.\n";
69 
70     static final String EEK_ED25519_BASE64 = "goRDoQEnoFgqpAEBAycgBiFYIJm57t1e5FL2hcZMYtw+YatXSH11N"
71             + "ymtdoAy0rPLY1jZWEAeIghLpLekyNdOAw7+uK8UTKc7b6XN3Np5xitk/pk5r3bngPpmAIUNB5gqrJFcpyUUS"
72             + "QY0dcqKJ3rZ41pJ6wIDhEOhASegWE6lAQECWCDQrsEVyirPc65rzMvRlh1l6LHd10oaN7lDOpfVmd+YCAM4G"
73             + "CAEIVggvoXnRsSjQlpA2TY6phXQLFh+PdwzAjLS/F4ehyVfcmBYQJvPkOIuS6vRGLEOjl0gJ0uEWP78MpB+c"
74             + "gWDvNeCvvpkeC1UEEvAMb9r6B414vAtzmwvT/L1T6XUg62WovGHWAQ=";
75 
76     static final String EEK_P256_BASE64 = "goRDoQEmoFhNpQECAyYgASFYIPcUituX9MxT79JkEcTjdR9mH6RxDGzP"
77             + "+glGgHSHVPKtIlggXn9b9uzk9hnM/xM3/Q+hyJPbGAZ2xF3m12p3hsMtr49YQC+XjkL7vgctlUeFR5NAsB/U"
78             + "m0ekxESp8qEHhxDHn8sR9L+f6Dvg5zRMFfx7w34zBfTRNDztAgRgehXgedOK/ySEQ6EBJqBYcaYBAgJYIDVz"
79             + "tz+gioCJsSZn6ct8daGvAmH8bmUDkTvTS30UlD5GAzgYIAEhWCDgQc8vDzQPHDMsQbDP1wwwVTXSHmpHE0su"
80             + "0UiWfiScaCJYIB/ORcX7YbqBIfnlBZubOQ52hoZHuB4vRfHOr9o/gGjbWECMs7p+ID4ysGjfYNEdffCsOI5R"
81             + "vP9s4Wc7Snm8Vnizmdh8igfY2rW1f3H02GvfMyc0e2XRKuuGmZirOrSAqr1Q";
82 
83     private static final int ERROR = -1;
84     private static final int SUCCESS = 0;
85 
86     private static final Duration BIND_TIMEOUT = Duration.ofSeconds(10);
87     private static final int KEY_ID = 452436;
88 
89     private final Context mContext;
90     private final int mCallerUid;
91     private final Injector mInjector;
92 
RemoteProvisioningShellCommand(Context context, int callerUid)93     RemoteProvisioningShellCommand(Context context, int callerUid) {
94         this(context, callerUid, new Injector());
95     }
96 
RemoteProvisioningShellCommand(Context context, int callerUid, Injector injector)97     RemoteProvisioningShellCommand(Context context, int callerUid, Injector injector) {
98         mContext = context;
99         mCallerUid = callerUid;
100         mInjector = injector;
101     }
102 
103     @Override
onHelp()104     public void onHelp() {
105         getOutPrintWriter().print(USAGE);
106     }
107 
108     @Override
109     @SuppressWarnings("CatchAndPrintStackTrace")
onCommand(String cmd)110     public int onCommand(String cmd) {
111         if (cmd == null) {
112             return handleDefaultCommands(cmd);
113         }
114         try {
115             switch (cmd) {
116                 case "list":
117                     return list();
118                 case "csr":
119                     return csr();
120                 case "certify":
121                     return certify();
122                 default:
123                     return handleDefaultCommands(cmd);
124             }
125         } catch (Exception e) {
126             e.printStackTrace(getErrPrintWriter());
127             return ERROR;
128         }
129     }
130 
131     @SuppressWarnings("CatchAndPrintStackTrace")
dump(PrintWriter pw)132     void dump(PrintWriter pw) {
133         try {
134             IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
135             for (String name : mInjector.getIrpcNames()) {
136                 ipw.println(name + ":");
137                 ipw.increaseIndent();
138                 dumpRpcInstance(ipw, name);
139                 ipw.decreaseIndent();
140             }
141         } catch (Exception e) {
142             e.printStackTrace(pw);
143         }
144     }
145 
dumpRpcInstance(PrintWriter pw, String name)146     private void dumpRpcInstance(PrintWriter pw, String name) throws RemoteException {
147         RpcHardwareInfo info = mInjector.getIrpcBinder(name).getHardwareInfo();
148         pw.println("hwVersion=" + info.versionNumber);
149         pw.println("rpcAuthorName=" + info.rpcAuthorName);
150         if (info.versionNumber < 3) {
151             pw.println("supportedEekCurve=" + info.supportedEekCurve);
152         }
153         pw.println("uniqueId=" + info.uniqueId);
154         if (info.versionNumber >= 3) {
155             pw.println("supportedNumKeysInCsr=" + info.supportedNumKeysInCsr);
156         }
157     }
158 
list()159     private int list() throws RemoteException {
160         for (String name : mInjector.getIrpcNames()) {
161             getOutPrintWriter().println(name);
162         }
163         return SUCCESS;
164     }
165 
csr()166     private int csr() throws RemoteException, CborException {
167         byte[] challenge = {};
168         String opt;
169         while ((opt = getNextOption()) != null) {
170             switch (opt) {
171                 case "--challenge":
172                     challenge = Base64.getDecoder().decode(getNextArgRequired());
173                     break;
174                 default:
175                     getErrPrintWriter().println("error: unknown option " + opt);
176                     return ERROR;
177             }
178         }
179         String name = getNextArgRequired();
180 
181         IRemotelyProvisionedComponent binder = mInjector.getIrpcBinder(name);
182         RpcHardwareInfo info = binder.getHardwareInfo();
183         MacedPublicKey[] emptyKeys = new MacedPublicKey[] {};
184         byte[] csrBytes;
185         switch (info.versionNumber) {
186             case 1:
187             case 2:
188                 DeviceInfo deviceInfo = new DeviceInfo();
189                 ProtectedData protectedData = new ProtectedData();
190                 byte[] eek = getEekChain(info.supportedEekCurve);
191                 byte[] keysToSignMac = binder.generateCertificateRequest(
192                         /*testMode=*/false, emptyKeys, eek, challenge, deviceInfo, protectedData);
193                 csrBytes = composeCertificateRequestV1(
194                         deviceInfo, challenge, protectedData, keysToSignMac);
195                 break;
196             case 3:
197                 csrBytes = binder.generateCertificateRequestV2(emptyKeys, challenge);
198                 break;
199             default:
200                 getErrPrintWriter().println("error: unsupported hwVersion: " + info.versionNumber);
201                 return ERROR;
202         }
203         getOutPrintWriter().println(Base64.getEncoder().encodeToString(csrBytes));
204         return SUCCESS;
205     }
206 
getEekChain(int supportedEekCurve)207     private byte[] getEekChain(int supportedEekCurve) {
208         switch (supportedEekCurve) {
209             case RpcHardwareInfo.CURVE_25519:
210                 return Base64.getDecoder().decode(EEK_ED25519_BASE64);
211             case RpcHardwareInfo.CURVE_P256:
212                 return Base64.getDecoder().decode(EEK_P256_BASE64);
213             default:
214                 throw new IllegalArgumentException("unsupported EEK curve: " + supportedEekCurve);
215         }
216     }
217 
composeCertificateRequestV1(DeviceInfo deviceInfo, byte[] challenge, ProtectedData protectedData, byte[] keysToSignMac)218     private byte[] composeCertificateRequestV1(DeviceInfo deviceInfo, byte[] challenge,
219             ProtectedData protectedData, byte[] keysToSignMac) throws CborException {
220         Array info = new Array()
221                 .add(decode(deviceInfo.deviceInfo))
222                 .add(new Map());
223 
224         // COSE_Signature with the hmac-sha256 algorithm and without a payload.
225         Array mac = new Array()
226                 .add(new ByteString(encode(
227                             new Map().put(new UnsignedInteger(1), new UnsignedInteger(5)))))
228                 .add(new Map())
229                 .add(SimpleValue.NULL)
230                 .add(new ByteString(keysToSignMac));
231 
232         Array csr = new Array()
233                 .add(info)
234                 .add(new ByteString(challenge))
235                 .add(decode(protectedData.protectedData))
236                 .add(mac);
237 
238         return encode(csr);
239     }
240 
encode(DataItem item)241     private byte[] encode(DataItem item) throws CborException {
242         ByteArrayOutputStream baos = new ByteArrayOutputStream();
243         new CborEncoder(baos).encode(item);
244         return baos.toByteArray();
245     }
246 
decode(byte[] data)247     private DataItem decode(byte[] data) throws CborException {
248         ByteArrayInputStream bais = new ByteArrayInputStream(data);
249         return new CborDecoder(bais).decodeNext();
250     }
251 
certify()252     private int certify() throws Exception {
253         String name = getNextArgRequired();
254 
255         Executor executor = mContext.getMainExecutor();
256         CancellationSignal cancellationSignal = new CancellationSignal();
257         OutcomeFuture<RemotelyProvisionedKey> key = new OutcomeFuture<>();
258         mInjector.getRegistrationProxy(mContext, mCallerUid, name, executor)
259                 .getKeyAsync(KEY_ID, cancellationSignal, executor, key);
260         byte[] encodedCertChain = key.join().getEncodedCertChain();
261         ByteArrayInputStream is = new ByteArrayInputStream(encodedCertChain);
262         PrintWriter pw = getOutPrintWriter();
263         for (Certificate cert : CertificateFactory.getInstance("X.509").generateCertificates(is)) {
264             String encoded = Base64.getEncoder().encodeToString(cert.getEncoded());
265             pw.println("-----BEGIN CERTIFICATE-----");
266             pw.println(encoded.replaceAll("(.{64})", "$1\n").stripTrailing());
267             pw.println("-----END CERTIFICATE-----");
268         }
269         return SUCCESS;
270     }
271 
272     /** Treat an OutcomeReceiver as a future for use in synchronous code. */
273     private static class OutcomeFuture<T> implements OutcomeReceiver<T, Exception> {
274         private CompletableFuture<T> mFuture = new CompletableFuture<>();
275 
276         @Override
onResult(T result)277           public void onResult(T result) {
278             mFuture.complete(result);
279         }
280 
281         @Override
onError(Exception e)282         public void onError(Exception e) {
283             mFuture.completeExceptionally(e);
284         }
285 
join()286         public T join() {
287             return mFuture.join();
288         }
289     }
290 
291     static class Injector {
getIrpcNames()292         String[] getIrpcNames() {
293             return ServiceManager.getDeclaredInstances(IRemotelyProvisionedComponent.DESCRIPTOR);
294         }
295 
getIrpcBinder(String name)296         IRemotelyProvisionedComponent getIrpcBinder(String name) {
297             String irpc = IRemotelyProvisionedComponent.DESCRIPTOR + "/" + name;
298             IRemotelyProvisionedComponent binder =
299                     IRemotelyProvisionedComponent.Stub.asInterface(
300                             ServiceManager.waitForDeclaredService(irpc));
301             if (binder == null) {
302                 throw new IllegalArgumentException("failed to find " + irpc);
303             }
304             return binder;
305         }
306 
getRegistrationProxy( Context context, int callerUid, String name, Executor executor)307         RegistrationProxy getRegistrationProxy(
308                 Context context, int callerUid, String name, Executor executor) {
309             String irpc = IRemotelyProvisionedComponent.DESCRIPTOR + "/" + name;
310             OutcomeFuture<RegistrationProxy> registration = new OutcomeFuture<>();
311             RegistrationProxy.createAsync(
312                     context, callerUid, irpc, BIND_TIMEOUT, executor, registration);
313             return registration.join();
314         }
315     }
316 }
317