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