Enforce carrier privileges when setting/clearing VCN configs
This change ensures that only carrier-privileged apps can modify VCN
configs.
Since carrier privilege is checked per-subId, we iterate through all
subIds in the group, and check if any of them grant the calling app
carrier privileges.
Bug: 165670724
Test: New tests added, passing.
Change-Id: Iac032136d9c1975e6b95a2d2ad9b811ce45c9a53
diff --git a/services/core/java/com/android/server/VcnManagementService.java b/services/core/java/com/android/server/VcnManagementService.java
index 165b6a1..e9f17ff 100644
--- a/services/core/java/com/android/server/VcnManagementService.java
+++ b/services/core/java/com/android/server/VcnManagementService.java
@@ -25,13 +25,22 @@
import android.net.NetworkRequest;
import android.net.vcn.IVcnManagementService;
import android.net.vcn.VcnConfig;
+import android.os.Binder;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.ParcelUuid;
+import android.os.Process;
+import android.os.UserHandle;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.annotations.VisibleForTesting.Visibility;
+import java.util.ArrayList;
+import java.util.List;
+
/**
* VcnManagementService manages Virtual Carrier Network profiles and lifecycles.
*
@@ -130,6 +139,18 @@
}
return mHandlerThread.getLooper();
}
+
+ /**
+ * Retrieves the caller's UID
+ *
+ * <p>This call MUST be made before calling {@link Binder#clearCallingIdentity}, otherwise
+ * this will not work properly.
+ *
+ * @return
+ */
+ public int getBinderCallingUid() {
+ return Binder.getCallingUid();
+ }
}
/** Notifies the VcnManagementService that external dependencies can be set up. */
@@ -140,6 +161,50 @@
.registerNetworkProvider(mNetworkProvider);
}
+ private void enforcePrimaryUser() {
+ final int uid = mDeps.getBinderCallingUid();
+ if (uid == Process.SYSTEM_UID) {
+ throw new IllegalStateException(
+ "Calling identity was System Server. Was Binder calling identity cleared?");
+ }
+
+ if (!UserHandle.getUserHandleForUid(uid).isSystem()) {
+ throw new SecurityException(
+ "VcnManagementService can only be used by callers running as the primary user");
+ }
+ }
+
+ private void enforceCallingUserAndCarrierPrivilege(ParcelUuid subscriptionGroup) {
+ // Only apps running in the primary (system) user are allowed to configure the VCN. This is
+ // in line with Telephony's behavior with regards to binding to a Carrier App provided
+ // CarrierConfigService.
+ enforcePrimaryUser();
+
+ // TODO (b/172619301): Check based on events propagated from CarrierPrivilegesTracker
+ final SubscriptionManager subMgr = mContext.getSystemService(SubscriptionManager.class);
+ final List<SubscriptionInfo> subscriptionInfos = new ArrayList<>();
+ Binder.withCleanCallingIdentity(
+ () -> {
+ subscriptionInfos.addAll(subMgr.getSubscriptionsInGroup(subscriptionGroup));
+ });
+
+ final TelephonyManager telMgr = mContext.getSystemService(TelephonyManager.class);
+ for (SubscriptionInfo info : subscriptionInfos) {
+ // Check subscription is active first; much cheaper/faster check, and an app (currently)
+ // cannot be carrier privileged for inactive subscriptions.
+ if (subMgr.isValidSlotIndex(info.getSimSlotIndex())
+ && telMgr.hasCarrierPrivileges(info.getSubscriptionId())) {
+ // TODO (b/173717728): Allow configuration for inactive, but manageable
+ // subscriptions.
+ // TODO (b/173718661): Check for whole subscription groups at a time.
+ return;
+ }
+ }
+
+ throw new SecurityException(
+ "Carrier privilege required for subscription group to set VCN Config");
+ }
+
/**
* Sets a VCN config for a given subscription group.
*
@@ -150,6 +215,10 @@
requireNonNull(subscriptionGroup, "subscriptionGroup was null");
requireNonNull(config, "config was null");
+ enforceCallingUserAndCarrierPrivilege(subscriptionGroup);
+
+ // TODO: Clear Binder calling identity
+
// TODO: Store VCN configuration, trigger startup as necessary
}
@@ -162,6 +231,10 @@
public void clearVcnConfig(@NonNull ParcelUuid subscriptionGroup) {
requireNonNull(subscriptionGroup, "subscriptionGroup was null");
+ enforceCallingUserAndCarrierPrivilege(subscriptionGroup);
+
+ // TODO: Clear Binder calling identity
+
// TODO: Clear VCN configuration, trigger teardown as necessary
}
diff --git a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
index c91fdbf..633cf64 100644
--- a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
+++ b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
@@ -16,14 +16,23 @@
package com.android.server;
+import static org.junit.Assert.fail;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import android.content.Context;
import android.net.ConnectivityManager;
+import android.net.vcn.VcnConfig;
+import android.os.ParcelUuid;
+import android.os.Process;
+import android.os.UserHandle;
import android.os.test.TestLooper;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -31,27 +40,73 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.Collections;
+import java.util.UUID;
+
/** Tests for {@link VcnManagementService}. */
@RunWith(AndroidJUnit4.class)
@SmallTest
public class VcnManagementServiceTest {
+ private static final ParcelUuid TEST_UUID_1 = new ParcelUuid(new UUID(0, 0));
+ private static final SubscriptionInfo TEST_SUBSCRIPTION_INFO =
+ new SubscriptionInfo(
+ 1 /* id */,
+ "" /* iccId */,
+ 0 /* simSlotIndex */,
+ "Carrier" /* displayName */,
+ "Carrier" /* carrierName */,
+ 0 /* nameSource */,
+ 255 /* iconTint */,
+ "12345" /* number */,
+ 0 /* roaming */,
+ null /* icon */,
+ "0" /* mcc */,
+ "0" /* mnc */,
+ "0" /* countryIso */,
+ false /* isEmbedded */,
+ null /* nativeAccessRules */,
+ null /* cardString */,
+ false /* isOpportunistic */,
+ TEST_UUID_1.toString() /* groupUUID */,
+ 0 /* carrierId */,
+ 0 /* profileClass */);
+
private final Context mMockContext = mock(Context.class);
private final VcnManagementService.Dependencies mMockDeps =
mock(VcnManagementService.Dependencies.class);
private final TestLooper mTestLooper = new TestLooper();
private final ConnectivityManager mConnMgr = mock(ConnectivityManager.class);
+ private final TelephonyManager mTelMgr = mock(TelephonyManager.class);
+ private final SubscriptionManager mSubMgr = mock(SubscriptionManager.class);
private final VcnManagementService mVcnMgmtSvc;
- public VcnManagementServiceTest() {
- doReturn(Context.CONNECTIVITY_SERVICE)
- .when(mMockContext)
- .getSystemServiceName(ConnectivityManager.class);
- doReturn(mConnMgr).when(mMockContext).getSystemService(Context.CONNECTIVITY_SERVICE);
+ public VcnManagementServiceTest() throws Exception {
+ setupSystemService(mConnMgr, Context.CONNECTIVITY_SERVICE, ConnectivityManager.class);
+ setupSystemService(mTelMgr, Context.TELEPHONY_SERVICE, TelephonyManager.class);
+ setupSystemService(
+ mSubMgr, Context.TELEPHONY_SUBSCRIPTION_SERVICE, SubscriptionManager.class);
doReturn(mTestLooper.getLooper()).when(mMockDeps).getLooper();
+ doReturn(Process.FIRST_APPLICATION_UID).when(mMockDeps).getBinderCallingUid();
+
+ setupMockedCarrierPrivilege(true);
mVcnMgmtSvc = new VcnManagementService(mMockContext, mMockDeps);
}
+ private void setupSystemService(Object service, String name, Class<?> serviceClass) {
+ doReturn(name).when(mMockContext).getSystemServiceName(serviceClass);
+ doReturn(service).when(mMockContext).getSystemService(name);
+ }
+
+ private void setupMockedCarrierPrivilege(boolean isPrivileged) {
+ doReturn(Collections.singletonList(TEST_SUBSCRIPTION_INFO))
+ .when(mSubMgr)
+ .getSubscriptionsInGroup(any());
+ doReturn(isPrivileged)
+ .when(mTelMgr)
+ .hasCarrierPrivileges(eq(TEST_SUBSCRIPTION_INFO.getSubscriptionId()));
+ }
+
@Test
public void testSystemReady() throws Exception {
mVcnMgmtSvc.systemReady();
@@ -59,4 +114,74 @@
verify(mConnMgr)
.registerNetworkProvider(any(VcnManagementService.VcnNetworkProvider.class));
}
+
+ @Test
+ public void testSetVcnConfigRequiresNonSystemServer() throws Exception {
+ doReturn(Process.SYSTEM_UID).when(mMockDeps).getBinderCallingUid();
+
+ try {
+ mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, new VcnConfig.Builder().build());
+ fail("Expected IllegalStateException exception for system server");
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test
+ public void testSetVcnConfigRequiresSystemUser() throws Exception {
+ doReturn(UserHandle.getUid(UserHandle.MIN_SECONDARY_USER_ID, Process.FIRST_APPLICATION_UID))
+ .when(mMockDeps)
+ .getBinderCallingUid();
+
+ try {
+ mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, new VcnConfig.Builder().build());
+ fail("Expected security exception for non system user");
+ } catch (SecurityException expected) {
+ }
+ }
+
+ @Test
+ public void testSetVcnConfigRequiresCarrierPrivileges() throws Exception {
+ setupMockedCarrierPrivilege(false);
+
+ try {
+ mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, new VcnConfig.Builder().build());
+ fail("Expected security exception for missing carrier privileges");
+ } catch (SecurityException expected) {
+ }
+ }
+
+ @Test
+ public void testClearVcnConfigRequiresNonSystemServer() throws Exception {
+ doReturn(Process.SYSTEM_UID).when(mMockDeps).getBinderCallingUid();
+
+ try {
+ mVcnMgmtSvc.clearVcnConfig(TEST_UUID_1);
+ fail("Expected IllegalStateException exception for system server");
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test
+ public void testClearVcnConfigRequiresSystemUser() throws Exception {
+ doReturn(UserHandle.getUid(UserHandle.MIN_SECONDARY_USER_ID, Process.FIRST_APPLICATION_UID))
+ .when(mMockDeps)
+ .getBinderCallingUid();
+
+ try {
+ mVcnMgmtSvc.clearVcnConfig(TEST_UUID_1);
+ fail("Expected security exception for non system user");
+ } catch (SecurityException expected) {
+ }
+ }
+
+ @Test
+ public void testClearVcnConfigRequiresCarrierPrivileges() throws Exception {
+ setupMockedCarrierPrivilege(false);
+
+ try {
+ mVcnMgmtSvc.clearVcnConfig(TEST_UUID_1);
+ fail("Expected security exception for missing carrier privileges");
+ } catch (SecurityException expected) {
+ }
+ }
}