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