[PM-13101] Validate FIDO2 privileged apps against community allow list (#4022)

This commit is contained in:
Patrick Honkonen 2024-10-07 08:57:07 -04:00 committed by GitHub
parent 60fce08c7e
commit 73a802a483
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 81 additions and 7 deletions

View file

@ -31,7 +31,8 @@ import kotlinx.serialization.SerializationException
import kotlinx.serialization.encodeToString
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].
@ -203,7 +204,8 @@ class Fido2CredentialManagerImpl(
callingAppInfo: CallingAppInfo,
relyingPartyId: String,
): Fido2ValidateOriginResult {
return digitalAssetLinkService.getDigitalAssetLinkForRp(relyingParty = relyingPartyId)
return digitalAssetLinkService
.getDigitalAssetLinkForRp(relyingParty = relyingPartyId)
.onFailure {
return Fido2ValidateOriginResult.Error.AssetLinkNotFound
}
@ -215,7 +217,8 @@ class Fido2CredentialManagerImpl(
?: return Fido2ValidateOriginResult.Error.ApplicationNotFound
}
.map { matchingStatements ->
callingAppInfo.getSignatureFingerprintAsHexString()
callingAppInfo
.getSignatureFingerprintAsHexString()
?.let { certificateFingerprint ->
matchingStatements
.filterMatchingAppSignaturesOrNull(
@ -236,9 +239,46 @@ class Fido2CredentialManagerImpl(
private suspend fun validatePrivilegedAppOrigin(
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 =
assetManager
.readAsset(ALLOW_LIST_FILE_NAME)
.readAsset(fileName)
.map { allowList ->
callingAppInfo.validatePrivilegedApp(
allowList = allowList,

View file

@ -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.ui.vault.feature.addedit.util.createMockPasskeyAssertionOptions
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.createMockPasskeyAttestationOptions
import io.mockk.Ordering
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
@ -139,11 +140,43 @@ class Fido2CredentialManagerTest {
coVerify(exactly = 1) {
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
fun `validateOrigin should return Success when privileged app is allowed`() =
runTest {
@ -934,7 +967,7 @@ class Fido2CredentialManagerTest {
)
coVerify {
assetManager.readAsset(DEFAULT_ALLOW_LIST_FILENAME)
assetManager.readAsset(GOOGLE_ALLOW_LIST_FILENAME)
mockVaultSdkSource.authenticateFido2Credential(
request = 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 const val DEFAULT_ALLOW_LIST = """
{