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