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.rkpdapp;
18 
19 import android.hardware.security.keymint.DeviceInfo;
20 import android.hardware.security.keymint.IRemotelyProvisionedComponent;
21 import android.hardware.security.keymint.MacedPublicKey;
22 import android.hardware.security.keymint.ProtectedData;
23 import android.hardware.security.keymint.RpcHardwareInfo;
24 import android.net.Uri;
25 import android.os.Build;
26 import android.os.RemoteException;
27 import android.os.ServiceManager;
28 import android.os.ServiceSpecificException;
29 import android.os.SystemProperties;
30 import android.util.Base64;
31 import android.util.Log;
32 
33 import java.io.BufferedInputStream;
34 import java.io.ByteArrayInputStream;
35 import java.io.ByteArrayOutputStream;
36 import java.io.IOException;
37 import java.io.OutputStream;
38 import java.net.HttpURLConnection;
39 import java.net.URL;
40 import java.security.cert.Certificate;
41 import java.security.cert.CertificateException;
42 import java.security.cert.CertificateFactory;
43 import java.security.cert.X509Certificate;
44 import java.util.ArrayList;
45 import java.util.HashMap;
46 import java.util.List;
47 import java.util.UUID;
48 
49 import co.nstant.in.cbor.CborBuilder;
50 import co.nstant.in.cbor.CborDecoder;
51 import co.nstant.in.cbor.CborEncoder;
52 import co.nstant.in.cbor.CborException;
53 import co.nstant.in.cbor.model.Array;
54 import co.nstant.in.cbor.model.ByteString;
55 import co.nstant.in.cbor.model.DataItem;
56 import co.nstant.in.cbor.model.Map;
57 import co.nstant.in.cbor.model.UnicodeString;
58 import co.nstant.in.cbor.model.UnsignedInteger;
59 
60 /**
61  * Command-line utility that verifies each KeyMint instance on this device is able to
62  * get production RKP keys.
63  */
64 public class RkpRegistrationCheck {
65     private static final String TAG = "RegistrationTest";
66     private static final int COSE_HEADER_ALGORITHM = 1;
67     private static final int COSE_ALGORITHM_HMAC_256 = 5;
68 
69     private static final int SHARED_CERTIFICATES_INDEX = 0;
70     private static final int UNIQUE_CERTIFICATES_INDEX = 1;
71 
72     private static final int TIMEOUT_MS = 20_000;
73     private final String mRequestId = UUID.randomUUID().toString();
74     private final String mInstanceName;
75 
76     private static class NotRegisteredException extends Exception {
77     }
78 
79     private static class FetchEekResponse {
80         private static final int EEK_AND_CURVE_INDEX = 0;
81         private static final int CHALLENGE_INDEX = 1;
82 
83         private static final int CURVE_INDEX = 0;
84         private static final int EEK_CERT_CHAIN_INDEX = 1;
85 
86         private final byte[] mChallenge;
87         private final HashMap<Integer, byte[]> mCurveToGeek = new HashMap<>();
88 
FetchEekResponse(DataItem response)89         FetchEekResponse(DataItem response) throws CborException, RemoteException {
90             List<DataItem> respItems = ((Array) response).getDataItems();
91             List<DataItem> allEekChains =
92                     ((Array) respItems.get(EEK_AND_CURVE_INDEX)).getDataItems();
93             for (DataItem entry : allEekChains) {
94                 List<DataItem> curveAndEekChain = ((Array) entry).getDataItems();
95                 UnsignedInteger curve = (UnsignedInteger) curveAndEekChain.get(CURVE_INDEX);
96                 mCurveToGeek.put(curve.getValue().intValue(),
97                         encodeCbor(curveAndEekChain.get(EEK_CERT_CHAIN_INDEX)));
98             }
99 
100             mChallenge = ((ByteString) respItems.get(CHALLENGE_INDEX)).getBytes();
101         }
102 
getEekChain(int curve)103         public byte[] getEekChain(int curve) {
104             return mCurveToGeek.get(curve);
105         }
106 
getChallenge()107         public byte[] getChallenge() {
108             return mChallenge;
109         }
110     }
111 
112     /** Main entry point. */
main(String[] args)113     public static void main(String[] args) {
114         if (SystemProperties.get("remote_provisioning.hostname").isEmpty()) {
115             System.out.println(
116                     "The RKP server hostname is not configured -- RKP is disabled.");
117         }
118 
119         new RkpRegistrationCheck("default").checkNow();
120         new RkpRegistrationCheck("strongbox").checkNow();
121     }
122 
RkpRegistrationCheck(String instanceName)123     RkpRegistrationCheck(String instanceName) {
124         mInstanceName = instanceName;
125     }
126 
checkNow()127     void checkNow() {
128         System.out.println();
129         System.out.println("Checking to see if the device key for HAL '" + mInstanceName
130                 + "' has been registered...");
131 
132         if (!isValidInstance()) {
133             System.err.println("Skipping registration check for '" + mInstanceName + "'.");
134             System.err.println("The HAL does not exist.");
135             return;
136         }
137 
138         try {
139             FetchEekResponse eekResponse = fetchEek();
140             String serviceName = IRemotelyProvisionedComponent.DESCRIPTOR + "/" + mInstanceName;
141             IRemotelyProvisionedComponent binder = IRemotelyProvisionedComponent.Stub.asInterface(
142                     ServiceManager.waitForDeclaredService(serviceName));
143             byte[] csr = generateCsr(binder, eekResponse);
144             X509Certificate[] certs = signCertificates(csr, eekResponse.getChallenge());
145             Log.i(TAG, "Cert chain:");
146             for (X509Certificate c : certs) {
147                 Log.i(TAG, "  " + c.toString());
148             }
149             System.out.println("SUCCESS: Device key for '" + mInstanceName + "' is registered");
150         } catch (ServiceSpecificException e) {
151             Log.e(TAG, e.getMessage(), e);
152             System.err.println("Error getting CSR for '" + mInstanceName + "': '" + e
153                     + "', skipping.");
154         } catch (NotRegisteredException e) {
155             Log.e(TAG, e.getMessage(), e);
156             System.out.println("FAIL: Device key for '" + mInstanceName + "' is NOT registered");
157         } catch (IOException | CborException | RemoteException | CertificateException e) {
158             Log.e(TAG, e.getMessage(), e);
159             System.err.println("Error checking device registration for '" + mInstanceName
160                     + "': '" + e + "', skipping.");
161         }
162     }
163 
isValidInstance()164     private boolean isValidInstance() {
165         // The SE policy checks appear to be very strict for shell, and we'll get a security
166         // exception for any HALs not actually declared. Instead, check to see if the given
167         // instance is in the list we can query.
168         String[] instances = ServiceManager.getDeclaredInstances(
169                 IRemotelyProvisionedComponent.DESCRIPTOR);
170         for (String i : instances) {
171             if (i.equals(mInstanceName)) {
172                 return true;
173             }
174         }
175         return false;
176     }
177 
getBaseUri()178     private Uri getBaseUri() {
179         String hostnameProperty = "remote_provisioning.hostname";
180         String hostname = SystemProperties.get(hostnameProperty);
181         if (hostname.isEmpty()) {
182             throw new RuntimeException("System property '" + hostnameProperty + "' is empty. "
183                     + "This device does not support RKP.");
184         }
185         return new Uri.Builder().scheme("https").authority(hostname).appendPath("v1").build();
186     }
187 
fetchEek()188     FetchEekResponse fetchEek()
189             throws IOException, CborException, RemoteException, NotRegisteredException {
190         final Uri uri = getBaseUri().buildUpon().appendEncodedPath(":fetchEekChain").build();
191 
192         final ByteArrayOutputStream input = new ByteArrayOutputStream();
193         new CborEncoder(input).encode(new CborBuilder()
194                 .addMap()
195                 .put("fingerprint", getFingerprint())
196                 .put(new UnicodeString("id"), new UnsignedInteger(0))
197                 .end()
198                 .build());
199 
200         return new FetchEekResponse(httpPost(uri, input.toByteArray()));
201     }
202 
getFingerprint()203     private String getFingerprint() {
204         // Fake a user build fingerprint so that we will get 444 on unregistered devices instead
205         // of test certs.
206         Log.i(TAG, "Original fingerprint: " + Build.FINGERPRINT);
207         String fingerprint = Build.FINGERPRINT
208                 .replace(":userdebug", ":user")
209                 .replace(":eng", ":user")
210                 .replace("cf_", "cephalopod_");
211         Log.i(TAG, "Modified (prod-like) fingerprint: " + fingerprint);
212         return fingerprint;
213     }
214 
signCertificates(byte[] csr, byte[] challenge)215     X509Certificate[] signCertificates(byte[] csr, byte[] challenge)
216             throws IOException, CborException, CertificateException,
217             NotRegisteredException {
218         String encodedChallenge = Base64.encodeToString(challenge,
219                 Base64.URL_SAFE | Base64.NO_WRAP);
220         final Uri uri = getBaseUri().buildUpon()
221                 .appendEncodedPath(":signCertificates")
222                 .appendQueryParameter("challenge", encodedChallenge)
223                 .build();
224         DataItem response = httpPost(uri, csr);
225         List<DataItem> dataItems = ((Array) response).getDataItems();
226         byte[] sharedCertificates = ((ByteString) dataItems.get(
227                 SHARED_CERTIFICATES_INDEX)).getBytes();
228         DataItem leaf = ((Array) dataItems.get(UNIQUE_CERTIFICATES_INDEX)).getDataItems().get(0);
229 
230         ByteArrayOutputStream fullChainWriter = new ByteArrayOutputStream();
231         fullChainWriter.write(((ByteString) leaf).getBytes());
232         fullChainWriter.write(sharedCertificates);
233 
234         ByteArrayInputStream fullChainReader = new ByteArrayInputStream(
235                 fullChainWriter.toByteArray());
236         CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
237         ArrayList<Certificate> parsedCerts = new ArrayList<>(
238                 certFactory.generateCertificates(fullChainReader));
239         return parsedCerts.toArray(new X509Certificate[0]);
240     }
241 
httpPost(Uri uri, byte[] input)242     DataItem httpPost(Uri uri, byte[] input)
243             throws IOException, CborException, NotRegisteredException {
244         uri = uri.buildUpon().appendQueryParameter("requestId", mRequestId).build();
245         Log.i(TAG, "querying " + uri);
246         HttpURLConnection con = (HttpURLConnection) new URL(uri.toString()).openConnection();
247         con.setRequestMethod("POST");
248         con.setConnectTimeout(TIMEOUT_MS);
249         con.setReadTimeout(TIMEOUT_MS);
250         con.setDoOutput(true);
251 
252         try (OutputStream os = con.getOutputStream()) {
253             os.write(input, 0, input.length);
254         }
255 
256         Log.i(TAG, "HTTP status: " + con.getResponseCode());
257 
258         if (con.getResponseCode() == 444) {
259             throw new NotRegisteredException();
260         }
261 
262         if (con.getResponseCode() != HttpURLConnection.HTTP_OK) {
263             throw new RuntimeException("Server connection failed for url: " + uri
264                     + ", HTTP response code: " + con.getResponseCode());
265         }
266 
267         BufferedInputStream inputStream = new BufferedInputStream(con.getInputStream());
268         ByteArrayOutputStream cborBytes = new ByteArrayOutputStream();
269         byte[] buffer = new byte[1024];
270         int read;
271         while ((read = inputStream.read(buffer, 0, buffer.length)) != -1) {
272             cborBytes.write(buffer, 0, read);
273         }
274         inputStream.close();
275         byte[] response = cborBytes.toByteArray();
276         Log.i(TAG, "response (CBOR): " + Base64.encodeToString(response,
277                 Base64.URL_SAFE | Base64.NO_WRAP));
278         return decodeCbor(response);
279     }
280 
generateCsr(IRemotelyProvisionedComponent irpc, FetchEekResponse eekResponse)281     byte[] generateCsr(IRemotelyProvisionedComponent irpc, FetchEekResponse eekResponse)
282             throws RemoteException, CborException {
283         Map unverifiedDeviceInfo = new Map().put(
284                 new UnicodeString("fingerprint"), new UnicodeString(getFingerprint()));
285 
286         RpcHardwareInfo hwInfo = irpc.getHardwareInfo();
287 
288         MacedPublicKey[] macedKeysToSign = new MacedPublicKey[]{new MacedPublicKey()};
289         irpc.generateEcdsaP256KeyPair(false, macedKeysToSign[0]);
290 
291         if (hwInfo.versionNumber < 3) {
292             Log.i(TAG, "Generating CSRv1");
293             DeviceInfo deviceInfo = new DeviceInfo();
294             ProtectedData protectedData = new ProtectedData();
295             byte[] geekChain = eekResponse.getEekChain(hwInfo.supportedEekCurve);
296             byte[] csrTag = irpc.generateCertificateRequest(false, macedKeysToSign, geekChain,
297                     eekResponse.getChallenge(), deviceInfo, protectedData);
298             Array mac0Message = buildMac0MessageForV1Csr(macedKeysToSign[0], csrTag);
299             return encodeCbor(new CborBuilder()
300                     .addArray()
301                     .addArray()
302                     .add(decodeCbor(deviceInfo.deviceInfo))
303                     .add(unverifiedDeviceInfo)
304                     .end()
305                     .add(eekResponse.getChallenge())
306                     .add(decodeCbor(protectedData.protectedData))
307                     .add(mac0Message)
308                     .end()
309                     .build().get(0));
310         } else {
311             Log.i(TAG, "Generating CSRv2");
312             byte[] csrBytes = irpc.generateCertificateRequestV2(macedKeysToSign,
313                     eekResponse.getChallenge());
314             Array array = (Array) decodeCbor(csrBytes);
315             array.add(unverifiedDeviceInfo);
316             return encodeCbor(array);
317         }
318     }
319 
buildMac0MessageForV1Csr(MacedPublicKey macedKeyToSign, byte[] csrTag)320     Array buildMac0MessageForV1Csr(MacedPublicKey macedKeyToSign, byte[] csrTag)
321             throws CborException {
322         DataItem macedPayload = ((Array) decodeCbor(
323                 macedKeyToSign.macedKey)).getDataItems().get(2);
324         Map macedCoseKey = (Map) decodeCbor(((ByteString) macedPayload).getBytes());
325         byte[] macedKeys = encodeCbor(new Array().add(macedCoseKey));
326 
327         Map protectedHeaders = new Map().put(
328                 new UnsignedInteger(COSE_HEADER_ALGORITHM),
329                 new UnsignedInteger(COSE_ALGORITHM_HMAC_256));
330         return new Array()
331                 .add(new ByteString(encodeCbor(protectedHeaders)))
332                 .add(new Map())
333                 .add(new ByteString(macedKeys))
334                 .add(new ByteString(csrTag));
335     }
336 
decodeCbor(byte[] encodedBytes)337     static DataItem decodeCbor(byte[] encodedBytes) throws CborException {
338         ByteArrayInputStream inputStream = new ByteArrayInputStream(encodedBytes);
339         return new CborDecoder(inputStream).decode().get(0);
340     }
341 
encodeCbor(final DataItem dataItem)342     static byte[] encodeCbor(final DataItem dataItem) throws CborException {
343         final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
344         new CborEncoder(outputStream).encode(dataItem);
345         return outputStream.toByteArray();
346     }
347 }
348