| /** |
| * 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; |
| } |
| } |