/** * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.remoteprovisioner; import android.content.Context; import android.os.Build; import android.util.Log; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.time.Duration; import java.util.ArrayList; import java.util.List; import co.nstant.in.cbor.CborBuilder; import co.nstant.in.cbor.CborDecoder; import co.nstant.in.cbor.CborEncoder; import co.nstant.in.cbor.CborException; import co.nstant.in.cbor.model.Array; import co.nstant.in.cbor.model.ByteString; import co.nstant.in.cbor.model.DataItem; import co.nstant.in.cbor.model.MajorType; import co.nstant.in.cbor.model.Map; import co.nstant.in.cbor.model.UnicodeString; import co.nstant.in.cbor.model.UnsignedInteger; public class CborUtils { public static final int EC_CURVE_P256 = 1; public static final int EC_CURVE_25519 = 2; public static final String EXTRA_KEYS = "num_extra_attestation_keys"; public static final String TIME_TO_REFRESH = "time_to_refresh_hours"; public static final String PROVISIONING_URL = "provisioning_url"; private static final int RESPONSE_CERT_ARRAY_INDEX = 0; private static final int RESPONSE_ARRAY_SIZE = 1; private static final int SHARED_CERTIFICATES_INDEX = 0; private static final int UNIQUE_CERTIFICATES_INDEX = 1; private static final int CERT_ARRAY_ENTRIES = 2; private static final int EEK_AND_CURVE_INDEX = 0; private static final int CHALLENGE_INDEX = 1; private static final int CONFIG_INDEX = 2; private static final int CURVE_AND_EEK_CHAIN_LENGTH = 2; private static final int CURVE_INDEX = 0; private static final int EEK_CERT_CHAIN_INDEX = 1; private static final int EEK_ARRAY_ENTRIES_NO_CONFIG = 2; private static final int EEK_ARRAY_ENTRIES_WITH_CONFIG = 3; private static final String TAG = "RemoteProvisioningService"; private static final byte[] EMPTY_MAP = new byte[] {(byte) 0xA0}; /** * Parses the signed certificate chains returned by the server. In order to reduce data use over * the wire, shared certificate chain prefixes are separated from the remaining unique portions * of each individual certificate chain. This method first parses the shared prefix certificates * and then prepends them to each unique certificate chain. Each PEM-encoded certificate chain * is returned in a byte array. * * @param serverResp The CBOR blob received from the server which contains all signed * certificate chains. * * @return A List object where each byte[] entry is an entire DER-encoded certificate chain. */ public static List parseSignedCertificates(byte[] serverResp) { try { ByteArrayInputStream bais = new ByteArrayInputStream(serverResp); List dataItems = new CborDecoder(bais).decode(); if (dataItems.size() != RESPONSE_ARRAY_SIZE || !checkType(dataItems.get(RESPONSE_CERT_ARRAY_INDEX), MajorType.ARRAY, "CborResponse")) { Log.e(TAG, "Improper formatting of CBOR response. Expected size 1. Actual: " + dataItems.size()); return null; } dataItems = ((Array) dataItems.get(RESPONSE_CERT_ARRAY_INDEX)).getDataItems(); if (dataItems.size() != CERT_ARRAY_ENTRIES) { Log.e(TAG, "Incorrect number of certificate array entries. Expected: 2. Actual: " + dataItems.size()); return null; } if (!checkType(dataItems.get(SHARED_CERTIFICATES_INDEX), MajorType.BYTE_STRING, "SharedCertificates") || !checkType(dataItems.get(UNIQUE_CERTIFICATES_INDEX), MajorType.ARRAY, "UniqueCertificates")) { return null; } byte[] sharedCertificates = ((ByteString) dataItems.get(SHARED_CERTIFICATES_INDEX)).getBytes(); Array uniqueCertificates = (Array) dataItems.get(UNIQUE_CERTIFICATES_INDEX); List uniqueCertificateChains = new ArrayList(); for (DataItem entry : uniqueCertificates.getDataItems()) { if (!checkType(entry, MajorType.BYTE_STRING, "UniqueCertificate")) { return null; } ByteArrayOutputStream concat = new ByteArrayOutputStream(); // DER encoding specifies certificate chains ordered from leaf to root. concat.write(((ByteString) entry).getBytes()); concat.write(sharedCertificates); uniqueCertificateChains.add(concat.toByteArray()); } return uniqueCertificateChains; } catch (CborException e) { Log.e(TAG, "CBOR decoding failed.", e); } catch (IOException e) { Log.e(TAG, "Writing bytes failed.", e); } return null; } private static boolean checkType(DataItem item, MajorType majorType, String field) { if (item.getMajorType() != majorType) { Log.e(TAG, "Incorrect CBOR type for field: " + field + ". Expected " + majorType.name() + ". Actual: " + item.getMajorType().name()); return false; } return true; } private static boolean parseDeviceConfig(GeekResponse resp, DataItem deviceConfig) { if (!checkType(deviceConfig, MajorType.MAP, "DeviceConfig")) { return false; } Map deviceConfiguration = (Map) deviceConfig; DataItem extraKeys = deviceConfiguration.get(new UnicodeString(EXTRA_KEYS)); DataItem timeToRefreshHours = deviceConfiguration.get(new UnicodeString(TIME_TO_REFRESH)); DataItem newUrl = deviceConfiguration.get(new UnicodeString(PROVISIONING_URL)); if (extraKeys != null) { if (!checkType(extraKeys, MajorType.UNSIGNED_INTEGER, "ExtraKeys")) { return false; } resp.numExtraAttestationKeys = ((UnsignedInteger) extraKeys).getValue().intValue(); } if (timeToRefreshHours != null) { if (!checkType(timeToRefreshHours, MajorType.UNSIGNED_INTEGER, "TimeToRefresh")) { return false; } resp.timeToRefresh = Duration.ofHours(((UnsignedInteger) timeToRefreshHours).getValue().intValue()); } if (newUrl != null) { if (!checkType(newUrl, MajorType.UNICODE_STRING, "ProvisioningURL")) { return false; } resp.provisioningUrl = ((UnicodeString) newUrl).getString(); } return true; } /** * Parses the Google Endpoint Encryption Key response provided by the server which contains a * Google signed EEK and a challenge for use by the underlying IRemotelyProvisionedComponent HAL */ public static GeekResponse parseGeekResponse(byte[] serverResp) { try { GeekResponse resp = new GeekResponse(); ByteArrayInputStream bais = new ByteArrayInputStream(serverResp); List dataItems = new CborDecoder(bais).decode(); if (dataItems.size() != RESPONSE_ARRAY_SIZE || !checkType(dataItems.get(RESPONSE_CERT_ARRAY_INDEX), MajorType.ARRAY, "CborResponse")) { Log.e(TAG, "Improper formatting of CBOR response. Expected size 1. Actual: " + dataItems.size()); return null; } List respItems = ((Array) dataItems.get(RESPONSE_CERT_ARRAY_INDEX)).getDataItems(); if (respItems.size() != EEK_ARRAY_ENTRIES_NO_CONFIG && respItems.size() != EEK_ARRAY_ENTRIES_WITH_CONFIG) { Log.e(TAG, "Incorrect number of certificate array entries. Expected: " + EEK_ARRAY_ENTRIES_NO_CONFIG + " or " + EEK_ARRAY_ENTRIES_WITH_CONFIG + ". Actual: " + respItems.size()); return null; } if (!checkType(respItems.get(EEK_AND_CURVE_INDEX), MajorType.ARRAY, "EekAndCurveArr")) { return null; } List curveAndEekChains = ((Array) respItems.get(EEK_AND_CURVE_INDEX)).getDataItems(); for (int i = 0; i < curveAndEekChains.size(); i++) { if (!checkType(curveAndEekChains.get(i), MajorType.ARRAY, "EekAndCurve")) { return null; } List curveAndEekChain = ((Array) curveAndEekChains.get(i)).getDataItems(); if (curveAndEekChain.size() != CURVE_AND_EEK_CHAIN_LENGTH) { Log.e(TAG, "Wrong size. Expected: " + CURVE_AND_EEK_CHAIN_LENGTH + ". Actual: " + curveAndEekChain.size()); return null; } if (!checkType(curveAndEekChain.get(CURVE_INDEX), MajorType.UNSIGNED_INTEGER, "Curve") || !checkType(curveAndEekChain.get(EEK_CERT_CHAIN_INDEX), MajorType.ARRAY, "EekCertChain")) { return null; } ByteArrayOutputStream baos = new ByteArrayOutputStream(); new CborEncoder(baos).encode(curveAndEekChain.get(EEK_CERT_CHAIN_INDEX)); UnsignedInteger curve = (UnsignedInteger) curveAndEekChain.get(CURVE_INDEX); resp.addGeek(curve.getValue().intValue(), baos.toByteArray()); } if (!checkType(respItems.get(CHALLENGE_INDEX), MajorType.BYTE_STRING, "Challenge")) { return null; } resp.setChallenge(((ByteString) respItems.get(CHALLENGE_INDEX)).getBytes()); if (respItems.size() == EEK_ARRAY_ENTRIES_WITH_CONFIG && !parseDeviceConfig(resp, respItems.get(CONFIG_INDEX))) { return null; } return resp; } catch (CborException e) { Log.e(TAG, "CBOR parsing/serializing failed.", e); return null; } } /** * Creates the bundle of data that the server needs in order to make a decision over what * device configuration values to return. In general, this boils down to if remote provisioning * is turned on at all or not. * * @return the CBOR encoded provisioning information relevant to the server. */ public static byte[] buildProvisioningInfo(Context context) { try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); new CborEncoder(baos).encode(new CborBuilder() .addMap() .put("fingerprint", Build.FINGERPRINT) .put(new UnicodeString("id"), new UnsignedInteger(SettingsManager.getId(context))) .end() .build()); return baos.toByteArray(); } catch (CborException e) { Log.e(TAG, "CBOR serialization failed.", e); return EMPTY_MAP; } } /** * Takes the various fields fetched from the server and the remote provisioning service and * formats them in the CBOR blob the server is expecting as defined by the * IRemotelyProvisionedComponent HAL AIDL files. */ public static byte[] buildCertificateRequest(byte[] deviceInfo, byte[] challenge, byte[] protectedData, byte[] macedKeysToSign) { // This CBOR library doesn't support adding already serialized CBOR structures into a // CBOR builder. Because of this, we have to first deserialize the provided parameters // back into the library's CBOR object types, and then reserialize them into the // desired structure. try { // Deserialize the protectedData blob ByteArrayInputStream bais = new ByteArrayInputStream(protectedData); List dataItems = new CborDecoder(bais).decode(); if (dataItems.size() != 1 || !checkType(dataItems.get(0), MajorType.ARRAY, "ProtectedData")) { return null; } Array protectedDataArray = (Array) dataItems.get(0); // Deserialize macedKeysToSign bais = new ByteArrayInputStream(macedKeysToSign); dataItems = new CborDecoder(bais).decode(); if (dataItems.size() != 1 || !checkType(dataItems.get(0), MajorType.ARRAY, "MacedKeysToSign")) { return null; } Array macedKeysToSignArray = (Array) dataItems.get(0); // Deserialize deviceInfo bais = new ByteArrayInputStream(deviceInfo); dataItems = new CborDecoder(bais).decode(); if (dataItems.size() != 1 || !checkType(dataItems.get(0), MajorType.MAP, "DeviceInfo")) { return null; } Map verifiedDeviceInfoMap = (Map) dataItems.get(0); Map unverifiedDeviceInfoMap = new Map(); unverifiedDeviceInfoMap.put(new UnicodeString("fingerprint"), new UnicodeString(Build.FINGERPRINT)); // Serialize the actual CertificateSigningRequest structure ByteArrayOutputStream baos = new ByteArrayOutputStream(); new CborEncoder(baos).encode(new CborBuilder() .addArray() .addArray() .add(verifiedDeviceInfoMap) .add(unverifiedDeviceInfoMap) .end() .add(challenge) .add(protectedDataArray) .add(macedKeysToSignArray) .end() .build()); return baos.toByteArray(); } catch (CborException e) { Log.e(TAG, "Malformed CBOR", e); return null; } } }