1 /* 2 * Copyright 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 android.security.identity.cts; 18 19 import static org.junit.Assert.assertArrayEquals; 20 import static org.junit.Assert.assertEquals; 21 import static org.junit.Assert.assertNotNull; 22 import static org.junit.Assert.assertNull; 23 import static org.junit.Assert.assertTrue; 24 import static org.junit.Assume.assumeTrue; 25 26 import android.content.Context; 27 28 import android.os.SystemClock; 29 import android.security.identity.AuthenticationKeyMetadata; 30 import android.security.identity.EphemeralPublicKeyNotFoundException; 31 import android.security.identity.IdentityCredential; 32 import android.security.identity.IdentityCredentialException; 33 import android.security.identity.IdentityCredentialStore; 34 import android.security.identity.NoAuthenticationKeyAvailableException; 35 import android.security.identity.ResultData; 36 import androidx.test.InstrumentationRegistry; 37 import com.android.security.identity.internal.Util; 38 39 import org.junit.Test; 40 41 import java.io.ByteArrayOutputStream; 42 import java.security.InvalidKeyException; 43 import java.security.NoSuchAlgorithmException; 44 import java.security.NoSuchProviderException; 45 import java.security.KeyPair; 46 import java.security.SignatureException; 47 import java.security.cert.CertificateException; 48 import java.security.cert.X509Certificate; 49 import java.time.Instant; 50 import java.util.ArrayList; 51 import java.util.Arrays; 52 import java.util.Collection; 53 import java.util.LinkedHashMap; 54 import java.util.List; 55 import java.util.Map; 56 57 import javax.crypto.SecretKey; 58 59 import co.nstant.in.cbor.CborBuilder; 60 import co.nstant.in.cbor.CborEncoder; 61 import co.nstant.in.cbor.CborException; 62 63 public class DynamicAuthTest { 64 private static final String TAG = "DynamicAuthTest"; 65 66 @Test dynamicAuthTest()67 public void dynamicAuthTest() throws Exception { 68 assumeTrue("IC HAL is not implemented", TestUtil.isHalImplemented()); 69 70 Context appContext = InstrumentationRegistry.getTargetContext(); 71 IdentityCredentialStore store = IdentityCredentialStore.getInstance(appContext); 72 73 String credentialName = "test"; 74 75 store.deleteCredentialByName(credentialName); 76 Collection<X509Certificate> certChain = ProvisioningTest.createCredential(store, 77 credentialName); 78 79 IdentityCredential credential = store.getCredentialByName(credentialName, 80 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 81 assertNotNull(credential); 82 assertArrayEquals(new int[0], credential.getAuthenticationDataUsageCount()); 83 84 credential.setAvailableAuthenticationKeys(5, 3); 85 assertArrayEquals( 86 new int[]{0, 0, 0, 0, 0}, 87 credential.getAuthenticationDataUsageCount()); 88 89 // Getting data without device authentication should work even in the case where we haven't 90 // provisioned any authentication keys. Check that. 91 Map<String, Collection<String>> entriesToRequest = new LinkedHashMap<>(); 92 entriesToRequest.put("org.iso.18013-5.2019", Arrays.asList("First name", "Last name")); 93 ResultData rd = credential.getEntries( 94 Util.createItemsRequest(entriesToRequest, null), 95 entriesToRequest, 96 null, // sessionTranscript null indicates Device Authentication not requested. 97 null); 98 byte[] resultCbor = rd.getAuthenticatedData(); 99 try { 100 String pretty = Util.cborPrettyPrint(Util.canonicalizeCbor(resultCbor)); 101 assertEquals("{\n" 102 + " 'org.iso.18013-5.2019' : {\n" 103 + " 'Last name' : 'Turing',\n" 104 + " 'First name' : 'Alan'\n" 105 + " }\n" 106 + "}", 107 pretty); 108 } catch (CborException e) { 109 e.printStackTrace(); 110 assertTrue(false); 111 } 112 113 KeyPair readerEphemeralKeyPair = Util.createEphemeralKeyPair(); 114 115 credential = store.getCredentialByName(credentialName, 116 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 117 KeyPair ephemeralKeyPair = credential.createEphemeralKeyPair(); 118 credential.setReaderEphemeralPublicKey(readerEphemeralKeyPair.getPublic()); 119 120 byte[] sessionTranscript = Util.buildSessionTranscript(ephemeralKeyPair); 121 // Then check that getEntries() throw NoAuthenticationKeyAvailableException (_even_ when 122 // allowing using exhausted keys). 123 try { 124 rd = credential.getEntries( 125 Util.createItemsRequest(entriesToRequest, null), 126 entriesToRequest, 127 sessionTranscript, 128 null); 129 assertTrue(false); 130 } catch (NoAuthenticationKeyAvailableException e) { 131 // This is the expected path... 132 } catch (IdentityCredentialException e) { 133 e.printStackTrace(); 134 assertTrue(false); 135 } 136 137 // Get auth keys needing certification. This should be all of them. Note that 138 // this forces the creation of the authentication keys in the HAL. 139 Collection<X509Certificate> certificates = null; 140 certificates = credential.getAuthKeysNeedingCertification(); 141 assertEquals(5, certificates.size()); 142 143 // Do it one more time to check that an auth key is still pending even 144 // when the corresponding key has been created. 145 Collection<X509Certificate> certificates2 = null; 146 certificates2 = credential.getAuthKeysNeedingCertification(); 147 assertArrayEquals(certificates.toArray(), certificates2.toArray()); 148 149 // Now set auth data for the *first* key (this is the act of certifying the key) and check 150 // that one less key now needs certification. 151 X509Certificate key0Cert = certificates.iterator().next(); 152 153 // Check key0Cert is signed by CredentialKey. 154 try { 155 key0Cert.verify(certChain.iterator().next().getPublicKey()); 156 } catch (CertificateException 157 | InvalidKeyException 158 | NoSuchAlgorithmException 159 | NoSuchProviderException 160 | SignatureException e) { 161 e.printStackTrace(); 162 assertTrue(false); 163 } 164 165 try { 166 credential.storeStaticAuthenticationData(key0Cert, new byte[]{42, 43, 44}); 167 certificates = credential.getAuthKeysNeedingCertification(); 168 } catch (IdentityCredentialException e) { 169 e.printStackTrace(); 170 assertTrue(false); 171 } 172 assertEquals(4, certificates.size()); 173 174 // Now certify the *last* key. 175 X509Certificate key4Cert = new ArrayList<X509Certificate>(certificates).get( 176 certificates.size() - 1); 177 try { 178 key4Cert.verify(certChain.iterator().next().getPublicKey()); 179 } catch (CertificateException 180 | InvalidKeyException 181 | NoSuchAlgorithmException 182 | NoSuchProviderException 183 | SignatureException e) { 184 e.printStackTrace(); 185 assertTrue(false); 186 } 187 188 try { 189 credential.storeStaticAuthenticationData(key4Cert, new byte[]{43, 44, 45}); 190 certificates = credential.getAuthKeysNeedingCertification(); 191 } catch (IdentityCredentialException e) { 192 e.printStackTrace(); 193 assertTrue(false); 194 } 195 assertEquals(3, certificates.size()); 196 197 // Now that we've provisioned authentication keys, presentation will no longer fail with 198 // NoAuthenticationKeyAvailableException ... So now we can try a sessionTranscript without 199 // the ephemeral public key that was created in the Secure Area and check it fails with 200 // EphemeralPublicKeyNotFoundException instead... 201 ByteArrayOutputStream stBaos = new ByteArrayOutputStream(); 202 try { 203 new CborEncoder(stBaos).encode(new CborBuilder() 204 .addArray() 205 .add(new byte[]{0x01, 0x02}) // The device engagement structure, encoded 206 .add(new byte[]{0x03, 0x04}) // Reader ephemeral public key, encoded 207 .end() 208 .build()); 209 } catch (CborException e) { 210 e.printStackTrace(); 211 assertTrue(false); 212 } 213 byte[] wrongSessionTranscript = stBaos.toByteArray(); 214 try { 215 rd = credential.getEntries( 216 Util.createItemsRequest(entriesToRequest, null), 217 entriesToRequest, 218 wrongSessionTranscript, 219 null); 220 assertTrue(false); 221 } catch (EphemeralPublicKeyNotFoundException e) { 222 // This is the expected path... 223 } catch (IdentityCredentialException e) { 224 e.printStackTrace(); 225 assertTrue(false); 226 } 227 228 // Now use one of the keys... 229 entriesToRequest = new LinkedHashMap<>(); 230 entriesToRequest.put("org.iso.18013-5.2019", 231 Arrays.asList("First name", 232 "Last name", 233 "Home address", 234 "Birth date", 235 "Cryptanalyst", 236 "Portrait image", 237 "Height")); 238 credential = store.getCredentialByName(credentialName, 239 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 240 ephemeralKeyPair = credential.createEphemeralKeyPair(); 241 credential.setReaderEphemeralPublicKey(readerEphemeralKeyPair.getPublic()); 242 sessionTranscript = Util.buildSessionTranscript(ephemeralKeyPair); 243 rd = credential.getEntries( 244 Util.createItemsRequest(entriesToRequest, null), 245 entriesToRequest, 246 sessionTranscript, 247 null); 248 resultCbor = rd.getAuthenticatedData(); 249 try { 250 String pretty = Util.cborPrettyPrint(Util.canonicalizeCbor(resultCbor)); 251 assertEquals("{\n" 252 + " 'org.iso.18013-5.2019' : {\n" 253 + " 'Height' : 180,\n" 254 + " 'Last name' : 'Turing',\n" 255 + " 'Birth date' : '19120623',\n" 256 + " 'First name' : 'Alan',\n" 257 + " 'Cryptanalyst' : true,\n" 258 + " 'Home address' : 'Maida Vale, London, England',\n" 259 + " 'Portrait image' : [0x01, 0x02]\n" 260 + " }\n" 261 + "}", 262 pretty); 263 } catch (CborException e) { 264 e.printStackTrace(); 265 assertTrue(false); 266 } 267 268 byte[] deviceAuthenticationCbor = Util.buildDeviceAuthenticationCbor( 269 "org.iso.18013-5.2019.mdl", 270 sessionTranscript, 271 resultCbor); 272 273 // Calculate the MAC by deriving the key using ECDH and HKDF. 274 SecretKey eMacKey = Util.calcEMacKeyForReader( 275 key0Cert.getPublicKey(), 276 readerEphemeralKeyPair.getPrivate(), 277 sessionTranscript); 278 byte[] deviceAuthenticationBytes = 279 Util.prependSemanticTagForEncodedCbor(deviceAuthenticationCbor); 280 byte[] expectedMac = Util.coseMac0(eMacKey, 281 new byte[0], // payload 282 deviceAuthenticationBytes); // detached content 283 284 // Then compare it with what the TA produced. 285 assertArrayEquals(expectedMac, rd.getMessageAuthenticationCode()); 286 287 // Check that key0's static auth data is returned and that this 288 // key has an increased use-count. 289 assertArrayEquals(new byte[]{42, 43, 44}, rd.getStaticAuthenticationData()); 290 assertArrayEquals(new int[]{1, 0, 0, 0, 0}, credential.getAuthenticationDataUsageCount()); 291 292 // Now do this one more time.... this time key4 should have been used. Check this by 293 // inspecting use-counts and the static authentication data. 294 credential = store.getCredentialByName(credentialName, 295 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 296 ephemeralKeyPair = credential.createEphemeralKeyPair(); 297 credential.setReaderEphemeralPublicKey(readerEphemeralKeyPair.getPublic()); 298 sessionTranscript = Util.buildSessionTranscript(ephemeralKeyPair); 299 rd = credential.getEntries( 300 Util.createItemsRequest(entriesToRequest, null), 301 entriesToRequest, 302 sessionTranscript, 303 null); 304 assertArrayEquals(new byte[]{43, 44, 45}, rd.getStaticAuthenticationData()); 305 assertArrayEquals(new int[]{1, 0, 0, 0, 1}, credential.getAuthenticationDataUsageCount()); 306 307 // Verify MAC was made with key4. 308 deviceAuthenticationCbor = Util.buildDeviceAuthenticationCbor( 309 "org.iso.18013-5.2019.mdl", 310 sessionTranscript, 311 resultCbor); 312 eMacKey = Util.calcEMacKeyForReader( 313 key4Cert.getPublicKey(), 314 readerEphemeralKeyPair.getPrivate(), 315 sessionTranscript); 316 deviceAuthenticationBytes = 317 Util.prependSemanticTagForEncodedCbor(deviceAuthenticationCbor); 318 expectedMac = Util.coseMac0(eMacKey, 319 new byte[0], // payload 320 deviceAuthenticationBytes); // detached content 321 assertArrayEquals(expectedMac, rd.getMessageAuthenticationCode()); 322 323 // And again.... this time key0 should have been used. Check it. 324 credential = store.getCredentialByName(credentialName, 325 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 326 ephemeralKeyPair = credential.createEphemeralKeyPair(); 327 credential.setReaderEphemeralPublicKey(readerEphemeralKeyPair.getPublic()); 328 sessionTranscript = Util.buildSessionTranscript(ephemeralKeyPair); 329 rd = credential.getEntries( 330 Util.createItemsRequest(entriesToRequest, null), 331 entriesToRequest, 332 sessionTranscript, 333 null); 334 assertArrayEquals(new byte[]{42, 43, 44}, rd.getStaticAuthenticationData()); 335 assertArrayEquals(new int[]{2, 0, 0, 0, 1}, credential.getAuthenticationDataUsageCount()); 336 337 // And again.... this time key4 should have been used. Check it. 338 credential = store.getCredentialByName(credentialName, 339 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 340 ephemeralKeyPair = credential.createEphemeralKeyPair(); 341 credential.setReaderEphemeralPublicKey(readerEphemeralKeyPair.getPublic()); 342 sessionTranscript = Util.buildSessionTranscript(ephemeralKeyPair); 343 rd = credential.getEntries( 344 Util.createItemsRequest(entriesToRequest, null), 345 entriesToRequest, 346 sessionTranscript, 347 null); 348 assertArrayEquals(new byte[]{43, 44, 45}, rd.getStaticAuthenticationData()); 349 assertArrayEquals(new int[]{2, 0, 0, 0, 2}, credential.getAuthenticationDataUsageCount()); 350 351 // We configured each key to have three uses only. So we have two more presentations 352 // to go until we run out... first, check that only three keys need certifications 353 certificates = credential.getAuthKeysNeedingCertification(); 354 assertEquals(3, certificates.size()); 355 356 // Then exhaust the two we've already configured. 357 for (int n = 0; n < 2; n++) { 358 credential = store.getCredentialByName(credentialName, 359 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 360 ephemeralKeyPair = credential.createEphemeralKeyPair(); 361 credential.setReaderEphemeralPublicKey(readerEphemeralKeyPair.getPublic()); 362 sessionTranscript = Util.buildSessionTranscript(ephemeralKeyPair); 363 rd = credential.getEntries( 364 Util.createItemsRequest(entriesToRequest, null), 365 entriesToRequest, 366 sessionTranscript, 367 null); 368 assertNotNull(rd); 369 } 370 assertArrayEquals(new int[]{3, 0, 0, 0, 3}, credential.getAuthenticationDataUsageCount()); 371 372 // Now we should have five certs needing certification. 373 certificates = credential.getAuthKeysNeedingCertification(); 374 assertEquals(5, certificates.size()); 375 376 // We still have the two keys which have been exhausted. 377 assertArrayEquals(new int[]{3, 0, 0, 0, 3}, credential.getAuthenticationDataUsageCount()); 378 379 // Check that we fail when running out of presentations (and explicitly don't allow 380 // exhausting keys). 381 try { 382 credential = store.getCredentialByName(credentialName, 383 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 384 ephemeralKeyPair = credential.createEphemeralKeyPair(); 385 credential.setReaderEphemeralPublicKey(readerEphemeralKeyPair.getPublic()); 386 sessionTranscript = Util.buildSessionTranscript(ephemeralKeyPair); 387 credential.setAllowUsingExhaustedKeys(false); 388 rd = credential.getEntries( 389 Util.createItemsRequest(entriesToRequest, null), 390 entriesToRequest, 391 sessionTranscript, 392 null); 393 assertTrue(false); 394 } catch (IdentityCredentialException e) { 395 assertTrue(e instanceof NoAuthenticationKeyAvailableException); 396 } 397 assertArrayEquals(new int[]{3, 0, 0, 0, 3}, credential.getAuthenticationDataUsageCount()); 398 399 // Now try with allowing using auth keys already exhausted... this should work! 400 try { 401 credential = store.getCredentialByName(credentialName, 402 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 403 ephemeralKeyPair = credential.createEphemeralKeyPair(); 404 credential.setReaderEphemeralPublicKey(readerEphemeralKeyPair.getPublic()); 405 sessionTranscript = Util.buildSessionTranscript(ephemeralKeyPair); 406 rd = credential.getEntries( 407 Util.createItemsRequest(entriesToRequest, null), 408 entriesToRequest, 409 sessionTranscript, 410 null); 411 } catch (IdentityCredentialException e) { 412 e.printStackTrace(); 413 assertTrue(false); 414 } 415 assertArrayEquals(new int[]{4, 0, 0, 0, 3}, credential.getAuthenticationDataUsageCount()); 416 417 // Check that replenishing works... 418 certificates = credential.getAuthKeysNeedingCertification(); 419 assertEquals(5, certificates.size()); 420 X509Certificate keyNewCert = certificates.iterator().next(); 421 try { 422 credential.storeStaticAuthenticationData(keyNewCert, new byte[]{10, 11, 12}); 423 certificates = credential.getAuthKeysNeedingCertification(); 424 } catch (IdentityCredentialException e) { 425 e.printStackTrace(); 426 assertTrue(false); 427 } 428 assertEquals(4, certificates.size()); 429 assertArrayEquals(new int[]{0, 0, 0, 0, 3}, credential.getAuthenticationDataUsageCount()); 430 credential = store.getCredentialByName(credentialName, 431 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 432 ephemeralKeyPair = credential.createEphemeralKeyPair(); 433 credential.setReaderEphemeralPublicKey(readerEphemeralKeyPair.getPublic()); 434 sessionTranscript = Util.buildSessionTranscript(ephemeralKeyPair); 435 rd = credential.getEntries( 436 Util.createItemsRequest(entriesToRequest, null), 437 entriesToRequest, 438 sessionTranscript, 439 null); 440 assertArrayEquals(new byte[]{10, 11, 12}, rd.getStaticAuthenticationData()); 441 assertArrayEquals(new int[]{1, 0, 0, 0, 3}, credential.getAuthenticationDataUsageCount()); 442 443 deviceAuthenticationCbor = Util.buildDeviceAuthenticationCbor( 444 "org.iso.18013-5.2019.mdl", 445 sessionTranscript, 446 resultCbor); 447 eMacKey = Util.calcEMacKeyForReader( 448 keyNewCert.getPublicKey(), 449 readerEphemeralKeyPair.getPrivate(), 450 sessionTranscript); 451 deviceAuthenticationBytes = 452 Util.prependSemanticTagForEncodedCbor(deviceAuthenticationCbor); 453 expectedMac = Util.coseMac0(eMacKey, 454 new byte[0], // payload 455 deviceAuthenticationBytes); // detached content 456 assertArrayEquals(expectedMac, rd.getMessageAuthenticationCode()); 457 458 // ... and we're done. Clean up after ourselves. 459 store.deleteCredentialByName(credentialName); 460 } 461 462 463 @Test dynamicAuthWithExpirationTest()464 public void dynamicAuthWithExpirationTest() throws Exception { 465 assumeTrue("IC HAL is not implemented", TestUtil.isHalImplemented()); 466 467 Context appContext = InstrumentationRegistry.getTargetContext(); 468 IdentityCredentialStore store = IdentityCredentialStore.getInstance(appContext); 469 470 String credentialName = "test"; 471 472 store.deleteCredentialByName(credentialName); 473 Collection<X509Certificate> certChain = ProvisioningTest.createCredential(store, 474 credentialName); 475 476 IdentityCredential credential = store.getCredentialByName(credentialName, 477 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 478 assertNotNull(credential); 479 480 credential.setAvailableAuthenticationKeys(3, 5); 481 482 Collection<X509Certificate> certificates = null; 483 certificates = credential.getAuthKeysNeedingCertification(); 484 assertEquals(3, certificates.size()); 485 486 // Endorse an auth-key but set expiration to 10 seconds in the future. 487 // 488 Instant now = Instant.now(); 489 Instant tenSecondsFromNow = now.plusSeconds(10); 490 try { 491 X509Certificate key0Cert = certificates.iterator().next(); 492 credential.storeStaticAuthenticationData(key0Cert, 493 tenSecondsFromNow, 494 new byte[]{52, 53, 44}); 495 certificates = credential.getAuthKeysNeedingCertification(); 496 } catch (IdentityCredentialException e) { 497 e.printStackTrace(); 498 assertTrue(false); 499 } 500 assertEquals(2, certificates.size()); 501 assertArrayEquals( 502 new int[]{0, 0, 0}, 503 credential.getAuthenticationDataUsageCount()); 504 // Check that presentation works. 505 try { 506 IdentityCredential tc = store.getCredentialByName(credentialName, 507 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 508 KeyPair ekp = tc.createEphemeralKeyPair(); 509 KeyPair rekp = Util.createEphemeralKeyPair(); 510 tc.setReaderEphemeralPublicKey(rekp.getPublic()); 511 byte[] st = Util.buildSessionTranscript(ekp); 512 Map<String, Collection<String>> etr = new LinkedHashMap<>(); 513 etr.put("org.iso.18013-5.2019", Arrays.asList("First name", "Last name")); 514 ResultData rd = tc.getEntries( 515 Util.createItemsRequest(etr, null), 516 etr, 517 st, 518 null); 519 } catch (IdentityCredentialException e) { 520 e.printStackTrace(); 521 assertTrue(false); 522 } 523 credential = store.getCredentialByName(credentialName, 524 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 525 assertArrayEquals( 526 new int[]{1, 0, 0}, 527 credential.getAuthenticationDataUsageCount()); 528 529 SystemClock.sleep(11 * 1000); 530 531 certificates = credential.getAuthKeysNeedingCertification(); 532 assertEquals(3, certificates.size()); 533 534 // Check that presentation now fails.. 535 try { 536 IdentityCredential tc = store.getCredentialByName(credentialName, 537 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 538 KeyPair ekp = tc.createEphemeralKeyPair(); 539 KeyPair rekp = Util.createEphemeralKeyPair(); 540 tc.setReaderEphemeralPublicKey(rekp.getPublic()); 541 byte[] st = Util.buildSessionTranscript(ekp); 542 Map<String, Collection<String>> etr = new LinkedHashMap<>(); 543 etr.put("org.iso.18013-5.2019", Arrays.asList("First name", "Last name")); 544 ResultData rd = tc.getEntries( 545 Util.createItemsRequest(etr, null), 546 etr, 547 st, 548 null); 549 assertTrue(false); 550 } catch (NoAuthenticationKeyAvailableException e) { 551 // This is the expected path... 552 } catch (IdentityCredentialException e) { 553 e.printStackTrace(); 554 assertTrue(false); 555 } 556 credential = store.getCredentialByName(credentialName, 557 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 558 assertArrayEquals( 559 new int[]{1, 0, 0}, 560 credential.getAuthenticationDataUsageCount()); 561 562 // Check that it works if we use setAllowUsingExpiredKeys(true) 563 try { 564 IdentityCredential tc = store.getCredentialByName(credentialName, 565 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 566 tc.setAllowUsingExpiredKeys(true); // <-- this is the call that makes the difference! 567 KeyPair ekp = tc.createEphemeralKeyPair(); 568 KeyPair rekp = Util.createEphemeralKeyPair(); 569 tc.setReaderEphemeralPublicKey(rekp.getPublic()); 570 byte[] st = Util.buildSessionTranscript(ekp); 571 Map<String, Collection<String>> etr = new LinkedHashMap<>(); 572 etr.put("org.iso.18013-5.2019", Arrays.asList("First name", "Last name")); 573 ResultData rd = tc.getEntries( 574 Util.createItemsRequest(etr, null), 575 etr, 576 st, 577 null); 578 } catch (IdentityCredentialException e) { 579 e.printStackTrace(); 580 assertTrue(false); 581 } 582 credential = store.getCredentialByName(credentialName, 583 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 584 assertArrayEquals( 585 new int[]{2, 0, 0}, 586 credential.getAuthenticationDataUsageCount()); 587 588 // ... and we're done. Clean up after ourselves. 589 store.deleteCredentialByName(credentialName); 590 } 591 592 @Test dynamicAuthMinValidTimeTest()593 public void dynamicAuthMinValidTimeTest() throws Exception { 594 assumeTrue("IC HAL is not implemented", TestUtil.isHalImplemented()); 595 596 Context appContext = InstrumentationRegistry.getTargetContext(); 597 IdentityCredentialStore store = IdentityCredentialStore.getInstance(appContext); 598 599 String credentialName = "test"; 600 601 store.deleteCredentialByName(credentialName); 602 Collection<X509Certificate> certChain = ProvisioningTest.createCredential(store, 603 credentialName); 604 605 IdentityCredential credential = store.getCredentialByName(credentialName, 606 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 607 assertNotNull(credential); 608 609 // Set minValidTime to five seconds... 610 credential.setAvailableAuthenticationKeys(3, 5, 5000); 611 612 Collection<X509Certificate> authKeys = null; 613 authKeys = credential.getAuthKeysNeedingCertification(); 614 assertEquals(3, authKeys.size()); 615 616 // Endorse all auth-key but set expiration to 10 seconds in the future. 617 // 618 Instant beginTime = Instant.now(); 619 Instant tenSecondsFromBeginning = beginTime.plusSeconds(10); 620 for (X509Certificate authKey : authKeys) { 621 try { 622 credential.storeStaticAuthenticationData(authKey, 623 tenSecondsFromBeginning, 624 new byte[]{52, 53, 44}); 625 } catch (IdentityCredentialException e) { 626 e.printStackTrace(); 627 assertTrue(false); 628 } 629 } 630 authKeys = credential.getAuthKeysNeedingCertification(); 631 assertEquals(0, authKeys.size()); 632 assertArrayEquals( 633 new int[]{0, 0, 0}, 634 credential.getAuthenticationDataUsageCount()); 635 // Check that presentation works. 636 try { 637 IdentityCredential tc = store.getCredentialByName(credentialName, 638 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 639 KeyPair ekp = tc.createEphemeralKeyPair(); 640 KeyPair rekp = Util.createEphemeralKeyPair(); 641 tc.setReaderEphemeralPublicKey(rekp.getPublic()); 642 byte[] st = Util.buildSessionTranscript(ekp); 643 Map<String, Collection<String>> etr = new LinkedHashMap<>(); 644 etr.put("org.iso.18013-5.2019", Arrays.asList("First name", "Last name")); 645 ResultData rd = tc.getEntries( 646 Util.createItemsRequest(etr, null), 647 etr, 648 st, 649 null); 650 } catch (IdentityCredentialException e) { 651 e.printStackTrace(); 652 assertTrue(false); 653 } 654 // At this point, no authKeys are tagged as needing replacement 655 credential = store.getCredentialByName(credentialName, 656 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 657 assertArrayEquals( 658 new int[]{1, 0, 0}, 659 credential.getAuthenticationDataUsageCount()); 660 authKeys = credential.getAuthKeysNeedingCertification(); 661 assertEquals(0, authKeys.size()); 662 663 // **Six** seconds later the AuthKeys are still usable but all three should be tagged as 664 // needing replacement. 665 SystemClock.sleep(6 * 1000); 666 authKeys = credential.getAuthKeysNeedingCertification(); 667 assertEquals(3, authKeys.size()); 668 // However presentations should _still_ work since they haven't expired. Check this. 669 try { 670 IdentityCredential tc = store.getCredentialByName(credentialName, 671 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 672 KeyPair ekp = tc.createEphemeralKeyPair(); 673 KeyPair rekp = Util.createEphemeralKeyPair(); 674 tc.setReaderEphemeralPublicKey(rekp.getPublic()); 675 byte[] st = Util.buildSessionTranscript(ekp); 676 Map<String, Collection<String>> etr = new LinkedHashMap<>(); 677 etr.put("org.iso.18013-5.2019", Arrays.asList("First name", "Last name")); 678 ResultData rd = tc.getEntries( 679 Util.createItemsRequest(etr, null), 680 etr, 681 st, 682 null); 683 } catch (IdentityCredentialException e) { 684 e.printStackTrace(); 685 assertTrue(false); 686 } 687 credential = store.getCredentialByName(credentialName, 688 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 689 assertArrayEquals( 690 new int[]{1, 1, 0}, 691 credential.getAuthenticationDataUsageCount()); 692 authKeys = credential.getAuthKeysNeedingCertification(); 693 assertEquals(3, authKeys.size()); 694 695 // Now replace all AuthKeys, set expiration to 15 seconds from beginning 696 Instant fifteenSecondsFromBeginning = beginTime.plusSeconds(15); 697 for (X509Certificate authKey : authKeys) { 698 try { 699 credential.storeStaticAuthenticationData(authKey, 700 fifteenSecondsFromBeginning, 701 new byte[]{52, 53, 44}); 702 } catch (IdentityCredentialException e) { 703 e.printStackTrace(); 704 assertTrue(false); 705 } 706 } 707 // At this point (T + 6 sec), no authKeys are tagged as needing replacement 708 authKeys = credential.getAuthKeysNeedingCertification(); 709 assertEquals(0, authKeys.size()); 710 assertArrayEquals( 711 new int[]{0, 0, 0}, 712 credential.getAuthenticationDataUsageCount()); 713 714 // **Five** seconds after this (T + 11sec), these new authKeys should be tagged 715 // as needing replacement but still work. Check this. 716 SystemClock.sleep(5 * 1000); 717 try { 718 IdentityCredential tc = store.getCredentialByName(credentialName, 719 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 720 KeyPair ekp = tc.createEphemeralKeyPair(); 721 KeyPair rekp = Util.createEphemeralKeyPair(); 722 tc.setReaderEphemeralPublicKey(rekp.getPublic()); 723 byte[] st = Util.buildSessionTranscript(ekp); 724 Map<String, Collection<String>> etr = new LinkedHashMap<>(); 725 etr.put("org.iso.18013-5.2019", Arrays.asList("First name", "Last name")); 726 ResultData rd = tc.getEntries( 727 Util.createItemsRequest(etr, null), 728 etr, 729 st, 730 null); 731 } catch (IdentityCredentialException e) { 732 e.printStackTrace(); 733 assertTrue(false); 734 } 735 credential = store.getCredentialByName(credentialName, 736 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 737 assertArrayEquals( 738 new int[]{1, 0, 0}, 739 credential.getAuthenticationDataUsageCount()); 740 authKeys = credential.getAuthKeysNeedingCertification(); 741 assertEquals(3, authKeys.size()); 742 743 // **Five** seconds later (T + 16sec) all AuthKeys should be expired and still 744 // tagged as needing replacement. Check this. 745 SystemClock.sleep(5 * 1000); 746 authKeys = credential.getAuthKeysNeedingCertification(); 747 assertEquals(3, authKeys.size()); 748 // Check that presentation now fails.. 749 try { 750 IdentityCredential tc = store.getCredentialByName(credentialName, 751 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 752 KeyPair ekp = tc.createEphemeralKeyPair(); 753 KeyPair rekp = Util.createEphemeralKeyPair(); 754 tc.setReaderEphemeralPublicKey(rekp.getPublic()); 755 byte[] st = Util.buildSessionTranscript(ekp); 756 Map<String, Collection<String>> etr = new LinkedHashMap<>(); 757 etr.put("org.iso.18013-5.2019", Arrays.asList("First name", "Last name")); 758 ResultData rd = tc.getEntries( 759 Util.createItemsRequest(etr, null), 760 etr, 761 st, 762 null); 763 assertTrue(false); 764 } catch (NoAuthenticationKeyAvailableException e) { 765 // This is the expected path... 766 } catch (IdentityCredentialException e) { 767 e.printStackTrace(); 768 assertTrue(false); 769 } 770 credential = store.getCredentialByName(credentialName, 771 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 772 assertArrayEquals( 773 new int[]{1, 0, 0}, 774 credential.getAuthenticationDataUsageCount()); 775 776 // Check that it works if we use setAllowUsingExpiredKeys(true) 777 try { 778 IdentityCredential tc = store.getCredentialByName(credentialName, 779 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 780 tc.setAllowUsingExpiredKeys(true); // <-- this is the call that makes the difference! 781 KeyPair ekp = tc.createEphemeralKeyPair(); 782 KeyPair rekp = Util.createEphemeralKeyPair(); 783 tc.setReaderEphemeralPublicKey(rekp.getPublic()); 784 byte[] st = Util.buildSessionTranscript(ekp); 785 Map<String, Collection<String>> etr = new LinkedHashMap<>(); 786 etr.put("org.iso.18013-5.2019", Arrays.asList("First name", "Last name")); 787 ResultData rd = tc.getEntries( 788 Util.createItemsRequest(etr, null), 789 etr, 790 st, 791 null); 792 } catch (IdentityCredentialException e) { 793 e.printStackTrace(); 794 assertTrue(false); 795 } 796 credential = store.getCredentialByName(credentialName, 797 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 798 assertArrayEquals( 799 new int[]{1, 1, 0}, 800 credential.getAuthenticationDataUsageCount()); 801 802 // ... and we're done. Clean up after ourselves. 803 store.deleteCredentialByName(credentialName); 804 } 805 806 @Test dynamicAuthCanGetExpirations()807 public void dynamicAuthCanGetExpirations() throws Exception { 808 assumeTrue("IC HAL is not implemented", TestUtil.isHalImplemented()); 809 810 Context appContext = InstrumentationRegistry.getTargetContext(); 811 IdentityCredentialStore store = IdentityCredentialStore.getInstance(appContext); 812 813 String credentialName = "test"; 814 815 store.deleteCredentialByName(credentialName); 816 Collection<X509Certificate> certChain = ProvisioningTest.createCredential(store, 817 credentialName); 818 819 IdentityCredential credential = store.getCredentialByName(credentialName, 820 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 821 assertNotNull(credential); 822 823 int numAuthKeys = 10; 824 credential.setAvailableAuthenticationKeys(numAuthKeys, 1, 0); 825 826 Collection<X509Certificate> authKeys = null; 827 authKeys = credential.getAuthKeysNeedingCertification(); 828 assertEquals(numAuthKeys, authKeys.size()); 829 830 // Endorse all auth keys and set expiration times to known values in the future 831 // 832 long tenSecondsFromBeginning = Instant.now().plusSeconds(10).toEpochMilli(); 833 int n = 0; 834 for (X509Certificate authKey : authKeys) { 835 try { 836 Instant expiration = Instant.ofEpochMilli(tenSecondsFromBeginning + n); 837 // For the fourth certificate, don't set expiration 838 if (n == 3) { 839 credential.storeStaticAuthenticationData(authKey, 840 new byte[]{52, 53, 44}); 841 } else { 842 credential.storeStaticAuthenticationData(authKey, 843 expiration, 844 new byte[]{52, 53, 44}); 845 } 846 } catch (IdentityCredentialException e) { 847 e.printStackTrace(); 848 assertTrue(false); 849 } 850 n++; 851 } 852 authKeys = credential.getAuthKeysNeedingCertification(); 853 assertEquals(0, authKeys.size()); 854 855 // Check we can read back expirations and usage counts 856 List<AuthenticationKeyMetadata> mds = credential.getAuthenticationKeyMetadata(); 857 assertEquals(mds.size(), numAuthKeys); 858 for (n = 0; n < numAuthKeys; n++) { 859 AuthenticationKeyMetadata md = mds.get(n); 860 if (n == 3) { 861 assertNull(md); 862 } else { 863 assertEquals(tenSecondsFromBeginning + n, md.getExpirationDate().toEpochMilli()); 864 assertEquals(0, md.getUsageCount()); 865 } 866 } 867 868 // ... and we're done. Clean up after ourselves. 869 store.deleteCredentialByName(credentialName); 870 } 871 872 @Test dynamicAuthMultipleGetEntries()873 public void dynamicAuthMultipleGetEntries() throws Exception { 874 assumeTrue("IC HAL is not implemented", TestUtil.isHalImplemented()); 875 876 Context appContext = InstrumentationRegistry.getTargetContext(); 877 IdentityCredentialStore store = IdentityCredentialStore.getInstance(appContext); 878 879 String credentialName = "test"; 880 881 store.deleteCredentialByName(credentialName); 882 Collection<X509Certificate> certChain = ProvisioningTest.createCredential(store, 883 credentialName); 884 885 IdentityCredential credential = store.getCredentialByName(credentialName, 886 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 887 assertNotNull(credential); 888 assertArrayEquals(new int[0], credential.getAuthenticationDataUsageCount()); 889 890 credential.setAvailableAuthenticationKeys(5, 3); 891 assertArrayEquals(new int[]{0, 0, 0, 0, 0}, 892 credential.getAuthenticationDataUsageCount()); 893 894 Collection<X509Certificate> certs = credential.getAuthKeysNeedingCertification(); 895 assertEquals(5, certs.size()); 896 897 // Certify authKeys 0 and 1 898 // 899 X509Certificate key0Cert = new ArrayList<X509Certificate>(certs).get(0); 900 credential.storeStaticAuthenticationData(key0Cert, new byte[]{42, 43, 44}); 901 X509Certificate key1Cert = new ArrayList<X509Certificate>(certs).get(1); 902 credential.storeStaticAuthenticationData(key1Cert, new byte[]{43, 44, 45}); 903 904 // Get ready to present... 905 // 906 Map<String, Collection<String>> entriesToRequest = new LinkedHashMap<>(); 907 entriesToRequest.put("org.iso.18013-5.2019", Arrays.asList("First name", "Last name")); 908 KeyPair ephemeralKeyPair = credential.createEphemeralKeyPair(); 909 KeyPair readerEphemeralKeyPair = Util.createEphemeralKeyPair(); 910 byte[] sessionTranscript = Util.buildSessionTranscript(ephemeralKeyPair); 911 credential.setReaderEphemeralPublicKey(readerEphemeralKeyPair.getPublic()); 912 913 // First getEntries() call should pick key0 914 // 915 ResultData rd = credential.getEntries( 916 Util.createItemsRequest(entriesToRequest, null), 917 entriesToRequest, 918 sessionTranscript, 919 null); 920 assertArrayEquals(new int[]{1, 0, 0, 0, 0}, credential.getAuthenticationDataUsageCount()); 921 assertArrayEquals(new byte[]{42, 43, 44}, rd.getStaticAuthenticationData()); 922 923 // Doing another getEntries() call on the same object should use the same key. Check this 924 // by inspecting use-counts and the static authentication data. 925 // 926 rd = credential.getEntries( 927 Util.createItemsRequest(entriesToRequest, null), 928 entriesToRequest, 929 sessionTranscript, 930 null); 931 assertArrayEquals(new int[]{1, 0, 0, 0, 0}, credential.getAuthenticationDataUsageCount()); 932 assertArrayEquals(new byte[]{42, 43, 44}, rd.getStaticAuthenticationData()); 933 } 934 935 @Test dynamicAuthNoUsageCountIncrement()936 public void dynamicAuthNoUsageCountIncrement() throws Exception { 937 assumeTrue("IC HAL is not implemented", TestUtil.isHalImplemented()); 938 939 Context appContext = InstrumentationRegistry.getTargetContext(); 940 IdentityCredentialStore store = IdentityCredentialStore.getInstance(appContext); 941 assumeTrue( 942 "IdentityCredential.setIncrementKeyUsageCount(boolean) not supported", 943 TestUtil.getFeatureVersion() >= 202201); 944 945 String credentialName = "test"; 946 947 store.deleteCredentialByName(credentialName); 948 Collection<X509Certificate> certChain = ProvisioningTest.createCredential(store, 949 credentialName); 950 951 IdentityCredential credential = store.getCredentialByName( 952 credentialName, 953 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 954 assertNotNull(credential); 955 assertArrayEquals(new int[0], credential.getAuthenticationDataUsageCount()); 956 957 credential.setAvailableAuthenticationKeys(5, 3); 958 assertArrayEquals(new int[]{0, 0, 0, 0, 0}, 959 credential.getAuthenticationDataUsageCount()); 960 961 Collection<X509Certificate> certs = credential.getAuthKeysNeedingCertification(); 962 assertEquals(5, certs.size()); 963 964 // Certify authKeys 0 and 1 965 // 966 X509Certificate key0Cert = new ArrayList<X509Certificate>(certs).get(0); 967 credential.storeStaticAuthenticationData(key0Cert, new byte[]{42, 43, 44}); 968 X509Certificate key1Cert = new ArrayList<X509Certificate>(certs).get(1); 969 credential.storeStaticAuthenticationData(key1Cert, new byte[]{43, 44, 45}); 970 971 // Get ready to present... 972 // 973 Map<String, Collection<String>> entriesToRequest = new LinkedHashMap<>(); 974 entriesToRequest.put("org.iso.18013-5.2019", Arrays.asList("First name", "Last name")); 975 KeyPair ephemeralKeyPair = credential.createEphemeralKeyPair(); 976 KeyPair readerEphemeralKeyPair = Util.createEphemeralKeyPair(); 977 byte[] sessionTranscript = Util.buildSessionTranscript(ephemeralKeyPair); 978 credential.setReaderEphemeralPublicKey(readerEphemeralKeyPair.getPublic()); 979 980 // First getEntries() call should pick key0 981 // 982 ResultData rd = credential.getEntries( 983 Util.createItemsRequest(entriesToRequest, null), 984 entriesToRequest, 985 sessionTranscript, 986 null); 987 assertArrayEquals(new int[]{1, 0, 0, 0, 0}, credential.getAuthenticationDataUsageCount()); 988 assertArrayEquals(new byte[]{42, 43, 44}, rd.getStaticAuthenticationData()); 989 990 // Now configure to not increase the usage count... we should be able to do as many 991 // presentations as we want and usage counts shouldn't change. We'll just do 3. 992 // 993 // Note that key1 will be picked (we can check that by inspecting static authentication 994 // data) but its usage count won't be increased. 995 // 996 credential = store.getCredentialByName(credentialName, 997 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 998 ephemeralKeyPair = credential.createEphemeralKeyPair(); 999 readerEphemeralKeyPair = Util.createEphemeralKeyPair(); 1000 sessionTranscript = Util.buildSessionTranscript(ephemeralKeyPair); 1001 credential.setReaderEphemeralPublicKey(readerEphemeralKeyPair.getPublic()); 1002 credential.setIncrementKeyUsageCount(false); 1003 for (int n = 0; n < 3; n++) { 1004 rd = credential.getEntries( 1005 Util.createItemsRequest(entriesToRequest, null), 1006 entriesToRequest, 1007 sessionTranscript, 1008 null); 1009 assertArrayEquals(new int[]{1, 0, 0, 0, 0}, 1010 credential.getAuthenticationDataUsageCount()); 1011 assertArrayEquals(new byte[]{43, 44, 45}, rd.getStaticAuthenticationData()); 1012 } 1013 1014 // With usage counts incrementing again, do another getEntries() call. It should pick key1 1015 // and use it up. Check this by inspecting use-counts and the static authentication data. 1016 // 1017 credential = store.getCredentialByName(credentialName, 1018 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256); 1019 ephemeralKeyPair = credential.createEphemeralKeyPair(); 1020 readerEphemeralKeyPair = Util.createEphemeralKeyPair(); 1021 sessionTranscript = Util.buildSessionTranscript(ephemeralKeyPair); 1022 credential.setReaderEphemeralPublicKey(readerEphemeralKeyPair.getPublic()); 1023 rd = credential.getEntries( 1024 Util.createItemsRequest(entriesToRequest, null), 1025 entriesToRequest, 1026 sessionTranscript, 1027 null); 1028 assertArrayEquals(new int[]{1, 1, 0, 0, 0}, credential.getAuthenticationDataUsageCount()); 1029 assertArrayEquals(new byte[]{43, 44, 45}, rd.getStaticAuthenticationData()); 1030 } 1031 1032 // TODO: test storeStaticAuthenticationData() throwing UnknownAuthenticationKeyException 1033 // on an unknown auth key 1034 } 1035