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