1 /*
2  * Copyright (C) 2015 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.cts.verifier.security;
18 
19 import android.app.Activity;
20 import android.content.Intent;
21 import android.content.res.Resources;
22 import android.os.AsyncTask;
23 import android.os.Bundle;
24 import android.provider.Settings;
25 import android.security.KeyChain;
26 import android.security.KeyChainAliasCallback;
27 import android.security.KeyChainException;
28 import android.text.method.ScrollingMovementMethod;
29 import android.util.Log;
30 import android.view.View;
31 import android.widget.Button;
32 import android.widget.TextView;
33 
34 import com.android.cts.verifier.PassFailButtons;
35 import com.android.cts.verifier.R;
36 
37 import com.google.mockwebserver.MockResponse;
38 import com.google.mockwebserver.MockWebServer;
39 
40 import java.io.InputStream;
41 import java.io.IOException;
42 import java.io.ByteArrayOutputStream;
43 import java.net.Socket;
44 import java.net.URL;
45 import java.security.GeneralSecurityException;
46 import java.security.Key;
47 import java.security.KeyFactory;
48 import java.security.KeyStore;
49 import java.security.Principal;
50 import java.security.PrivateKey;
51 import java.security.cert.Certificate;
52 import java.security.cert.CertificateFactory;
53 import java.security.cert.X509Certificate;
54 import java.security.spec.PKCS8EncodedKeySpec;
55 import java.util.ArrayList;
56 import java.util.concurrent.TimeUnit;
57 import java.util.List;
58 import javax.net.ssl.HttpsURLConnection;
59 import javax.net.ssl.KeyManager;
60 import javax.net.ssl.KeyManagerFactory;
61 import javax.net.ssl.SSLContext;
62 import javax.net.ssl.SSLSocketFactory;
63 import javax.net.ssl.X509ExtendedKeyManager;
64 
65 import libcore.java.security.TestKeyStore;
66 import libcore.javax.net.ssl.TestSSLContext;
67 
68 import org.mockito.ArgumentCaptor;
69 import org.mockito.Mockito;
70 
71 /**
72  * Simple activity based test that exercises the KeyChain API
73  */
74 public class KeyChainTest extends PassFailButtons.Activity implements View.OnClickListener {
75 
76     private static final String TAG = "KeyChainTest";
77 
78     private static final int REQUEST_CA_INSTALL = 1;
79 
80     private TextView mInstructionView;
81     private TextView mLogView;
82     private Button mResetButton;
83     private Button mSkipButton;
84     private Button mNextButton;
85 
86     private List<Step> mSteps;
87     int mCurrentStep;
88 
89     private KeyStore mKeyStore;
90     private static final char[] KEYSTORE_PASSWORD = "".toCharArray();
91 
92     // How long to wait before giving up on the user selecting a key alias.
93     private static final int KEYCHAIN_ALIAS_TIMEOUT_MS = (int) TimeUnit.MINUTES.toMillis(5L);
94 
onCreate(Bundle savedInstanceState)95     @Override public void onCreate(Bundle savedInstanceState) {
96         super.onCreate(savedInstanceState);
97 
98         View root = getLayoutInflater().inflate(R.layout.keychain_main, null);
99         setContentView(root);
100 
101         setInfoResources(R.string.keychain_test, R.string.keychain_info, -1);
102         setPassFailButtonClickListeners();
103 
104         mInstructionView = (TextView) root.findViewById(R.id.test_instruction);
105         mLogView = (TextView) root.findViewById(R.id.test_log);
106         mLogView.setMovementMethod(new ScrollingMovementMethod());
107 
108         mNextButton = (Button) root.findViewById(R.id.action_next);
109         mNextButton.setOnClickListener(this);
110 
111         mResetButton = (Button) root.findViewById(R.id.action_reset);
112         mResetButton.setOnClickListener(this);
113 
114         mSkipButton = (Button) root.findViewById(R.id.action_skip);
115         mSkipButton.setOnClickListener(this);
116 
117         resetProgress();
118     }
119 
120     @Override
onClick(View v)121     public void onClick(View v) {
122         Step step = mSteps.get(mCurrentStep);
123         if (v == mNextButton) {
124             switch (step.task.getStatus()) {
125                 case PENDING: {
126                     step.task.execute();
127                     break;
128                 }
129                 case FINISHED: {
130                     if (mCurrentStep + 1 < mSteps.size()) {
131                         mCurrentStep += 1;
132                         updateUi();
133                     } else {
134                         mSkipButton.setVisibility(View.INVISIBLE);
135                         mNextButton.setVisibility(View.INVISIBLE);
136                     }
137                     break;
138                 }
139             }
140         } else if (v == mSkipButton) {
141             step.task.cancel(false);
142             mCurrentStep += 1;
143             updateUi();
144         } else if (v == mResetButton) {
145             resetProgress();
146         }
147     }
148 
149     @Override
onActivityResult(int requestCode, int resultCode, Intent data)150     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
151         switch (requestCode) {
152             case REQUEST_CA_INSTALL: {
153                 if (resultCode == RESULT_OK) {
154                     log("CA Certificate installed successfully");
155                 } else {
156                     log("REQUEST_CA_INSTALL failed with result code: " + resultCode);
157                 }
158                 break;
159             }
160             default:
161                 throw new IllegalStateException("requestCode == " + requestCode);
162         }
163     }
164 
resetProgress()165     private void resetProgress() {
166         getPassButton().setEnabled(false);
167         mLogView.setText("");
168 
169         mSteps = new ArrayList<>();
170         mSteps.add(new Step(R.string.keychain_setup_desc, false, new SetupTestKeyStoreTask()));
171         mSteps.add(new Step(R.string.keychain_install_desc, true, new InstallCredentialsTask()));
172         mSteps.add(new Step(R.string.keychain_https_desc, false, new TestHttpsRequestTask()));
173         mSteps.add(new Step(R.string.keychain_reset_desc, true, new ClearCredentialsTask()));
174         mCurrentStep = 0;
175 
176         updateUi();
177     }
178 
updateUi()179     private void updateUi() {
180         mLogView.setText("");
181 
182         if (mCurrentStep >= mSteps.size()) {
183             mSkipButton.setVisibility(View.INVISIBLE);
184             mNextButton.setVisibility(View.INVISIBLE);
185             getPassButton().setEnabled(true);
186             return;
187         }
188 
189         final Step step = mSteps.get(mCurrentStep);
190         if (step.task.getStatus() == AsyncTask.Status.PENDING) {
191             mInstructionView.setText(step.instructionTextId);
192         }
193         mSkipButton.setVisibility(step.skippable ? View.VISIBLE : View.INVISIBLE);
194         mNextButton.setVisibility(View.VISIBLE);
195     }
196 
197     private class SetupTestKeyStoreTask extends AsyncTask<Void, Void, Void> {
198         @Override
doInBackground(Void... params)199         protected Void doInBackground(Void... params) {
200             final Certificate[] chain = new Certificate[2];
201             final Key privKey;
202 
203             log("Reading resources");
204             Resources res = getResources();
205             ByteArrayOutputStream userKey = new ByteArrayOutputStream();
206             try {
207                 InputStream is = res.openRawResource(R.raw.userkey);
208                 byte[] buffer = new byte[4096];
209                 for (int n; (n = is.read(buffer, 0, buffer.length)) != -1;) {
210                     userKey.write(buffer, 0, n);
211                 }
212             } catch (IOException e) {
213                 Log.e(TAG, "Reading private key failed", e);
214                 return null;
215             }
216             log("Private key length: " + userKey.size() + " bytes");
217 
218             log("Setting up KeyStore");
219             try {
220                 KeyFactory keyFact = KeyFactory.getInstance("RSA");
221                 privKey = keyFact.generatePrivate(new PKCS8EncodedKeySpec(userKey.toByteArray()));
222 
223                 final CertificateFactory f = CertificateFactory.getInstance("X.509");
224                 chain[0] = f.generateCertificate(res.openRawResource(R.raw.usercert));
225                 chain[1] = f.generateCertificate(res.openRawResource(R.raw.cacert));
226             } catch (GeneralSecurityException gse) {
227                 Log.w(TAG, "Certificate generation failed", gse);
228                 return null;
229             }
230 
231             try {
232                 // Create a PKCS12 keystore populated with key + certificate chain
233                 KeyStore ks = KeyStore.getInstance("PKCS12");
234                 ks.load(null, null);
235                 ks.setKeyEntry("alias", privKey, KEYSTORE_PASSWORD, chain);
236                 mKeyStore = ks;
237                 log("KeyStore initialized");
238             } catch (Exception e) {
239                 log("KeyStore initialization failed");
240                 Log.e(TAG, "", e);
241             }
242             return null;
243         }
244     }
245 
246     private class InstallCredentialsTask extends AsyncTask<Void, Void, Void> {
247         @Override
doInBackground(Void... params)248         protected Void doInBackground(Void... params) {
249             try {
250                 Intent intent = KeyChain.createInstallIntent();
251                 intent.putExtra(KeyChain.EXTRA_NAME, TAG);
252 
253                 // Write keystore to byte array for installation
254                 ByteArrayOutputStream pkcs12 = new ByteArrayOutputStream();
255                 mKeyStore.store(pkcs12, KEYSTORE_PASSWORD);
256                 if (pkcs12.size() == 0) {
257                     throw new AssertionError("Credential archive is empty");
258                 }
259                 log("Requesting install of server's credentials");
260                 intent.putExtra(KeyChain.EXTRA_PKCS12, pkcs12.toByteArray());
261                 startActivityForResult(intent, REQUEST_CA_INSTALL);
262             } catch (Exception e) {
263                 log("Failed to install credentials: " + e);
264             }
265             return null;
266         }
267     }
268 
269     private class TestHttpsRequestTask extends AsyncTask<Void, Void, Void> {
270         @Override
doInBackground(Void... params)271         protected Void doInBackground(Void... params) {
272             try {
273                 URL url = startWebServer();
274                 makeHttpsRequest(url);
275             } catch (Exception e) {
276                 Log.e(TAG, "HTTPS request unsuccessful", e);
277                 log("Connection failed");
278                 return null;
279             }
280 
281             runOnUiThread(new Runnable() {
282                 @Override public void run() {
283                     getPassButton().setEnabled(true);
284                 }
285             });
286             return null;
287         }
288 
289         /**
290          * Create a mock web server.
291          * The server authenticates itself to the client using the key pair and certificate from the
292          * PKCS#12 keystore used in this test. Client authentication uses default trust management:
293          * the server trusts only the certificates installed in the credential storage of this
294          * user/profile.
295          */
startWebServer()296         private URL startWebServer() throws Exception {
297             log("Starting web server");
298             String kmfAlgoritm = KeyManagerFactory.getDefaultAlgorithm();
299             KeyManagerFactory kmf = KeyManagerFactory.getInstance(kmfAlgoritm);
300             kmf.init(mKeyStore, KEYSTORE_PASSWORD);
301             SSLContext serverContext = SSLContext.getInstance("TLS");
302             serverContext.init(kmf.getKeyManagers(),
303                     null /* TrustManager[] */,
304                     null /* SecureRandom */);
305             SSLSocketFactory sf = serverContext.getSocketFactory();
306             SSLSocketFactory needsClientAuth = TestSSLContext.clientAuth(sf,
307                     false /* Want client auth */,
308                     true /* Need client auth */);
309             MockWebServer server = new MockWebServer();
310             server.useHttps(needsClientAuth, false /* tunnelProxy */);
311             server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
312             server.play();
313             return server.getUrl("/");
314         }
315 
316         /**
317          * Open a new connection to the server.
318          * The client authenticates itself to the server using a private key and certificate
319          * supplied by KeyChain. Server authentication uses default trust management: the client
320          * trusts only certificates installed in the credential storage of this user/profile. This
321          * setup is expected to work because the server uses a private key whose certificate was
322          * installed earlier during this test.
323          */
makeHttpsRequest(URL url)324         private void makeHttpsRequest(URL url) throws Exception {
325             log("Making https request to " + url);
326             SSLContext clientContext = SSLContext.getInstance("TLS");
327             clientContext.init(new KeyManager[] { new KeyChainKeyManager() },
328                     null /* TrustManager[] */,
329                     null /* SecureRandom */);
330             HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
331             connection.setSSLSocketFactory(clientContext.getSocketFactory());
332             if (connection.getResponseCode() != 200) {
333                 log("Connection failed. Response code: " + connection.getResponseCode());
334                 throw new AssertionError();
335             }
336             log("Connection succeeded.");
337         }
338     }
339 
340     private class ClearCredentialsTask extends AsyncTask<Void, Void, Void> {
341         @Override
doInBackground(Void... params)342         protected Void doInBackground(Void... params) {
343             final Intent securitySettingsIntent = new Intent(Settings.ACTION_SECURITY_SETTINGS);
344             startActivity(securitySettingsIntent);
345             log("Started action: " + Settings.ACTION_SECURITY_SETTINGS);
346             log("All tests complete!");
347             return null;
348         }
349     }
350 
351     /**
352      * Key manager which synchronously prompts for its aliases via KeyChain
353      */
354     private class KeyChainKeyManager extends X509ExtendedKeyManager {
355         @Override
chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket)356         public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) {
357             log("KeyChainKeyManager chooseClientAlias");
358             KeyChainAliasCallback aliasCallback = Mockito.mock(KeyChainAliasCallback.class);
359             KeyChain.choosePrivateKeyAlias(KeyChainTest.this, aliasCallback,
360                                            keyTypes, issuers,
361                                            socket.getInetAddress().getHostName(), socket.getPort(),
362                                            null);
363 
364             ArgumentCaptor<String> aliasCaptor = ArgumentCaptor.forClass(String.class);
365             Mockito.verify(aliasCallback, Mockito.timeout((int) KEYCHAIN_ALIAS_TIMEOUT_MS))
366                     .alias(aliasCaptor.capture());
367 
368             log("Certificate alias: \"" + aliasCaptor.getValue() + "\"");
369             return aliasCaptor.getValue();
370         }
371 
372         @Override
chooseServerAlias(String keyType, Principal[] issuers, Socket socket)373         public String chooseServerAlias(String keyType,
374                                                   Principal[] issuers,
375                                                   Socket socket) {
376             // Not a client SSLSocket callback
377             throw new UnsupportedOperationException();
378         }
379 
380         @Override
getCertificateChain(String alias)381         public X509Certificate[] getCertificateChain(String alias) {
382             try {
383                 log("KeyChainKeyManager getCertificateChain");
384                 X509Certificate[] certificateChain =
385                         KeyChain.getCertificateChain(KeyChainTest.this, alias);
386                 if (certificateChain == null) {
387                     log("Null certificate chain!");
388                     return null;
389                 }
390                 log("Returned " + certificateChain.length + " certificates in chain");
391                 for (int i = 0; i < certificateChain.length; i++) {
392                     Log.d(TAG, "certificate[" + i + "]=" + certificateChain[i]);
393                 }
394                 return certificateChain;
395             } catch (InterruptedException e) {
396                 Thread.currentThread().interrupt();
397                 return null;
398             } catch (KeyChainException e) {
399                 throw new RuntimeException(e);
400             }
401         }
402 
403         @Override
getClientAliases(String keyType, Principal[] issuers)404         public String[] getClientAliases(String keyType, Principal[] issuers) {
405             // not a client SSLSocket callback
406             throw new UnsupportedOperationException();
407         }
408 
409         @Override
getServerAliases(String keyType, Principal[] issuers)410         public String[] getServerAliases(String keyType, Principal[] issuers) {
411             // not a client SSLSocket callback
412             throw new UnsupportedOperationException();
413         }
414 
415         @Override
getPrivateKey(String alias)416         public PrivateKey getPrivateKey(String alias) {
417             try {
418                 log("KeyChainKeyManager.getPrivateKey(\"" + alias + "\")");
419                 PrivateKey privateKey = KeyChain.getPrivateKey(KeyChainTest.this, alias);
420                 Log.d(TAG, "privateKey = " + privateKey);
421                 return privateKey;
422             } catch (InterruptedException e) {
423                 Thread.currentThread().interrupt();
424                 return null;
425             } catch (KeyChainException e) {
426                 throw new RuntimeException(e);
427             }
428         }
429     }
430 
431     /**
432      * Write a message to the log, also to a visible TextView if available.
433      */
log(final String message)434     private void log(final String message) {
435         Log.d(TAG, message);
436         if (mLogView != null) {
437             runOnUiThread(new Runnable() {
438                 @Override public void run() {
439                     mLogView.append(message + "\n");
440                 }
441             });
442         }
443     }
444 
445     /**
446      * Utility class to store one step per object.
447      */
448     private static class Step {
449         // Instruction message to show before running
450         int instructionTextId;
451 
452         // Whether to allow a 'skip' button for this step
453         boolean skippable;
454 
455         // Set of commands to run when 'next' is pressed
456         AsyncTask<Void, Void, Void> task;
457 
Step(int instructionTextId, boolean skippable, AsyncTask<Void, Void, Void> task)458         public Step(int instructionTextId, boolean skippable, AsyncTask<Void, Void, Void> task) {
459             this.instructionTextId = instructionTextId;
460             this.skippable = skippable;
461             this.task = task;
462         }
463     }
464 }
465