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 junit.framework.TestCase.assertTrue;
20 
21 import static org.junit.Assert.assertArrayEquals;
22 import static org.junit.Assert.assertNotEquals;
23 import static org.junit.Assert.assertNotNull;
24 import static org.junit.Assume.assumeTrue;
25 
26 import android.content.Context;
27 import android.security.keystore.KeyProperties;
28 
29 import android.security.identity.IdentityCredential;
30 import android.security.identity.IdentityCredentialException;
31 import android.security.identity.IdentityCredentialStore;
32 import androidx.test.InstrumentationRegistry;
33 
34 import org.junit.Test;
35 
36 import java.nio.ByteBuffer;
37 import java.security.InvalidAlgorithmParameterException;
38 import java.security.InvalidKeyException;
39 import java.security.KeyPair;
40 import java.security.KeyPairGenerator;
41 import java.security.NoSuchAlgorithmException;
42 import java.security.PublicKey;
43 import java.security.SecureRandom;
44 import java.security.cert.X509Certificate;
45 import java.security.spec.ECGenParameterSpec;
46 import java.util.Collection;
47 
48 import javax.crypto.BadPaddingException;
49 import javax.crypto.Cipher;
50 import javax.crypto.IllegalBlockSizeException;
51 import javax.crypto.KeyAgreement;
52 import javax.crypto.NoSuchPaddingException;
53 import javax.crypto.SecretKey;
54 import javax.crypto.spec.GCMParameterSpec;
55 import javax.crypto.spec.SecretKeySpec;
56 
57 // TODO: For better coverage, use different ECDH and HKDF implementations in test code.
58 public class EphemeralKeyTest {
59     private static final String TAG = "EphemeralKeyTest";
60 
61     @Test
createEphemeralKey()62     public void createEphemeralKey() throws IdentityCredentialException {
63         assumeTrue("IC HAL is not implemented", Util.isHalImplemented());
64 
65         Context appContext = InstrumentationRegistry.getTargetContext();
66         IdentityCredentialStore store = IdentityCredentialStore.getInstance(appContext);
67 
68         String credentialName = "ephemeralKeyTest";
69 
70         store.deleteCredentialByName(credentialName);
71         Collection<X509Certificate> certChain = ProvisioningTest.createCredential(store,
72                 credentialName);
73         IdentityCredential credential = store.getCredentialByName(credentialName,
74                 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256);
75         assertNotNull(credential);
76 
77         // Check we can get both the public and private keys.
78         KeyPair ephemeralKeyPair = credential.createEphemeralKeyPair();
79         assertNotNull(ephemeralKeyPair);
80         assertTrue(ephemeralKeyPair.getPublic().getEncoded().length > 0);
81         assertTrue(ephemeralKeyPair.getPrivate().getEncoded().length > 0);
82 
83         TestReader reader = new TestReader(
84                 IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256,
85                 ephemeralKeyPair.getPublic());
86 
87         try {
88             credential.setReaderEphemeralPublicKey(reader.getEphemeralPublicKey());
89         } catch (InvalidKeyException e) {
90             e.printStackTrace();
91             assertTrue(false);
92         }
93 
94         // Exchange a couple of messages... this is to test that the nonce/counter
95         // state works as expected.
96         for (int n = 0; n < 5; n++) {
97             // First send a message from the Reader to the Holder...
98             byte[] messageToHolder = ("Hello Holder! (serial=" + n + ")").getBytes();
99             byte[] encryptedMessageToHolder = reader.encryptMessageToHolder(messageToHolder);
100             assertNotEquals(messageToHolder, encryptedMessageToHolder);
101             byte[] decryptedMessageToHolder = credential.decryptMessageFromReader(
102                     encryptedMessageToHolder);
103             assertArrayEquals(messageToHolder, decryptedMessageToHolder);
104 
105             // Then from the Holder to the Reader...
106             byte[] messageToReader = ("Hello Reader! (serial=" + n + ")").getBytes();
107             byte[] encryptedMessageToReader = credential.encryptMessageToReader(messageToReader);
108             assertNotEquals(messageToReader, encryptedMessageToReader);
109             byte[] decryptedMessageToReader = reader.decryptMessageFromHolder(
110                     encryptedMessageToReader);
111             assertArrayEquals(messageToReader, decryptedMessageToReader);
112         }
113     }
114 
115     static class TestReader {
116 
117         @IdentityCredentialStore.Ciphersuite
118         private int mCipherSuite;
119 
120         private PublicKey mHolderEphemeralPublicKey;
121         private KeyPair mEphemeralKeyPair;
122         private SecretKey mSecretKey;
123         private SecretKey mReaderSecretKey;
124         private int mCounter;
125         private int mMdlExpectedCounter;
126 
127         private SecureRandom mSecureRandom;
128 
129         private boolean mRemoteIsReaderDevice;
130 
131         // This is basically the reader-side of what needs to happen for encryption/decryption
132         // of messages.. could easily be re-used in an mDL reader application.
TestReader(@dentityCredentialStore.Ciphersuite int cipherSuite, PublicKey holderEphemeralPublicKey)133         TestReader(@IdentityCredentialStore.Ciphersuite int cipherSuite,
134                 PublicKey holderEphemeralPublicKey) throws IdentityCredentialException {
135             mCipherSuite = cipherSuite;
136             mHolderEphemeralPublicKey = holderEphemeralPublicKey;
137             mCounter = 1;
138             mMdlExpectedCounter = 1;
139 
140             try {
141                 KeyPairGenerator kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC);
142                 ECGenParameterSpec ecSpec = new ECGenParameterSpec("prime256v1");
143                 kpg.initialize(ecSpec);
144                 mEphemeralKeyPair = kpg.generateKeyPair();
145             } catch (NoSuchAlgorithmException
146                     | InvalidAlgorithmParameterException e) {
147                 e.printStackTrace();
148                 throw new IdentityCredentialException("Error generating ephemeral key", e);
149             }
150 
151             try {
152                 KeyAgreement ka = KeyAgreement.getInstance("ECDH");
153                 ka.init(mEphemeralKeyPair.getPrivate());
154                 ka.doPhase(mHolderEphemeralPublicKey, true);
155                 byte[] sharedSecret = ka.generateSecret();
156 
157                 byte[] salt = new byte[1];
158                 byte[] info = new byte[0];
159 
160                 salt[0] = 0x01;
161                 byte[] derivedKey = Util.computeHkdf("HmacSha256", sharedSecret, salt, info, 32);
162                 mSecretKey = new SecretKeySpec(derivedKey, "AES");
163 
164                 salt[0] = 0x00;
165                 derivedKey = Util.computeHkdf("HmacSha256", sharedSecret, salt, info,32);
166                 mReaderSecretKey = new SecretKeySpec(derivedKey, "AES");
167 
168                 mSecureRandom = new SecureRandom();
169 
170             } catch (InvalidKeyException
171                     | NoSuchAlgorithmException e) {
172                 e.printStackTrace();
173                 throw new IdentityCredentialException("Error performing key agreement", e);
174             }
175         }
176 
getEphemeralPublicKey()177         PublicKey getEphemeralPublicKey() {
178             return mEphemeralKeyPair.getPublic();
179         }
180 
encryptMessageToHolder(byte[] messagePlaintext)181         byte[] encryptMessageToHolder(byte[] messagePlaintext) throws IdentityCredentialException {
182             byte[] messageCiphertext = null;
183             try {
184                 ByteBuffer iv = ByteBuffer.allocate(12);
185                 iv.putInt(0, 0x00000000);
186                 iv.putInt(4, 0x00000000);
187                 iv.putInt(8, mCounter);
188                 Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
189                 GCMParameterSpec encryptionParameterSpec = new GCMParameterSpec(128, iv.array());
190                 cipher.init(Cipher.ENCRYPT_MODE, mReaderSecretKey, encryptionParameterSpec);
191                 messageCiphertext = cipher.doFinal(messagePlaintext); // This includes the auth tag
192             } catch (BadPaddingException
193                     | IllegalBlockSizeException
194                     | NoSuchPaddingException
195                     | InvalidKeyException
196                     | NoSuchAlgorithmException
197                     | InvalidAlgorithmParameterException e) {
198                 e.printStackTrace();
199                 throw new IdentityCredentialException("Error encrypting message", e);
200             }
201             mCounter += 1;
202             return messageCiphertext;
203         }
204 
decryptMessageFromHolder(byte[] messageCiphertext)205         byte[] decryptMessageFromHolder(byte[] messageCiphertext)
206                 throws IdentityCredentialException {
207             ByteBuffer iv = ByteBuffer.allocate(12);
208             iv.putInt(0, 0x00000000);
209             iv.putInt(4, 0x00000001);
210             iv.putInt(8, mMdlExpectedCounter);
211             byte[] plaintext = null;
212             try {
213                 final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
214                 cipher.init(Cipher.DECRYPT_MODE, mSecretKey, new GCMParameterSpec(128, iv.array()));
215                 plaintext = cipher.doFinal(messageCiphertext);
216             } catch (BadPaddingException
217                     | IllegalBlockSizeException
218                     | InvalidAlgorithmParameterException
219                     | InvalidKeyException
220                     | NoSuchAlgorithmException
221                     | NoSuchPaddingException e) {
222                 e.printStackTrace();
223                 throw new IdentityCredentialException("Error decrypting message", e);
224             }
225             mMdlExpectedCounter += 1;
226             return plaintext;
227         }
228     }
229 }
230