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