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