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