• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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