blob: 3aca3157bdfc717df7d2c1614eac49af01e29947 [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.annotation.NonNull;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.RemoteException;
import android.security.remoteprovisioning.IRemoteProvisioning;
import android.util.Log;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.ArrayList;
/**
* Provides an easy package to run the provisioning process from start to finish, interfacing
* with the remote provisioning system service and the server backend in order to provision
* attestation certificates to the device.
*/
public class Provisioner {
private static final String PROVISIONING_URL = "";
private static final String GEEK_URL = PROVISIONING_URL + "/v1/eekchain";
private static final String CERTIFICATE_SIGNING_URL =
PROVISIONING_URL + "/v1:signCertificates?challenge=";
private static final String TAG = "RemoteProvisioningService";
/**
* Takes a byte stream composed of PEM encoded certificates and returns the X.509 certificates
* contained within as an X509Certificate array.
*/
private static X509Certificate[] formatX509Certs(byte[] certStream)
throws CertificateException {
CertificateFactory fact = CertificateFactory.getInstance("X.509");
ByteArrayInputStream in = new ByteArrayInputStream(certStream);
ArrayList<Certificate> certs = new ArrayList<Certificate>(fact.generateCertificates(in));
return certs.toArray(new X509Certificate[certs.size()]);
}
/**
* Calls out to the specified backend servers to retrieve an Endpoint Encryption Key and
* corresponding certificate chain to provide to KeyMint. This public key will be used to
* perform an ECDH computation, using the shared secret to encrypt privacy sensitive components
* in the bundle that the server needs from the device in order to provision certificates.
*
* A challenge is also returned from the server so that it can check freshness of the follow-up
* request to get keys signed.
*/
private static GeekResponse fetchGeek() {
try {
URL url = new URL(GEEK_URL);
HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("GET");
if (con.getResponseCode() != HttpURLConnection.HTTP_OK) {
Log.w(TAG, "Server connection for GEEK failed, response code: "
+ con.getResponseCode());
}
BufferedInputStream inputStream = new BufferedInputStream(con.getInputStream());
ByteArrayOutputStream cborBytes = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int read = 0;
while((read = inputStream.read(buffer, 0, buffer.length)) != -1) {
cborBytes.write(buffer, 0, read);
}
inputStream.close();
return CborUtils.parseGeekResponse(cborBytes.toByteArray());
} catch (IOException e) {
Log.e(TAG, "Failed to fetch GEEK from the servers.", e);
return null;
}
}
/**
* Ferries the CBOR blobs returned by KeyMint to the provisioning server. The data sent to the
* provisioning server contains the MAC'ed CSRs and encrypted bundle containing the MAC key and
* the hardware unique public key.
*
* @param cborBlob The CBOR encoded data containing the relevant pieces needed for the server to
* sign the CSRs. The data encoded within comes from Keystore / KeyMint.
*
* @param challenge The challenge that was sent from the server. It is included here even though
* it is also included in `cborBlob` in order to allow the server to more
* easily reject bad requests.
*
* @return A List of byte arrays, where each array contains an entire PEM-encoded certificate
* for one attestation key pair.
*/
private static List<byte[]> requestSignedCertificates(byte[] cborBlob, byte[] challenge) {
try {
URL url = new URL(CERTIFICATE_SIGNING_URL + new String(challenge, "UTF-8"));
HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("POST");
con.setDoOutput(true);
if (con.getResponseCode() != HttpURLConnection.HTTP_OK) {
Log.e(TAG, "Server connection for signing failed, response code: "
+ con.getResponseCode());
}
// May not be able to use try-with-resources here if the connection gets closed due to
// the output stream being automatically closed.
try (OutputStream os = con.getOutputStream()) {
os.write(cborBlob, 0, cborBlob.length);
} catch (Exception e) {
return null;
}
BufferedInputStream inputStream = new BufferedInputStream(con.getInputStream());
ByteArrayOutputStream cborBytes = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int read = 0;
while((read = inputStream.read(buffer, 0, buffer.length)) != -1) {
cborBytes.write(buffer, 0, read);
}
return CborUtils.parseSignedCertificates(cborBytes.toByteArray());
} catch (IOException e) {
Log.w(TAG, "Failed to request signed certificates from the server", e);
return null;
}
}
/**
* Drives the process of provisioning certs. The method first contacts the provided backend
* server to retrieve an Endpoing Encryption Key with an accompanying certificate chain and a
* challenge. It passes this data and the requested number of keys to the remote provisioning
* system backend, which then works with KeyMint in order to get a CSR bundle generated, along
* with an encrypted package containing metadata that the server needs in order to make
* decisions about provisioning.
*
* This method then passes that bundle back out to the server backend, waits for the response,
* and, if successful, passes the certificate chains back to the remote provisioning service to
* be stored and later assigned to apps requesting a key attestation.
*
* @param numKeys The number of keys to be signed. The service will do a best-effort to
* provision the number requested, but if the number requested is larger
* than the number of unsigned attestation key pairs available, it will
* only sign the number that is available at time of calling.
*
* @param binder The IRemoteProvisioning binder interface needed by the method to handle talking
* to the remote provisioning system component.
*
* @return True if certificates were successfully provisioned for the signing keys.
*/
public static boolean provisionCerts(int numKeys, @NonNull IRemoteProvisioning binder) {
if (numKeys < 1) {
Log.e(TAG, "Request at least 1 key to be signed. Num requested: " + numKeys);
return false;
}
GeekResponse geek = fetchGeek();
if (geek == null) {
Log.e(TAG, "The geek is null");
return false;
}
byte[] payload = null;
try {
payload = binder.generateCsr(false /* testMode */,
numKeys,
geek.geek,
geek.challenge);
} catch (RemoteException e) {
Log.e(TAG, "Failed to generate CSR blob", e);
return false;
}
if (payload == null) {
Log.e(TAG, "Keystore failed to generate a payload");
return false;
}
ArrayList<byte[]> certChains =
new ArrayList<byte[]>(requestSignedCertificates(payload, geek.challenge));
// TODO: Fix AIDL here to add a matching scheme, since public key in KeyStore is a CBOR
// blob
//binder.provisionCertChain();
return true;
}
}