mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
[PM-13101] Validate FIDO2 privileged apps against community allow list (#4022)
This commit is contained in:
parent
60fce08c7e
commit
73a802a483
3 changed files with 81 additions and 7 deletions
|
@ -31,7 +31,8 @@ import kotlinx.serialization.SerializationException
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
private const val ALLOW_LIST_FILE_NAME = "fido2_privileged_allow_list.json"
|
private const val GOOGLE_ALLOW_LIST_FILE_NAME = "fido2_privileged_google.json"
|
||||||
|
private const val COMMUNITY_ALLOW_LIST_FILE_NAME = "fido2_privileged_community.json"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Primary implementation of [Fido2CredentialManager].
|
* Primary implementation of [Fido2CredentialManager].
|
||||||
|
@ -203,7 +204,8 @@ class Fido2CredentialManagerImpl(
|
||||||
callingAppInfo: CallingAppInfo,
|
callingAppInfo: CallingAppInfo,
|
||||||
relyingPartyId: String,
|
relyingPartyId: String,
|
||||||
): Fido2ValidateOriginResult {
|
): Fido2ValidateOriginResult {
|
||||||
return digitalAssetLinkService.getDigitalAssetLinkForRp(relyingParty = relyingPartyId)
|
return digitalAssetLinkService
|
||||||
|
.getDigitalAssetLinkForRp(relyingParty = relyingPartyId)
|
||||||
.onFailure {
|
.onFailure {
|
||||||
return Fido2ValidateOriginResult.Error.AssetLinkNotFound
|
return Fido2ValidateOriginResult.Error.AssetLinkNotFound
|
||||||
}
|
}
|
||||||
|
@ -215,7 +217,8 @@ class Fido2CredentialManagerImpl(
|
||||||
?: return Fido2ValidateOriginResult.Error.ApplicationNotFound
|
?: return Fido2ValidateOriginResult.Error.ApplicationNotFound
|
||||||
}
|
}
|
||||||
.map { matchingStatements ->
|
.map { matchingStatements ->
|
||||||
callingAppInfo.getSignatureFingerprintAsHexString()
|
callingAppInfo
|
||||||
|
.getSignatureFingerprintAsHexString()
|
||||||
?.let { certificateFingerprint ->
|
?.let { certificateFingerprint ->
|
||||||
matchingStatements
|
matchingStatements
|
||||||
.filterMatchingAppSignaturesOrNull(
|
.filterMatchingAppSignaturesOrNull(
|
||||||
|
@ -236,9 +239,46 @@ class Fido2CredentialManagerImpl(
|
||||||
|
|
||||||
private suspend fun validatePrivilegedAppOrigin(
|
private suspend fun validatePrivilegedAppOrigin(
|
||||||
callingAppInfo: CallingAppInfo,
|
callingAppInfo: CallingAppInfo,
|
||||||
|
): Fido2ValidateOriginResult {
|
||||||
|
val googleAllowListResult =
|
||||||
|
validatePrivilegedAppSignatureWithGoogleList(callingAppInfo)
|
||||||
|
return when (googleAllowListResult) {
|
||||||
|
is Fido2ValidateOriginResult.Success -> {
|
||||||
|
// Application was found and successfully validated against the Google allow list so
|
||||||
|
// we can return the result as the final validation result.
|
||||||
|
googleAllowListResult
|
||||||
|
}
|
||||||
|
|
||||||
|
is Fido2ValidateOriginResult.Error -> {
|
||||||
|
// Check the community allow list if the Google allow list failed, and return the
|
||||||
|
// result as the final validation result.
|
||||||
|
validatePrivilegedAppSignatureWithCommunityList(callingAppInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun validatePrivilegedAppSignatureWithGoogleList(
|
||||||
|
callingAppInfo: CallingAppInfo,
|
||||||
|
): Fido2ValidateOriginResult =
|
||||||
|
validatePrivilegedAppSignatureWithAllowList(
|
||||||
|
callingAppInfo = callingAppInfo,
|
||||||
|
fileName = GOOGLE_ALLOW_LIST_FILE_NAME,
|
||||||
|
)
|
||||||
|
|
||||||
|
private suspend fun validatePrivilegedAppSignatureWithCommunityList(
|
||||||
|
callingAppInfo: CallingAppInfo,
|
||||||
|
): Fido2ValidateOriginResult =
|
||||||
|
validatePrivilegedAppSignatureWithAllowList(
|
||||||
|
callingAppInfo = callingAppInfo,
|
||||||
|
fileName = COMMUNITY_ALLOW_LIST_FILE_NAME,
|
||||||
|
)
|
||||||
|
|
||||||
|
private suspend fun validatePrivilegedAppSignatureWithAllowList(
|
||||||
|
callingAppInfo: CallingAppInfo,
|
||||||
|
fileName: String,
|
||||||
): Fido2ValidateOriginResult =
|
): Fido2ValidateOriginResult =
|
||||||
assetManager
|
assetManager
|
||||||
.readAsset(ALLOW_LIST_FILE_NAME)
|
.readAsset(fileName)
|
||||||
.map { allowList ->
|
.map { allowList ->
|
||||||
callingAppInfo.validatePrivilegedApp(
|
callingAppInfo.validatePrivilegedApp(
|
||||||
allowList = allowList,
|
allowList = allowList,
|
||||||
|
|
|
@ -34,6 +34,7 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockPublicKeyAt
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidFido2PublicKeyCredential
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidFido2PublicKeyCredential
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.createMockPasskeyAssertionOptions
|
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.createMockPasskeyAssertionOptions
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.createMockPasskeyAttestationOptions
|
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.createMockPasskeyAttestationOptions
|
||||||
|
import io.mockk.Ordering
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.coVerify
|
import io.mockk.coVerify
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
|
@ -139,11 +140,43 @@ class Fido2CredentialManagerTest {
|
||||||
|
|
||||||
coVerify(exactly = 1) {
|
coVerify(exactly = 1) {
|
||||||
assetManager.readAsset(
|
assetManager.readAsset(
|
||||||
fileName = DEFAULT_ALLOW_LIST_FILENAME,
|
fileName = GOOGLE_ALLOW_LIST_FILENAME,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `validateOrigin should validate with community allow list when google allow list validation fails`() =
|
||||||
|
runTest {
|
||||||
|
coEvery {
|
||||||
|
assetManager.readAsset(GOOGLE_ALLOW_LIST_FILENAME)
|
||||||
|
} returns MISSING_PACKAGE_ALLOW_LIST.asSuccess()
|
||||||
|
every {
|
||||||
|
mockPrivilegedCallingAppInfo.getOrigin(
|
||||||
|
privilegedAllowlist = MISSING_PACKAGE_ALLOW_LIST,
|
||||||
|
)
|
||||||
|
} throws IllegalStateException()
|
||||||
|
coEvery {
|
||||||
|
assetManager.readAsset(COMMUNITY_ALLOW_LIST_FILENAME)
|
||||||
|
} returns DEFAULT_ALLOW_LIST.asSuccess()
|
||||||
|
every {
|
||||||
|
mockPrivilegedCallingAppInfo.getOrigin(
|
||||||
|
privilegedAllowlist = DEFAULT_ALLOW_LIST,
|
||||||
|
)
|
||||||
|
} returns DEFAULT_PACKAGE_NAME
|
||||||
|
|
||||||
|
fido2CredentialManager.validateOrigin(
|
||||||
|
mockPrivilegedAppRequest.callingAppInfo,
|
||||||
|
mockPrivilegedAppRequest.requestJson,
|
||||||
|
)
|
||||||
|
|
||||||
|
coVerify(ordering = Ordering.ORDERED) {
|
||||||
|
assetManager.readAsset(GOOGLE_ALLOW_LIST_FILENAME)
|
||||||
|
assetManager.readAsset(COMMUNITY_ALLOW_LIST_FILENAME)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `validateOrigin should return Success when privileged app is allowed`() =
|
fun `validateOrigin should return Success when privileged app is allowed`() =
|
||||||
runTest {
|
runTest {
|
||||||
|
@ -934,7 +967,7 @@ class Fido2CredentialManagerTest {
|
||||||
)
|
)
|
||||||
|
|
||||||
coVerify {
|
coVerify {
|
||||||
assetManager.readAsset(DEFAULT_ALLOW_LIST_FILENAME)
|
assetManager.readAsset(GOOGLE_ALLOW_LIST_FILENAME)
|
||||||
mockVaultSdkSource.authenticateFido2Credential(
|
mockVaultSdkSource.authenticateFido2Credential(
|
||||||
request = any(),
|
request = any(),
|
||||||
fido2CredentialStore = any(),
|
fido2CredentialStore = any(),
|
||||||
|
@ -1033,7 +1066,8 @@ private val DEFAULT_STATEMENT = DigitalAssetLinkResponseJson(
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
private const val DEFAULT_ALLOW_LIST_FILENAME = "fido2_privileged_allow_list.json"
|
private const val GOOGLE_ALLOW_LIST_FILENAME = "fido2_privileged_google.json"
|
||||||
|
private const val COMMUNITY_ALLOW_LIST_FILENAME = "fido2_privileged_community.json"
|
||||||
private val DEFAULT_STATEMENT_LIST = listOf(DEFAULT_STATEMENT)
|
private val DEFAULT_STATEMENT_LIST = listOf(DEFAULT_STATEMENT)
|
||||||
private const val DEFAULT_ALLOW_LIST = """
|
private const val DEFAULT_ALLOW_LIST = """
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue