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