1 /**
2  * Copyright (C) 2020 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.remoteprovisioner;
18 
19 import android.content.Context;
20 import android.os.Build;
21 import android.util.Log;
22 
23 import java.io.ByteArrayInputStream;
24 import java.io.ByteArrayOutputStream;
25 import java.io.IOException;
26 import java.time.Duration;
27 import java.util.ArrayList;
28 import java.util.List;
29 
30 import co.nstant.in.cbor.CborBuilder;
31 import co.nstant.in.cbor.CborDecoder;
32 import co.nstant.in.cbor.CborEncoder;
33 import co.nstant.in.cbor.CborException;
34 import co.nstant.in.cbor.model.Array;
35 import co.nstant.in.cbor.model.ByteString;
36 import co.nstant.in.cbor.model.DataItem;
37 import co.nstant.in.cbor.model.MajorType;
38 import co.nstant.in.cbor.model.Map;
39 import co.nstant.in.cbor.model.UnicodeString;
40 import co.nstant.in.cbor.model.UnsignedInteger;
41 
42 public class CborUtils {
43     public static final int EC_CURVE_P256 = 1;
44     public static final int EC_CURVE_25519 = 2;
45 
46     public static final String EXTRA_KEYS = "num_extra_attestation_keys";
47     public static final String TIME_TO_REFRESH = "time_to_refresh_hours";
48     public static final String PROVISIONING_URL = "provisioning_url";
49 
50     private static final int RESPONSE_CERT_ARRAY_INDEX = 0;
51     private static final int RESPONSE_ARRAY_SIZE = 1;
52 
53     private static final int SHARED_CERTIFICATES_INDEX = 0;
54     private static final int UNIQUE_CERTIFICATES_INDEX = 1;
55     private static final int CERT_ARRAY_ENTRIES = 2;
56 
57     private static final int EEK_AND_CURVE_INDEX = 0;
58     private static final int CHALLENGE_INDEX = 1;
59     private static final int CONFIG_INDEX = 2;
60 
61     private static final int CURVE_AND_EEK_CHAIN_LENGTH = 2;
62     private static final int CURVE_INDEX = 0;
63     private static final int EEK_CERT_CHAIN_INDEX = 1;
64 
65     private static final int EEK_ARRAY_ENTRIES_NO_CONFIG = 2;
66     private static final int EEK_ARRAY_ENTRIES_WITH_CONFIG = 3;
67     private static final String TAG = "RemoteProvisioningService";
68     private static final byte[] EMPTY_MAP = new byte[] {(byte) 0xA0};
69 
70     /**
71      * Parses the signed certificate chains returned by the server. In order to reduce data use over
72      * the wire, shared certificate chain prefixes are separated from the remaining unique portions
73      * of each individual certificate chain. This method first parses the shared prefix certificates
74      * and then prepends them to each unique certificate chain. Each PEM-encoded certificate chain
75      * is returned in a byte array.
76      *
77      * @param serverResp The CBOR blob received from the server which contains all signed
78      *                      certificate chains.
79      *
80      * @return A List object where each byte[] entry is an entire DER-encoded certificate chain.
81      */
parseSignedCertificates(byte[] serverResp)82     public static List<byte[]> parseSignedCertificates(byte[] serverResp) {
83         try {
84             ByteArrayInputStream bais = new ByteArrayInputStream(serverResp);
85             List<DataItem> dataItems = new CborDecoder(bais).decode();
86             if (dataItems.size() != RESPONSE_ARRAY_SIZE
87                     || !checkType(dataItems.get(RESPONSE_CERT_ARRAY_INDEX),
88                                   MajorType.ARRAY, "CborResponse")) {
89                 Log.e(TAG, "Improper formatting of CBOR response. Expected size 1. Actual: "
90                             + dataItems.size());
91                 return null;
92             }
93             dataItems = ((Array) dataItems.get(RESPONSE_CERT_ARRAY_INDEX)).getDataItems();
94             if (dataItems.size() != CERT_ARRAY_ENTRIES) {
95                 Log.e(TAG, "Incorrect number of certificate array entries. Expected: 2. Actual: "
96                             + dataItems.size());
97                 return null;
98             }
99             if (!checkType(dataItems.get(SHARED_CERTIFICATES_INDEX),
100                            MajorType.BYTE_STRING, "SharedCertificates")
101                     || !checkType(dataItems.get(UNIQUE_CERTIFICATES_INDEX),
102                                   MajorType.ARRAY, "UniqueCertificates")) {
103                 return null;
104             }
105             byte[] sharedCertificates =
106                     ((ByteString) dataItems.get(SHARED_CERTIFICATES_INDEX)).getBytes();
107             Array uniqueCertificates = (Array) dataItems.get(UNIQUE_CERTIFICATES_INDEX);
108             List<byte[]> uniqueCertificateChains = new ArrayList<byte[]>();
109             for (DataItem entry : uniqueCertificates.getDataItems()) {
110                 if (!checkType(entry, MajorType.BYTE_STRING, "UniqueCertificate")) {
111                     return null;
112                 }
113                 ByteArrayOutputStream concat = new ByteArrayOutputStream();
114                 // DER encoding specifies certificate chains ordered from leaf to root.
115                 concat.write(((ByteString) entry).getBytes());
116                 concat.write(sharedCertificates);
117                 uniqueCertificateChains.add(concat.toByteArray());
118             }
119             return uniqueCertificateChains;
120         } catch (CborException e) {
121             Log.e(TAG, "CBOR decoding failed.", e);
122         } catch (IOException e) {
123             Log.e(TAG, "Writing bytes failed.", e);
124         }
125         return null;
126     }
127 
checkType(DataItem item, MajorType majorType, String field)128     private static boolean checkType(DataItem item, MajorType majorType, String field) {
129         if (item.getMajorType() != majorType) {
130             Log.e(TAG, "Incorrect CBOR type for field: " + field + ". Expected " + majorType.name()
131                         + ". Actual: " + item.getMajorType().name());
132             return false;
133         }
134         return true;
135     }
136 
parseDeviceConfig(GeekResponse resp, DataItem deviceConfig)137     private static boolean parseDeviceConfig(GeekResponse resp, DataItem deviceConfig) {
138         if (!checkType(deviceConfig, MajorType.MAP, "DeviceConfig")) {
139             return false;
140         }
141         Map deviceConfiguration = (Map) deviceConfig;
142         DataItem extraKeys =
143                 deviceConfiguration.get(new UnicodeString(EXTRA_KEYS));
144         DataItem timeToRefreshHours =
145                 deviceConfiguration.get(new UnicodeString(TIME_TO_REFRESH));
146         DataItem newUrl =
147                 deviceConfiguration.get(new UnicodeString(PROVISIONING_URL));
148         if (extraKeys != null) {
149             if (!checkType(extraKeys, MajorType.UNSIGNED_INTEGER, "ExtraKeys")) {
150                 return false;
151             }
152             resp.numExtraAttestationKeys = ((UnsignedInteger) extraKeys).getValue().intValue();
153         }
154         if (timeToRefreshHours != null) {
155             if (!checkType(timeToRefreshHours, MajorType.UNSIGNED_INTEGER, "TimeToRefresh")) {
156                 return false;
157             }
158             resp.timeToRefresh =
159                     Duration.ofHours(((UnsignedInteger) timeToRefreshHours).getValue().intValue());
160         }
161         if (newUrl != null) {
162             if (!checkType(newUrl, MajorType.UNICODE_STRING, "ProvisioningURL")) {
163                 return false;
164             }
165             resp.provisioningUrl = ((UnicodeString) newUrl).getString();
166         }
167         return true;
168     }
169 
170     /**
171      * Parses the Google Endpoint Encryption Key response provided by the server which contains a
172      * Google signed EEK and a challenge for use by the underlying IRemotelyProvisionedComponent HAL
173      */
parseGeekResponse(byte[] serverResp)174     public static GeekResponse parseGeekResponse(byte[] serverResp) {
175         try {
176             GeekResponse resp = new GeekResponse();
177             ByteArrayInputStream bais = new ByteArrayInputStream(serverResp);
178             List<DataItem> dataItems = new CborDecoder(bais).decode();
179             if (dataItems.size() != RESPONSE_ARRAY_SIZE
180                     || !checkType(dataItems.get(RESPONSE_CERT_ARRAY_INDEX),
181                                   MajorType.ARRAY, "CborResponse")) {
182                 Log.e(TAG, "Improper formatting of CBOR response. Expected size 1. Actual: "
183                             + dataItems.size());
184                 return null;
185             }
186             List<DataItem> respItems =
187                     ((Array) dataItems.get(RESPONSE_CERT_ARRAY_INDEX)).getDataItems();
188             if (respItems.size() != EEK_ARRAY_ENTRIES_NO_CONFIG
189                     && respItems.size() != EEK_ARRAY_ENTRIES_WITH_CONFIG) {
190                 Log.e(TAG, "Incorrect number of certificate array entries. Expected: "
191                             + EEK_ARRAY_ENTRIES_NO_CONFIG + " or " + EEK_ARRAY_ENTRIES_WITH_CONFIG
192                             + ". Actual: " + respItems.size());
193                 return null;
194             }
195             if (!checkType(respItems.get(EEK_AND_CURVE_INDEX), MajorType.ARRAY, "EekAndCurveArr")) {
196                 return null;
197             }
198             List<DataItem> curveAndEekChains =
199                     ((Array) respItems.get(EEK_AND_CURVE_INDEX)).getDataItems();
200             for (int i = 0; i < curveAndEekChains.size(); i++) {
201                 if (!checkType(curveAndEekChains.get(i), MajorType.ARRAY, "EekAndCurve")) {
202                     return null;
203                 }
204                 List<DataItem> curveAndEekChain =
205                         ((Array) curveAndEekChains.get(i)).getDataItems();
206                 if (curveAndEekChain.size() != CURVE_AND_EEK_CHAIN_LENGTH) {
207                     Log.e(TAG, "Wrong size. Expected: " + CURVE_AND_EEK_CHAIN_LENGTH + ". Actual: "
208                                + curveAndEekChain.size());
209                     return null;
210                 }
211                 if (!checkType(curveAndEekChain.get(CURVE_INDEX),
212                                MajorType.UNSIGNED_INTEGER, "Curve")
213                         || !checkType(curveAndEekChain.get(EEK_CERT_CHAIN_INDEX),
214                                                            MajorType.ARRAY, "EekCertChain")) {
215                     return null;
216                 }
217                 ByteArrayOutputStream baos = new ByteArrayOutputStream();
218                 new CborEncoder(baos).encode(curveAndEekChain.get(EEK_CERT_CHAIN_INDEX));
219                 UnsignedInteger curve = (UnsignedInteger) curveAndEekChain.get(CURVE_INDEX);
220                 resp.addGeek(curve.getValue().intValue(), baos.toByteArray());
221             }
222             if (!checkType(respItems.get(CHALLENGE_INDEX), MajorType.BYTE_STRING, "Challenge")) {
223                 return null;
224             }
225             resp.setChallenge(((ByteString) respItems.get(CHALLENGE_INDEX)).getBytes());
226             if (respItems.size() == EEK_ARRAY_ENTRIES_WITH_CONFIG
227                     && !parseDeviceConfig(resp, respItems.get(CONFIG_INDEX))) {
228                 return null;
229             }
230             return resp;
231         } catch (CborException e) {
232             Log.e(TAG, "CBOR parsing/serializing failed.", e);
233             return null;
234         }
235     }
236 
237     /**
238      * Creates the bundle of data that the server needs in order to make a decision over what
239      * device configuration values to return. In general, this boils down to if remote provisioning
240      * is turned on at all or not.
241      *
242      * @return the CBOR encoded provisioning information relevant to the server.
243      */
buildProvisioningInfo(Context context)244     public static byte[] buildProvisioningInfo(Context context) {
245         try {
246             ByteArrayOutputStream baos = new ByteArrayOutputStream();
247             new CborEncoder(baos).encode(new CborBuilder()
248                     .addMap()
249                         .put("fingerprint", Build.FINGERPRINT)
250                         .put(new UnicodeString("id"),
251                              new UnsignedInteger(SettingsManager.getId(context)))
252                         .end()
253                     .build());
254             return baos.toByteArray();
255         } catch (CborException e) {
256             Log.e(TAG, "CBOR serialization failed.", e);
257             return EMPTY_MAP;
258         }
259     }
260 
261     /**
262      * Takes the various fields fetched from the server and the remote provisioning service and
263      * formats them in the CBOR blob the server is expecting as defined by the
264      * IRemotelyProvisionedComponent HAL AIDL files.
265      */
buildCertificateRequest(byte[] deviceInfo, byte[] challenge, byte[] protectedData, byte[] macedKeysToSign)266     public static byte[] buildCertificateRequest(byte[] deviceInfo, byte[] challenge,
267                                                  byte[] protectedData, byte[] macedKeysToSign) {
268         // This CBOR library doesn't support adding already serialized CBOR structures into a
269         // CBOR builder. Because of this, we have to first deserialize the provided parameters
270         // back into the library's CBOR object types, and then reserialize them into the
271         // desired structure.
272         try {
273             // Deserialize the protectedData blob
274             ByteArrayInputStream bais = new ByteArrayInputStream(protectedData);
275             List<DataItem> dataItems = new CborDecoder(bais).decode();
276             if (dataItems.size() != 1
277                     || !checkType(dataItems.get(0), MajorType.ARRAY, "ProtectedData")) {
278                 return null;
279             }
280             Array protectedDataArray = (Array) dataItems.get(0);
281 
282             // Deserialize macedKeysToSign
283             bais = new ByteArrayInputStream(macedKeysToSign);
284             dataItems = new CborDecoder(bais).decode();
285             if (dataItems.size() != 1
286                     || !checkType(dataItems.get(0), MajorType.ARRAY, "MacedKeysToSign")) {
287                 return null;
288             }
289             Array macedKeysToSignArray = (Array) dataItems.get(0);
290 
291             // Deserialize deviceInfo
292             bais = new ByteArrayInputStream(deviceInfo);
293             dataItems = new CborDecoder(bais).decode();
294             if (dataItems.size() != 1
295                     || !checkType(dataItems.get(0), MajorType.MAP, "DeviceInfo")) {
296                 return null;
297             }
298             Map verifiedDeviceInfoMap = (Map) dataItems.get(0);
299             Map unverifiedDeviceInfoMap = new Map();
300             unverifiedDeviceInfoMap.put(new UnicodeString("fingerprint"),
301                                         new UnicodeString(Build.FINGERPRINT));
302 
303             // Serialize the actual CertificateSigningRequest structure
304             ByteArrayOutputStream baos = new ByteArrayOutputStream();
305             new CborEncoder(baos).encode(new CborBuilder()
306                     .addArray()
307                         .addArray()
308                             .add(verifiedDeviceInfoMap)
309                             .add(unverifiedDeviceInfoMap)
310                             .end()
311                         .add(challenge)
312                         .add(protectedDataArray)
313                         .add(macedKeysToSignArray)
314                         .end()
315                     .build());
316             return baos.toByteArray();
317         } catch (CborException e) {
318             Log.e(TAG, "Malformed CBOR", e);
319             return null;
320         }
321     }
322 }
323