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