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