blob: ebdb4c079da67799a52483e17393263d5a047f64 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.cts.verifier.security;
import android.app.Activity;
import android.content.Intent;
import android.content.res.Resources;
import android.os.AsyncTask;
import android.os.Bundle;
import android.provider.Settings;
import android.security.KeyChain;
import android.security.KeyChainAliasCallback;
import android.security.KeyChainException;
import android.text.method.ScrollingMovementMethod;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import com.android.cts.verifier.PassFailButtons;
import com.android.cts.verifier.R;
import com.google.mockwebserver.MockResponse;
import com.google.mockwebserver.MockWebServer;
import java.io.InputStream;
import java.io.IOException;
import java.io.ByteArrayOutputStream;
import java.net.Socket;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyStore;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
import java.util.List;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.X509ExtendedKeyManager;
import libcore.java.security.TestKeyStore;
import libcore.javax.net.ssl.TestSSLContext;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
/**
* Simple activity based test that exercises the KeyChain API
*/
public class KeyChainTest extends PassFailButtons.Activity implements View.OnClickListener {
private static final String TAG = "KeyChainTest";
private static final int REQUEST_CA_INSTALL = 1;
private TextView mInstructionView;
private TextView mLogView;
private Button mResetButton;
private Button mSkipButton;
private Button mNextButton;
private List<Step> mSteps;
int mCurrentStep;
private KeyStore mKeyStore;
private static final char[] KEYSTORE_PASSWORD = "".toCharArray();
// How long to wait before giving up on the user selecting a key alias.
private static final int KEYCHAIN_ALIAS_TIMEOUT_MS = (int) TimeUnit.MINUTES.toMillis(5L);
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
View root = getLayoutInflater().inflate(R.layout.keychain_main, null);
setContentView(root);
setInfoResources(R.string.keychain_test, R.string.keychain_info, -1);
setPassFailButtonClickListeners();
mInstructionView = (TextView) root.findViewById(R.id.test_instruction);
mLogView = (TextView) root.findViewById(R.id.test_log);
mLogView.setMovementMethod(new ScrollingMovementMethod());
mNextButton = (Button) root.findViewById(R.id.action_next);
mNextButton.setOnClickListener(this);
mResetButton = (Button) root.findViewById(R.id.action_reset);
mResetButton.setOnClickListener(this);
mSkipButton = (Button) root.findViewById(R.id.action_skip);
mSkipButton.setOnClickListener(this);
resetProgress();
}
@Override
public void onClick(View v) {
Step step = mSteps.get(mCurrentStep);
if (v == mNextButton) {
switch (step.task.getStatus()) {
case PENDING: {
step.task.execute();
break;
}
case FINISHED: {
if (mCurrentStep + 1 < mSteps.size()) {
mCurrentStep += 1;
updateUi();
} else {
mSkipButton.setVisibility(View.INVISIBLE);
mNextButton.setVisibility(View.INVISIBLE);
}
break;
}
}
} else if (v == mSkipButton) {
step.task.cancel(false);
mCurrentStep += 1;
updateUi();
} else if (v == mResetButton) {
resetProgress();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case REQUEST_CA_INSTALL: {
if (resultCode == RESULT_OK) {
log("CA Certificate installed successfully");
} else {
log("REQUEST_CA_INSTALL failed with result code: " + resultCode);
}
break;
}
default:
throw new IllegalStateException("requestCode == " + requestCode);
}
}
private void resetProgress() {
getPassButton().setEnabled(false);
mLogView.setText("");
mSteps = new ArrayList<>();
mSteps.add(new Step(R.string.keychain_setup_desc, false, new SetupTestKeyStoreTask()));
mSteps.add(new Step(R.string.keychain_install_desc, true, new InstallCredentialsTask()));
mSteps.add(new Step(R.string.keychain_https_desc, false, new TestHttpsRequestTask()));
mSteps.add(new Step(R.string.keychain_reset_desc, true, new ClearCredentialsTask()));
mCurrentStep = 0;
updateUi();
}
private void updateUi() {
mLogView.setText("");
if (mCurrentStep >= mSteps.size()) {
mSkipButton.setVisibility(View.INVISIBLE);
mNextButton.setVisibility(View.INVISIBLE);
getPassButton().setEnabled(true);
return;
}
final Step step = mSteps.get(mCurrentStep);
if (step.task.getStatus() == AsyncTask.Status.PENDING) {
mInstructionView.setText(step.instructionTextId);
}
mSkipButton.setVisibility(step.skippable ? View.VISIBLE : View.INVISIBLE);
mNextButton.setVisibility(View.VISIBLE);
}
private class SetupTestKeyStoreTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
final Certificate[] chain = new Certificate[2];
final Key privKey;
log("Reading resources");
Resources res = getResources();
ByteArrayOutputStream userKey = new ByteArrayOutputStream();
try {
InputStream is = res.openRawResource(R.raw.userkey);
byte[] buffer = new byte[4096];
for (int n; (n = is.read(buffer, 0, buffer.length)) != -1;) {
userKey.write(buffer, 0, n);
}
} catch (IOException e) {
Log.e(TAG, "Reading private key failed", e);
return null;
}
log("Private key length: " + userKey.size() + " bytes");
log("Setting up KeyStore");
try {
KeyFactory keyFact = KeyFactory.getInstance("RSA");
privKey = keyFact.generatePrivate(new PKCS8EncodedKeySpec(userKey.toByteArray()));
final CertificateFactory f = CertificateFactory.getInstance("X.509");
chain[0] = f.generateCertificate(res.openRawResource(R.raw.usercert));
chain[1] = f.generateCertificate(res.openRawResource(R.raw.cacert));
} catch (GeneralSecurityException gse) {
Log.w(TAG, "Certificate generation failed", gse);
return null;
}
try {
// Create a PKCS12 keystore populated with key + certificate chain
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(null, null);
ks.setKeyEntry("alias", privKey, KEYSTORE_PASSWORD, chain);
mKeyStore = ks;
log("KeyStore initialized");
} catch (Exception e) {
log("KeyStore initialization failed");
Log.e(TAG, "", e);
}
return null;
}
}
private class InstallCredentialsTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
try {
Intent intent = KeyChain.createInstallIntent();
intent.putExtra(KeyChain.EXTRA_NAME, TAG);
// Write keystore to byte array for installation
ByteArrayOutputStream pkcs12 = new ByteArrayOutputStream();
mKeyStore.store(pkcs12, KEYSTORE_PASSWORD);
if (pkcs12.size() == 0) {
throw new AssertionError("Credential archive is empty");
}
log("Requesting install of server's credentials");
intent.putExtra(KeyChain.EXTRA_PKCS12, pkcs12.toByteArray());
startActivityForResult(intent, REQUEST_CA_INSTALL);
} catch (Exception e) {
log("Failed to install credentials: " + e);
}
return null;
}
}
private class TestHttpsRequestTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
try {
URL url = startWebServer();
makeHttpsRequest(url);
} catch (Exception e) {
Log.e(TAG, "HTTPS request unsuccessful", e);
log("Connection failed");
return null;
}
runOnUiThread(new Runnable() {
@Override public void run() {
getPassButton().setEnabled(true);
}
});
return null;
}
/**
* Create a mock web server.
* The server authenticates itself to the client using the key pair and certificate from the
* PKCS#12 keystore used in this test. Client authentication uses default trust management:
* the server trusts only the certificates installed in the credential storage of this
* user/profile.
*/
private URL startWebServer() throws Exception {
log("Starting web server");
String kmfAlgoritm = KeyManagerFactory.getDefaultAlgorithm();
KeyManagerFactory kmf = KeyManagerFactory.getInstance(kmfAlgoritm);
kmf.init(mKeyStore, KEYSTORE_PASSWORD);
SSLContext serverContext = SSLContext.getInstance("TLS");
serverContext.init(kmf.getKeyManagers(),
null /* TrustManager[] */,
null /* SecureRandom */);
SSLSocketFactory sf = serverContext.getSocketFactory();
SSLSocketFactory needsClientAuth = TestSSLContext.clientAuth(sf,
false /* Want client auth */,
true /* Need client auth */);
MockWebServer server = new MockWebServer();
server.useHttps(needsClientAuth, false /* tunnelProxy */);
server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
server.play();
return server.getUrl("/");
}
/**
* Open a new connection to the server.
* The client authenticates itself to the server using a private key and certificate
* supplied by KeyChain. Server authentication uses default trust management: the client
* trusts only certificates installed in the credential storage of this user/profile. This
* setup is expected to work because the server uses a private key whose certificate was
* installed earlier during this test.
*/
private void makeHttpsRequest(URL url) throws Exception {
log("Making https request to " + url);
SSLContext clientContext = SSLContext.getInstance("TLS");
clientContext.init(new KeyManager[] { new KeyChainKeyManager() },
null /* TrustManager[] */,
null /* SecureRandom */);
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setSSLSocketFactory(clientContext.getSocketFactory());
if (connection.getResponseCode() != 200) {
log("Connection failed. Response code: " + connection.getResponseCode());
throw new AssertionError();
}
log("Connection succeeded.");
}
}
private class ClearCredentialsTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
final Intent securitySettingsIntent = new Intent(Settings.ACTION_SECURITY_SETTINGS);
startActivity(securitySettingsIntent);
log("Started action: " + Settings.ACTION_SECURITY_SETTINGS);
log("All tests complete!");
return null;
}
}
/**
* Key manager which synchronously prompts for its aliases via KeyChain
*/
private class KeyChainKeyManager extends X509ExtendedKeyManager {
@Override
public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) {
log("KeyChainKeyManager chooseClientAlias");
KeyChainAliasCallback aliasCallback = Mockito.mock(KeyChainAliasCallback.class);
KeyChain.choosePrivateKeyAlias(KeyChainTest.this, aliasCallback,
keyTypes, issuers,
socket.getInetAddress().getHostName(), socket.getPort(),
null);
ArgumentCaptor<String> aliasCaptor = ArgumentCaptor.forClass(String.class);
Mockito.verify(aliasCallback, Mockito.timeout((int) KEYCHAIN_ALIAS_TIMEOUT_MS))
.alias(aliasCaptor.capture());
log("Certificate alias: \"" + aliasCaptor.getValue() + "\"");
return aliasCaptor.getValue();
}
@Override
public String chooseServerAlias(String keyType,
Principal[] issuers,
Socket socket) {
// Not a client SSLSocket callback
throw new UnsupportedOperationException();
}
@Override
public X509Certificate[] getCertificateChain(String alias) {
try {
log("KeyChainKeyManager getCertificateChain");
X509Certificate[] certificateChain =
KeyChain.getCertificateChain(KeyChainTest.this, alias);
if (certificateChain == null) {
log("Null certificate chain!");
return null;
}
log("Returned " + certificateChain.length + " certificates in chain");
for (int i = 0; i < certificateChain.length; i++) {
Log.d(TAG, "certificate[" + i + "]=" + certificateChain[i]);
}
return certificateChain;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
} catch (KeyChainException e) {
throw new RuntimeException(e);
}
}
@Override
public String[] getClientAliases(String keyType, Principal[] issuers) {
// not a client SSLSocket callback
throw new UnsupportedOperationException();
}
@Override
public String[] getServerAliases(String keyType, Principal[] issuers) {
// not a client SSLSocket callback
throw new UnsupportedOperationException();
}
@Override
public PrivateKey getPrivateKey(String alias) {
try {
log("KeyChainKeyManager.getPrivateKey(\"" + alias + "\")");
PrivateKey privateKey = KeyChain.getPrivateKey(KeyChainTest.this, alias);
Log.d(TAG, "privateKey = " + privateKey);
return privateKey;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
} catch (KeyChainException e) {
throw new RuntimeException(e);
}
}
}
/**
* Write a message to the log, also to a visible TextView if available.
*/
private void log(final String message) {
Log.d(TAG, message);
if (mLogView != null) {
runOnUiThread(new Runnable() {
@Override public void run() {
mLogView.append(message + "\n");
}
});
}
}
/**
* Utility class to store one step per object.
*/
private static class Step {
// Instruction message to show before running
int instructionTextId;
// Whether to allow a 'skip' button for this step
boolean skippable;
// Set of commands to run when 'next' is pressed
AsyncTask<Void, Void, Void> task;
public Step(int instructionTextId, boolean skippable, AsyncTask<Void, Void, Void> task) {
this.instructionTextId = instructionTextId;
this.skippable = skippable;
this.task = task;
}
}
}