| /* |
| * Copyright (C) 2021 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.hardware.security.keymint.SecurityLevel.TRUSTED_ENVIRONMENT; |
| import static android.security.keystore.KeyProperties.KEY_ALGORITHM_EC; |
| import static android.security.keystore.KeyProperties.PURPOSE_SIGN; |
| |
| import static org.junit.Assert.assertArrayEquals; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertTrue; |
| |
| import android.content.Context; |
| import android.os.ServiceManager; |
| import android.security.keystore.KeyGenParameterSpec; |
| import android.security.remoteprovisioning.AttestationPoolStatus; |
| import android.security.remoteprovisioning.ImplInfo; |
| import android.security.remoteprovisioning.IRemoteProvisioning; |
| |
| import androidx.test.core.app.ApplicationProvider; |
| import androidx.test.runner.AndroidJUnit4; |
| |
| import com.android.remoteprovisioner.GeekResponse; |
| import com.android.remoteprovisioner.Provisioner; |
| import com.android.remoteprovisioner.ServerInterface; |
| import com.android.remoteprovisioner.SettingsManager; |
| |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.BeforeClass; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import java.security.KeyPairGenerator; |
| import java.security.KeyStore; |
| import java.security.cert.Certificate; |
| import java.time.Duration; |
| import java.util.Arrays; |
| |
| @RunWith(AndroidJUnit4.class) |
| public class ServerToSystemTest { |
| |
| private static final boolean IS_TEST_MODE = false; |
| private static final String SERVICE = "android.security.remoteprovisioning"; |
| |
| private static Context sContext; |
| private static IRemoteProvisioning sBinder; |
| private static int sCurve = 0; |
| |
| private Duration mDuration; |
| |
| private void assertPoolStatus(int total, int attested, |
| int unassigned, int expiring, Duration time) throws Exception { |
| AttestationPoolStatus pool = sBinder.getPoolStatus(time.toMillis(), TRUSTED_ENVIRONMENT); |
| assertEquals(total, pool.total); |
| assertEquals(attested, pool.attested); |
| assertEquals(unassigned, pool.unassigned); |
| assertEquals(expiring, pool.expiring); |
| } |
| |
| private static Certificate[] generateKeyStoreKey(String alias) throws Exception { |
| 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()) |
| .build(); |
| keyPairGenerator.initialize(spec); |
| keyPairGenerator.generateKeyPair(); |
| Certificate[] certs = keyStore.getCertificateChain(spec.getKeystoreAlias()); |
| keyStore.deleteEntry(alias); |
| return certs; |
| } |
| |
| @BeforeClass |
| public static void init() throws Exception { |
| sContext = ApplicationProvider.getApplicationContext(); |
| sBinder = |
| IRemoteProvisioning.Stub.asInterface(ServiceManager.getService(SERVICE)); |
| assertNotNull(sBinder); |
| ImplInfo[] info = sBinder.getImplementationInfo(); |
| for (int i = 0; i < info.length; i++) { |
| if (info[i].secLevel == TRUSTED_ENVIRONMENT) { |
| sCurve = info[i].supportedCurve; |
| break; |
| } |
| } |
| } |
| |
| @Before |
| public void setUp() throws Exception { |
| SettingsManager.clearPreferences(sContext); |
| sBinder.deleteAllKeys(); |
| mDuration = Duration.ofMillis(System.currentTimeMillis()); |
| } |
| |
| @After |
| public void tearDown() throws Exception { |
| SettingsManager.clearPreferences(sContext); |
| sBinder.deleteAllKeys(); |
| } |
| |
| @Test |
| public void testFullRoundTrip() throws Exception { |
| int numTestKeys = 1; |
| assertPoolStatus(0, 0, 0, 0, mDuration); |
| sBinder.generateKeyPair(IS_TEST_MODE, TRUSTED_ENVIRONMENT); |
| assertPoolStatus(numTestKeys, 0, 0, 0, mDuration); |
| GeekResponse geek = ServerInterface.fetchGeek(sContext); |
| assertNotNull(geek); |
| int numProvisioned = |
| Provisioner.provisionCerts(numTestKeys, TRUSTED_ENVIRONMENT, |
| geek.getGeekChain(sCurve), geek.getChallenge(), sBinder, |
| sContext); |
| assertEquals(numTestKeys, numProvisioned); |
| assertPoolStatus(numTestKeys, numTestKeys, numTestKeys, 0, mDuration); |
| // Certificate duration sent back from the server may change, however ~6 months should be |
| // pretty safe. |
| assertPoolStatus(numTestKeys, numTestKeys, numTestKeys, |
| numTestKeys, mDuration.plusDays(180)); |
| } |
| |
| @Test |
| public void testFallback() throws Exception { |
| // Feed a fake URL into the device config to ensure that remote provisioning fails. |
| SettingsManager.setDeviceConfig(sContext, 2 /* extraKeys */, mDuration /* expiringBy */, |
| "Not even a URL" /* url */); |
| int numTestKeys = 1; |
| assertPoolStatus(0, 0, 0, 0, mDuration); |
| Certificate[] fallbackKeyCerts1 = generateKeyStoreKey("test1"); |
| |
| SettingsManager.clearPreferences(sContext); |
| sBinder.generateKeyPair(IS_TEST_MODE, TRUSTED_ENVIRONMENT); |
| GeekResponse geek = ServerInterface.fetchGeek(sContext); |
| int numProvisioned = |
| Provisioner.provisionCerts(numTestKeys, TRUSTED_ENVIRONMENT, |
| geek.getGeekChain(sCurve), geek.getChallenge(), sBinder, |
| sContext); |
| assertEquals(numTestKeys, numProvisioned); |
| assertPoolStatus(numTestKeys, numTestKeys, numTestKeys, 0, mDuration); |
| Certificate[] provisionedKeyCerts = generateKeyStoreKey("test2"); |
| sBinder.deleteAllKeys(); |
| sBinder.generateKeyPair(IS_TEST_MODE, TRUSTED_ENVIRONMENT); |
| |
| SettingsManager.setDeviceConfig(sContext, 2 /* extraKeys */, mDuration /* expiringBy */, |
| "Not even a URL" /* url */); |
| // Even if there is an unsigned key hanging around, fallback should still occur. |
| Certificate[] fallbackKeyCerts2 = generateKeyStoreKey("test3"); |
| // Due to there being no attested keys in the pool, the provisioning service should not |
| // have even attempted to provision more certificates. |
| assertEquals(0, SettingsManager.getFailureCounter(sContext)); |
| assertTrue(fallbackKeyCerts1.length == fallbackKeyCerts2.length); |
| for (int i = 1; i < fallbackKeyCerts1.length; i++) { |
| assertArrayEquals("Cert: " + i, fallbackKeyCerts1[i].getEncoded(), |
| fallbackKeyCerts2[i].getEncoded()); |
| } |
| assertTrue(provisionedKeyCerts.length > 0); |
| // The root certificates should not match. |
| assertFalse("Provisioned and fallback attestation key root certificates match.", |
| Arrays.equals(fallbackKeyCerts1[fallbackKeyCerts1.length - 1].getEncoded(), |
| provisionedKeyCerts[provisionedKeyCerts.length - 1].getEncoded())); |
| } |
| } |