blob: acf34bbf634944e27c1b0e56a69687ea14ff7fd6 [file] [log] [blame]
/*
* Copyright (C) 2022 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 com.android.remoteprovisioner.unittest;
import static android.security.keystore.KeyProperties.KEY_ALGORITHM_EC;
import static android.security.keystore.KeyProperties.PURPOSE_SIGN;
import static com.android.remoteprovisioner.unittest.Utils.CURVE_ED25519;
import static com.android.remoteprovisioner.unittest.Utils.CURVE_P256;
import static com.android.remoteprovisioner.unittest.Utils.generateEcdsaKeyPair;
import static com.android.remoteprovisioner.unittest.Utils.getP256PubKeyFromBytes;
import static com.android.remoteprovisioner.unittest.Utils.signPublicKey;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import android.hardware.security.keymint.DeviceInfo;
import android.hardware.security.keymint.ProtectedData;
import android.hardware.security.keymint.SecurityLevel;
import android.os.ServiceManager;
import android.platform.test.annotations.Presubmit;
import android.security.keystore.KeyGenParameterSpec;
import android.security.remoteprovisioning.IRemoteProvisioning;
import android.security.remoteprovisioning.ImplInfo;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.runner.AndroidJUnit4;
import com.android.remoteprovisioner.CborUtils;
import com.android.remoteprovisioner.ProvisionerMetrics;
import com.android.remoteprovisioner.SystemInterface;
import com.android.remoteprovisioner.X509Utils;
import com.google.crypto.tink.subtle.EllipticCurves;
import com.google.crypto.tink.subtle.Hkdf;
import com.google.crypto.tink.subtle.X25519;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import co.nstant.in.cbor.CborBuilder;
import co.nstant.in.cbor.CborDecoder;
import co.nstant.in.cbor.CborEncoder;
import co.nstant.in.cbor.model.Array;
import co.nstant.in.cbor.model.ByteString;
import co.nstant.in.cbor.model.DataItem;
import co.nstant.in.cbor.model.MajorType;
import co.nstant.in.cbor.model.Map;
import co.nstant.in.cbor.model.NegativeInteger;
import co.nstant.in.cbor.model.UnsignedInteger;
@RunWith(AndroidJUnit4.class)
public class SystemInterfaceTest {
private static final String SERVICE = "android.security.remoteprovisioning";
private IRemoteProvisioning mBinder;
private ImplInfo[] mInfo;
@Before
public void setUp() throws Exception {
mBinder =
IRemoteProvisioning.Stub.asInterface(ServiceManager.getService(SERVICE));
assertNotNull(mBinder);
mInfo = mBinder.getImplementationInfo();
mBinder.deleteAllKeys();
}
@After
public void tearDown() throws Exception {
mBinder.deleteAllKeys();
}
private byte[] generateEekChain(int curve, byte[] eek) throws Exception {
if (curve == Utils.CURVE_ED25519) {
com.google.crypto.tink.subtle.Ed25519Sign.KeyPair kp =
com.google.crypto.tink.subtle.Ed25519Sign.KeyPair.newKeyPair();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
new CborEncoder(baos).encode(new CborBuilder()
.addArray()
.add(Utils.encodeAndSignSign1Ed25519(
Utils.encodeEd25519PubKey(kp.getPublicKey()), kp.getPrivateKey()))
.add(Utils.encodeAndSignSign1Ed25519(
Utils.encodeX25519PubKey(eek), kp.getPrivateKey()))
.end()
.build());
return baos.toByteArray();
} else if (curve == Utils.CURVE_P256) { // P256
// Root
KeyPair ecdsaKeyPair = generateEcdsaKeyPair();
ECPublicKey pubKey = (ECPublicKey) ecdsaKeyPair.getPublic();
ECPrivateKey privKey = (ECPrivateKey) ecdsaKeyPair.getPrivate();
byte[] pubKeyBytes = Utils.getBytesFromP256PublicKey(pubKey);
byte[] pubx = new byte[32];
byte[] puby = new byte[32];
System.arraycopy(pubKeyBytes, 0, pubx, 0, 32);
System.arraycopy(pubKeyBytes, 32, puby, 0, 32);
BigInteger priv = privKey.getS();
byte[] privBytes = priv.toByteArray();
byte[] signingKey = new byte[32];
if (privBytes.length <= 32) {
System.arraycopy(privBytes, 0, signingKey, 32
- privBytes.length, privBytes.length);
} else if (privBytes.length == 33 && privBytes[0] == 0) {
System.arraycopy(privBytes, 1, signingKey, 0, 32);
} else {
throw new IllegalStateException("EC private key value is too large");
}
byte[] eekPubX = new byte[32];
byte[] eekPubY = new byte[32];
System.arraycopy(eek, 0, eekPubX, 0, 32);
System.arraycopy(eek, 32, eekPubY, 0, 32);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
new CborEncoder(baos).encode(new CborBuilder()
.addArray()
.add(Utils.encodeAndSignSign1Ecdsa256(
Utils.encodeP256PubKey(pubx, puby, false), signingKey))
.add(Utils.encodeAndSignSign1Ecdsa256(
Utils.encodeP256PubKey(eekPubX, eekPubY, true), signingKey))
.end()
.build());
return baos.toByteArray();
} else {
Assert.fail("Unsupported curve: " + curve);
}
return null;
}
@Presubmit
@Test
public void testGenerateCSR() throws Exception {
for (int i = 0; i < mInfo.length; i++) {
if (mInfo[i].supportedCurve == 0) {
continue;
}
ProvisionerMetrics metrics = ProvisionerMetrics.createOutOfKeysAttemptMetrics(
ApplicationProvider.getApplicationContext(), mInfo[i].secLevel);
DeviceInfo deviceInfo = new DeviceInfo();
ProtectedData encryptedBundle = new ProtectedData();
byte[] eekChain = null;
byte[] eekPub;
if (mInfo[i].supportedCurve == CborUtils.EC_CURVE_P256) {
KeyPair eekEcdsaKeyPair = generateEcdsaKeyPair();
ECPublicKey eekPubKey = (ECPublicKey) eekEcdsaKeyPair.getPublic();
eekPub = Utils.getBytesFromP256PublicKey(eekPubKey);
eekChain = generateEekChain(CURVE_P256, eekPub);
} else if (mInfo[i].supportedCurve == CborUtils.EC_CURVE_25519) {
eekPub = new byte[32];
new Random().nextBytes(eekPub);
eekChain = generateEekChain(CURVE_ED25519, eekPub);
} else {
Assert.fail("Unsupported curve: " + mInfo[i].supportedCurve);
}
assertNotNull(eekChain);
byte[] bundle =
SystemInterface.generateCsr(true /* testMode */, 0 /* numKeys */,
mInfo[i].secLevel,
eekChain,
new byte[]{0x02}, encryptedBundle,
deviceInfo, mBinder, metrics);
// encryptedBundle should contain a COSE_Encrypt message
ByteArrayInputStream bais = new ByteArrayInputStream(encryptedBundle.protectedData);
List<DataItem> dataItems = new CborDecoder(bais).decode();
assertEquals(1, dataItems.size());
assertEquals(MajorType.ARRAY, dataItems.get(0).getMajorType());
Array encMsg = (Array) dataItems.get(0);
assertEquals(4, encMsg.getDataItems().size());
}
}
private static Certificate[] generateKeyStoreKey(String alias, int securityLevel)
throws Exception {
final boolean isStrongboxBacked = (securityLevel == SecurityLevel.STRONGBOX) ? true : false;
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM_EC,
"AndroidKeyStore");
KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(alias, PURPOSE_SIGN)
.setAttestationChallenge("challenge".getBytes())
.setIsStrongBoxBacked(isStrongboxBacked)
.build();
keyPairGenerator.initialize(spec);
keyPairGenerator.generateKeyPair();
Certificate[] certs = keyStore.getCertificateChain(spec.getKeystoreAlias());
keyStore.deleteEntry(alias);
return certs;
}
@Presubmit
@Test
public void testGenerateCSRProvisionAndUseKey() throws Exception {
int numKeys = 10;
int securityLevel;
for (int k = 0; k < mInfo.length; k++) {
if (mInfo[k].supportedCurve == 0) {
continue;
}
ProvisionerMetrics metrics = ProvisionerMetrics.createOutOfKeysAttemptMetrics(
ApplicationProvider.getApplicationContext(), mInfo[k].secLevel);
securityLevel = mInfo[k].secLevel;
DeviceInfo deviceInfo = new DeviceInfo();
ProtectedData encryptedBundle = new ProtectedData();
byte[] eekChain = null;
byte[] eekPub;
if (mInfo[k].supportedCurve == CborUtils.EC_CURVE_P256) {
KeyPair eekEcdsaKeyPair = generateEcdsaKeyPair();
ECPublicKey eekPubKey = (ECPublicKey) eekEcdsaKeyPair.getPublic();
eekPub = Utils.getBytesFromP256PublicKey(eekPubKey);
eekChain = generateEekChain(CURVE_P256, eekPub);
} else if (mInfo[k].supportedCurve == CborUtils.EC_CURVE_25519) {
eekPub = new byte[32];
new Random().nextBytes(eekPub);
eekChain = generateEekChain(CURVE_ED25519, eekPub);
} else {
Assert.fail("Unsupported curve: " + mInfo[k].supportedCurve);
}
assertNotNull(eekChain);
for (int i = 0; i < numKeys; i++) {
mBinder.generateKeyPair(true /* testMode */, securityLevel);
}
byte[] bundle =
SystemInterface.generateCsr(true /* testMode */, numKeys,
securityLevel,
eekChain,
new byte[]{0x02}, encryptedBundle,
deviceInfo, mBinder, metrics);
assertNotNull(bundle);
// The return value of generateCsr should be a COSE_Mac0 message
ByteArrayInputStream bais = new ByteArrayInputStream(bundle);
List<DataItem> dataItems = new CborDecoder(bais).decode();
assertEquals(1, dataItems.size());
assertEquals(MajorType.ARRAY, dataItems.get(0).getMajorType());
Array macMsg = (Array) dataItems.get(0);
assertEquals(4, macMsg.getDataItems().size());
// The payload for the COSE_Mac0 should contain the array of public keys
bais = new ByteArrayInputStream(((ByteString) macMsg.getDataItems().get(2)).getBytes());
List<DataItem> publicKeysArr = new CborDecoder(bais).decode();
assertEquals(1, publicKeysArr.size());
assertEquals(MajorType.ARRAY, publicKeysArr.get(0).getMajorType());
Array publicKeys = (Array) publicKeysArr.get(0);
assertEquals(numKeys, publicKeys.getDataItems().size());
KeyPair rootKeyPair = generateEcdsaKeyPair();
KeyPair intermediateKeyPair = generateEcdsaKeyPair();
X509Certificate[][] certChain = new X509Certificate[numKeys][3];
for (int i = 0; i < numKeys; i++) {
Map publicKey = (Map) publicKeys.getDataItems().get(i);
byte[] xPub = ((ByteString) publicKey.get(new NegativeInteger(-2))).getBytes();
byte[] yPub = ((ByteString) publicKey.get(new NegativeInteger(-3))).getBytes();
assertEquals(xPub.length, 32);
assertEquals(yPub.length, 32);
PublicKey leafKeyToSign = getP256PubKeyFromBytes(xPub, yPub);
certChain[i][0] = signPublicKey(intermediateKeyPair, leafKeyToSign);
certChain[i][1] = signPublicKey(rootKeyPair, intermediateKeyPair.getPublic());
certChain[i][2] = signPublicKey(rootKeyPair, rootKeyPair.getPublic());
ByteArrayOutputStream os = new ByteArrayOutputStream();
for (int j = 0; j < certChain[i].length; j++) {
os.write(certChain[i][j].getEncoded());
}
Instant expiringBy = Instant.now().plusMillis(Duration.ofDays(4).toMillis());
SystemInterface.provisionCertChain(
X509Utils.getAndFormatRawPublicKey(certChain[i][0]),
certChain[i][0].getEncoded() /* leafCert */,
os.toByteArray() /* certChain */,
expiringBy.toEpochMilli() /* validity */,
securityLevel,
mBinder, metrics);
}
// getPoolStatus will clean the key pool before we go to assign a new provisioned key
mBinder.getPoolStatus(0, securityLevel);
Certificate[] provisionedCerts1 = generateKeyStoreKey("alias", securityLevel);
Certificate[] provisionedCerts2 = generateKeyStoreKey("alias2", securityLevel);
assertEquals(4, provisionedCerts1.length);
assertEquals(4, provisionedCerts2.length);
boolean matched = false;
for (int i = 0; i < certChain.length; i++) {
if (Arrays.equals(provisionedCerts1[1].getEncoded(),
certChain[i][0].getEncoded())) {
matched = true;
assertArrayEquals("Second key: j = 0",
provisionedCerts2[1].getEncoded(), certChain[i][0].getEncoded());
for (int j = 1; j < certChain[i].length; j++) {
assertArrayEquals("First key: j = " + j,
provisionedCerts1[j + 1].getEncoded(),
certChain[i][j].getEncoded());
assertArrayEquals("Second key: j = " + j,
provisionedCerts2[j + 1].getEncoded(),
certChain[i][j].getEncoded());
}
}
}
assertTrue(matched);
}
}
private static byte[] extractRecipientKey(Array recipients) {
// Recipients is an Array of recipient Arrays
Map recipientUnprotectedHeaders = (Map) ((Array) recipients.getDataItems().get(0))
.getDataItems().get(1);
Map recipientKeyMap = (Map) recipientUnprotectedHeaders.get(new NegativeInteger(-1));
byte[] pubx = ((ByteString) recipientKeyMap.get(new NegativeInteger(-2))).getBytes();
DataItem di = recipientKeyMap.get(new NegativeInteger(-3));
if (di != null) {
byte[] puby = ((ByteString) di).getBytes();
assertNotNull(puby);
assertEquals(puby.length, 32);
byte[] ret = new byte[64];
System.arraycopy(pubx, 0, ret, 0, 32);
System.arraycopy(puby, 0, ret, 32, 32);
return ret;
}
return pubx;
}
private static byte[] buildKdfContext(byte[] serverPub, byte[] ephemeralPub) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
new CborEncoder(baos).encode(new CborBuilder()
.addArray()
.add(3) // AlgorithmID: AES-GCM 256
.addArray()
.add("client".getBytes("UTF8"))
.add(new byte[0])
.add(ephemeralPub)
.end()
.addArray()
.add("server".getBytes("UTF8"))
.add(new byte[0])
.add(serverPub)
.end()
.addArray()
.add(256) // key length
.add(new byte[0])
.end()
.end()
.build());
return baos.toByteArray();
}
private static byte[] buildEncStructure(byte[] protectedHeaders, byte[] externalAad)
throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
new CborEncoder(baos).encode(new CborBuilder()
.addArray()
.add("Encrypt")
.add(protectedHeaders)
.add(externalAad)
.end()
.build());
return baos.toByteArray();
}
@Presubmit
@Test
public void testDecryptProtectedPayload() throws Exception {
int securityLevel;
for (int i = 0; i < mInfo.length; i++) {
if (mInfo[i].supportedCurve == 0) {
continue;
}
ProvisionerMetrics metrics = ProvisionerMetrics.createOutOfKeysAttemptMetrics(
ApplicationProvider.getApplicationContext(), mInfo[i].secLevel);
securityLevel = mInfo[i].secLevel;
DeviceInfo deviceInfo = new DeviceInfo();
ProtectedData encryptedBundle = new ProtectedData();
byte[] eekPriv = null;
byte[] eekPub = null;
byte[] eekChain = null;
int numKeys = 1;
if (mInfo[i].supportedCurve == CborUtils.EC_CURVE_P256) {
KeyPair eekEcdsaKeyPair = generateEcdsaKeyPair();
ECPublicKey eekPubKey = (ECPublicKey) eekEcdsaKeyPair.getPublic();
ECPrivateKey eekPrivKey = (ECPrivateKey) eekEcdsaKeyPair.getPrivate();
eekPub = Utils.getBytesFromP256PublicKey(eekPubKey);
eekPriv = eekPrivKey.getS().toByteArray();
eekChain = generateEekChain(CURVE_P256, eekPub);
} else if (mInfo[i].supportedCurve == CborUtils.EC_CURVE_25519) {
eekPriv = X25519.generatePrivateKey();
eekPub = X25519.publicFromPrivate(eekPriv);
eekChain = generateEekChain(CURVE_ED25519, eekPub);
} else {
Assert.fail("Unsupported curve: " + mInfo[i].supportedCurve);
}
assertNotNull(eekChain);
assertNotNull(eekPriv);
assertNotNull(eekPub);
mBinder.generateKeyPair(true /* testMode */, securityLevel);
byte[] bundle =
SystemInterface.generateCsr(true /* testMode */, numKeys,
securityLevel,
eekChain,
new byte[]{0x02}, encryptedBundle,
deviceInfo, mBinder, metrics);
ByteArrayInputStream bais = new ByteArrayInputStream(encryptedBundle.protectedData);
List<DataItem> dataItems = new CborDecoder(bais).decode();
// Parse encMsg into components: protected and unprotected headers, payload,
// and recipient
List<DataItem> encMsg = ((Array) dataItems.get(0)).getDataItems();
byte[] protectedHeaders = ((ByteString) encMsg.get(0)).getBytes();
Map unprotectedHeaders = (Map) encMsg.get(1);
byte[] encryptedContent = ((ByteString) encMsg.get(2)).getBytes();
Array recipients = (Array) encMsg.get(3);
byte[] iv = ((ByteString) unprotectedHeaders.get(
new UnsignedInteger(5))).getBytes();
byte[] ephemeralPub = extractRecipientKey(recipients);
byte[] sharedSecret;
if (mInfo[i].supportedCurve == CborUtils.EC_CURVE_P256) {
assertEquals(64, ephemeralPub.length);
ECPrivateKey privKey = EllipticCurves.getEcPrivateKey(
EllipticCurves.CurveType.NIST_P256, eekPriv);
byte[] pubx = new byte[32];
byte[] puby = new byte[32];
System.arraycopy(ephemeralPub, 0, pubx, 0, 32);
System.arraycopy(ephemeralPub, 32, puby, 0, 32);
ECPublicKey pubKey = EllipticCurves.getEcPublicKey(
EllipticCurves.CurveType.NIST_P256, pubx, puby);
sharedSecret = EllipticCurves.computeSharedSecret(privKey, pubKey);
} else { // CborUtils.EC_CURVE_25519
assertEquals(32, ephemeralPub.length);
sharedSecret = X25519.computeSharedSecret(eekPriv, ephemeralPub);
}
byte[] context = buildKdfContext(eekPub, ephemeralPub);
byte[] decryptionKey = Hkdf.computeHkdf("HMACSHA256", sharedSecret,
null /* salt */, context, 32);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(
Cipher.DECRYPT_MODE,
new SecretKeySpec(decryptionKey, "AES"),
new GCMParameterSpec(128 /* iv length */, iv));
cipher.updateAAD(buildEncStructure(protectedHeaders, new byte[0]));
byte[] protectedData = cipher.doFinal(encryptedContent);
bais = new ByteArrayInputStream(protectedData);
List<DataItem> protectedDataArray = new CborDecoder(bais).decode();
assertEquals(1, protectedDataArray.size());
assertEquals(MajorType.ARRAY, protectedDataArray.get(0).getMajorType());
List<DataItem> protectedDataPayload = ((Array) protectedDataArray.get(
0)).getDataItems();
assertTrue(protectedDataPayload.size() == 2 || protectedDataPayload.size() == 3);
}
}
}