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