• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /**
2  * Copyright (C) 2022 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.utils;
18 
19 import static android.content.pm.PackageManager.MATCH_APEX;
20 
21 import android.content.Context;
22 import android.content.pm.PackageManager;
23 import android.hardware.security.keymint.MacedPublicKey;
24 import android.os.Build;
25 import android.util.Log;
26 
27 import com.android.rkpdapp.GeekResponse;
28 import com.android.rkpdapp.RkpdException;
29 import com.android.rkpdapp.database.InstantConverter;
30 import com.android.rkpdapp.database.RkpKey;
31 
32 import java.io.ByteArrayInputStream;
33 import java.io.ByteArrayOutputStream;
34 import java.io.IOException;
35 import java.time.Duration;
36 import java.util.ArrayList;
37 import java.util.List;
38 
39 import co.nstant.in.cbor.CborBuilder;
40 import co.nstant.in.cbor.CborDecoder;
41 import co.nstant.in.cbor.CborEncoder;
42 import co.nstant.in.cbor.CborException;
43 import co.nstant.in.cbor.model.Array;
44 import co.nstant.in.cbor.model.ByteString;
45 import co.nstant.in.cbor.model.DataItem;
46 import co.nstant.in.cbor.model.MajorType;
47 import co.nstant.in.cbor.model.Map;
48 import co.nstant.in.cbor.model.NegativeInteger;
49 import co.nstant.in.cbor.model.UnicodeString;
50 import co.nstant.in.cbor.model.UnsignedInteger;
51 
52 public class CborUtils {
53     public static final int EC_CURVE_P256 = 1;
54     public static final int EC_CURVE_25519 = 2;
55 
56     public static final String EXTRA_KEYS = "num_extra_attestation_keys";
57     public static final String TIME_TO_REFRESH = "time_to_refresh_hours";
58     public static final String PROVISIONING_URL = "provisioning_url";
59     public static final String LAST_BAD_CERT_TIME_START_MILLIS = "bad_cert_start";
60     public static final String LAST_BAD_CERT_TIME_END_MILLIS = "bad_cert_end";
61 
62     private static final int RESPONSE_CERT_ARRAY_INDEX = 0;
63     private static final int RESPONSE_ARRAY_SIZE = 1;
64 
65     private static final int SHARED_CERTIFICATES_INDEX = 0;
66     private static final int UNIQUE_CERTIFICATES_INDEX = 1;
67     private static final int CERT_ARRAY_ENTRIES = 2;
68 
69     private static final int EEK_AND_CURVE_INDEX = 0;
70     private static final int CHALLENGE_INDEX = 1;
71     private static final int CONFIG_INDEX = 2;
72 
73     private static final int CURVE_AND_EEK_CHAIN_LENGTH = 2;
74     private static final int CURVE_INDEX = 0;
75     private static final int EEK_CERT_CHAIN_INDEX = 1;
76 
77     private static final int EEK_ARRAY_ENTRIES_NO_CONFIG = 2;
78     private static final int EEK_ARRAY_ENTRIES_WITH_CONFIG = 3;
79     private static final String TAG = "RkpdCborUtils";
80     private static final byte[] EMPTY_MAP = new byte[] {(byte) 0xA0};
81     private static final int KEY_PARAMETER_X = -2;
82     private static final int KEY_PARAMETER_Y = -3;
83     private static final int COSE_HEADER_ALGORITHM = 1;
84     private static final int COSE_ALGORITHM_HMAC_256 = 5;
85 
86     /**
87      * Parses the signed certificate chains returned by the server. In order to reduce data use over
88      * the wire, shared certificate chain prefixes are separated from the remaining unique portions
89      * of each individual certificate chain. This method first parses the shared prefix certificates
90      * and then prepends them to each unique certificate chain. Each PEM-encoded certificate chain
91      * is returned in a byte array.
92      *
93      * @param serverResp The CBOR blob received from the server which contains all signed
94      *                      certificate chains.
95      *
96      * @return A List object where each byte[] entry is an entire DER-encoded certificate chain.
97      */
parseSignedCertificates(byte[] serverResp)98     public static List<byte[]> parseSignedCertificates(byte[] serverResp) {
99         try {
100             ByteArrayInputStream bais = new ByteArrayInputStream(serverResp);
101             List<DataItem> dataItems = new CborDecoder(bais).decode();
102             if (dataItems.size() != RESPONSE_ARRAY_SIZE
103                     || !checkType(dataItems.get(RESPONSE_CERT_ARRAY_INDEX),
104                                   MajorType.ARRAY, "CborResponse")) {
105                 Log.e(TAG, "Improper formatting of CBOR response. Expected size 1. Actual: "
106                             + dataItems.size());
107                 return null;
108             }
109             dataItems = ((Array) dataItems.get(RESPONSE_CERT_ARRAY_INDEX)).getDataItems();
110             if (dataItems.size() != CERT_ARRAY_ENTRIES) {
111                 Log.e(TAG, "Incorrect number of certificate array entries. Expected: 2. Actual: "
112                             + dataItems.size());
113                 return null;
114             }
115             if (!checkType(dataItems.get(SHARED_CERTIFICATES_INDEX),
116                            MajorType.BYTE_STRING, "SharedCertificates")
117                     || !checkType(dataItems.get(UNIQUE_CERTIFICATES_INDEX),
118                                   MajorType.ARRAY, "UniqueCertificates")) {
119                 return null;
120             }
121             byte[] sharedCertificates =
122                     ((ByteString) dataItems.get(SHARED_CERTIFICATES_INDEX)).getBytes();
123             Array uniqueCertificates = (Array) dataItems.get(UNIQUE_CERTIFICATES_INDEX);
124             List<byte[]> uniqueCertificateChains = new ArrayList<>();
125             for (DataItem entry : uniqueCertificates.getDataItems()) {
126                 if (!checkType(entry, MajorType.BYTE_STRING, "UniqueCertificate")) {
127                     return null;
128                 }
129                 ByteArrayOutputStream concat = new ByteArrayOutputStream();
130                 // DER encoding specifies certificate chains ordered from leaf to root.
131                 concat.write(((ByteString) entry).getBytes());
132                 concat.write(sharedCertificates);
133                 uniqueCertificateChains.add(concat.toByteArray());
134             }
135             return uniqueCertificateChains;
136         } catch (CborException e) {
137             Log.e(TAG, "CBOR decoding failed.", e);
138         } catch (IOException e) {
139             Log.e(TAG, "Writing bytes failed.", e);
140         }
141         return null;
142     }
143 
checkType(DataItem item, MajorType majorType, String field)144     private static boolean checkType(DataItem item, MajorType majorType, String field) {
145         if (item.getMajorType() != majorType) {
146             Log.e(TAG, "Incorrect CBOR type for field: " + field + ". Expected " + majorType.name()
147                         + ". Actual: " + item.getMajorType().name());
148             return false;
149         }
150         return true;
151     }
152 
parseDeviceConfig(GeekResponse resp, DataItem deviceConfig)153     private static boolean parseDeviceConfig(GeekResponse resp, DataItem deviceConfig) {
154         if (!checkType(deviceConfig, MajorType.MAP, "DeviceConfig")) {
155             return false;
156         }
157         Map deviceConfiguration = (Map) deviceConfig;
158         DataItem extraKeys =
159                 deviceConfiguration.get(new UnicodeString(EXTRA_KEYS));
160         DataItem timeToRefreshHours =
161                 deviceConfiguration.get(new UnicodeString(TIME_TO_REFRESH));
162         DataItem newUrl =
163                 deviceConfiguration.get(new UnicodeString(PROVISIONING_URL));
164         DataItem lastBadCertTimeStart =
165                 deviceConfiguration.get(new UnicodeString(LAST_BAD_CERT_TIME_START_MILLIS));
166         DataItem lastBadCertTimeEnd =
167                 deviceConfiguration.get(new UnicodeString(LAST_BAD_CERT_TIME_END_MILLIS));
168         if (extraKeys != null) {
169             if (!checkType(extraKeys, MajorType.UNSIGNED_INTEGER, "ExtraKeys")) {
170                 return false;
171             }
172             resp.numExtraAttestationKeys = ((UnsignedInteger) extraKeys).getValue().intValue();
173         }
174         if (timeToRefreshHours != null) {
175             if (!checkType(timeToRefreshHours, MajorType.UNSIGNED_INTEGER, "TimeToRefresh")) {
176                 return false;
177             }
178             resp.timeToRefresh =
179                     Duration.ofHours(((UnsignedInteger) timeToRefreshHours).getValue().intValue());
180         }
181         if (newUrl != null) {
182             if (!checkType(newUrl, MajorType.UNICODE_STRING, "ProvisioningURL")) {
183                 return false;
184             }
185             resp.provisioningUrl = ((UnicodeString) newUrl).getString();
186         }
187         if (lastBadCertTimeStart != null) {
188             if (!checkType(lastBadCertTimeStart, MajorType.UNSIGNED_INTEGER, "BadCertTimeStart")) {
189                 return false;
190             }
191             resp.lastBadCertTimeStart = InstantConverter.fromTimestamp(
192                     ((UnsignedInteger) lastBadCertTimeStart).getValue().longValue());
193         }
194         if (lastBadCertTimeEnd != null) {
195             if (!checkType(lastBadCertTimeEnd, MajorType.UNSIGNED_INTEGER, "BadCertTimeEnd")) {
196                 return false;
197             }
198             resp.lastBadCertTimeEnd = InstantConverter.fromTimestamp(
199                     ((UnsignedInteger) lastBadCertTimeEnd).getValue().longValue());
200         }
201         return true;
202     }
203 
204     /**
205      * Parses the Google Endpoint Encryption Key response provided by the server which contains a
206      * Google signed EEK and a challenge for use by the underlying IRemotelyProvisionedComponent HAL
207      */
parseGeekResponse(byte[] serverResp)208     public static GeekResponse parseGeekResponse(byte[] serverResp) {
209         try {
210             GeekResponse resp = new GeekResponse();
211             ByteArrayInputStream bais = new ByteArrayInputStream(serverResp);
212             List<DataItem> dataItems = new CborDecoder(bais).decode();
213             if (dataItems.size() != RESPONSE_ARRAY_SIZE
214                     || !checkType(dataItems.get(RESPONSE_CERT_ARRAY_INDEX),
215                                   MajorType.ARRAY, "CborResponse")) {
216                 Log.e(TAG, "Improper formatting of CBOR response. Expected size 1. Actual: "
217                             + dataItems.size());
218                 return null;
219             }
220             List<DataItem> respItems =
221                     ((Array) dataItems.get(RESPONSE_CERT_ARRAY_INDEX)).getDataItems();
222             if (respItems.size() != EEK_ARRAY_ENTRIES_NO_CONFIG
223                     && respItems.size() != EEK_ARRAY_ENTRIES_WITH_CONFIG) {
224                 Log.e(TAG, "Incorrect number of certificate array entries. Expected: "
225                             + EEK_ARRAY_ENTRIES_NO_CONFIG + " or " + EEK_ARRAY_ENTRIES_WITH_CONFIG
226                             + ". Actual: " + respItems.size());
227                 return null;
228             }
229             if (!checkType(respItems.get(EEK_AND_CURVE_INDEX), MajorType.ARRAY, "EekAndCurveArr")) {
230                 return null;
231             }
232             List<DataItem> curveAndEekChains =
233                     ((Array) respItems.get(EEK_AND_CURVE_INDEX)).getDataItems();
234             for (int i = 0; i < curveAndEekChains.size(); i++) {
235                 if (!checkType(curveAndEekChains.get(i), MajorType.ARRAY, "EekAndCurve")) {
236                     return null;
237                 }
238                 List<DataItem> curveAndEekChain =
239                         ((Array) curveAndEekChains.get(i)).getDataItems();
240                 if (curveAndEekChain.size() != CURVE_AND_EEK_CHAIN_LENGTH) {
241                     Log.e(TAG, "Wrong size. Expected: " + CURVE_AND_EEK_CHAIN_LENGTH + ". Actual: "
242                                + curveAndEekChain.size());
243                     return null;
244                 }
245                 if (!checkType(curveAndEekChain.get(CURVE_INDEX),
246                                MajorType.UNSIGNED_INTEGER, "Curve")
247                         || !checkType(curveAndEekChain.get(EEK_CERT_CHAIN_INDEX),
248                                                            MajorType.ARRAY, "EekCertChain")) {
249                     return null;
250                 }
251                 ByteArrayOutputStream baos = new ByteArrayOutputStream();
252                 new CborEncoder(baos).encode(curveAndEekChain.get(EEK_CERT_CHAIN_INDEX));
253                 UnsignedInteger curve = (UnsignedInteger) curveAndEekChain.get(CURVE_INDEX);
254                 resp.addGeek(curve.getValue().intValue(), baos.toByteArray());
255             }
256             if (!checkType(respItems.get(CHALLENGE_INDEX), MajorType.BYTE_STRING, "Challenge")) {
257                 return null;
258             }
259             resp.setChallenge(((ByteString) respItems.get(CHALLENGE_INDEX)).getBytes());
260             if (respItems.size() == EEK_ARRAY_ENTRIES_WITH_CONFIG
261                     && !parseDeviceConfig(resp, respItems.get(CONFIG_INDEX))) {
262                 return null;
263             }
264             return resp;
265         } catch (CborException e) {
266             Log.e(TAG, "CBOR parsing/serializing failed.", e);
267             return null;
268         }
269     }
270 
271     /**
272      * Creates the bundle of data that the server needs in order to make a decision over what
273      * device configuration values to return. In general, this boils down to if remote provisioning
274      * is turned on at all or not.
275      *
276      * @return the CBOR encoded provisioning information relevant to th.
277      */
buildProvisioningInfo(Context context)278     public static byte[] buildProvisioningInfo(Context context) {
279         try {
280             ByteArrayOutputStream baos = new ByteArrayOutputStream();
281             new CborEncoder(baos).encode(new CborBuilder()
282                     .addMap()
283                         .put("fingerprint", Build.FINGERPRINT)
284                         .put("id", Settings.getId(context))
285                         .put("version", getPackageVersion(context))
286                         .end()
287                     .build());
288             return baos.toByteArray();
289         } catch (CborException e) {
290             Log.e(TAG, "CBOR serialization failed.", e);
291             return EMPTY_MAP;
292         }
293     }
294 
getPackageVersion(Context context)295     private static long getPackageVersion(Context context) {
296         String packageName = context.getPackageName();
297         try {
298             return context
299                 .getPackageManager()
300                 .getPackageInfo(packageName, MATCH_APEX)
301                 .getLongVersionCode();
302         } catch (PackageManager.NameNotFoundException e) {
303             Log.e(TAG, "Error looking up package " + packageName, e);
304             return 0;
305         }
306     }
307 
308     /**
309      * Takes the various fields fetched from the server and the remote provisioning service and
310      * formats them in the CBOR blob the server is expecting as defined by the
311      * IRemotelyProvisionedComponent HAL AIDL files.
312      */
buildCertificateRequest(byte[] deviceInfo, byte[] challenge, byte[] protectedData, byte[] macedKeysToSign, Map unverifiedDeviceInfo)313     public static byte[] buildCertificateRequest(byte[] deviceInfo, byte[] challenge,
314             byte[] protectedData, byte[] macedKeysToSign, Map unverifiedDeviceInfo)
315             throws RkpdException {
316         // This CBOR library doesn't support adding already serialized CBOR structures into a
317         // CBOR builder. Because of this, we have to first deserialize the provided parameters
318         // back into the library's CBOR object types, and then reserialize them into the
319         // desired structure.
320         try {
321             Array protectedDataArray = (Array) decodeCbor(protectedData, "ProtectedData",
322                     MajorType.ARRAY);
323             Array macedKeysToSignArray = (Array) decodeCbor(macedKeysToSign, "MacedKeysToSign",
324                     MajorType.ARRAY);
325             Map verifiedDeviceInfoMap = (Map) decodeCbor(deviceInfo, "DeviceInfo", MajorType.MAP);
326 
327             if (unverifiedDeviceInfo.get(new UnicodeString("fingerprint")) == null) {
328                 Log.e(TAG, "UnverifiedDeviceInfo is missing a fingerprint entry");
329                 throw new RkpdException(RkpdException.ErrorCode.INTERNAL_ERROR,
330                         "UnverifiedDeviceInfo missing fingerprint entry.");
331             }
332             // Serialize the actual CertificateSigningRequest structure
333             ByteArrayOutputStream baos = new ByteArrayOutputStream();
334             new CborEncoder(baos).encode(new CborBuilder()
335                     .addArray()
336                         .addArray()
337                             .add(verifiedDeviceInfoMap)
338                             .add(unverifiedDeviceInfo)
339                             .end()
340                         .add(challenge)
341                         .add(protectedDataArray)
342                         .add(macedKeysToSignArray)
343                         .end()
344                     .build());
345             return baos.toByteArray();
346         } catch (CborException e) {
347             Log.e(TAG, "Malformed CBOR", e);
348             throw new RkpdException(RkpdException.ErrorCode.INTERNAL_ERROR, "Malformed CBOR", e);
349         }
350     }
351 
352     /**
353      * Produce a CBOR Map object which contains the unverified device information for a certificate
354      * signing request.
355      *
356      * @return the CBOR Map object.
357      */
buildUnverifiedDeviceInfo()358     public static Map buildUnverifiedDeviceInfo() {
359         Map unverifiedDeviceInfo = new Map();
360         unverifiedDeviceInfo.put(new UnicodeString("fingerprint"),
361                                     new UnicodeString(Build.FINGERPRINT));
362         return unverifiedDeviceInfo;
363     }
364 
365     /**
366      * Extracts provisioned key for storage from Maced key pair received from underlying binder
367      * service.
368      */
extractRkpKeyFromMacedKey(byte[] privKey, String serviceName, MacedPublicKey macedPublicKey)369     public static RkpKey extractRkpKeyFromMacedKey(byte[] privKey, String serviceName,
370             MacedPublicKey macedPublicKey) throws CborException, RkpdException {
371         Array cborMessage = (Array) decodeCbor(macedPublicKey.macedKey, "MacedPublicKeys",
372                 MajorType.ARRAY);
373         List<DataItem> messageArray = cborMessage.getDataItems();
374         byte[] macedMessage = getBytesFromBstr(messageArray.get(2));
375         Map keyMap = (Map) decodeCbor(macedMessage, "byte stream", MajorType.MAP);
376         byte[] xCor = ((ByteString) keyMap.get(new NegativeInteger(KEY_PARAMETER_X))).getBytes();
377         if (xCor.length != 32) {
378             throw new IllegalStateException("COSE_Key x-coordinate is not correct.");
379         }
380         byte[] yCor = ((ByteString) keyMap.get(new NegativeInteger(KEY_PARAMETER_Y))).getBytes();
381         if (yCor.length != 32) {
382             throw new IllegalStateException("COSE_Key y-coordinate is not correct.");
383         }
384         byte[] rawKey = concatenateByteArrays(xCor, yCor);
385         return new RkpKey(privKey, macedPublicKey.macedKey, keyMap, serviceName, rawKey);
386     }
387 
388     /**
389      * Decodes and returns the CBOR encoded DataItem in encodedBytes. Also verifies that the
390      * majorType actually matches what is being assumed.
391      */
decodeCbor(byte[] encodedBytes, String debugName, MajorType majorType)392     public static DataItem decodeCbor(byte[] encodedBytes, String debugName,
393             MajorType majorType) throws CborException, RkpdException {
394         ByteArrayInputStream bais = new ByteArrayInputStream(encodedBytes);
395         List<DataItem> dataItems = new CborDecoder(bais).decode();
396         if (dataItems.size() != RESPONSE_ARRAY_SIZE
397                 || !checkType(dataItems.get(RESPONSE_CERT_ARRAY_INDEX), majorType, debugName)) {
398             throw new RkpdException(RkpdException.ErrorCode.INTERNAL_ERROR, debugName
399                     + " not in proper Cbor format. Expected size 1. Actual: " + dataItems.size());
400         }
401         return dataItems.get(0);
402     }
403 
concatenateByteArrays(byte[] a, byte[] b)404     private static byte[] concatenateByteArrays(byte[] a, byte[] b) {
405         byte[] result = new byte[a.length + b.length];
406         System.arraycopy(a, 0, result, 0, a.length);
407         System.arraycopy(b, 0, result, a.length, b.length);
408         return result;
409     }
410 
getBytesFromBstr(DataItem item)411     private static byte[] getBytesFromBstr(DataItem item) throws CborException {
412         if (item.getMajorType() == MajorType.BYTE_STRING) {
413             return ((ByteString) item).getBytes();
414         }
415         throw new CborException("Error while decoding CBOR. Expected bstr value.");
416     }
417 
418     /**
419      * Make protected headers for certificate request.
420      */
makeProtectedHeaders()421     public static Map makeProtectedHeaders() throws CborException {
422         Map protectedHeaders = new Map();
423         protectedHeaders.put(new UnsignedInteger(COSE_HEADER_ALGORITHM),
424                 new UnsignedInteger(COSE_ALGORITHM_HMAC_256));
425         return protectedHeaders;
426     }
427 
428     /**
429      * Encodes CBOR to byte array.
430      */
encodeCbor(final DataItem dataItem)431     public static byte[] encodeCbor(final DataItem dataItem) throws CborException {
432         final ByteArrayOutputStream baos = new ByteArrayOutputStream();
433         CborEncoder encoder = new CborEncoder(baos);
434         encoder.encode(dataItem);
435         return baos.toByteArray();
436     }
437 }
438