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