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