Fix DomainVerificationEnforcer for app visibility

Cleans up the permission logic and introduces app filtering.

This will require non-user targeted APIs that hit a package name
to hold QUERY_ALL_PACKAGES. This ensures the caller can see the packages
without needing to check instant app state by passing a userId.

Currently it's not clear how to handle instant app visibility, but it's
assumed that the verification agent and settings have visibility.

Bug: 171251883

Test: atest DomainVerificationEnforcerTest

Change-Id: I3ffe2cc0307c9efa97dfcf8474a620622e7cfcfe
This commit is contained in:
Winson
2021-01-27 16:42:38 -08:00
parent 1afc2cdb46
commit 6821adee46
7 changed files with 592 additions and 275 deletions

View File

@@ -1747,6 +1747,11 @@ public class PackageManagerService extends IPackageManager.Stub
public AndroidPackage getPackage(@NonNull String packageName) {
return getPackageLocked(packageName);
}
@Override
public boolean filterAppAccess(String packageName, int callingUid, int userId) {
return mPmInternal.filterAppAccess(packageName, callingUid, userId);
}
}
/**
@@ -16156,8 +16161,7 @@ public class PackageManagerService extends IPackageManager.Stub
@Deprecated
@Override
public boolean updateIntentVerificationStatus(String packageName, int status, int userId) {
mDomainVerificationManager.setLegacyUserState(packageName, userId, status);
return true;
return mDomainVerificationManager.setLegacyUserState(packageName, userId, status);
}
@Deprecated

View File

@@ -105,9 +105,6 @@ import com.android.permission.persistence.RuntimePermissionsState;
import com.android.server.LocalServices;
import com.android.server.backup.PreferredActivityBackupHelper;
import com.android.server.pm.Installer.InstallerException;
import com.android.server.pm.verify.domain.DomainVerificationLegacySettings;
import com.android.server.pm.verify.domain.DomainVerificationManagerInternal;
import com.android.server.pm.verify.domain.DomainVerificationPersistence;
import com.android.server.pm.parsing.PackageInfoUtils;
import com.android.server.pm.parsing.pkg.AndroidPackage;
import com.android.server.pm.parsing.pkg.AndroidPackageUtils;
@@ -115,6 +112,9 @@ import com.android.server.pm.permission.LegacyPermissionDataProvider;
import com.android.server.pm.permission.LegacyPermissionSettings;
import com.android.server.pm.permission.LegacyPermissionState;
import com.android.server.pm.permission.LegacyPermissionState.PermissionState;
import com.android.server.pm.verify.domain.DomainVerificationLegacySettings;
import com.android.server.pm.verify.domain.DomainVerificationManagerInternal;
import com.android.server.pm.verify.domain.DomainVerificationPersistence;
import com.android.server.utils.Snappable;
import com.android.server.utils.TimingsTraceAndSlog;
import com.android.server.utils.Watchable;
@@ -2707,7 +2707,6 @@ public final class Settings implements Watchable, Snappable {
writeSigningKeySetLPr(serializer, pkg.keySetData);
writeUpgradeKeySetsLPr(serializer, pkg.keySetData);
writeKeySetAliasesLPr(serializer, pkg.keySetData);
mDomainVerificationManager.writeLegacySettings(serializer, pkg.name);
writeMimeGroupLPr(serializer, pkg.mimeGroups);
serializer.endTag(null, "package");

View File

@@ -18,8 +18,10 @@ package com.android.server.pm.verify.domain;
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Binder;
import android.os.Process;
@@ -30,10 +32,17 @@ public class DomainVerificationEnforcer {
@NonNull
private final Context mContext;
@NonNull
private Callback mCallback;
public DomainVerificationEnforcer(@NonNull Context context) {
mContext = context;
}
public void setCallback(@NonNull Callback callback) {
mCallback = callback;
}
/**
* Enforced when mutating any state from shell or internally in the system process.
*/
@@ -67,6 +76,11 @@ public class DomainVerificationEnforcer {
"Caller " + callingUid
+ " is not allowed to query domain verification state");
}
mContext.enforcePermission(android.Manifest.permission.QUERY_ALL_PACKAGES,
Binder.getCallingPid(), callingUid,
"Caller " + callingUid + " does not hold "
+ android.Manifest.permission.QUERY_ALL_PACKAGES);
break;
}
}
@@ -84,28 +98,42 @@ public class DomainVerificationEnforcer {
isAllowed = true;
break;
default:
// TODO(b/159952358): Remove permission check? The component package should
// have been checked when the verifier component was first scanned in PMS.
mContext.enforcePermission(
android.Manifest.permission.DOMAIN_VERIFICATION_AGENT,
Binder.getCallingPid(), callingUid,
"Caller " + callingUid + " does not hold DOMAIN_VERIFICATION_AGENT");
final int callingPid = Binder.getCallingPid();
boolean isLegacyVerificationAgent = false;
if (mContext.checkPermission(
android.Manifest.permission.DOMAIN_VERIFICATION_AGENT, callingPid,
callingUid) != PackageManager.PERMISSION_GRANTED) {
isLegacyVerificationAgent = mContext.checkPermission(
android.Manifest.permission.INTENT_FILTER_VERIFICATION_AGENT,
callingPid, callingUid) == PackageManager.PERMISSION_GRANTED;
if (!isLegacyVerificationAgent) {
throw new SecurityException("Caller " + callingUid + " does not hold "
+ android.Manifest.permission.DOMAIN_VERIFICATION_AGENT);
}
}
// If the caller isn't a legacy verifier, it needs the QUERY_ALL permission
if (!isLegacyVerificationAgent) {
mContext.enforcePermission(android.Manifest.permission.QUERY_ALL_PACKAGES,
callingPid, callingUid, "Caller " + callingUid + " does not hold "
+ android.Manifest.permission.QUERY_ALL_PACKAGES);
}
isAllowed = proxy.isCallerVerifier(callingUid);
break;
}
if (!isAllowed) {
throw new SecurityException("Caller " + callingUid
+ " is not the approved domain verification agent, isVerifier = "
+ proxy.isCallerVerifier(callingUid));
+ " is not the approved domain verification agent");
}
}
/**
* Enforced when mutating user selection state inside an exposed API method.
*/
public void assertApprovedUserSelector(int callingUid, @UserIdInt int callingUserId,
@UserIdInt int targetUserId) throws SecurityException {
public boolean assertApprovedUserSelector(int callingUid, @UserIdInt int callingUserId,
@Nullable String packageName, @UserIdInt int targetUserId) throws SecurityException {
if (callingUserId != targetUserId) {
mContext.enforcePermission(
Manifest.permission.INTERACT_ACROSS_USERS,
@@ -117,12 +145,51 @@ public class DomainVerificationEnforcer {
android.Manifest.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION,
Binder.getCallingPid(), callingUid,
"Caller is not allowed to edit user selections");
if (packageName == null) {
return true;
}
return !mCallback.filterAppAccess(packageName, callingUid, targetUserId);
}
public void callerIsLegacyUserSelector(int callingUid) {
public boolean callerIsLegacyUserSelector(int callingUid, @UserIdInt int callingUserId,
@NonNull String packageName, @UserIdInt int targetUserId) {
mContext.enforcePermission(
android.Manifest.permission.SET_PREFERRED_APPLICATIONS,
Binder.getCallingPid(), callingUid,
"Caller is not allowed to edit user state");
if (callingUserId != targetUserId) {
if (mContext.checkPermission(
Manifest.permission.INTERACT_ACROSS_USERS,
Binder.getCallingPid(), callingUid) != PackageManager.PERMISSION_GRANTED) {
// Legacy API did not enforce this, so for backwards compatibility, fail silently
return false;
}
}
return !mCallback.filterAppAccess(packageName, callingUid, targetUserId);
}
public boolean callerIsLegacyUserQuerent(int callingUid, @UserIdInt int callingUserId,
@NonNull String packageName, @UserIdInt int targetUserId) {
if (callingUserId != targetUserId) {
// The legacy API enforces the _FULL variant, so maintain that here
mContext.enforcePermission(
Manifest.permission.INTERACT_ACROSS_USERS_FULL,
Binder.getCallingPid(), callingUid,
"Caller is not allowed to edit other users");
}
return !mCallback.filterAppAccess(packageName, callingUid, targetUserId);
}
public interface Callback {
/**
* @return true if access to the given package should be filtered and the method failed as
* if the package was not installed
*/
boolean filterAppAccess(@NonNull String packageName, int callingUid, @UserIdInt int userId);
}
}

View File

@@ -174,8 +174,10 @@ public interface DomainVerificationManagerInternal extends DomainVerificationMan
* Set aside a legacy user selection that will be restored to a pending
* {@link DomainVerificationPkgState} once it's added through
* {@link #addPackage(PackageSetting)}.
*
* @return true if state changed successfully
*/
void setLegacyUserState(@NonNull String packageName, @UserIdInt int userId, int state);
boolean setLegacyUserState(@NonNull String packageName, @UserIdInt int userId, int state);
/**
* Until the legacy APIs are entirely removed, returns the legacy state from the previously
@@ -183,12 +185,6 @@ public interface DomainVerificationManagerInternal extends DomainVerificationMan
*/
int getLegacyState(@NonNull String packageName, @UserIdInt int userId);
/**
* Serialize a legacy setting that wasn't attached yet.
* TODO: Does this even matter? Should consider for removal.
*/
void writeLegacySettings(TypedXmlSerializer serializer, String name);
/**
* Print the verification state and user selection state of a package.
*
@@ -235,7 +231,8 @@ public interface DomainVerificationManagerInternal extends DomainVerificationMan
throws IllegalArgumentException, NameNotFoundException;
interface Connection extends Function<String, PackageSetting> {
interface Connection extends DomainVerificationEnforcer.Callback,
Function<String, PackageSetting> {
/**
* Notify that a settings change has been made and that eventually

View File

@@ -47,12 +47,12 @@ import com.android.server.SystemConfig;
import com.android.server.SystemService;
import com.android.server.compat.PlatformCompat;
import com.android.server.pm.PackageSetting;
import com.android.server.pm.parsing.pkg.AndroidPackage;
import com.android.server.pm.verify.domain.models.DomainVerificationPkgState;
import com.android.server.pm.verify.domain.models.DomainVerificationStateMap;
import com.android.server.pm.verify.domain.models.DomainVerificationUserState;
import com.android.server.pm.verify.domain.proxy.DomainVerificationProxy;
import com.android.server.pm.verify.domain.proxy.DomainVerificationProxyUnavailable;
import com.android.server.pm.parsing.pkg.AndroidPackage;
import org.xmlpull.v1.XmlPullParserException;
@@ -92,9 +92,9 @@ public class DomainVerificationService extends SystemService
* immediately attached once its available.
* <p>
* Generally this should be not accessed directly. Prefer calling {@link
* #getAndValidateAttachedLocked(UUID, Set, boolean)}.
* #getAndValidateAttachedLocked(UUID, Set, boolean, int, Integer)}.
*
* @see #getAndValidateAttachedLocked(UUID, Set, boolean)
* @see #getAndValidateAttachedLocked(UUID, Set, boolean, int, Integer)
**/
@GuardedBy("mLock")
@NonNull
@@ -160,6 +160,7 @@ public class DomainVerificationService extends SystemService
@Override
public void setConnection(@NonNull Connection connection) {
mConnection = connection;
mEnforcer.setCallback(mConnection);
}
@NonNull
@@ -285,7 +286,7 @@ public class DomainVerificationService extends SystemService
mEnforcer.assertApprovedVerifier(callingUid, mProxy);
synchronized (mLock) {
DomainVerificationPkgState pkgState = getAndValidateAttachedLocked(domainSetId, domains,
true /* forAutoVerify */);
true /* forAutoVerify */, callingUid, null /* userId */);
ArrayMap<String, Integer> stateMap = pkgState.getStateMap();
for (String domain : domains) {
Integer previousState = stateMap.get(domain);
@@ -389,8 +390,10 @@ public class DomainVerificationService extends SystemService
public void setDomainVerificationLinkHandlingAllowed(@NonNull String packageName,
boolean allowed, @UserIdInt int userId) throws NameNotFoundException {
mEnforcer.assertApprovedUserSelector(mConnection.getCallingUid(),
mConnection.getCallingUserId(), userId);
if (!mEnforcer.assertApprovedUserSelector(mConnection.getCallingUid(),
mConnection.getCallingUserId(), packageName, userId)) {
throw DomainVerificationUtils.throwPackageUnavailable(packageName);
}
synchronized (mLock) {
DomainVerificationPkgState pkgState = mAttachedPkgStates.get(packageName);
if (pkgState == null) {
@@ -455,11 +458,18 @@ public class DomainVerificationService extends SystemService
public void setDomainVerificationUserSelection(@NonNull UUID domainSetId,
@NonNull Set<String> domains, boolean enabled, @UserIdInt int userId)
throws InvalidDomainSetException, NameNotFoundException {
mEnforcer.assertApprovedUserSelector(mConnection.getCallingUid(),
mConnection.getCallingUserId(), userId);
synchronized (mLock) {
final int callingUid = mConnection.getCallingUid();
// Pass null for package name here and do the app visibility enforcement inside
// getAndValidateAttachedLocked instead, since this has to fail with the same invalid
// ID reason if the target app is invisible
if (!mEnforcer.assertApprovedUserSelector(callingUid, mConnection.getCallingUserId(),
null /* packageName */, userId)) {
throw new InvalidDomainSetException(domainSetId, null,
InvalidDomainSetException.REASON_ID_INVALID);
}
DomainVerificationPkgState pkgState = getAndValidateAttachedLocked(domainSetId, domains,
false /* forAutoVerify */);
false /* forAutoVerify */, callingUid, userId);
DomainVerificationUserState userState = pkgState.getOrCreateUserSelectionState(userId);
if (enabled) {
userState.addHosts(domains);
@@ -556,8 +566,10 @@ public class DomainVerificationService extends SystemService
@Override
public DomainVerificationUserSelection getDomainVerificationUserSelection(
@NonNull String packageName, @UserIdInt int userId) throws NameNotFoundException {
mEnforcer.assertApprovedUserSelector(mConnection.getCallingUid(),
mConnection.getCallingUserId(), userId);
if (!mEnforcer.assertApprovedUserSelector(mConnection.getCallingUid(),
mConnection.getCallingUserId(), packageName, userId)) {
throw DomainVerificationUtils.throwPackageUnavailable(packageName);
}
synchronized (mLock) {
DomainVerificationPkgState pkgState = mAttachedPkgStates.get(packageName);
if (pkgState == null) {
@@ -844,22 +856,26 @@ public class DomainVerificationService extends SystemService
}
@Override
public void setLegacyUserState(@NonNull String packageName, @UserIdInt int userId, int state) {
mEnforcer.callerIsLegacyUserSelector(mConnection.getCallingUid());
public boolean setLegacyUserState(@NonNull String packageName, @UserIdInt int userId,
int state) {
if (!mEnforcer.callerIsLegacyUserSelector(mConnection.getCallingUid(),
mConnection.getCallingUserId(), packageName, userId)) {
return false;
}
mLegacySettings.add(packageName, userId, state);
mConnection.scheduleWriteSettings();
return true;
}
@Override
public int getLegacyState(@NonNull String packageName, @UserIdInt int userId) {
if (!mEnforcer.callerIsLegacyUserQuerent(mConnection.getCallingUid(),
mConnection.getCallingUserId(), packageName, userId)) {
return PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED;
}
return mLegacySettings.getUserState(packageName, userId);
}
@Override
public void writeLegacySettings(TypedXmlSerializer serializer, String name) {
}
@Override
public void clearPackage(@NonNull String packageName) {
synchronized (mLock) {
@@ -935,10 +951,14 @@ public class DomainVerificationService extends SystemService
* Validates parameters provided by an external caller. Checks that an ID is still live and that
* any provided domains are valid. Should be called at the beginning of each API that takes in a
* {@link UUID} domain set ID.
*
* @param userIdForFilter which user to filter app access to, or null if the caller has already
* validated package visibility
*/
@GuardedBy("mLock")
private DomainVerificationPkgState getAndValidateAttachedLocked(@NonNull UUID domainSetId,
@NonNull Set<String> domains, boolean forAutoVerify)
@NonNull Set<String> domains, boolean forAutoVerify, int callingUid,
@Nullable Integer userIdForFilter)
throws InvalidDomainSetException, NameNotFoundException {
if (domainSetId == null) {
throw new InvalidDomainSetException(null, null,
@@ -952,6 +972,13 @@ public class DomainVerificationService extends SystemService
}
String pkgName = pkgState.getPackageName();
if (userIdForFilter != null
&& mConnection.filterAppAccess(pkgName, callingUid, userIdForFilter)) {
throw new InvalidDomainSetException(domainSetId, null,
InvalidDomainSetException.REASON_ID_INVALID);
}
PackageSetting pkgSetting = mConnection.getPackageSettingLocked(pkgName);
if (pkgSetting == null || pkgSetting.getPkg() == null) {
throw DomainVerificationUtils.throwPackageUnavailable(pkgName);

View File

@@ -18,6 +18,7 @@ package com.android.server.pm.test.verify.domain
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.PackageUserState
import android.content.pm.verify.domain.DomainVerificationManager
import android.content.pm.parsing.component.ParsedActivity
@@ -25,7 +26,6 @@ import android.content.pm.parsing.component.ParsedIntentInfo
import android.os.Build
import android.os.Process
import android.util.ArraySet
import android.util.Singleton
import android.util.SparseArray
import androidx.test.platform.app.InstrumentationRegistry
import com.android.server.pm.PackageSetting
@@ -46,14 +46,11 @@ import org.mockito.Mockito.anyLong
import org.mockito.Mockito.anyString
import org.mockito.Mockito.eq
import org.mockito.Mockito.verifyNoMoreInteractions
import org.testng.Assert.assertThrows
import java.io.File
import java.util.UUID
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
private typealias Enforcer = DomainVerificationEnforcer
@RunWith(Parameterized::class)
class DomainVerificationEnforcerTest {
@@ -64,64 +61,27 @@ class DomainVerificationEnforcerTest {
private const val VERIFIER_UID = Process.FIRST_APPLICATION_UID + 1
private const val NON_VERIFIER_UID = Process.FIRST_APPLICATION_UID + 2
private const val TEST_PKG = "com.test"
private const val VISIBLE_PKG = "com.test.visible"
private val VISIBLE_UUID = UUID.fromString("8db01272-270d-4606-a3db-bb35228ff9a2")
private const val INVISIBLE_PKG = "com.test.invisible"
private val INVISIBLE_UUID = UUID.fromString("16dcb029-d96c-4a19-833a-4c9d72e2ebc3")
@JvmStatic
@Parameterized.Parameters(name = "{0}")
fun parameters(): Array<Any> {
val visiblePkg = mockPkg(VISIBLE_PKG)
val visiblePkgSetting = mockPkgSetting(VISIBLE_PKG, VISIBLE_UUID)
val invisiblePkg = mockPkg(INVISIBLE_PKG)
val invisiblePkgSetting = mockPkgSetting(INVISIBLE_PKG, INVISIBLE_UUID)
val makeEnforcer: (Context) -> DomainVerificationEnforcer = {
DomainVerificationEnforcer(it)
}
val mockPkg = mockThrowOnUnmocked<AndroidPackage> {
whenever(packageName) { TEST_PKG }
whenever(targetSdkVersion) { Build.VERSION_CODES.S }
whenever(activities) {
listOf(
ParsedActivity().apply {
addIntent(
ParsedIntentInfo().apply {
autoVerify = true
addAction(Intent.ACTION_VIEW)
addCategory(Intent.CATEGORY_BROWSABLE)
addCategory(Intent.CATEGORY_DEFAULT)
addDataScheme("https")
addDataAuthority("example.com", null)
}
)
DomainVerificationEnforcer(it).apply {
setCallback(mockThrowOnUnmocked {
whenever(filterAppAccess(eq(VISIBLE_PKG), anyInt(), anyInt())) { false }
whenever(filterAppAccess(eq(INVISIBLE_PKG), anyInt(), anyInt())) {
true
}
)
}
}
val uuid = UUID.randomUUID()
// TODO: PackageSetting field encapsulation to move to whenever(name)
val mockPkgSetting = spyThrowOnUnmocked(
PackageSetting(
TEST_PKG,
TEST_PKG,
File("/test"),
null,
null,
null,
null,
1,
0,
0,
0,
null,
null,
null,
uuid
)
) {
whenever(getPkg()) { mockPkg }
whenever(domainSetId) { uuid }
whenever(userState) {
SparseArray<PackageUserState>().apply {
this[0] = PackageUserState()
}
})
}
}
@@ -129,142 +89,160 @@ class DomainVerificationEnforcerTest {
{
val callingUidInt = AtomicInteger(-1)
val callingUserIdInt = AtomicInteger(-1)
Triple(
callingUidInt, callingUserIdInt, DomainVerificationService(
it,
mockThrowOnUnmocked { whenever(linkedApps) { ArraySet<String>() } },
mockThrowOnUnmocked {
whenever(
isChangeEnabled(
anyLong(),
any()
)
) { true }
}).apply {
setConnection(mockThrowOnUnmocked {
whenever(callingUid) { callingUidInt.get() }
whenever(callingUserId) { callingUserIdInt.get() }
whenever(getPackageSettingLocked(TEST_PKG)) { mockPkgSetting }
whenever(getPackageLocked(TEST_PKG)) { mockPkg }
whenever(schedule(anyInt(), any()))
whenever(scheduleWriteSettings())
})
val connection: DomainVerificationManagerInternal.Connection =
mockThrowOnUnmocked {
whenever(callingUid) { callingUidInt.get() }
whenever(callingUserId) { callingUserIdInt.get() }
whenever(getPackageSettingLocked(VISIBLE_PKG)) { visiblePkgSetting }
whenever(getPackageLocked(VISIBLE_PKG)) { visiblePkg }
whenever(getPackageSettingLocked(INVISIBLE_PKG)) { invisiblePkgSetting }
whenever(getPackageLocked(INVISIBLE_PKG)) { invisiblePkg }
whenever(schedule(anyInt(), any()))
whenever(scheduleWriteSettings())
whenever(filterAppAccess(eq(VISIBLE_PKG), anyInt(), anyInt())) { false }
whenever(filterAppAccess(eq(INVISIBLE_PKG), anyInt(), anyInt())) {
true
}
}
)
val service = DomainVerificationService(
it,
mockThrowOnUnmocked { whenever(linkedApps) { ArraySet<String>() } },
mockThrowOnUnmocked {
whenever(
isChangeEnabled(
anyLong(),
any()
)
) { true }
}).apply {
setConnection(connection)
}
Triple(callingUidInt, callingUserIdInt, service)
}
fun enforcer(
type: Type,
name: String,
block: DomainVerificationEnforcer.(
callingUid: Int, callingUserId: Int, userId: Int, proxy: DomainVerificationProxy
) -> Unit
) = Params(
type,
makeEnforcer,
name
) { enforcer, callingUid, callingUserId, userId, proxy ->
enforcer.block(callingUid, callingUserId, userId, proxy)
block: DomainVerificationEnforcer.(Params.Input<DomainVerificationEnforcer>) -> Any?
) = Params(type, makeEnforcer, name) {
it.target.block(it)
}
fun service(
type: Type,
name: String,
block: DomainVerificationService.(
callingUid: Int, callingUserId: Int, userId: Int
) -> Unit
) = Params(
type,
makeService,
name
) { uidAndUserIdAndService, callingUid, callingUserId, userId, proxy ->
val (callingUidInt, callingUserIdInt, service) = uidAndUserIdAndService
callingUidInt.set(callingUid)
callingUserIdInt.set(callingUserId)
service.setProxy(proxy)
service.addPackage(mockPkgSetting)
service.block(callingUid, callingUserId, userId)
block: DomainVerificationService.(Params.Input<Triple<AtomicInteger, AtomicInteger, DomainVerificationService>>) -> Any?
) = Params(type, makeService, name) {
val (callingUidInt, callingUserIdInt, service) = it.target
callingUidInt.set(it.callingUid)
callingUserIdInt.set(it.callingUserId)
service.proxy = it.proxy
service.addPackage(visiblePkgSetting)
service.addPackage(invisiblePkgSetting)
service.block(it)
}
return arrayOf(
enforcer(Type.INTERNAL, "internal") { callingUid, _, _, _ ->
assertInternal(callingUid)
enforcer(Type.INTERNAL, "internal") {
assertInternal(it.callingUid)
},
enforcer(Type.QUERENT, "approvedQuerent") { callingUid, _, _, proxy ->
assertApprovedQuerent(callingUid, proxy)
enforcer(Type.QUERENT, "approvedQuerent") {
assertApprovedQuerent(it.callingUid, it.proxy)
},
enforcer(Type.VERIFIER, "approvedVerifier") { callingUid, _, _, proxy ->
assertApprovedVerifier(callingUid, proxy)
enforcer(Type.VERIFIER, "approvedVerifier") {
assertApprovedVerifier(it.callingUid, it.proxy)
},
enforcer(
Type.SELECTOR,
"approvedUserSelector"
) { callingUid, callingUserId, userId, _ ->
assertApprovedUserSelector(callingUid, callingUserId, userId)
) {
assertApprovedUserSelector(
it.callingUid, it.callingUserId,
it.targetPackageName, it.userId
)
},
service(Type.INTERNAL, "setStatusInternalPackageName") { _, _, _ ->
service(Type.INTERNAL, "setStatusInternalPackageName") {
setDomainVerificationStatusInternal(
TEST_PKG,
it.targetPackageName,
DomainVerificationManager.STATE_SUCCESS,
ArraySet(setOf("example.com"))
)
},
service(Type.INTERNAL, "setUserSelectionInternal") { _, _, userId ->
service(Type.INTERNAL, "setUserSelectionInternal") {
setDomainVerificationUserSelectionInternal(
userId,
TEST_PKG,
it.userId,
it.targetPackageName,
false,
ArraySet(setOf("example.com"))
)
},
service(Type.INTERNAL, "verifyPackages") { _, _, _ ->
verifyPackages(listOf(TEST_PKG), true)
service(Type.INTERNAL, "verifyPackages") {
verifyPackages(listOf(it.targetPackageName), true)
},
service(Type.INTERNAL, "clearState") { _, _, _ ->
clearDomainVerificationState(listOf(TEST_PKG))
service(Type.INTERNAL, "clearState") {
clearDomainVerificationState(listOf(it.targetPackageName))
},
service(Type.INTERNAL, "clearUserSelections") { _, _, userId ->
clearUserSelections(listOf(TEST_PKG), userId)
service(Type.INTERNAL, "clearUserSelections") {
clearUserSelections(listOf(it.targetPackageName), it.userId)
},
service(Type.VERIFIER, "getPackageNames") { _, _, _ ->
service(Type.VERIFIER, "getPackageNames") {
validVerificationPackageNames
},
service(Type.QUERENT, "getInfo") { _, _, _ ->
getDomainVerificationInfo(TEST_PKG)
service(Type.QUERENT, "getInfo") {
getDomainVerificationInfo(it.targetPackageName)
},
service(Type.VERIFIER, "setStatus") { _, _, _ ->
service(Type.VERIFIER, "setStatus") {
setDomainVerificationStatus(
uuid,
it.targetDomainSetId,
setOf("example.com"),
DomainVerificationManager.STATE_SUCCESS
)
},
service(Type.VERIFIER, "setStatusInternalUid") { callingUid, _, _ ->
service(Type.VERIFIER, "setStatusInternalUid") {
setDomainVerificationStatusInternal(
callingUid,
uuid,
it.callingUid,
it.targetDomainSetId,
setOf("example.com"),
DomainVerificationManager.STATE_SUCCESS
)
},
service(Type.SELECTOR, "setLinkHandlingAllowed") { _, _, _ ->
setDomainVerificationLinkHandlingAllowed(TEST_PKG, true)
service(Type.SELECTOR, "setLinkHandlingAllowed") {
setDomainVerificationLinkHandlingAllowed(it.targetPackageName, true)
},
service(Type.SELECTOR_USER, "setLinkHandlingAllowedUserId") { _, _, userId ->
setDomainVerificationLinkHandlingAllowed(TEST_PKG, true, userId)
service(Type.SELECTOR_USER, "setLinkHandlingAllowedUserId") {
setDomainVerificationLinkHandlingAllowed(it.targetPackageName, true, it.userId)
},
service(Type.SELECTOR, "getUserSelection") { _, _, _ ->
getDomainVerificationUserSelection(TEST_PKG)
service(Type.SELECTOR, "getUserSelection") {
getDomainVerificationUserSelection(it.targetPackageName)
},
service(Type.SELECTOR_USER, "getUserSelectionUserId") { _, _, userId ->
getDomainVerificationUserSelection(TEST_PKG, userId)
service(Type.SELECTOR_USER, "getUserSelectionUserId") {
getDomainVerificationUserSelection(it.targetPackageName, it.userId)
},
service(Type.SELECTOR, "setUserSelection") { _, _, _ ->
setDomainVerificationUserSelection(uuid, setOf("example.com"), true)
service(Type.SELECTOR, "setUserSelection") {
setDomainVerificationUserSelection(
it.targetDomainSetId,
setOf("example.com"),
true
)
},
service(Type.SELECTOR_USER, "setUserSelectionUserId") { _, _, userId ->
setDomainVerificationUserSelection(uuid, setOf("example.com"), true, userId)
service(Type.SELECTOR_USER, "setUserSelectionUserId") {
setDomainVerificationUserSelection(
it.targetDomainSetId,
setOf("example.com"),
true,
it.userId
)
},
service(Type.LEGACY_SELECTOR, "setLegacyUserState") {
setLegacyUserState(
it.targetPackageName, it.userId,
PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER
)
},
service(Type.LEGACY_QUERENT, "getLegacyUserState") {
getLegacyState(it.targetPackageName, it.userId)
},
)
}
@@ -273,9 +251,7 @@ class DomainVerificationEnforcerTest {
val type: Type,
val construct: (context: Context) -> T,
val name: String,
private val method: (
T, callingUid: Int, callingUserId: Int, userId: Int, proxy: DomainVerificationProxy
) -> Unit
private val method: (Input<T>) -> Any?
) {
override fun toString() = "${type}_$name"
@@ -284,10 +260,79 @@ class DomainVerificationEnforcerTest {
callingUid: Int,
callingUserId: Int,
userId: Int,
targetPackageName: String,
targetDomainSetId: UUID,
proxy: DomainVerificationProxy
) {
@Suppress("UNCHECKED_CAST")
method(target as T, callingUid, callingUserId, userId, proxy)
): Any? = method(
Input(
@Suppress("UNCHECKED_CAST")
target as T,
callingUid,
callingUserId,
userId,
targetPackageName,
targetDomainSetId,
proxy
)
)
data class Input<T>(
val target: T,
val callingUid: Int,
val callingUserId: Int,
val userId: Int,
val targetPackageName: String,
val targetDomainSetId: UUID,
val proxy: DomainVerificationProxy
)
}
fun mockPkg(packageName: String) = mockThrowOnUnmocked<AndroidPackage> {
whenever(this.packageName) { packageName }
whenever(targetSdkVersion) { Build.VERSION_CODES.S }
whenever(activities) {
listOf(
ParsedActivity().apply {
addIntent(
ParsedIntentInfo().apply {
autoVerify = true
addAction(Intent.ACTION_VIEW)
addCategory(Intent.CATEGORY_BROWSABLE)
addCategory(Intent.CATEGORY_DEFAULT)
addDataScheme("https")
addDataAuthority("example.com", null)
}
)
}
)
}
}
fun mockPkgSetting(packageName: String, domainSetId: UUID) = spyThrowOnUnmocked(
PackageSetting(
packageName,
packageName,
File("/test"),
null,
null,
null,
null,
1,
0,
0,
0,
null,
null,
null,
domainSetId
)
) {
whenever(getPkg()) { mockPkg(packageName) }
whenever(this.domainSetId) { domainSetId }
whenever(userState) {
SparseArray<PackageUserState>().apply {
this[0] = PackageUserState()
}
}
}
}
@@ -309,6 +354,8 @@ class DomainVerificationEnforcerTest {
Type.VERIFIER -> approvedVerifier()
Type.SELECTOR -> approvedUserSelector(verifyCrossUser = false)
Type.SELECTOR_USER -> approvedUserSelector(verifyCrossUser = true)
Type.LEGACY_QUERENT -> legacyQuerent()
Type.LEGACY_SELECTOR -> legacyUserSelector()
}.run { /*exhaust*/ }
}
@@ -316,24 +363,30 @@ class DomainVerificationEnforcerTest {
val context: Context = mockThrowOnUnmocked()
val target = params.construct(context)
INTERNAL_UIDS.forEach { runMethod(target, it) }
assertThrows(SecurityException::class.java) { runMethod(target, VERIFIER_UID) }
assertThrows(SecurityException::class.java) { runMethod(target, NON_VERIFIER_UID) }
// Internal doesn't care about visibility
listOf(true, false).forEach { visible ->
INTERNAL_UIDS.forEach { runMethod(target, it, visible) }
assertFails { runMethod(target, VERIFIER_UID, visible) }
assertFails {
runMethod(target, NON_VERIFIER_UID, visible)
}
}
}
fun approvedQuerent() {
val allowUserSelection = AtomicBoolean(false)
val allowPreferredApps = AtomicBoolean(false)
val allowQueryAll = AtomicBoolean(false)
val context: Context = mockThrowOnUnmocked {
whenever(
enforcePermission(
eq(android.Manifest.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION),
anyInt(), anyInt(), anyString()
)
) {
if (!allowUserSelection.get()) {
throw SecurityException()
}
}
initPermission(
allowUserSelection,
android.Manifest.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION
)
initPermission(
allowPreferredApps,
android.Manifest.permission.SET_PREFERRED_APPLICATIONS
)
initPermission(allowQueryAll, android.Manifest.permission.QUERY_ALL_PACKAGES)
}
val target = params.construct(context)
@@ -341,27 +394,41 @@ class DomainVerificationEnforcerTest {
verifyNoMoreInteractions(context)
assertFails { runMethod(target, VERIFIER_UID) }
assertFails { runMethod(target, NON_VERIFIER_UID) }
// Check that the verifier only needs QUERY_ALL to pass
allowQueryAll.set(true)
runMethod(target, VERIFIER_UID)
assertThrows(SecurityException::class.java) { runMethod(target, NON_VERIFIER_UID) }
allowQueryAll.set(false)
allowPreferredApps.set(true)
assertFails { runMethod(target, NON_VERIFIER_UID) }
allowUserSelection.set(true)
assertFails { runMethod(target, NON_VERIFIER_UID) }
allowQueryAll.set(true)
runMethod(target, NON_VERIFIER_UID)
}
fun approvedVerifier() {
val shouldThrow = AtomicBoolean(false)
val allowDomainVerificationAgent = AtomicBoolean(false)
val allowIntentVerificationAgent = AtomicBoolean(false)
val allowQueryAll = AtomicBoolean(false)
val context: Context = mockThrowOnUnmocked {
whenever(
enforcePermission(
eq(android.Manifest.permission.DOMAIN_VERIFICATION_AGENT),
anyInt(), anyInt(), anyString()
)
) {
if (shouldThrow.get()) {
throw SecurityException()
}
}
initPermission(
allowDomainVerificationAgent,
android.Manifest.permission.DOMAIN_VERIFICATION_AGENT
)
initPermission(
allowIntentVerificationAgent,
android.Manifest.permission.INTENT_FILTER_VERIFICATION_AGENT
)
initPermission(allowQueryAll, android.Manifest.permission.QUERY_ALL_PACKAGES)
}
val target = params.construct(context)
@@ -369,94 +436,241 @@ class DomainVerificationEnforcerTest {
verifyNoMoreInteractions(context)
assertFails { runMethod(target, VERIFIER_UID) }
assertFails { runMethod(target, NON_VERIFIER_UID) }
allowDomainVerificationAgent.set(true)
assertFails { runMethod(target, VERIFIER_UID) }
assertFails { runMethod(target, NON_VERIFIER_UID) }
allowQueryAll.set(true)
runMethod(target, VERIFIER_UID)
assertThrows(SecurityException::class.java) { runMethod(target, NON_VERIFIER_UID) }
assertFails { runMethod(target, NON_VERIFIER_UID) }
shouldThrow.set(true)
// Check that v1 verifiers are also allowed through
allowDomainVerificationAgent.set(false)
allowIntentVerificationAgent.set(true)
assertThrows(SecurityException::class.java) { runMethod(target, VERIFIER_UID) }
assertThrows(SecurityException::class.java) { runMethod(target, NON_VERIFIER_UID) }
runMethod(target, VERIFIER_UID)
assertFails { runMethod(target, NON_VERIFIER_UID) }
}
fun approvedUserSelector(verifyCrossUser: Boolean) {
val allowUserSelection = AtomicBoolean(true)
val allowInteractAcrossUsers = AtomicBoolean(true)
val allowUserSelection = AtomicBoolean(false)
val allowInteractAcrossUsers = AtomicBoolean(false)
val context: Context = mockThrowOnUnmocked {
whenever(
enforcePermission(
eq(android.Manifest.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION),
anyInt(), anyInt(), anyString()
)
) {
if (!allowUserSelection.get()) {
throw SecurityException()
}
}
whenever(
enforcePermission(
eq(android.Manifest.permission.INTERACT_ACROSS_USERS),
anyInt(), anyInt(), anyString()
)
) {
if (!allowInteractAcrossUsers.get()) {
throw SecurityException()
}
}
initPermission(
allowUserSelection,
android.Manifest.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION
)
initPermission(
allowInteractAcrossUsers,
android.Manifest.permission.INTERACT_ACROSS_USERS
)
}
val target = params.construct(context)
fun runEachTestCaseWrapped(
callingUserId: Int,
targetUserId: Int,
block: (testCase: () -> Unit) -> Unit = { it.invoke() }
) {
block { runMethod(target, VERIFIER_UID, callingUserId, targetUserId) }
block { runMethod(target, NON_VERIFIER_UID, callingUserId, targetUserId) }
fun runTestCases(callingUserId: Int, targetUserId: Int, throws: Boolean) {
// User selector makes no distinction by UID
val allUids = INTERNAL_UIDS + VERIFIER_UID + NON_VERIFIER_UID
if (throws) {
allUids.forEach {
assertFails {
runMethod(target, it, visible = true, callingUserId, targetUserId)
}
}
} else {
allUids.forEach {
runMethod(target, it, visible = true, callingUserId, targetUserId)
}
}
// User selector doesn't use QUERY_ALL, so the invisible package should always fail
allUids.forEach {
assertFails {
runMethod(target, it, visible = false, callingUserId, targetUserId)
}
}
}
val callingUserId = 0
val notCallingUserId = 1
runEachTestCaseWrapped(callingUserId, callingUserId)
runTestCases(callingUserId, callingUserId, throws = true)
if (verifyCrossUser) {
runEachTestCaseWrapped(callingUserId, notCallingUserId)
runTestCases(callingUserId, notCallingUserId, throws = true)
}
allowInteractAcrossUsers.set(false)
runEachTestCaseWrapped(callingUserId, callingUserId)
allowUserSelection.set(true)
runTestCases(callingUserId, callingUserId, throws = false)
if (verifyCrossUser) {
runEachTestCaseWrapped(callingUserId, notCallingUserId) {
assertThrows(SecurityException::class.java, it)
}
}
allowUserSelection.set(false)
runEachTestCaseWrapped(callingUserId, callingUserId) {
assertThrows(SecurityException::class.java, it)
}
if (verifyCrossUser) {
runEachTestCaseWrapped(callingUserId, notCallingUserId) {
assertThrows(SecurityException::class.java, it)
}
runTestCases(callingUserId, notCallingUserId, throws = true)
}
allowInteractAcrossUsers.set(true)
runEachTestCaseWrapped(callingUserId, callingUserId) {
assertThrows(SecurityException::class.java, it)
}
runTestCases(callingUserId, callingUserId, throws = false)
if (verifyCrossUser) {
runEachTestCaseWrapped(callingUserId, notCallingUserId) {
assertThrows(SecurityException::class.java, it)
runTestCases(callingUserId, notCallingUserId, throws = false)
}
}
private fun legacyUserSelector() {
val allowInteractAcrossUsers = AtomicBoolean(false)
val allowPreferredApps = AtomicBoolean(false)
val context: Context = mockThrowOnUnmocked {
initPermission(
allowInteractAcrossUsers,
android.Manifest.permission.INTERACT_ACROSS_USERS
)
initPermission(
allowPreferredApps,
android.Manifest.permission.SET_PREFERRED_APPLICATIONS
)
}
val target = params.construct(context)
fun runTestCases(callingUserId: Int, targetUserId: Int, throws: Boolean) {
// Legacy makes no distinction by UID
val allUids = INTERNAL_UIDS + VERIFIER_UID + NON_VERIFIER_UID
if (throws) {
allUids.forEach {
assertFails {
runMethod(target, it, visible = true, callingUserId, targetUserId)
}
}
} else {
allUids.forEach {
runMethod(target, it, visible = true, callingUserId, targetUserId)
}
}
// Legacy doesn't use QUERY_ALL, so the invisible package should always fail
allUids.forEach {
assertFails {
runMethod(target, it, visible = false, callingUserId, targetUserId)
}
}
}
val callingUserId = 0
val notCallingUserId = 1
runTestCases(callingUserId, callingUserId, throws = true)
runTestCases(callingUserId, notCallingUserId, throws = true)
allowPreferredApps.set(true)
runTestCases(callingUserId, callingUserId, throws = false)
runTestCases(callingUserId, notCallingUserId, throws = true)
allowInteractAcrossUsers.set(true)
runTestCases(callingUserId, callingUserId, throws = false)
runTestCases(callingUserId, notCallingUserId, throws = false)
}
private fun legacyQuerent() {
val allowInteractAcrossUsers = AtomicBoolean(false)
val allowInteractAcrossUsersFull = AtomicBoolean(false)
val context: Context = mockThrowOnUnmocked {
initPermission(
allowInteractAcrossUsers,
android.Manifest.permission.INTERACT_ACROSS_USERS
)
initPermission(
allowInteractAcrossUsersFull,
android.Manifest.permission.INTERACT_ACROSS_USERS_FULL
)
}
val target = params.construct(context)
fun runTestCases(callingUserId: Int, targetUserId: Int, throws: Boolean) {
// Legacy makes no distinction by UID
val allUids = INTERNAL_UIDS + VERIFIER_UID + NON_VERIFIER_UID
if (throws) {
allUids.forEach {
assertFails {
runMethod(target, it, visible = true, callingUserId, targetUserId)
}
}
} else {
allUids.forEach {
runMethod(target, it, visible = true, callingUserId, targetUserId)
}
}
// Legacy doesn't use QUERY_ALL, so the invisible package should always fail
allUids.forEach {
assertFails {
runMethod(target, it, visible = false, callingUserId, targetUserId)
}
}
}
val callingUserId = 0
val notCallingUserId = 1
runTestCases(callingUserId, callingUserId, throws = false)
runTestCases(callingUserId, notCallingUserId, throws = true)
// Legacy requires the _FULL permission, so this should continue to fail
allowInteractAcrossUsers.set(true)
runTestCases(callingUserId, callingUserId, throws = false)
runTestCases(callingUserId, notCallingUserId, throws = true)
allowInteractAcrossUsersFull.set(true)
runTestCases(callingUserId, callingUserId, throws = false)
runTestCases(callingUserId, notCallingUserId, throws = false)
}
private fun Context.initPermission(boolean: AtomicBoolean, permission: String) {
whenever(enforcePermission(eq(permission), anyInt(), anyInt(), anyString())) {
if (!boolean.get()) {
throw SecurityException()
}
}
whenever(checkPermission(eq(permission), anyInt(), anyInt())) {
if (boolean.get()) {
PackageManager.PERMISSION_GRANTED
} else {
PackageManager.PERMISSION_DENIED
}
}
}
private fun runMethod(target: Any, callingUid: Int, callingUserId: Int = 0, userId: Int = 0) {
params.runMethod(target, callingUid, callingUserId, userId, proxy)
private fun runMethod(
target: Any,
callingUid: Int,
visible: Boolean = true,
callingUserId: Int = 0,
userId: Int = 0
): Any? {
val packageName = if (visible) VISIBLE_PKG else INVISIBLE_PKG
val uuid = if (visible) VISIBLE_UUID else INVISIBLE_UUID
return params.runMethod(target, callingUid, callingUserId, userId, packageName, uuid, proxy)
}
private fun assertFails(block: () -> Any?) {
try {
val value = block()
// Some methods return false rather than throwing, so check that as well
if ((value as? Boolean) != false) {
// Can also return default value if it's a legacy call
if ((value as? Int)
!= PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED
) {
throw AssertionError("Expected call to return false, was $value")
}
}
} catch (e: SecurityException) {
} catch (e: PackageManager.NameNotFoundException) {
} catch (e: DomainVerificationManager.InvalidDomainSetException) {
// Any of these 3 exceptions are considered failures, which is expected
}
}
enum class Type {
@@ -473,6 +687,12 @@ class DomainVerificationEnforcerTest {
SELECTOR,
// Holding the user setting permission, but targeting cross user
SELECTOR_USER
SELECTOR_USER,
// Legacy required no permissions except when cross-user
LEGACY_QUERENT,
// Holding the legacy preferred apps permission
LEGACY_SELECTOR
}
}

View File

@@ -267,5 +267,8 @@ class DomainVerificationSettingsMutationTest {
whenever(getPackageLocked(TEST_PKG)) { mockPkg() }
whenever(schedule(anyInt(), any()))
whenever(scheduleWriteSettings())
// This doesn't check for visibility; that's done in the enforcer test
whenever(filterAppAccess(anyString(), anyInt(), anyInt())) { false }
}
}