blob: d3fc3d757cc8de66ad1add917664020820c7d335 [file] [log] [blame]
/**
* 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<byte[]> parseSignedCertificates(byte[] serverResp) {
try {
ByteArrayInputStream bais = new ByteArrayInputStream(serverResp);
List<DataItem> 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<byte[]> uniqueCertificateChains = new ArrayList<byte[]>();
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<DataItem> 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<DataItem> 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<DataItem> 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<DataItem> 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<DataItem> 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;
}
}
}