1 /* 2 * Copyright (C) 2022 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.rkpdapp.interfaces; 18 19 import android.content.Context; 20 import android.net.ConnectivityManager; 21 import android.net.NetworkInfo; 22 import android.net.TrafficStats; 23 import android.util.Base64; 24 import android.util.Log; 25 26 import com.android.rkpdapp.GeekResponse; 27 import com.android.rkpdapp.RkpdException; 28 import com.android.rkpdapp.metrics.ProvisioningAttempt; 29 import com.android.rkpdapp.utils.CborUtils; 30 import com.android.rkpdapp.utils.Settings; 31 import com.android.rkpdapp.utils.StopWatch; 32 import com.android.rkpdapp.utils.X509Utils; 33 34 import java.io.BufferedInputStream; 35 import java.io.ByteArrayOutputStream; 36 import java.io.IOException; 37 import java.io.InputStream; 38 import java.io.OutputStream; 39 import java.net.HttpURLConnection; 40 import java.net.SocketTimeoutException; 41 import java.net.URL; 42 import java.nio.charset.Charset; 43 import java.nio.charset.StandardCharsets; 44 import java.security.MessageDigest; 45 import java.security.NoSuchAlgorithmException; 46 import java.security.cert.X509Certificate; 47 import java.util.List; 48 import java.util.Objects; 49 import java.util.UUID; 50 51 /** 52 * Provides convenience methods for interfacing with the remote provisioning server. 53 */ 54 public class ServerInterface { 55 56 private static final int TIMEOUT_MS = 20000; 57 private static final int BACKOFF_TIME_MS = 100; 58 59 private static final String TAG = "RkpdServerInterface"; 60 private static final String GEEK_URL = ":fetchEekChain"; 61 private static final String CERTIFICATE_SIGNING_URL = ":signCertificates?"; 62 private static final String CHALLENGE_PARAMETER = "challenge="; 63 private static final String REQUEST_ID_PARAMETER = "request_id="; 64 private final Context mContext; 65 66 private enum Operation { 67 FETCH_GEEK, 68 SIGN_CERTS; 69 getHttpErrorStatus()70 public ProvisioningAttempt.Status getHttpErrorStatus() { 71 if (Objects.equals(name(), FETCH_GEEK.name())) { 72 return ProvisioningAttempt.Status.FETCH_GEEK_HTTP_ERROR; 73 } else if (Objects.equals(name(), SIGN_CERTS.name())) { 74 return ProvisioningAttempt.Status.SIGN_CERTS_HTTP_ERROR; 75 } 76 throw new IllegalStateException("Please declare status for new operation."); 77 } 78 getIoExceptionStatus()79 public ProvisioningAttempt.Status getIoExceptionStatus() { 80 if (Objects.equals(name(), FETCH_GEEK.name())) { 81 return ProvisioningAttempt.Status.FETCH_GEEK_IO_EXCEPTION; 82 } else if (Objects.equals(name(), SIGN_CERTS.name())) { 83 return ProvisioningAttempt.Status.SIGN_CERTS_IO_EXCEPTION; 84 } 85 throw new IllegalStateException("Please declare status for new operation."); 86 } 87 getTimedOutStatus()88 public ProvisioningAttempt.Status getTimedOutStatus() { 89 if (Objects.equals(name(), FETCH_GEEK.name())) { 90 return ProvisioningAttempt.Status.FETCH_GEEK_TIMED_OUT; 91 } else if (Objects.equals(name(), SIGN_CERTS.name())) { 92 return ProvisioningAttempt.Status.SIGN_CERTS_TIMED_OUT; 93 } 94 throw new IllegalStateException("Please declare status for new operation."); 95 } 96 } 97 ServerInterface(Context context)98 public ServerInterface(Context context) { 99 this.mContext = context; 100 } 101 102 /** 103 * Ferries the CBOR blobs returned by KeyMint to the provisioning server. The data sent to the 104 * provisioning server contains the MAC'ed CSRs and encrypted bundle containing the MAC key and 105 * the hardware unique public key. 106 * 107 * @param csr The CBOR encoded data containing the relevant pieces needed for the server to 108 * sign the CSRs. The data encoded within comes from Keystore / KeyMint. 109 * @param challenge The challenge that was sent from the server. It is included here even though 110 * it is also included in `cborBlob` in order to allow the server to more 111 * easily reject bad requests. 112 * @return A List of byte arrays, where each array contains an entire DER-encoded certificate 113 * chain for one attestation key pair. 114 */ requestSignedCertificates(byte[] csr, byte[] challenge, ProvisioningAttempt metrics)115 public List<byte[]> requestSignedCertificates(byte[] csr, byte[] challenge, 116 ProvisioningAttempt metrics) throws RkpdException, InterruptedException { 117 final String challengeParam = CHALLENGE_PARAMETER + Base64.encodeToString(challenge, 118 Base64.URL_SAFE | Base64.NO_WRAP); 119 final String fullUrl = CERTIFICATE_SIGNING_URL + String.join("&", challengeParam, 120 REQUEST_ID_PARAMETER + generateAndLogRequestId()); 121 final byte[] cborBytes = connectAndGetData(metrics, fullUrl, csr, Operation.SIGN_CERTS); 122 List<byte[]> certChains = CborUtils.parseSignedCertificates(cborBytes); 123 if (certChains == null) { 124 metrics.setStatus(ProvisioningAttempt.Status.INTERNAL_ERROR); 125 throw new RkpdException( 126 RkpdException.ErrorCode.INTERNAL_ERROR, 127 "Response failed to parse."); 128 } else if (certChains.isEmpty()) { 129 metrics.setCertChainLength(0); 130 metrics.setRootCertFingerprint(""); 131 } else { 132 try { 133 X509Certificate[] certs = X509Utils.formatX509Certs(certChains.get(0)); 134 metrics.setCertChainLength(certs.length); 135 byte[] pubKey = certs[certs.length - 1].getPublicKey().getEncoded(); 136 byte[] pubKeyDigest = MessageDigest.getInstance("SHA-256").digest(pubKey); 137 metrics.setRootCertFingerprint(Base64.encodeToString(pubKeyDigest, Base64.DEFAULT)); 138 } catch (NoSuchAlgorithmException e) { 139 throw new RkpdException(RkpdException.ErrorCode.INTERNAL_ERROR, 140 "Algorithm not found", e); 141 } 142 } 143 return certChains; 144 } 145 generateAndLogRequestId()146 private String generateAndLogRequestId() { 147 String reqId = UUID.randomUUID().toString(); 148 Log.i(TAG, "request_id: " + reqId); 149 return reqId; 150 } 151 152 /** 153 * Calls out to the specified backend servers to retrieve an Endpoint Encryption Key and 154 * corresponding certificate chain to provide to KeyMint. This public key will be used to 155 * perform an ECDH computation, using the shared secret to encrypt privacy sensitive components 156 * in the bundle that the server needs from the device in order to provision certificates. 157 * 158 * A challenge is also returned from the server so that it can check freshness of the follow-up 159 * request to get keys signed. 160 * 161 * @return A GeekResponse object which optionally contains configuration data. 162 */ fetchGeek(ProvisioningAttempt metrics)163 public GeekResponse fetchGeek(ProvisioningAttempt metrics) 164 throws RkpdException, InterruptedException { 165 byte[] input = CborUtils.buildProvisioningInfo(mContext); 166 byte[] cborBytes = connectAndGetData(metrics, GEEK_URL, input, Operation.FETCH_GEEK); 167 GeekResponse resp = CborUtils.parseGeekResponse(cborBytes); 168 if (resp == null) { 169 metrics.setStatus(ProvisioningAttempt.Status.FETCH_GEEK_HTTP_ERROR); 170 throw new RkpdException( 171 RkpdException.ErrorCode.HTTP_SERVER_ERROR, 172 "Response failed to parse."); 173 } 174 return resp; 175 } 176 checkDataBudget(ProvisioningAttempt metrics)177 private void checkDataBudget(ProvisioningAttempt metrics) 178 throws RkpdException { 179 if (!Settings.hasErrDataBudget(mContext, null /* curTime */)) { 180 metrics.setStatus(ProvisioningAttempt.Status.OUT_OF_ERROR_BUDGET); 181 int bytesConsumed = Settings.getErrDataBudgetConsumed(mContext); 182 throw makeNetworkError("Out of data budget due to repeated errors. Consumed " 183 + bytesConsumed + " bytes.", metrics); 184 } 185 } 186 makeNetworkError(String message, ProvisioningAttempt metrics)187 private RkpdException makeNetworkError(String message, 188 ProvisioningAttempt metrics) { 189 ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class); 190 NetworkInfo networkInfo = cm.getActiveNetworkInfo(); 191 if (networkInfo != null && networkInfo.isConnected()) { 192 return new RkpdException( 193 RkpdException.ErrorCode.NETWORK_COMMUNICATION_ERROR, message); 194 } 195 metrics.setStatus(ProvisioningAttempt.Status.NO_NETWORK_CONNECTIVITY); 196 return new RkpdException( 197 RkpdException.ErrorCode.NO_NETWORK_CONNECTIVITY, message); 198 } 199 200 /** 201 * Fetch a GEEK from the server and update SettingsManager appropriately with the return 202 * values. This will also delete all keys in the attestation key pool if the server has 203 * indicated that RKP should be turned off. 204 */ fetchGeekAndUpdate(ProvisioningAttempt metrics)205 public GeekResponse fetchGeekAndUpdate(ProvisioningAttempt metrics) 206 throws InterruptedException, RkpdException { 207 GeekResponse resp = fetchGeek(metrics); 208 209 Settings.setDeviceConfig(mContext, 210 resp.numExtraAttestationKeys, 211 resp.timeToRefresh, 212 resp.provisioningUrl); 213 return resp; 214 } 215 216 /** 217 * Reads error data from the RKP server suitable for logging. 218 * @param con The HTTP connection from which to read the error 219 * @return The error string, or a description of why we couldn't read an error. 220 */ readErrorFromConnection(HttpURLConnection con)221 public static String readErrorFromConnection(HttpURLConnection con) { 222 final String contentType = con.getContentType(); 223 if (!contentType.startsWith("text") && !contentType.startsWith("application/json")) { 224 return "Unexpected content type from the server: " + contentType; 225 } 226 227 InputStream inputStream; 228 try { 229 inputStream = con.getInputStream(); 230 } catch (IOException exception) { 231 inputStream = con.getErrorStream(); 232 } 233 234 if (inputStream == null) { 235 return "No error data returned by server."; 236 } 237 238 byte[] bytes; 239 try { 240 bytes = new byte[1024]; 241 final int read = inputStream.read(bytes); 242 if (read <= 0) { 243 return "No error data returned by server."; 244 } 245 bytes = java.util.Arrays.copyOf(bytes, read); 246 } catch (IOException e) { 247 return "Error reading error string from server: " + e; 248 } 249 250 final Charset charset = getCharsetFromContentTypeHeader(contentType); 251 return new String(bytes, charset); 252 } 253 getCharsetFromContentTypeHeader(String contentType)254 private static Charset getCharsetFromContentTypeHeader(String contentType) { 255 final String[] contentTypeParts = contentType.split(";"); 256 if (contentTypeParts.length != 2) { 257 Log.w(TAG, "Simple content type; defaulting to ASCII"); 258 return StandardCharsets.US_ASCII; 259 } 260 261 final String[] charsetParts = contentTypeParts[1].strip().split("="); 262 if (charsetParts.length != 2 || !charsetParts[0].equals("charset")) { 263 Log.w(TAG, "The charset is missing from content-type, defaulting to ASCII"); 264 return StandardCharsets.US_ASCII; 265 } 266 267 final String charsetString = charsetParts[1].strip(); 268 try { 269 return Charset.forName(charsetString); 270 } catch (IllegalArgumentException e) { 271 Log.w(TAG, "Unsupported charset: " + charsetString + "; defaulting to ASCII"); 272 return StandardCharsets.US_ASCII; 273 } 274 } 275 connectAndGetData(ProvisioningAttempt metrics, String endpoint, byte[] input, Operation operation)276 private byte[] connectAndGetData(ProvisioningAttempt metrics, String endpoint, byte[] input, 277 Operation operation) throws RkpdException, InterruptedException { 278 TrafficStats.setThreadStatsTag(0); 279 int backoff_time = BACKOFF_TIME_MS; 280 int attempt = 1; 281 try (StopWatch retryTimer = new StopWatch(TAG)) { 282 retryTimer.start(); 283 while (true) { 284 checkDataBudget(metrics); 285 try { 286 Log.v(TAG, "Requesting data from server. Attempt " + attempt); 287 return requestData(metrics, new URL(Settings.getUrl(mContext) + endpoint), 288 input); 289 } catch (SocketTimeoutException e) { 290 metrics.setStatus(operation.getTimedOutStatus()); 291 Log.e(TAG, "Server timed out. " + e.getMessage()); 292 } catch (IOException e) { 293 metrics.setStatus(operation.getIoExceptionStatus()); 294 Log.e(TAG, "Failed to complete request from server." + e.getMessage()); 295 } catch (RkpdException e) { 296 if (e.getErrorCode() == RkpdException.ErrorCode.DEVICE_NOT_REGISTERED) { 297 metrics.setStatus( 298 ProvisioningAttempt.Status.SIGN_CERTS_DEVICE_NOT_REGISTERED); 299 throw e; 300 } else { 301 metrics.setStatus(operation.getHttpErrorStatus()); 302 if (e.getErrorCode() == RkpdException.ErrorCode.HTTP_CLIENT_ERROR) { 303 throw e; 304 } 305 } 306 } 307 if (retryTimer.getElapsedMillis() > Settings.getMaxRequestTime(mContext)) { 308 break; 309 } else { 310 Thread.sleep(backoff_time); 311 backoff_time *= 2; 312 attempt += 1; 313 } 314 } 315 } 316 Settings.incrementFailureCounter(mContext); 317 throw makeNetworkError("Error getting data from server.", metrics); 318 } 319 requestData(ProvisioningAttempt metrics, URL url, byte[] input)320 private byte[] requestData(ProvisioningAttempt metrics, URL url, byte[] input) 321 throws IOException, RkpdException { 322 int bytesTransacted = 0; 323 try (StopWatch serverWaitTimer = metrics.startServerWait()) { 324 HttpURLConnection con = (HttpURLConnection) url.openConnection(); 325 con.setRequestMethod("POST"); 326 con.setConnectTimeout(TIMEOUT_MS); 327 con.setReadTimeout(TIMEOUT_MS); 328 con.setDoOutput(true); 329 330 try (OutputStream os = con.getOutputStream()) { 331 os.write(input, 0, input.length); 332 bytesTransacted += input.length; 333 } 334 335 metrics.setHttpStatusError(con.getResponseCode()); 336 if (con.getResponseCode() != HttpURLConnection.HTTP_OK) { 337 int failures = Settings.incrementFailureCounter(mContext); 338 Log.e(TAG, "Server connection failed for url: " + url + ", response code: " 339 + con.getResponseCode() + "\nRepeated failure count: " + failures); 340 Log.e(TAG, readErrorFromConnection(con)); 341 throw RkpdException.createFromHttpError(con.getResponseCode()); 342 } 343 serverWaitTimer.stop(); 344 Settings.clearFailureCounter(mContext); 345 BufferedInputStream inputStream = new BufferedInputStream(con.getInputStream()); 346 ByteArrayOutputStream cborBytes = new ByteArrayOutputStream(); 347 byte[] buffer = new byte[1024]; 348 int read; 349 serverWaitTimer.start(); 350 while ((read = inputStream.read(buffer, 0, buffer.length)) != -1) { 351 cborBytes.write(buffer, 0, read); 352 bytesTransacted += read; 353 } 354 inputStream.close(); 355 Log.v(TAG, "Network request completed successfully."); 356 return cborBytes.toByteArray(); 357 } catch (Exception e) { 358 Settings.consumeErrDataBudget(mContext, bytesTransacted); 359 throw e; 360 } 361 } 362 } 363