/*
* Copyright (C) 2018 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.managedprovisioning;
import static android.keystore.cts.CertificateUtils.createCertificate;
import android.app.admin.DevicePolicyManager;
import android.os.AsyncTask;
import android.os.Bundle;
import android.security.AttestedKeyPair;
import android.security.KeyChain;
import android.security.KeyChainAliasCallback;
import android.security.KeyChainException;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.util.Log;
import android.view.View;
import com.android.cts.verifier.R;
import java.security.GeneralSecurityException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import javax.security.auth.x500.X500Principal;
/**
* Activity to test KeyChain key generation. The following flows are tested: * Generating a key. *
* Installing a (self-signed) certificate associated with the key, visible to users. * Setting
* visibility of the certificate to not be visible to user.
*
*
After the key generation and certificate installation, it should be possible for a user to
* select the key from the certificate selection prompt when {@code KeyChain.choosePrivateKeyAlias}
* is called. The test then tests that the key is indeed usable for signing.
*
*
After the visibility is set to not-user-visible, the prompt is shown again, this time the
* testes is asked to verify no keys are selectable and cancel the dialog.
*/
public class KeyChainTestActivity extends ByodTestActivityWithPrepare {
private static final String TAG = "ByodKeyChainActivity";
public static final String ACTION_KEYCHAIN =
"com.android.cts.verifier.managedprovisioning.KEYCHAIN";
public static final String ALIAS = "cts-verifier-gen-rsa-1";
public static final String KEY_ALGORITHM = "RSA";
private DevicePolicyManager mDevicePolicyManager;
private AttestedKeyPair mAttestedKeyPair;
private X509Certificate mCert;
// Callback interface for when a key is generated.
static interface KeyGenerationListener {
void onKeyPairGenerated(AttestedKeyPair keyPair);
}
// Task for generating a key pair using {@code DevicePolicyManager.generateKeyPair}.
// The listener, if provided, will be invoked after the key has been generated successfully.
class GenerateKeyTask extends AsyncTask {
KeyGenerationListener mListener;
public GenerateKeyTask(KeyGenerationListener listener) {
mListener = listener;
}
@Override
protected AttestedKeyPair doInBackground(KeyGenParameterSpec... specs) {
Log.i(TAG, "Generating key pair.");
try {
AttestedKeyPair kp =
mDevicePolicyManager.generateKeyPair(
DeviceAdminTestReceiver.getReceiverComponentName(),
KEY_ALGORITHM,
specs[0],
0);
if (kp != null) {
getLogView().setText("Key generated successfully.");
} else {
getLogView().setText("Failed generating key.");
}
return kp;
} catch (SecurityException e) {
getLogView().setText("Security exception while generating key.");
Log.w(TAG, "Security exception", e);
}
return null;
}
@Override
protected void onPostExecute(AttestedKeyPair kp) {
super.onPostExecute(kp);
if (mListener != null && kp != null) {
mListener.onKeyPairGenerated(kp);
}
}
}
// Helper for generating and installing a self-signed certificate.
class CertificateInstaller implements KeyGenerationListener {
@Override
public void onKeyPairGenerated(AttestedKeyPair keyPair) {
mAttestedKeyPair = keyPair;
X500Principal issuer = new X500Principal("CN=SelfSigned, O=Android, C=US");
X500Principal subject = new X500Principal("CN=Subject, O=Android, C=US");
try {
mCert = createCertificate(mAttestedKeyPair.getKeyPair(), subject, issuer);
boolean installResult = installCertificate(mCert, true);
// called from onPostExecute so safe to interact with the UI here.
if (installResult) {
getLogView().setText("Test ready");
getInstructionsView().setText(
R.string.provisioning_byod_keychain_info_first_test);
getGoButton().setEnabled(true);
} else {
getLogView().setText("FAILED certificate installation.");
}
} catch (Exception e) {
Log.w(TAG, "Failed installing certificate", e);
getLogView().setText("Error generating a certificate.");
}
}
}
// Helper for calling {@code DevicePolicyManager.setKeyPairCertificate} with the user-visibility
// specified in the constructor. Returns true if the call was successful (and no exceptions
// were thrown).
protected boolean installCertificate(X509Certificate cert, boolean isUserVisible) {
try {
return mDevicePolicyManager.setKeyPairCertificate(
DeviceAdminTestReceiver.getReceiverComponentName(),
ALIAS,
Arrays.asList(new X509Certificate[] {cert}),
isUserVisible);
} catch (SecurityException e) {
logStatus("Security exception while installing cert.");
Log.w(TAG, "Security exception", e);
}
return false;
}
// Invokes choosePrivateKeyAlias.
void selectCertificate(KeyChainAliasCallback callback) {
String[] keyTypes = new String[] {KEY_ALGORITHM};
Principal[] issuers = new Principal[0];
KeyChain.choosePrivateKeyAlias(
KeyChainTestActivity.this, callback, keyTypes, issuers, null, null);
}
class TestPreparator implements View.OnClickListener {
@Override
public void onClick(View v) {
getLogView().setText("Starting key generation");
KeyGenParameterSpec spec =
new KeyGenParameterSpec.Builder(
ALIAS,
KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY)
.setKeySize(2048)
.setDigests(KeyProperties.DIGEST_SHA256)
.setSignaturePaddings(
KeyProperties.SIGNATURE_PADDING_RSA_PSS,
KeyProperties.SIGNATURE_PADDING_RSA_PKCS1)
.build();
new GenerateKeyTask(new CertificateInstaller()).execute(spec);
}
}
class SelectCertificate implements View.OnClickListener, KeyChainAliasCallback {
@Override
public void onClick(View v) {
Log.i(TAG, "Selecting certificate");
getLogView().setText("Waiting for prompt");
selectCertificate(this);
}
@Override
public void alias(String alias) {
Log.i(TAG, "Got alias: " + alias);
if (alias == null) {
logStatus("FAILED (no alias)");
return;
} else if (!alias.equals(ALIAS)) {
logStatus("FAILED (wrong alias)");
return;
}
logStatus("Got right alias.");
try {
PrivateKey privateKey = KeyChain.getPrivateKey(KeyChainTestActivity.this, alias);
if (privateKey == null) {
logStatus("FAILED (key unavailable)");
return;
}
byte[] data = new String("hello").getBytes();
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(privateKey);
sign.update(data);
if (sign.sign() != null) {
prepareSecondTest();
} else {
logStatus("FAILED (cannot sign)");
}
} catch (GeneralSecurityException | KeyChainException | InterruptedException e) {
Log.w(TAG, "Failed using the key", e);
logStatus("FAILED (key unusable)");
}
}
}
class SelectCertificateExpectingNone implements View.OnClickListener, KeyChainAliasCallback {
@Override
public void onClick(View v) {
Log.i(TAG, "Selecting certificate");
getLogView().setText("Prompt should not appear.");
selectCertificate(this);
}
@Override
public void alias(String alias) {
Log.i(TAG, "Got alias: " + alias);
if (alias != null) {
logStatus("FAILED: Should have no certificate.");
} else {
logStatus("PASSED (2/2)");
runOnUiThread(
() -> {
getPassButton().setEnabled(true);
});
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mDevicePolicyManager = getSystemService(DevicePolicyManager.class);
getInstructionsView().setText(R.string.provisioning_byod_keychain_info_start);
getPrepareButton().setOnClickListener(new TestPreparator());
getGoButton().setOnClickListener(new SelectCertificate());
}
protected void prepareSecondTest() {
Runnable uiChanges;
if (installCertificate(mCert, false)) {
uiChanges =
() -> {
getLogView().setText("Second test ready.");
getInstructionsView().setText(
R.string.provisioning_byod_keychain_info_second_test);
getGoButton().setText("Run 2nd test");
getGoButton().setOnClickListener(new SelectCertificateExpectingNone());
};
} else {
uiChanges =
() -> {
getLogView().setText("FAILED second test setup.");
getGoButton().setEnabled(false);
};
}
runOnUiThread(uiChanges);
}
@Override
public void finish() {
super.finish();
try {
mDevicePolicyManager.removeKeyPair(
DeviceAdminTestReceiver.getReceiverComponentName(), ALIAS);
Log.i(TAG, "Deleted alias " + ALIAS);
} catch (SecurityException e) {
Log.w(TAG, "Failed deleting alias", e);
}
}
private void logStatus(String status) {
runOnUiThread(
() -> {
getLogView().setText(status);
});
}
}