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