1 /* 2 * Copyright (C) 2021 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 com.android.remoteprovisioner.unittest; 18 19 import static android.hardware.security.keymint.SecurityLevel.TRUSTED_ENVIRONMENT; 20 import static android.security.keystore.KeyProperties.KEY_ALGORITHM_EC; 21 import static android.security.keystore.KeyProperties.PURPOSE_SIGN; 22 import static android.security.keystore.KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT; 23 24 import static org.junit.Assert.assertArrayEquals; 25 import static org.junit.Assert.assertEquals; 26 import static org.junit.Assert.assertFalse; 27 import static org.junit.Assert.assertNotNull; 28 import static org.junit.Assert.assertTrue; 29 import static org.junit.Assert.fail; 30 31 import android.Manifest; 32 import android.app.ActivityThread; 33 import android.content.Context; 34 import android.net.ConnectivityManager; 35 import android.net.NetworkInfo; 36 import android.os.ServiceManager; 37 import android.os.SystemProperties; 38 import android.security.GenerateRkpKey; 39 import android.security.KeyStoreException; 40 import android.security.NetworkSecurityPolicy; 41 import android.security.keystore.KeyGenParameterSpec; 42 import android.security.remoteprovisioning.AttestationPoolStatus; 43 import android.security.remoteprovisioning.IRemoteProvisioning; 44 import android.security.remoteprovisioning.ImplInfo; 45 import android.system.keystore2.ResponseCode; 46 import android.util.Base64; 47 import android.util.Log; 48 49 import androidx.test.core.app.ApplicationProvider; 50 import androidx.test.runner.AndroidJUnit4; 51 import androidx.work.ListenableWorker; 52 import androidx.work.testing.TestWorkerBuilder; 53 54 import com.android.bedstead.nene.TestApis; 55 import com.android.bedstead.nene.permissions.PermissionContext; 56 import com.android.remoteprovisioner.GeekResponse; 57 import com.android.remoteprovisioner.PeriodicProvisioner; 58 import com.android.remoteprovisioner.Provisioner; 59 import com.android.remoteprovisioner.ProvisionerMetrics; 60 import com.android.remoteprovisioner.RemoteProvisioningException; 61 import com.android.remoteprovisioner.ServerInterface; 62 import com.android.remoteprovisioner.SettingsManager; 63 64 import org.junit.After; 65 import org.junit.Assert; 66 import org.junit.Assume; 67 import org.junit.Before; 68 import org.junit.BeforeClass; 69 import org.junit.Test; 70 import org.junit.runner.RunWith; 71 72 import java.io.ByteArrayInputStream; 73 import java.io.IOException; 74 import java.security.KeyPairGenerator; 75 import java.security.KeyStore; 76 import java.security.ProviderException; 77 import java.security.cert.Certificate; 78 import java.time.Duration; 79 import java.util.Arrays; 80 import java.util.concurrent.Executors; 81 82 import fi.iki.elonen.NanoHTTPD; 83 84 @RunWith(AndroidJUnit4.class) 85 public class ServerToSystemTest { 86 87 private static final String Tag = "ServerToSystemTest"; 88 private static final boolean IS_TEST_MODE = false; 89 private static final String SERVICE = "android.security.remoteprovisioning"; 90 private static final String RKP_ONLY_PROP = "remote_provisioning.tee.rkp_only"; 91 92 private static final byte[] GEEK_RESPONSE = Base64.decode( 93 "g4KCAYOEQ6EBJqBYTaUBAgMmIAEhWCD3FIrbl/TMU+/SZBHE43UfZh+kcQxsz/oJRoB0h1TyrSJY" 94 + "IF5/W/bs5PYZzP8TN/0PociT2xgGdsRd5tdqd4bDLa+PWEAvl45C+74HLZVHhUeTQLAf1JtHpMRE" 95 + "qfKhB4cQx5/LEfS/n+g74Oc0TBX8e8N+MwX00TQ87QIEYHoV4HnTiv8khEOhASagWE2lAQIDJiAB" 96 + "IVggUYCsz4+WjOwPUOGpG7eQhjSL48OsZQJNtPYxDghGMjkiWCBU65Sd/ra05HM6JU4vH52dvfpm" 97 + "wRGL6ZaMQ+Qw9tp2q1hAmDj7NDpl23OYsSeiFXTyvgbnjSJO3fC/wgF0xLcpayQctdjSZvpE7/Uw" 98 + "LAR07ejGYNrOn1ZXJ3Qh096Tj+O4zYRDoQEmoFhxpgECAlggg5/4/RAcEp+SQcdbjeRO9BkTmscb" 99 + "bacOlfJkU12nHcEDOBggASFYIBakUhJjs4ZWUNjf8qCofbzZbqdoYOqMXPGT5ZcZDazeIlggib7M" 100 + "bD9esDk0r5e6ONEWHaHMHWTTjEhO+HKBGzs+Me5YQPrazy2rpTAMc8Xlq0mSWWBE+sTyM+UEsmwZ" 101 + "ZOkc42Q7NIYAZS313a+qAcmvg8lO+FqU6GWTUeMYHjmAp2lLM82CAoOEQ6EBJ6BYKqQBAQMnIAYh" 102 + "WCCZue7dXuRS9oXGTGLcPmGrV0h9dTcprXaAMtKzy2NY2VhAHiIIS6S3pMjXTgMO/rivFEynO2+l" 103 + "zdzaecYrZP6ZOa9254D6ZgCFDQeYKqyRXKclFEkGNHXKiid62eNaSesCA4RDoQEnoFgqpAEBAycg" 104 + "BiFYIOovhQ6eagxc973Z+igyv9pV6SCiUQPJA5MYzqAVKezRWECCa8ddpjZXt8dxEq0cwmqzLCMq" 105 + "3RQwy4IUtonF0x4xu7hQIUpJTbqRDG8zTYO8WCsuhNvFWQ+YYeLB6ony0K4EhEOhASegWE6lAQEC" 106 + "WCBvktEEbXHYp46I2NFWgV+W0XiD5jAbh+2/INFKO/5qLgM4GCAEIVggtl0cS5qDOp21FVk3oSb7" 107 + "D9/nnKwB1aTsyDopAIhYJTlYQICyn9Aynp1K/rAl8sLSImhGxiCwqugWrGShRYObzElUJX+rFgVT" 108 + "8L01k/PGu1lOXvneIQcUo7ako4uPgpaWugNYHQAAAYBINcxrASC0rWP9VTSO7LdABvcdkv7W2vh+" 109 + "onV0aW1lX3RvX3JlZnJlc2hfaG91cnMYSHgabnVtX2V4dHJhX2F0dGVzdGF0aW9uX2tleXMU", 110 Base64.DEFAULT); 111 112 // Same as GEEK_RESPONSE, but the "num_extra_attestation_keys" value is 0, disabling RKP. 113 private static final byte[] GEEK_RESPONSE_RKP_DISABLED = Base64.decode( 114 "g4KCAYOEQ6EBJqBYTaUBAgMmIAEhWCD3FIrbl/TMU+/SZBHE43UfZh+kcQxsz/oJRoB0h1TyrSJY" 115 + "IF5/W/bs5PYZzP8TN/0PociT2xgGdsRd5tdqd4bDLa+PWEAvl45C+74HLZVHhUeTQLAf1JtHpMRE" 116 + "qfKhB4cQx5/LEfS/n+g74Oc0TBX8e8N+MwX00TQ87QIEYHoV4HnTiv8khEOhASagWE2lAQIDJiAB" 117 + "IVggUYCsz4+WjOwPUOGpG7eQhjSL48OsZQJNtPYxDghGMjkiWCBU65Sd/ra05HM6JU4vH52dvfpm" 118 + "wRGL6ZaMQ+Qw9tp2q1hAmDj7NDpl23OYsSeiFXTyvgbnjSJO3fC/wgF0xLcpayQctdjSZvpE7/Uw" 119 + "LAR07ejGYNrOn1ZXJ3Qh096Tj+O4zYRDoQEmoFhxpgECAlggg5/4/RAcEp+SQcdbjeRO9BkTmscb" 120 + "bacOlfJkU12nHcEDOBggASFYIBakUhJjs4ZWUNjf8qCofbzZbqdoYOqMXPGT5ZcZDazeIlggib7M" 121 + "bD9esDk0r5e6ONEWHaHMHWTTjEhO+HKBGzs+Me5YQPrazy2rpTAMc8Xlq0mSWWBE+sTyM+UEsmwZ" 122 + "ZOkc42Q7NIYAZS313a+qAcmvg8lO+FqU6GWTUeMYHjmAp2lLM82CAoOEQ6EBJ6BYKqQBAQMnIAYh" 123 + "WCCZue7dXuRS9oXGTGLcPmGrV0h9dTcprXaAMtKzy2NY2VhAHiIIS6S3pMjXTgMO/rivFEynO2+l" 124 + "zdzaecYrZP6ZOa9254D6ZgCFDQeYKqyRXKclFEkGNHXKiid62eNaSesCA4RDoQEnoFgqpAEBAycg" 125 + "BiFYIOovhQ6eagxc973Z+igyv9pV6SCiUQPJA5MYzqAVKezRWECCa8ddpjZXt8dxEq0cwmqzLCMq" 126 + "3RQwy4IUtonF0x4xu7hQIUpJTbqRDG8zTYO8WCsuhNvFWQ+YYeLB6ony0K4EhEOhASegWE6lAQEC" 127 + "WCBvktEEbXHYp46I2NFWgV+W0XiD5jAbh+2/INFKO/5qLgM4GCAEIVggtl0cS5qDOp21FVk3oSb7" 128 + "D9/nnKwB1aTsyDopAIhYJTlYQICyn9Aynp1K/rAl8sLSImhGxiCwqugWrGShRYObzElUJX+rFgVT" 129 + "8L01k/PGu1lOXvneIQcUo7ako4uPgpaWugNYHQAAAYBINcxrASC0rWP9VTSO7LdABvcdkv7W2vh+" 130 + "onV0aW1lX3RvX3JlZnJlc2hfaG91cnMYSHgabnVtX2V4dHJhX2F0dGVzdGF0aW9uX2tleXMA", 131 Base64.DEFAULT); 132 133 private static Context sContext; 134 private static IRemoteProvisioning sBinder; 135 private static int sCurve = 0; 136 137 private Duration mDuration; 138 139 // Helper class that sets rkp_only to true if it's not already set, then restores the state on 140 // close. Intended to be used in a try expression: try (RkpOnlyContext c = new RkpOnlyContext()) 141 private static class ForceRkpOnlyContext implements AutoCloseable { 142 private final boolean mOriginalPropertyValue; 143 ForceRkpOnlyContext()144 ForceRkpOnlyContext() { 145 mOriginalPropertyValue = SystemProperties.getBoolean(RKP_ONLY_PROP, false); 146 if (!mOriginalPropertyValue) { 147 SystemProperties.set(RKP_ONLY_PROP, "true"); 148 } 149 } 150 151 @Override close()152 public void close() { 153 if (!mOriginalPropertyValue) { 154 SystemProperties.set(RKP_ONLY_PROP, "false"); 155 } 156 } 157 } 158 assertPoolStatus(int total, int attested, int unassigned, int expiring, Duration time)159 private void assertPoolStatus(int total, int attested, 160 int unassigned, int expiring, Duration time) throws Exception { 161 AttestationPoolStatus pool = sBinder.getPoolStatus(time.toMillis(), TRUSTED_ENVIRONMENT); 162 assertEquals(total, pool.total); 163 assertEquals(attested, pool.attested); 164 assertEquals(unassigned, pool.unassigned); 165 assertEquals(expiring, pool.expiring); 166 } 167 generateKeyStoreKey(String alias)168 private static Certificate[] generateKeyStoreKey(String alias) throws Exception { 169 KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); 170 keyStore.load(null); 171 if (keyStore.containsAlias(alias)) { 172 keyStore.deleteEntry(alias); 173 } 174 KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM_EC, 175 "AndroidKeyStore"); 176 KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(alias, PURPOSE_SIGN) 177 .setAttestationChallenge("challenge".getBytes()) 178 .build(); 179 keyPairGenerator.initialize(spec); 180 keyPairGenerator.generateKeyPair(); 181 Certificate[] certs = keyStore.getCertificateChain(spec.getKeystoreAlias()); 182 keyStore.deleteEntry(alias); 183 return certs; 184 } 185 186 @BeforeClass init()187 public static void init() throws Exception { 188 sContext = ApplicationProvider.getApplicationContext(); 189 sBinder = 190 IRemoteProvisioning.Stub.asInterface(ServiceManager.getService(SERVICE)); 191 assertNotNull(sBinder); 192 ImplInfo[] info = sBinder.getImplementationInfo(); 193 for (int i = 0; i < info.length; i++) { 194 if (info[i].secLevel == TRUSTED_ENVIRONMENT) { 195 sCurve = info[i].supportedCurve; 196 break; 197 } 198 } 199 } 200 201 @Before setUp()202 public void setUp() throws Exception { 203 SettingsManager.clearPreferences(sContext); 204 sBinder.deleteAllKeys(); 205 mDuration = Duration.ofMillis(System.currentTimeMillis()); 206 } 207 208 @After tearDown()209 public void tearDown() throws Exception { 210 SettingsManager.clearPreferences(sContext); 211 sBinder.deleteAllKeys(); 212 } 213 214 @Test testFullRoundTrip()215 public void testFullRoundTrip() throws Exception { 216 ProvisionerMetrics metrics = ProvisionerMetrics.createScheduledAttemptMetrics(sContext); 217 int numTestKeys = 1; 218 assertPoolStatus(0, 0, 0, 0, mDuration); 219 sBinder.generateKeyPair(IS_TEST_MODE, TRUSTED_ENVIRONMENT); 220 assertPoolStatus(numTestKeys, 0, 0, 0, mDuration); 221 GeekResponse geek = ServerInterface.fetchGeek(sContext, metrics); 222 assertEquals(0, SettingsManager.getErrDataBudgetConsumed(sContext)); 223 assertNotNull(geek); 224 int numProvisioned = 225 Provisioner.provisionCerts(numTestKeys, TRUSTED_ENVIRONMENT, 226 geek.getGeekChain(sCurve), geek.getChallenge(), sBinder, 227 sContext, metrics); 228 assertEquals(0, SettingsManager.getErrDataBudgetConsumed(sContext)); 229 assertEquals(numTestKeys, numProvisioned); 230 assertPoolStatus(numTestKeys, numTestKeys, numTestKeys, 0, mDuration); 231 // Certificate duration sent back from the server may change, however ~6 months should be 232 // pretty safe. 233 assertPoolStatus(numTestKeys, numTestKeys, numTestKeys, 234 numTestKeys, mDuration.plusDays(180)); 235 } 236 237 @Test testPeriodicProvisionerRoundTrip()238 public void testPeriodicProvisionerRoundTrip() throws Exception { 239 PeriodicProvisioner provisioner = TestWorkerBuilder.from( 240 sContext, 241 PeriodicProvisioner.class, 242 Executors.newSingleThreadExecutor()).build(); 243 assertEquals(provisioner.doWork(), ListenableWorker.Result.success()); 244 AttestationPoolStatus pool = sBinder.getPoolStatus(mDuration.toMillis(), 245 TRUSTED_ENVIRONMENT); 246 assertTrue("Pool must not be empty", pool.total > 0); 247 assertEquals("All keys must be attested", pool.total, pool.attested); 248 assertEquals("Nobody should have consumed keys yet", pool.total, pool.unassigned); 249 assertEquals("All keys should be freshly generated", 0, pool.expiring); 250 } 251 252 @Test testPeriodicProvisionerNoop()253 public void testPeriodicProvisionerNoop() throws Exception { 254 // Similar to the PeriodicProvisioner round trip, except first we actually populate the 255 // key pool to ensure that the PeriodicProvisioner just noops. 256 PeriodicProvisioner provisioner = TestWorkerBuilder.from( 257 sContext, 258 PeriodicProvisioner.class, 259 Executors.newSingleThreadExecutor()).build(); 260 assertEquals(provisioner.doWork(), ListenableWorker.Result.success()); 261 final AttestationPoolStatus pool = sBinder.getPoolStatus(mDuration.toMillis(), 262 TRUSTED_ENVIRONMENT); 263 assertTrue("Pool must not be empty", pool.total > 0); 264 assertEquals("All keys must be attested", pool.total, pool.attested); 265 assertEquals("Nobody should have consumed keys yet", pool.total, pool.unassigned); 266 assertEquals("All keys should be freshly generated", 0, pool.expiring); 267 268 // The metrics host test will perform additional validation by ensuring correct metrics 269 // are recorded. 270 assertEquals(provisioner.doWork(), ListenableWorker.Result.success()); 271 assertPoolStatus(pool.total, pool.attested, pool.unassigned, pool.expiring, mDuration); 272 } 273 274 @Test testPeriodicProvisionerDataBudgetEmpty()275 public void testPeriodicProvisionerDataBudgetEmpty() throws Exception { 276 // Check the data budget in order to initialize a rolling window. 277 assertTrue(SettingsManager.hasErrDataBudget(sContext, null /* curTime */)); 278 SettingsManager.consumeErrDataBudget(sContext, SettingsManager.FAILURE_DATA_USAGE_MAX); 279 280 PeriodicProvisioner provisioner = TestWorkerBuilder.from( 281 sContext, 282 PeriodicProvisioner.class, 283 Executors.newSingleThreadExecutor()).build(); 284 assertEquals(provisioner.doWork(), ListenableWorker.Result.failure()); 285 AttestationPoolStatus pool = sBinder.getPoolStatus(mDuration.toMillis(), 286 TRUSTED_ENVIRONMENT); 287 assertTrue("Keys should have been generated", pool.total > 0); 288 assertEquals("No keys should be attested", 0, pool.attested); 289 assertEquals("No keys should have been assigned", 0, pool.unassigned); 290 assertEquals("No keys can possibly be expiring yet", 0, pool.expiring); 291 } 292 293 @Test testPeriodicProvisionerProvisioningDisabled()294 public void testPeriodicProvisionerProvisioningDisabled() throws Exception { 295 // We need to run an HTTP server that returns a config indicating no keys are needed 296 final NanoHTTPD server = new NanoHTTPD("localhost", 0) { 297 @Override 298 public Response serve(IHTTPSession session) { 299 consumeRequestBody((HTTPSession) session); 300 if (session.getUri().contains(":fetchEekChain")) { 301 return newFixedLengthResponse(Response.Status.OK, "application/cbor", 302 new ByteArrayInputStream(GEEK_RESPONSE_RKP_DISABLED), 303 GEEK_RESPONSE_RKP_DISABLED.length); 304 } 305 Assert.fail("Unexpected HTTP request: " + session.getUri()); 306 return null; 307 } 308 309 void consumeRequestBody(HTTPSession session) { 310 try { 311 session.getInputStream().readNBytes((int) session.getBodySize()); 312 } catch (IOException e) { 313 Assert.fail("Error reading request bytes: " + e.toString()); 314 } 315 } 316 }; 317 server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false); 318 319 final boolean cleartextPolicy = 320 NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted(); 321 NetworkSecurityPolicy.getInstance().setCleartextTrafficPermitted(true); 322 SettingsManager.setDeviceConfig(sContext, 1 /* extraKeys */, mDuration /* expiringBy */, 323 "http://localhost:" + server.getListeningPort() + "/"); 324 325 PeriodicProvisioner provisioner = TestWorkerBuilder.from( 326 sContext, 327 PeriodicProvisioner.class, 328 Executors.newSingleThreadExecutor()).build(); 329 assertEquals(provisioner.doWork(), ListenableWorker.Result.success()); 330 assertPoolStatus(0, 0, 0, 0, mDuration); 331 } 332 333 @Test testFallback()334 public void testFallback() throws Exception { 335 ProvisionerMetrics metrics = ProvisionerMetrics.createScheduledAttemptMetrics(sContext); 336 Assume.assumeFalse( 337 "Skipping test as this system does not support fallback from RKP keys", 338 SystemProperties.getBoolean(RKP_ONLY_PROP, false)); 339 340 // Feed a fake URL into the device config to ensure that remote provisioning fails. 341 SettingsManager.setDeviceConfig(sContext, 1 /* extraKeys */, mDuration /* expiringBy */, 342 "Not even a URL" /* url */); 343 int numTestKeys = 1; 344 assertPoolStatus(0, 0, 0, 0, mDuration); 345 // Note that due to the GenerateRkpKeyService, this call to generate an attested key will 346 // still cause the service to generate keys up the number specified as `extraKeys` in the 347 // `setDeviceConfig`. This will provide us 1 key for the followup call to provisionCerts. 348 Certificate[] fallbackKeyCerts1 = generateKeyStoreKey("test1"); 349 350 SettingsManager.clearPreferences(sContext); 351 GeekResponse geek = ServerInterface.fetchGeek(sContext, metrics); 352 int numProvisioned = 353 Provisioner.provisionCerts(numTestKeys, TRUSTED_ENVIRONMENT, 354 geek.getGeekChain(sCurve), geek.getChallenge(), sBinder, 355 sContext, metrics); 356 assertEquals(numTestKeys, numProvisioned); 357 assertPoolStatus(numTestKeys, numTestKeys, numTestKeys, 0, mDuration); 358 Certificate[] provisionedKeyCerts = generateKeyStoreKey("test2"); 359 sBinder.deleteAllKeys(); 360 sBinder.generateKeyPair(IS_TEST_MODE, TRUSTED_ENVIRONMENT); 361 362 SettingsManager.setDeviceConfig(sContext, 2 /* extraKeys */, mDuration /* expiringBy */, 363 "Not even a URL" /* url */); 364 // Even if there is an unsigned key hanging around, fallback should still occur. 365 Certificate[] fallbackKeyCerts2 = generateKeyStoreKey("test3"); 366 assertTrue(fallbackKeyCerts1.length == fallbackKeyCerts2.length); 367 for (int i = 1; i < fallbackKeyCerts1.length; i++) { 368 assertArrayEquals("Cert: " + i, fallbackKeyCerts1[i].getEncoded(), 369 fallbackKeyCerts2[i].getEncoded()); 370 } 371 assertTrue(provisionedKeyCerts.length > 0); 372 // Match against the batch provisioned key, which should be the second entry in the array. 373 assertFalse("Provisioned and fallback attestation key intermediate certificates match.", 374 Arrays.equals(fallbackKeyCerts1[1].getEncoded(), 375 provisionedKeyCerts[1].getEncoded())); 376 } 377 378 @Test testDataBudgetEmptyFetchGeek()379 public void testDataBudgetEmptyFetchGeek() throws Exception { 380 // Check the data budget in order to initialize a rolling window. 381 assertTrue(SettingsManager.hasErrDataBudget(sContext, null /* curTime */)); 382 SettingsManager.consumeErrDataBudget(sContext, SettingsManager.FAILURE_DATA_USAGE_MAX); 383 ProvisionerMetrics metrics = ProvisionerMetrics.createScheduledAttemptMetrics(sContext); 384 try { 385 ServerInterface.fetchGeek(sContext, metrics); 386 fail("Network transaction should not have proceeded."); 387 } catch (RemoteProvisioningException e) { 388 return; 389 } 390 } 391 392 @Test testDataBudgetEmptySignCerts()393 public void testDataBudgetEmptySignCerts() throws Exception { 394 // Check the data budget in order to initialize a rolling window. 395 assertTrue(SettingsManager.hasErrDataBudget(sContext, null /* curTime */)); 396 SettingsManager.consumeErrDataBudget(sContext, SettingsManager.FAILURE_DATA_USAGE_MAX); 397 ProvisionerMetrics metrics = ProvisionerMetrics.createScheduledAttemptMetrics(sContext); 398 try { 399 ServerInterface.requestSignedCertificates(sContext, null, null, metrics); 400 fail("Network transaction should not have proceeded."); 401 } catch (RemoteProvisioningException e) { 402 return; 403 } 404 } 405 406 @Test testDataBudgetEmptyCallGenerateRkpKeyService()407 public void testDataBudgetEmptyCallGenerateRkpKeyService() throws Exception { 408 // Check the data budget in order to initialize a rolling window. 409 assertTrue(SettingsManager.hasErrDataBudget(sContext, null /* curTime */)); 410 SettingsManager.consumeErrDataBudget(sContext, SettingsManager.FAILURE_DATA_USAGE_MAX); 411 GenerateRkpKey keyGen = new GenerateRkpKey(ActivityThread.currentApplication()); 412 keyGen.notifyKeyGenerated(SECURITY_LEVEL_TRUSTED_ENVIRONMENT); 413 // Nothing to check here. This test is primarily used by the Metrics host test to 414 // validate that correct metrics are logged. 415 } 416 417 @Test testGenerateKeyRkpOnly()418 public void testGenerateKeyRkpOnly() throws Exception { 419 try (ForceRkpOnlyContext c = new ForceRkpOnlyContext()) { 420 Certificate[] certs = generateKeyStoreKey("this-better-work"); 421 assertTrue(certs.length > 0); 422 } 423 } 424 425 @Test testRetryableRkpError()426 public void testRetryableRkpError() throws Exception { 427 try (ForceRkpOnlyContext c = new ForceRkpOnlyContext()) { 428 SettingsManager.setDeviceConfig(sContext, 1 /* extraKeys */, mDuration /* expiringBy */, 429 "Not even a URL" /* url */); 430 generateKeyStoreKey("should-never-succeed"); 431 Assert.fail("Expected a keystore exception"); 432 } catch (ProviderException e) { 433 Assert.assertTrue(e.getCause() instanceof KeyStoreException); 434 KeyStoreException keyStoreException = (KeyStoreException) e.getCause(); 435 Assert.assertEquals(ResponseCode.OUT_OF_KEYS, keyStoreException.getErrorCode()); 436 Assert.assertTrue(keyStoreException.isTransientFailure()); 437 Assert.assertEquals(KeyStoreException.RETRY_WITH_EXPONENTIAL_BACKOFF, 438 keyStoreException.getRetryPolicy()); 439 } 440 } 441 setAirplaneMode(boolean enable)442 private void setAirplaneMode(boolean enable) throws Exception { 443 ConnectivityManager cm = sContext.getSystemService(ConnectivityManager.class); 444 try (PermissionContext c = TestApis.permissions().withPermission( 445 Manifest.permission.NETWORK_SETTINGS)) { 446 cm.setAirplaneMode(enable); 447 448 // Now wait a "reasonable" time for the network to go down 449 for (int i = 0; i < 100; ++i) { 450 NetworkInfo networkInfo = cm.getActiveNetworkInfo(); 451 Log.e(Tag, "Checking active network... " + networkInfo); 452 if (enable) { 453 if (networkInfo == null || !networkInfo.isConnected()) { 454 Log.e(Tag, "Successfully disconnected from to the network."); 455 return; 456 } 457 } else if (networkInfo != null && networkInfo.isConnected()) { 458 Log.e(Tag, "Successfully reconnected to the network."); 459 return; 460 } 461 Thread.sleep(300); 462 } 463 } 464 Assert.fail("Failed to successfully " + (enable ? "enable" : "disable") + " airplane mode"); 465 } 466 467 @Test testRetryWithoutNetworkTee()468 public void testRetryWithoutNetworkTee() throws Exception { 469 setAirplaneMode(true); 470 try (ForceRkpOnlyContext c = new ForceRkpOnlyContext()) { 471 assertPoolStatus(0, 0, 0, 0, mDuration); 472 generateKeyStoreKey("should-never-succeed"); 473 Assert.fail("Expected a keystore exception"); 474 } catch (ProviderException e) { 475 Assert.assertTrue(e.getCause() instanceof KeyStoreException); 476 KeyStoreException keyStoreException = (KeyStoreException) e.getCause(); 477 Assert.assertEquals(ResponseCode.OUT_OF_KEYS, keyStoreException.getErrorCode()); 478 Assert.assertTrue(keyStoreException.isTransientFailure()); 479 Assert.assertEquals(KeyStoreException.RETRY_WHEN_CONNECTIVITY_AVAILABLE, 480 keyStoreException.getRetryPolicy()); 481 } finally { 482 setAirplaneMode(false); 483 } 484 } 485 486 @Test testRetryNeverWhenDeviceNotRegistered()487 public void testRetryNeverWhenDeviceNotRegistered() throws Exception { 488 final NanoHTTPD server = new NanoHTTPD("localhost", 0) { 489 @Override 490 public Response serve(IHTTPSession session) { 491 // We must consume all bytes in the request, else they get interpreted as a 492 // sepearate (bad) request by the HTTP server. 493 consumeRequestBody((HTTPSession) session); 494 if (session.getUri().contains(":fetchEekChain")) { 495 return newFixedLengthResponse(Response.Status.OK, "application/cbor", 496 new ByteArrayInputStream(GEEK_RESPONSE), GEEK_RESPONSE.length); 497 } else if (session.getUri().contains(":signCertificates")) { 498 Response.IStatus status = new Response.IStatus() { 499 @Override 500 public String getDescription() { 501 return "444 Device Not Registered"; 502 } 503 504 @Override 505 public int getRequestStatus() { 506 return 444; 507 } 508 }; 509 return newFixedLengthResponse(status, NanoHTTPD.MIME_PLAINTEXT, 510 "device not registered"); 511 } 512 Assert.fail("Unexpected HTTP request: " + session.getUri()); 513 return null; 514 } 515 516 void consumeRequestBody(HTTPSession session) { 517 try { 518 session.getInputStream().readNBytes((int) session.getBodySize()); 519 } catch (IOException e) { 520 Assert.fail("Error reading request bytes: " + e.toString()); 521 } 522 } 523 }; 524 server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false); 525 526 final boolean cleartextPolicy = 527 NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted(); 528 NetworkSecurityPolicy.getInstance().setCleartextTrafficPermitted(true); 529 SettingsManager.setDeviceConfig(sContext, 1 /* extraKeys */, mDuration /* expiringBy */, 530 "http://localhost:" + server.getListeningPort() + "/"); 531 532 try (ForceRkpOnlyContext c = new ForceRkpOnlyContext()) { 533 assertPoolStatus(0, 0, 0, 0, mDuration); 534 generateKeyStoreKey("should-never-succeed"); 535 Assert.fail("Expected a keystore exception"); 536 } catch (ProviderException e) { 537 Assert.assertTrue(e.getCause() instanceof KeyStoreException); 538 KeyStoreException keyStoreException = (KeyStoreException) e.getCause(); 539 Assert.assertEquals(ResponseCode.OUT_OF_KEYS, keyStoreException.getErrorCode()); 540 Assert.assertFalse(keyStoreException.isTransientFailure()); 541 Assert.assertEquals(KeyStoreException.RETRY_NEVER, keyStoreException.getRetryPolicy()); 542 } finally { 543 NetworkSecurityPolicy.getInstance().setCleartextTrafficPermitted(cleartextPolicy); 544 SettingsManager.clearPreferences(sContext); 545 server.stop(); 546 } 547 } 548 } 549