blob: 4441dd5c2f4ab0f94fde0b14cfca01170d02500b [file] [log] [blame]
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.security.identity.cts;
import static junit.framework.TestCase.assertTrue;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assume.assumeTrue;
import android.content.Context;
import android.security.keystore.KeyProperties;
import android.security.identity.IdentityCredential;
import android.security.identity.IdentityCredentialException;
import android.security.identity.IdentityCredentialStore;
import androidx.test.InstrumentationRegistry;
import org.junit.Test;
import java.nio.ByteBuffer;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.security.spec.ECGenParameterSpec;
import java.util.Collection;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyAgreement;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
// TODO: For better coverage, use different ECDH and HKDF implementations in test code.
public class EphemeralKeyTest {
private static final String TAG = "EphemeralKeyTest";
@Test
public void createEphemeralKey() throws IdentityCredentialException {
assumeTrue("IC HAL is not implemented", Util.isHalImplemented());
Context appContext = InstrumentationRegistry.getTargetContext();
IdentityCredentialStore store = IdentityCredentialStore.getInstance(appContext);
String credentialName = "ephemeralKeyTest";
store.deleteCredentialByName(credentialName);
Collection<X509Certificate> certChain = ProvisioningTest.createCredential(store,
credentialName);
IdentityCredential credential = store.getCredentialByName(credentialName,
IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256);
assertNotNull(credential);
// Check we can get both the public and private keys.
KeyPair ephemeralKeyPair = credential.createEphemeralKeyPair();
assertNotNull(ephemeralKeyPair);
assertTrue(ephemeralKeyPair.getPublic().getEncoded().length > 0);
assertTrue(ephemeralKeyPair.getPrivate().getEncoded().length > 0);
TestReader reader = new TestReader(
IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256,
ephemeralKeyPair.getPublic());
try {
credential.setReaderEphemeralPublicKey(reader.getEphemeralPublicKey());
} catch (InvalidKeyException e) {
e.printStackTrace();
assertTrue(false);
}
// Exchange a couple of messages... this is to test that the nonce/counter
// state works as expected.
for (int n = 0; n < 5; n++) {
// First send a message from the Reader to the Holder...
byte[] messageToHolder = ("Hello Holder! (serial=" + n + ")").getBytes();
byte[] encryptedMessageToHolder = reader.encryptMessageToHolder(messageToHolder);
assertNotEquals(messageToHolder, encryptedMessageToHolder);
byte[] decryptedMessageToHolder = credential.decryptMessageFromReader(
encryptedMessageToHolder);
assertArrayEquals(messageToHolder, decryptedMessageToHolder);
// Then from the Holder to the Reader...
byte[] messageToReader = ("Hello Reader! (serial=" + n + ")").getBytes();
byte[] encryptedMessageToReader = credential.encryptMessageToReader(messageToReader);
assertNotEquals(messageToReader, encryptedMessageToReader);
byte[] decryptedMessageToReader = reader.decryptMessageFromHolder(
encryptedMessageToReader);
assertArrayEquals(messageToReader, decryptedMessageToReader);
}
}
static class TestReader {
@IdentityCredentialStore.Ciphersuite
private int mCipherSuite;
private PublicKey mHolderEphemeralPublicKey;
private KeyPair mEphemeralKeyPair;
private SecretKey mSecretKey;
private SecretKey mReaderSecretKey;
private int mCounter;
private int mMdlExpectedCounter;
private SecureRandom mSecureRandom;
private boolean mRemoteIsReaderDevice;
// This is basically the reader-side of what needs to happen for encryption/decryption
// of messages.. could easily be re-used in an mDL reader application.
TestReader(@IdentityCredentialStore.Ciphersuite int cipherSuite,
PublicKey holderEphemeralPublicKey) throws IdentityCredentialException {
mCipherSuite = cipherSuite;
mHolderEphemeralPublicKey = holderEphemeralPublicKey;
mCounter = 1;
mMdlExpectedCounter = 1;
try {
KeyPairGenerator kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC);
ECGenParameterSpec ecSpec = new ECGenParameterSpec("prime256v1");
kpg.initialize(ecSpec);
mEphemeralKeyPair = kpg.generateKeyPair();
} catch (NoSuchAlgorithmException
| InvalidAlgorithmParameterException e) {
e.printStackTrace();
throw new IdentityCredentialException("Error generating ephemeral key", e);
}
try {
KeyAgreement ka = KeyAgreement.getInstance("ECDH");
ka.init(mEphemeralKeyPair.getPrivate());
ka.doPhase(mHolderEphemeralPublicKey, true);
byte[] sharedSecret = ka.generateSecret();
byte[] salt = new byte[1];
byte[] info = new byte[0];
salt[0] = 0x01;
byte[] derivedKey = Util.computeHkdf("HmacSha256", sharedSecret, salt, info, 32);
mSecretKey = new SecretKeySpec(derivedKey, "AES");
salt[0] = 0x00;
derivedKey = Util.computeHkdf("HmacSha256", sharedSecret, salt, info,32);
mReaderSecretKey = new SecretKeySpec(derivedKey, "AES");
mSecureRandom = new SecureRandom();
} catch (InvalidKeyException
| NoSuchAlgorithmException e) {
e.printStackTrace();
throw new IdentityCredentialException("Error performing key agreement", e);
}
}
PublicKey getEphemeralPublicKey() {
return mEphemeralKeyPair.getPublic();
}
byte[] encryptMessageToHolder(byte[] messagePlaintext) throws IdentityCredentialException {
byte[] messageCiphertext = null;
try {
ByteBuffer iv = ByteBuffer.allocate(12);
iv.putInt(0, 0x00000000);
iv.putInt(4, 0x00000000);
iv.putInt(8, mCounter);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec encryptionParameterSpec = new GCMParameterSpec(128, iv.array());
cipher.init(Cipher.ENCRYPT_MODE, mReaderSecretKey, encryptionParameterSpec);
messageCiphertext = cipher.doFinal(messagePlaintext); // This includes the auth tag
} catch (BadPaddingException
| IllegalBlockSizeException
| NoSuchPaddingException
| InvalidKeyException
| NoSuchAlgorithmException
| InvalidAlgorithmParameterException e) {
e.printStackTrace();
throw new IdentityCredentialException("Error encrypting message", e);
}
mCounter += 1;
return messageCiphertext;
}
byte[] decryptMessageFromHolder(byte[] messageCiphertext)
throws IdentityCredentialException {
ByteBuffer iv = ByteBuffer.allocate(12);
iv.putInt(0, 0x00000000);
iv.putInt(4, 0x00000001);
iv.putInt(8, mMdlExpectedCounter);
byte[] plaintext = null;
try {
final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, mSecretKey, new GCMParameterSpec(128, iv.array()));
plaintext = cipher.doFinal(messageCiphertext);
} catch (BadPaddingException
| IllegalBlockSizeException
| InvalidAlgorithmParameterException
| InvalidKeyException
| NoSuchAlgorithmException
| NoSuchPaddingException e) {
e.printStackTrace();
throw new IdentityCredentialException("Error decrypting message", e);
}
mMdlExpectedCounter += 1;
return plaintext;
}
}
}