1 /* 2 * Copyright (C) 2019 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.content.pm.PackageManager; 20 import android.icu.util.Calendar; 21 import android.os.Bundle; 22 import android.security.ConfirmationAlreadyPresentingException; 23 import android.security.ConfirmationCallback; 24 import android.security.ConfirmationNotAvailableException; 25 import android.security.ConfirmationPrompt; 26 import android.security.keystore.KeyGenParameterSpec; 27 import android.security.keystore.KeyProperties; 28 import android.view.View; 29 import android.view.View.OnClickListener; 30 import android.widget.Button; 31 import android.widget.Toast; 32 33 import com.android.cts.verifier.PassFailButtons; 34 import com.android.cts.verifier.R; 35 36 import java.io.IOException; 37 import java.security.InvalidAlgorithmParameterException; 38 import java.security.InvalidKeyException; 39 import java.security.KeyPairGenerator; 40 import java.security.KeyStore; 41 import java.security.KeyStoreException; 42 import java.security.NoSuchAlgorithmException; 43 import java.security.NoSuchProviderException; 44 import java.security.Signature; 45 import java.security.SignatureException; 46 import java.security.UnrecoverableEntryException; 47 import java.security.cert.CertificateException; 48 import java.util.Date; 49 50 public class ProtectedConfirmationTest extends PassFailButtons.Activity { 51 52 /** 53 * Alias for our key in the Android Key Store. 54 */ 55 private static final String KEY_NAME = "my_confirmation_key"; 56 private boolean teeTestSuccess = false; 57 private boolean strongboxTestSuccess = false; 58 private boolean mTeeNegativeTestSuccess = false; 59 private boolean mStrongboxNegativeTestSuccess = false; 60 61 @Override onCreate(Bundle savedInstanceState)62 protected void onCreate(Bundle savedInstanceState) { 63 super.onCreate(savedInstanceState); 64 setContentView(R.layout.sec_protected_confirmation_main); 65 setPassFailButtonClickListeners(); 66 67 boolean protectedConfirmationSupported = 68 ConfirmationPrompt.isSupported(getApplicationContext()); 69 if (protectedConfirmationSupported) { 70 setInfoResources(R.string.sec_protected_confirmation_test, 71 R.string.sec_protected_confirmation_test_info, -1); 72 getPassButton().setEnabled(false); 73 } else { 74 setInfoResources(R.string.sec_protected_confirmation_not_supported_title, 75 R.string.sec_protected_confirmation_not_supported_info, -1); 76 getPassButton().setEnabled(true); 77 return; 78 } 79 boolean hasStrongbox = this.getPackageManager() 80 .hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE); 81 82 findViewById(R.id.sec_protected_confirmation_tee_test_success) 83 .setVisibility(View.INVISIBLE); 84 findViewById(R.id.sec_protected_confirmation_strongbox_test_success) 85 .setVisibility(View.INVISIBLE); 86 findViewById(R.id.sec_protected_confirmation_tee_negative_test_success) 87 .setVisibility(View.INVISIBLE); 88 findViewById(R.id.sec_protected_confirmation_strongbox_negative_test_success) 89 .setVisibility(View.INVISIBLE); 90 Button startTestButton = (Button) findViewById(R.id.sec_start_test_button); 91 startTestButton.setOnClickListener(new OnClickListener() { 92 @Override 93 public void onClick(View v) { 94 showToast("TEE positive Test running..."); 95 v.post(new Runnable() { 96 @Override 97 public void run() { 98 runTest(false /* useStrongbox */, true /* positiveScenario */); 99 } 100 }); 101 } 102 103 }); 104 105 Button startStrongboxTestButton = 106 (Button) findViewById(R.id.sec_start_test_strongbox_button); 107 if (hasStrongbox) { 108 startStrongboxTestButton.setOnClickListener(new OnClickListener() { 109 @Override 110 public void onClick(View v) { 111 showToast("Strongbox Positive Test running..."); 112 v.post(new Runnable() { 113 @Override 114 public void run() { 115 runTest(true /* useStrongbox */, true /* positiveScenario */); 116 } 117 }); 118 } 119 120 }); 121 } else { 122 startStrongboxTestButton.setVisibility(View.GONE); 123 // since strongbox is unavailable we mark the strongbox test as passed so that the tee 124 // test alone can make the test pass. 125 strongboxTestSuccess = true; 126 } 127 128 Button startNegativeTestButton = 129 (Button) findViewById(R.id.sec_start_tee_negative_test_button); 130 startNegativeTestButton.setOnClickListener(new OnClickListener() { 131 @Override 132 public void onClick(View v) { 133 showToast("TEE negative Test running..."); 134 v.post(new Runnable() { 135 @Override 136 public void run() { 137 runTest(false /* useStrongbox */, false /* positiveScenario */); 138 } 139 }); 140 } 141 142 }); 143 144 Button startStrongboxNegativeTestButton = 145 (Button) findViewById(R.id.sec_start_test_strongbox_negative_button); 146 if (hasStrongbox) { 147 startStrongboxNegativeTestButton.setOnClickListener(new OnClickListener() { 148 @Override 149 public void onClick(View v) { 150 showToast("Strongbox negative Test running..."); 151 v.post(new Runnable() { 152 @Override 153 public void run() { 154 runTest(true /* useStrongbox */, false /* positiveScenario */); 155 } 156 }); 157 } 158 159 }); 160 } else { 161 startStrongboxTestButton.setVisibility(View.GONE); 162 // since strongbox is unavailable we mark the strongbox test as passed so that the tee 163 // test alone can make the test pass. 164 mStrongboxNegativeTestSuccess = true; 165 } 166 } 167 168 /** 169 * Creates an asymmetric signing key in AndroidKeyStore which can only be used for signing 170 * user confirmed messages. 171 */ createKey(boolean useStrongbox)172 private void createKey(boolean useStrongbox) { 173 Calendar calendar = Calendar.getInstance(); 174 Date validityStart = calendar.getTime(); 175 calendar.add(Calendar.YEAR, 1); 176 Date validityEnd = calendar.getTime(); 177 try { 178 KeyPairGenerator kpg = KeyPairGenerator.getInstance( 179 KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore"); 180 KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder( 181 KEY_NAME, 182 KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY); 183 builder.setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512); 184 builder.setAttestationChallenge("CtsVerifierTest".getBytes()); 185 builder.setUserConfirmationRequired(true); 186 builder.setIsStrongBoxBacked(useStrongbox); 187 builder.setKeyValidityStart(validityStart); 188 builder.setKeyValidityEnd(validityEnd); 189 kpg.initialize(builder.build()); 190 kpg.generateKeyPair(); 191 } catch (NoSuchAlgorithmException | NoSuchProviderException | 192 InvalidAlgorithmParameterException e) { 193 throw new RuntimeException("Failed to create confirmation key", e); 194 } 195 } 196 trySign(byte[] dataThatWasConfirmed, boolean positiveScenario)197 private boolean trySign(byte[] dataThatWasConfirmed, boolean positiveScenario) { 198 try { 199 KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); 200 keyStore.load(null); 201 KeyStore.Entry key = keyStore.getEntry(KEY_NAME, null); 202 Signature s = Signature.getInstance("SHA256withECDSA"); 203 s.initSign(((KeyStore.PrivateKeyEntry) key).getPrivateKey()); 204 if (!positiveScenario && dataThatWasConfirmed != null 205 && dataThatWasConfirmed.length > 0) { 206 // The data received in callback as confirmed data has prompt text and extra data 207 // included. So even using same prompt text for signing could be considered as 208 // corrupted data. 209 dataThatWasConfirmed[0] = (byte) ~dataThatWasConfirmed[0]; 210 } 211 s.update(dataThatWasConfirmed); 212 s.sign(); 213 } catch (CertificateException | KeyStoreException | IOException | NoSuchAlgorithmException | 214 UnrecoverableEntryException | InvalidKeyException e) { 215 throw new RuntimeException("Failed to load confirmation key", e); 216 } catch (SignatureException e) { 217 return !positiveScenario; 218 } 219 return positiveScenario; 220 } 221 runTest(boolean useStrongbox, boolean positiveScenario)222 private void runTest(boolean useStrongbox, boolean positiveScenario) { 223 createKey(useStrongbox); 224 if (trySign(getString(R.string.sec_protected_confirmation_message) 225 .getBytes(), true /* positiveScenario */)) { 226 showToast("Test failed. Key could sign without confirmation."); 227 } else { 228 showConfirmationPrompt( 229 getString(R.string.sec_protected_confirmation_message), 230 useStrongbox, positiveScenario); 231 } 232 } 233 showConfirmationPrompt(String confirmationMessage, boolean useStrongbox, boolean positiveScenario)234 private void showConfirmationPrompt(String confirmationMessage, boolean useStrongbox, 235 boolean positiveScenario) { 236 ConfirmationPrompt.Builder builder = new ConfirmationPrompt.Builder(this); 237 builder.setPromptText(confirmationMessage); 238 builder.setExtraData(new byte[]{0x1, 0x02, 0x03}); 239 ConfirmationPrompt prompt = builder.build(); 240 try { 241 prompt.presentPrompt(getMainExecutor(), 242 new ConfirmationCallback() { 243 @Override 244 public void onConfirmed(byte[] dataThatWasConfirmed) { 245 super.onConfirmed(dataThatWasConfirmed); 246 if (trySign(dataThatWasConfirmed, positiveScenario)) { 247 markTestSuccess(useStrongbox, positiveScenario); 248 } else { 249 if (positiveScenario) { 250 showToast("Failed to sign confirmed message"); 251 } else { 252 showToast("Failed! Corrupted data should not be signed."); 253 } 254 } 255 } 256 257 @Override 258 public void onDismissed() { 259 super.onDismissed(); 260 showToast("User dismissed the dialog."); 261 } 262 263 @Override 264 public void onCanceled() { 265 super.onCanceled(); 266 showToast("Confirmation dialog was canceled."); 267 } 268 269 @Override 270 public void onError(Throwable e) { 271 super.onError(e); 272 throw new RuntimeException("Confirmation Callback encountered an error", 273 e); 274 } 275 }); 276 } catch (ConfirmationAlreadyPresentingException | ConfirmationNotAvailableException e) { 277 throw new RuntimeException("Error trying to present the confirmation prompt", e); 278 } 279 } 280 showToast(String message)281 private void showToast(String message) { 282 Toast.makeText(this, message, Toast.LENGTH_LONG) 283 .show(); 284 } 285 markTestSuccess(boolean strongbox, boolean positiveScenario)286 private void markTestSuccess(boolean strongbox, boolean positiveScenario) { 287 if (strongbox) { 288 if (positiveScenario && !strongboxTestSuccess) { 289 findViewById(R.id.sec_protected_confirmation_strongbox_test_success) 290 .setVisibility(View.VISIBLE); 291 strongboxTestSuccess = true; 292 } else if (!mStrongboxNegativeTestSuccess) { 293 findViewById(R.id.sec_protected_confirmation_strongbox_negative_test_success) 294 .setVisibility(View.VISIBLE); 295 mStrongboxNegativeTestSuccess = true; 296 } 297 } else { 298 if (positiveScenario && !teeTestSuccess) { 299 findViewById(R.id.sec_protected_confirmation_tee_test_success) 300 .setVisibility(View.VISIBLE); 301 teeTestSuccess = true; 302 } else if (!mTeeNegativeTestSuccess) { 303 findViewById(R.id.sec_protected_confirmation_tee_negative_test_success) 304 .setVisibility(View.VISIBLE); 305 mTeeNegativeTestSuccess = true; 306 } 307 } 308 if (strongboxTestSuccess && teeTestSuccess && mStrongboxNegativeTestSuccess 309 && mTeeNegativeTestSuccess) { 310 showToast("Test passed."); 311 getPassButton().setEnabled(true); 312 } 313 } 314 } 315 316