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 androidx.annotation.NonNull; 20 import androidx.annotation.Nullable; 21 22 import android.Manifest; 23 import android.app.AlertDialog; 24 import android.app.Dialog; 25 import android.app.DialogFragment; 26 import android.app.KeyguardManager; 27 import android.content.Context; 28 import android.content.DialogInterface; 29 import android.content.pm.PackageManager; 30 import android.hardware.fingerprint.FingerprintManager; 31 import android.os.Bundle; 32 import android.os.CancellationSignal; 33 import android.security.keystore.KeyGenParameterSpec; 34 import android.security.keystore.KeyPermanentlyInvalidatedException; 35 import android.security.keystore.KeyProperties; 36 import android.security.keystore.UserNotAuthenticatedException; 37 import android.util.Log; 38 import android.view.View; 39 import android.view.View.OnClickListener; 40 import android.widget.Button; 41 import android.widget.Toast; 42 43 import android.security.identity.IdentityCredentialStore; 44 import android.security.identity.WritableIdentityCredential; 45 import android.security.identity.IdentityCredential; 46 import android.security.identity.AccessControlProfile; 47 import android.security.identity.AccessControlProfileId; 48 import android.security.identity.ResultData; 49 import android.security.identity.PersonalizationData; 50 import java.util.Arrays; 51 import java.util.Collection; 52 import java.util.LinkedList; 53 import java.util.LinkedHashMap; 54 import java.util.Map; 55 import java.io.ByteArrayOutputStream; 56 import co.nstant.in.cbor.CborBuilder; 57 import co.nstant.in.cbor.CborEncoder; 58 import co.nstant.in.cbor.CborException; 59 import co.nstant.in.cbor.builder.MapBuilder; 60 import java.security.cert.X509Certificate; 61 import android.hardware.biometrics.BiometricPrompt.CryptoObject; 62 import android.hardware.biometrics.BiometricPrompt; 63 64 import com.android.cts.verifier.PassFailButtons; 65 import com.android.cts.verifier.R; 66 67 import java.io.IOException; 68 import java.security.InvalidAlgorithmParameterException; 69 import java.security.InvalidKeyException; 70 import java.security.KeyStore; 71 import java.security.KeyStoreException; 72 import java.security.NoSuchAlgorithmException; 73 import java.security.NoSuchProviderException; 74 import java.security.UnrecoverableKeyException; 75 import java.security.cert.CertificateException; 76 77 import javax.crypto.BadPaddingException; 78 import javax.crypto.Cipher; 79 import javax.crypto.IllegalBlockSizeException; 80 import javax.crypto.KeyGenerator; 81 import javax.crypto.NoSuchPaddingException; 82 import javax.crypto.SecretKey; 83 84 public class IdentityCredentialAuthentication extends PassFailButtons.Activity { 85 private static final boolean DEBUG = false; 86 private static final String TAG = "IdentityCredentialAuthentication"; 87 88 private static final int BIOMETRIC_REQUEST_PERMISSION_CODE = 0; 89 90 private FingerprintManager mFingerprintManager; 91 private KeyguardManager mKeyguardManager; 92 getTitleRes()93 protected int getTitleRes() { 94 return R.string.sec_identity_credential_authentication_test; 95 } 96 getDescriptionRes()97 protected int getDescriptionRes() { 98 return R.string.sec_identity_credential_authentication_test_info; 99 } 100 101 @Override onCreate(Bundle savedInstanceState)102 protected void onCreate(Bundle savedInstanceState) { 103 super.onCreate(savedInstanceState); 104 setContentView(R.layout.sec_screen_lock_keys_main); 105 setPassFailButtonClickListeners(); 106 setInfoResources(getTitleRes(), getDescriptionRes(), -1); 107 getPassButton().setEnabled(false); 108 requestPermissions(new String[]{Manifest.permission.USE_BIOMETRIC}, 109 BIOMETRIC_REQUEST_PERMISSION_CODE); 110 } 111 112 @Override onRequestPermissionsResult(int requestCode, String[] permissions, int[] state)113 public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] state) { 114 if (requestCode == BIOMETRIC_REQUEST_PERMISSION_CODE && state[0] == PackageManager.PERMISSION_GRANTED) { 115 mFingerprintManager = (FingerprintManager) getSystemService(Context.FINGERPRINT_SERVICE); 116 mKeyguardManager = getSystemService(KeyguardManager.class); 117 Button startTestButton = findViewById(R.id.sec_start_test_button); 118 119 if (!mKeyguardManager.isKeyguardSecure()) { 120 // Show a message that the user hasn't set up a lock screen. 121 showToast( "Secure lock screen hasn't been set up.\n" 122 + "Go to 'Settings -> Security -> Screen lock' to set up a lock screen"); 123 startTestButton.setEnabled(false); 124 return; 125 } 126 127 onPermissionsGranted(); 128 129 startTestButton.setOnClickListener(new OnClickListener() { 130 @Override 131 public void onClick(View v) { 132 startTest(); 133 } 134 }); 135 } 136 } 137 138 /** 139 * Fingerprint-specific check before allowing test to be started 140 */ onPermissionsGranted()141 protected void onPermissionsGranted() { 142 mFingerprintManager = getSystemService(FingerprintManager.class); 143 if (!mFingerprintManager.hasEnrolledFingerprints()) { 144 showToast("No fingerprints enrolled.\n" 145 + "Go to 'Settings -> Security -> Fingerprint' to set up a fingerprint"); 146 Button startTestButton = findViewById(R.id.sec_start_test_button); 147 startTestButton.setEnabled(false); 148 } 149 } 150 showToast(String message)151 protected void showToast(String message) { 152 Toast.makeText(this, message, Toast.LENGTH_LONG).show(); 153 } 154 provisionFoo(IdentityCredentialStore store)155 private void provisionFoo(IdentityCredentialStore store) throws Exception { 156 store.deleteCredentialByName("test"); 157 WritableIdentityCredential wc = store.createCredential("test", 158 "org.iso.18013-5.2019.mdl"); 159 160 // 'Bar' encoded as CBOR tstr 161 byte[] barCbor = {0x63, 0x42, 0x61, 0x72}; 162 163 AccessControlProfile acp = new AccessControlProfile.Builder(new AccessControlProfileId(0)) 164 .setUserAuthenticationRequired(true) 165 .setUserAuthenticationTimeout(0) 166 .build(); 167 LinkedList<AccessControlProfileId> idsProfile0 = new LinkedList<AccessControlProfileId>(); 168 idsProfile0.add(new AccessControlProfileId(0)); 169 PersonalizationData pd = new PersonalizationData.Builder() 170 .addAccessControlProfile(acp) 171 .putEntry("org.iso.18013-5.2019", "Foo", idsProfile0, barCbor) 172 .build(); 173 byte[] proofOfProvisioningSignature = wc.personalize(pd); 174 175 // Create authentication keys. 176 IdentityCredential credential = store.getCredentialByName("test", 177 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 178 credential.setAvailableAuthenticationKeys(1, 10); 179 Collection<X509Certificate> dynAuthKeyCerts = credential.getAuthKeysNeedingCertification(); 180 credential.storeStaticAuthenticationData(dynAuthKeyCerts.iterator().next(), new byte[0]); 181 } 182 getFooAndCheckNotRetrievable(IdentityCredential credential)183 private boolean getFooAndCheckNotRetrievable(IdentityCredential credential) throws Exception { 184 Map<String, Collection<String>> entriesToRequest = new LinkedHashMap<>(); 185 entriesToRequest.put("org.iso.18013-5.2019", Arrays.asList("Foo")); 186 187 ResultData rd = credential.getEntries( 188 createItemsRequest(entriesToRequest, null), 189 entriesToRequest, 190 null, // sessionTranscript 191 null); // readerSignature 192 if (rd.getStatus("org.iso.18013-5.2019", "Foo") != ResultData.STATUS_USER_AUTHENTICATION_FAILED) { 193 return false; 194 } 195 return true; 196 } 197 getFooAndCheckRetrievable(IdentityCredential credential)198 private boolean getFooAndCheckRetrievable(IdentityCredential credential) throws Exception { 199 Map<String, Collection<String>> entriesToRequest = new LinkedHashMap<>(); 200 entriesToRequest.put("org.iso.18013-5.2019", Arrays.asList("Foo")); 201 202 ResultData rd = credential.getEntries( 203 createItemsRequest(entriesToRequest, null), 204 entriesToRequest, 205 null, // sessionTranscript 206 null); // readerSignature 207 if (rd.getStatus("org.iso.18013-5.2019", "Foo") != ResultData.STATUS_OK) { 208 return false; 209 } 210 return true; 211 } 212 startTest()213 protected void startTest() { 214 IdentityCredentialStore store = IdentityCredentialStore.getInstance(this); 215 if (store == null) { 216 showToast("No Identity Credential support, test passed."); 217 getPassButton().setEnabled(true); 218 return; 219 } 220 221 try { 222 223 provisionFoo(store); 224 225 // First, check that Foo cannot be retrieved without authentication. 226 // 227 IdentityCredential credentialWithoutAuth = store.getCredentialByName("test", 228 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 229 if (!getFooAndCheckNotRetrievable(credentialWithoutAuth)) { 230 showToast("Failed while checking that data element cannot be retrieved without authentication"); 231 return; 232 } 233 234 // Try one more time, this time with a CryptoObject that we'll use with 235 // BiometricPrompt. This should work. 236 // 237 final IdentityCredential credentialWithAuth = store.getCredentialByName("test", 238 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 239 CryptoObject cryptoObject = new BiometricPrompt.CryptoObject(credentialWithAuth); 240 BiometricPrompt.Builder builder = new BiometricPrompt.Builder(this); 241 builder.setTitle("Identity Credential"); 242 builder.setDescription("Authenticate to unlock credential."); 243 builder.setNegativeButton("Cancel", 244 getMainExecutor(), 245 new DialogInterface.OnClickListener() { 246 @Override 247 public void onClick(DialogInterface dialogInterface, int i) { 248 showToast("Canceled biometric prompt."); 249 } 250 }); 251 BiometricPrompt prompt = builder.build(); 252 prompt.authenticate(cryptoObject, 253 new CancellationSignal(), 254 getMainExecutor(), 255 new BiometricPrompt.AuthenticationCallback() { 256 @Override 257 public void onAuthenticationError(int errorCode, CharSequence errString) { 258 super.onAuthenticationError(errorCode, errString); 259 showToast("onAuthenticationError " + errorCode + ": " + errString); 260 } 261 @Override 262 public void onAuthenticationHelp(int helpCode, CharSequence helpString) { 263 super.onAuthenticationHelp(helpCode, helpString); 264 showToast("onAuthenticationHelp " + helpCode + ": " + helpString); 265 } 266 @Override 267 public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult authResult) { 268 super.onAuthenticationSucceeded(authResult); 269 try { 270 // Check that Foo can be retrieved because we used 271 // the CryptoObject to auth with. 272 if (!getFooAndCheckRetrievable(credentialWithAuth)) { 273 showToast("Failed while checking that data element can be retrieved with authentication"); 274 return; 275 } 276 277 // Finally, check that Foo cannot be retrieved again. 278 IdentityCredential credentialWithoutAuth2 = store.getCredentialByName("test", 279 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 280 if (!getFooAndCheckNotRetrievable(credentialWithoutAuth2)) { 281 showToast("Failed while checking that data element cannot be retrieved without authentication"); 282 return; 283 } 284 285 showToast("Test passed."); 286 getPassButton().setEnabled(true); 287 288 } catch (Exception e) { 289 showToast("Unexpection exception " + e); 290 } 291 } 292 @Override 293 public void onAuthenticationFailed() { 294 super.onAuthenticationFailed(); 295 showToast("onAuthenticationFailed"); 296 } 297 }); 298 299 } catch (Exception e) { 300 showToast("Unexpection exception " + e); 301 } 302 } 303 304 305 /* 306 * Helper function to create a CBOR data for requesting data items. The IntentToRetain 307 * value will be set to false for all elements. 308 * 309 * <p>The returned CBOR data conforms to the following CDDL schema:</p> 310 * 311 * <pre> 312 * ItemsRequest = { 313 * ? "docType" : DocType, 314 * "nameSpaces" : NameSpaces, 315 * ? "RequestInfo" : {* tstr => any} ; Additional info the reader wants to provide 316 * } 317 * 318 * NameSpaces = { 319 * + NameSpace => DataElements ; Requested data elements for each NameSpace 320 * } 321 * 322 * DataElements = { 323 * + DataElement => IntentToRetain 324 * } 325 * 326 * DocType = tstr 327 * 328 * DataElement = tstr 329 * IntentToRetain = bool 330 * NameSpace = tstr 331 * </pre> 332 * 333 * @param entriesToRequest The entries to request, organized as a map of namespace 334 * names with each value being a collection of data elements 335 * in the given namespace. 336 * @param docType The document type or {@code null} if there is no document 337 * type. 338 * @return CBOR data conforming to the CDDL mentioned above. 339 */ createItemsRequest( @onNull Map<String, Collection<String>> entriesToRequest, @Nullable String docType)340 static @NonNull byte[] createItemsRequest( 341 @NonNull Map<String, Collection<String>> entriesToRequest, 342 @Nullable String docType) { 343 CborBuilder builder = new CborBuilder(); 344 MapBuilder<CborBuilder> mapBuilder = builder.addMap(); 345 if (docType != null) { 346 mapBuilder.put("docType", docType); 347 } 348 349 MapBuilder<MapBuilder<CborBuilder>> nsMapBuilder = mapBuilder.putMap("nameSpaces"); 350 for (String namespaceName : entriesToRequest.keySet()) { 351 Collection<String> entryNames = entriesToRequest.get(namespaceName); 352 MapBuilder<MapBuilder<MapBuilder<CborBuilder>>> entryNameMapBuilder = 353 nsMapBuilder.putMap(namespaceName); 354 for (String entryName : entryNames) { 355 entryNameMapBuilder.put(entryName, false); 356 } 357 } 358 359 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 360 CborEncoder encoder = new CborEncoder(baos); 361 try { 362 encoder.encode(builder.build()); 363 } catch (CborException e) { 364 throw new RuntimeException("Error encoding CBOR", e); 365 } 366 return baos.toByteArray(); 367 } 368 369 370 371 } 372