1 /** 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.remoteprovisioner; 18 19 import android.content.Context; 20 import android.net.ConnectivityManager; 21 import android.net.NetworkInfo; 22 import android.security.IGenerateRkpKeyService; 23 import android.util.Base64; 24 import android.util.Log; 25 26 import java.io.BufferedInputStream; 27 import java.io.ByteArrayOutputStream; 28 import java.io.IOException; 29 import java.io.OutputStream; 30 import java.net.HttpURLConnection; 31 import java.net.SocketTimeoutException; 32 import java.net.URL; 33 import java.util.List; 34 35 /** 36 * Provides convenience methods for interfacing with the remote provisioning server. 37 */ 38 public class ServerInterface { 39 40 private static final int TIMEOUT_MS = 20000; 41 42 private static final String TAG = "ServerInterface"; 43 private static final String GEEK_URL = ":fetchEekChain"; 44 private static final String CERTIFICATE_SIGNING_URL = ":signCertificates?challenge="; 45 46 /** 47 * Ferries the CBOR blobs returned by KeyMint to the provisioning server. The data sent to the 48 * provisioning server contains the MAC'ed CSRs and encrypted bundle containing the MAC key and 49 * the hardware unique public key. 50 * 51 * @param context The application context which is required to use SettingsManager. 52 * @param csr The CBOR encoded data containing the relevant pieces needed for the server to 53 * sign the CSRs. The data encoded within comes from Keystore / KeyMint. 54 * @param challenge The challenge that was sent from the server. It is included here even though 55 * it is also included in `cborBlob` in order to allow the server to more 56 * easily reject bad requests. 57 * @return A List of byte arrays, where each array contains an entire DER-encoded certificate 58 * chain for one attestation key pair. 59 */ requestSignedCertificates(Context context, byte[] csr, byte[] challenge, ProvisionerMetrics metrics)60 public static List<byte[]> requestSignedCertificates(Context context, byte[] csr, 61 byte[] challenge, ProvisionerMetrics metrics) throws RemoteProvisioningException { 62 checkDataBudget(context, metrics); 63 int bytesTransacted = 0; 64 try (ProvisionerMetrics.StopWatch serverWaitTimer = metrics.startServerWait()) { 65 URL url = new URL(SettingsManager.getUrl(context) + CERTIFICATE_SIGNING_URL 66 + Base64.encodeToString(challenge, Base64.URL_SAFE)); 67 HttpURLConnection con = (HttpURLConnection) url.openConnection(); 68 con.setRequestMethod("POST"); 69 con.setDoOutput(true); 70 con.setConnectTimeout(TIMEOUT_MS); 71 con.setReadTimeout(TIMEOUT_MS); 72 73 // May not be able to use try-with-resources here if the connection gets closed due to 74 // the output stream being automatically closed. 75 try (OutputStream os = con.getOutputStream()) { 76 os.write(csr, 0, csr.length); 77 bytesTransacted += csr.length; 78 } 79 80 metrics.setHttpStatusError(con.getResponseCode()); 81 if (con.getResponseCode() != HttpURLConnection.HTTP_OK) { 82 serverWaitTimer.stop(); 83 int failures = SettingsManager.incrementFailureCounter(context); 84 Log.e(TAG, "Server connection for signing failed, response code: " 85 + con.getResponseCode() + "\nRepeated failure count: " + failures); 86 SettingsManager.consumeErrDataBudget(context, bytesTransacted); 87 RemoteProvisioningException ex = 88 RemoteProvisioningException.createFromHttpError(con.getResponseCode()); 89 if (ex.getErrorCode() == IGenerateRkpKeyService.Status.DEVICE_NOT_REGISTERED) { 90 metrics.setStatus(ProvisionerMetrics.Status.SIGN_CERTS_DEVICE_NOT_REGISTERED); 91 } else { 92 metrics.setStatus(ProvisionerMetrics.Status.SIGN_CERTS_HTTP_ERROR); 93 } 94 throw ex; 95 } 96 serverWaitTimer.stop(); 97 SettingsManager.clearFailureCounter(context); 98 BufferedInputStream inputStream = new BufferedInputStream(con.getInputStream()); 99 ByteArrayOutputStream cborBytes = new ByteArrayOutputStream(); 100 byte[] buffer = new byte[1024]; 101 int read = 0; 102 serverWaitTimer.start(); 103 while ((read = inputStream.read(buffer, 0, buffer.length)) != -1) { 104 cborBytes.write(buffer, 0, read); 105 bytesTransacted += read; 106 } 107 serverWaitTimer.stop(); 108 return CborUtils.parseSignedCertificates(cborBytes.toByteArray()); 109 } catch (SocketTimeoutException e) { 110 Log.e(TAG, "Server timed out", e); 111 metrics.setStatus(ProvisionerMetrics.Status.SIGN_CERTS_TIMED_OUT); 112 } catch (IOException e) { 113 Log.e(TAG, "Failed to request signed certificates from the server", e); 114 metrics.setStatus(ProvisionerMetrics.Status.SIGN_CERTS_IO_EXCEPTION); 115 } 116 SettingsManager.incrementFailureCounter(context); 117 SettingsManager.consumeErrDataBudget(context, bytesTransacted); 118 throw makeNetworkError(context, "Error getting CSR signed.", metrics); 119 } 120 121 /** 122 * Calls out to the specified backend servers to retrieve an Endpoint Encryption Key and 123 * corresponding certificate chain to provide to KeyMint. This public key will be used to 124 * perform an ECDH computation, using the shared secret to encrypt privacy sensitive components 125 * in the bundle that the server needs from the device in order to provision certificates. 126 * 127 * A challenge is also returned from the server so that it can check freshness of the follow-up 128 * request to get keys signed. 129 * 130 * @param context The application context which is required to use SettingsManager. 131 * @param metrics 132 * @return A GeekResponse object which optionally contains configuration data. 133 */ fetchGeek(Context context, ProvisionerMetrics metrics)134 public static GeekResponse fetchGeek(Context context, 135 ProvisionerMetrics metrics) throws RemoteProvisioningException { 136 checkDataBudget(context, metrics); 137 int bytesTransacted = 0; 138 try { 139 URL url = new URL(SettingsManager.getUrl(context) + GEEK_URL); 140 ByteArrayOutputStream cborBytes = new ByteArrayOutputStream(); 141 try (ProvisionerMetrics.StopWatch serverWaitTimer = metrics.startServerWait()) { 142 HttpURLConnection con = (HttpURLConnection) url.openConnection(); 143 con.setRequestMethod("POST"); 144 con.setConnectTimeout(TIMEOUT_MS); 145 con.setReadTimeout(TIMEOUT_MS); 146 con.setDoOutput(true); 147 148 byte[] config = CborUtils.buildProvisioningInfo(context); 149 try (OutputStream os = con.getOutputStream()) { 150 os.write(config, 0, config.length); 151 bytesTransacted += config.length; 152 } 153 154 metrics.setHttpStatusError(con.getResponseCode()); 155 if (con.getResponseCode() != HttpURLConnection.HTTP_OK) { 156 serverWaitTimer.stop(); 157 int failures = SettingsManager.incrementFailureCounter(context); 158 Log.e(TAG, "Server connection for GEEK failed, response code: " 159 + con.getResponseCode() + "\nRepeated failure count: " + failures); 160 SettingsManager.consumeErrDataBudget(context, bytesTransacted); 161 metrics.setStatus(ProvisionerMetrics.Status.FETCH_GEEK_HTTP_ERROR); 162 throw RemoteProvisioningException.createFromHttpError(con.getResponseCode()); 163 } 164 serverWaitTimer.stop(); 165 SettingsManager.clearFailureCounter(context); 166 BufferedInputStream inputStream = new BufferedInputStream(con.getInputStream()); 167 byte[] buffer = new byte[1024]; 168 int read = 0; 169 serverWaitTimer.start(); 170 while ((read = inputStream.read(buffer, 0, buffer.length)) != -1) { 171 cborBytes.write(buffer, 0, read); 172 bytesTransacted += read; 173 } 174 inputStream.close(); 175 } 176 GeekResponse resp = CborUtils.parseGeekResponse(cborBytes.toByteArray()); 177 if (resp == null) { 178 metrics.setStatus(ProvisionerMetrics.Status.FETCH_GEEK_HTTP_ERROR); 179 throw new RemoteProvisioningException( 180 IGenerateRkpKeyService.Status.HTTP_SERVER_ERROR, 181 "Response failed to parse."); 182 } 183 return resp; 184 } catch (SocketTimeoutException e) { 185 Log.e(TAG, "Server timed out", e); 186 metrics.setStatus(ProvisionerMetrics.Status.FETCH_GEEK_TIMED_OUT); 187 } catch (IOException e) { 188 // This exception will trigger on a completely malformed URL. 189 Log.e(TAG, "Failed to fetch GEEK from the servers.", e); 190 metrics.setStatus(ProvisionerMetrics.Status.FETCH_GEEK_IO_EXCEPTION); 191 } 192 SettingsManager.incrementFailureCounter(context); 193 SettingsManager.consumeErrDataBudget(context, bytesTransacted); 194 throw makeNetworkError(context, "Error fetching GEEK", metrics); 195 } 196 checkDataBudget(Context context, ProvisionerMetrics metrics)197 private static void checkDataBudget(Context context, ProvisionerMetrics metrics) 198 throws RemoteProvisioningException { 199 if (!SettingsManager.hasErrDataBudget(context, null /* curTime */)) { 200 metrics.setStatus(ProvisionerMetrics.Status.OUT_OF_ERROR_BUDGET); 201 int bytesConsumed = SettingsManager.getErrDataBudgetConsumed(context); 202 throw makeNetworkError(context, 203 "Out of data budget due to repeated errors. Consumed " 204 + bytesConsumed + " bytes.", metrics); 205 } 206 } 207 makeNetworkError(Context context, String message, ProvisionerMetrics metrics)208 private static RemoteProvisioningException makeNetworkError(Context context, String message, 209 ProvisionerMetrics metrics) { 210 ConnectivityManager cm = context.getSystemService(ConnectivityManager.class); 211 NetworkInfo networkInfo = cm.getActiveNetworkInfo(); 212 if (networkInfo != null && networkInfo.isConnected()) { 213 return new RemoteProvisioningException( 214 IGenerateRkpKeyService.Status.NETWORK_COMMUNICATION_ERROR, message); 215 } 216 metrics.setStatus(ProvisionerMetrics.Status.NO_NETWORK_CONNECTIVITY); 217 return new RemoteProvisioningException( 218 IGenerateRkpKeyService.Status.NO_NETWORK_CONNECTIVITY, message); 219 } 220 } 221