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