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