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