blob: 1754e2e135166eddaef2dc01750453d05236ec74 [file] [log] [blame]
/**
* Copyright (C) 2022 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.rkpdapp;
import android.util.Log;
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;
import com.android.rkpdapp.database.InstantConverter;
import com.android.rkpdapp.utils.CborUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
/**
* Convenience class for packaging up the values returned by the server when initially requesting
* an Endpoint Encryption Key for remote provisioning. Those values are described by the following
* CDDL Schema:
* GeekResponse = [
* [+CurveAndEek],
* challenge : bstr,
* ? Config,
* ]
* CurveAndEek = [
* curve: uint,
* EekChain
* ]
* Config = {
* ? "num_extra_attestation_keys": uint,
* ? "time_to_refresh_hours" : uint,
* ? "provisioning_url": tstr,
* }
*
* The CDDL that defines EekChain is defined in the RemoteProvisioning HAL, but this app does not
* require any semantic understanding of the format to perform its function.
*/
public class GeekResponse {
public static final int NO_EXTRA_KEY_UPDATE = -1;
private static final String TAG = "RkpdGeekResponse";
public static final int EC_CURVE_P256 = 1;
public static final int EC_CURVE_25519 = 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;
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";
public static final String LAST_BAD_CERT_TIME_START_MILLIS = "bad_cert_start";
public static final String LAST_BAD_CERT_TIME_END_MILLIS = "bad_cert_end";
private byte[] challenge;
private final java.util.Map<Integer, byte[]> curveToGeek;
public final String requestId;
public int numExtraAttestationKeys;
public Duration timeToRefresh;
public String provisioningUrl;
public Instant lastBadCertTimeStart;
public Instant lastBadCertTimeEnd;
/**
* Default initializer.
*/
public GeekResponse() {
curveToGeek = new HashMap<>();
numExtraAttestationKeys = NO_EXTRA_KEY_UPDATE;
lastBadCertTimeStart = null;
lastBadCertTimeEnd = null;
requestId = UUID.randomUUID().toString();
}
/**
* 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 parse(byte[] serverResp) {
try {
GeekResponse resp = new GeekResponse();
ByteArrayInputStream bais = new ByteArrayInputStream(serverResp);
List<DataItem> dataItems = new CborDecoder(bais).decode();
CborUtils.checkSize(dataItems, CborUtils.RESPONSE_ARRAY_SIZE, "GeekResponse");
CborUtils.checkType(
dataItems.get(CborUtils.RESPONSE_CERT_ARRAY_INDEX),
MajorType.ARRAY,
"CborResponse");
List<DataItem> respItems =
((Array) dataItems.get(CborUtils.RESPONSE_CERT_ARRAY_INDEX)).getDataItems();
if (respItems.size() != EEK_ARRAY_ENTRIES_NO_CONFIG
&& respItems.size() != EEK_ARRAY_ENTRIES_WITH_CONFIG) {
throw new CborException(
"Incorrect number of certificate array entries. Expected: "
+ EEK_ARRAY_ENTRIES_NO_CONFIG
+ " or "
+ EEK_ARRAY_ENTRIES_WITH_CONFIG
+ ". Actual: "
+ respItems.size());
}
CborUtils.checkType(
respItems.get(EEK_AND_CURVE_INDEX), MajorType.ARRAY, "EekAndCurveArr");
List<DataItem> curveAndEekChains =
((Array) respItems.get(EEK_AND_CURVE_INDEX)).getDataItems();
for (int i = 0; i < curveAndEekChains.size(); i++) {
CborUtils.checkType(curveAndEekChains.get(i), MajorType.ARRAY, "EekAndCurve");
List<DataItem> curveAndEekChain = ((Array) curveAndEekChains.get(i)).getDataItems();
CborUtils.checkSize(
curveAndEekChain, CURVE_AND_EEK_CHAIN_LENGTH, "CurveAndEekChain");
CborUtils.checkType(
curveAndEekChain.get(CURVE_INDEX), MajorType.UNSIGNED_INTEGER, "Curve");
CborUtils.checkType(
curveAndEekChain.get(EEK_CERT_CHAIN_INDEX),
MajorType.ARRAY,
"EekCertChain");
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());
}
CborUtils.checkType(respItems.get(CHALLENGE_INDEX), MajorType.BYTE_STRING, "Challenge");
resp.setChallenge(((ByteString) respItems.get(CHALLENGE_INDEX)).getBytes());
if (respItems.size() == EEK_ARRAY_ENTRIES_WITH_CONFIG) {
parseDeviceConfig(resp, respItems.get(CONFIG_INDEX));
}
return resp;
} catch (CborException e) {
Log.e(TAG, "CBOR parsing/serializing failed.", e);
return null;
}
}
private static void parseDeviceConfig(GeekResponse resp, DataItem deviceConfig)
throws CborException {
CborUtils.checkType(deviceConfig, MajorType.MAP, "DeviceConfig");
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));
DataItem lastBadCertTimeStart =
deviceConfiguration.get(new UnicodeString(LAST_BAD_CERT_TIME_START_MILLIS));
DataItem lastBadCertTimeEnd =
deviceConfiguration.get(new UnicodeString(LAST_BAD_CERT_TIME_END_MILLIS));
if (extraKeys != null) {
CborUtils.checkType(extraKeys, MajorType.UNSIGNED_INTEGER, "ExtraKeys");
resp.numExtraAttestationKeys = ((UnsignedInteger) extraKeys).getValue().intValue();
}
if (timeToRefreshHours != null) {
CborUtils.checkType(timeToRefreshHours, MajorType.UNSIGNED_INTEGER, "TimeToRefresh");
resp.timeToRefresh =
Duration.ofHours(((UnsignedInteger) timeToRefreshHours).getValue().intValue());
}
if (newUrl != null) {
String receivedUrl = ((UnicodeString) newUrl).getString();
CborUtils.checkType(newUrl, MajorType.UNICODE_STRING, "ProvisioningURL");
try {
URI uri = new URI(receivedUrl);
if (uri.isAbsolute()) {
resp.provisioningUrl = receivedUrl;
} else {
Log.e(TAG, "Ignoring relative URI received from server: " + receivedUrl);
}
} catch (URISyntaxException e) {
Log.e(TAG, "Ignoring invalid URL syntax received from server: " + receivedUrl, e);
}
}
if (lastBadCertTimeStart != null) {
CborUtils.checkType(
lastBadCertTimeStart, MajorType.UNSIGNED_INTEGER, "BadCertTimeStart");
resp.lastBadCertTimeStart =
InstantConverter.fromTimestamp(
((UnsignedInteger) lastBadCertTimeStart).getValue().longValue());
}
if (lastBadCertTimeEnd != null) {
CborUtils.checkType(lastBadCertTimeEnd, MajorType.UNSIGNED_INTEGER, "BadCertTimeEnd");
resp.lastBadCertTimeEnd =
InstantConverter.fromTimestamp(
((UnsignedInteger) lastBadCertTimeEnd).getValue().longValue());
}
}
/**
* Add a CBOR encoded array containing a GEEK and the corresponding certificate chain, keyed
* on the EC {@code curve}.
*
* @param curve an integer which represents an EC curve.
* @param geekChain the encoded CBOR array containing an ECDH key and corresponding certificate
* chain.
*/
public void addGeek(int curve, byte[] geekChain) {
curveToGeek.put(curve, geekChain);
}
/**
* Returns the encoded CBOR array with an ECDH key corresponding to the provided {@code curve}.
*
* @param curve an integer which represents an EC curve.
* @return the corresponding encoded CBOR array.
*/
public byte[] getGeekChain(int curve) {
return curveToGeek.get(curve);
}
/**
* Sets the {@code challenge}.
*/
public void setChallenge(byte[] challenge) {
this.challenge = challenge;
}
/**
* Returns the {@code challenge}.
*
* @return the challenge that will be embedded in the CSR sent to the server.
*/
public byte[] getChallenge() {
return challenge;
}
}