Skip to content

Commit afbaa48

Browse files
committed
fix: tighten protected key authorization handling
1 parent 7981416 commit afbaa48

5 files changed

Lines changed: 58 additions & 25 deletions

File tree

waltid-libraries/crypto/waltid-crypto/src/androidDeviceTest/kotlin/id/walt/crypto/BiometricProtectedKeyInteractiveTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ class BiometricProtectedKeyInteractiveTest {
5050
kid = alias,
5151
keyType = KeyType.secp256r1,
5252
keyUseAuthorizationPolicy = KeyUseAuthorizationPolicy.BiometricCurrentSet,
53-
interactionContext = activity,
53+
interactionContextProvider = { activity },
5454
)
5555
)
5656
val plaintext = "authorize every key use".encodeToByteArray()
@@ -74,7 +74,7 @@ class BiometricProtectedKeyInteractiveTest {
7474
kid = alias,
7575
keyType = KeyType.secp256r1,
7676
keyUseAuthorizationPolicy = KeyUseAuthorizationPolicy.BiometricCurrentSet,
77-
interactionContext = activity,
77+
interactionContextProvider = { activity },
7878
)
7979
)
8080

waltid-libraries/crypto/waltid-crypto/src/androidMain/kotlin/id/walt/crypto/AndroidKey.kt

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import kotlinx.serialization.json.Json
2828
import kotlinx.serialization.json.JsonElement
2929
import kotlinx.serialization.json.JsonObject
3030
import org.bouncycastle.jce.provider.BouncyCastleProvider
31-
import java.lang.ref.WeakReference
3231
import kotlin.io.encoding.Base64
3332
import kotlin.time.Duration
3433
import kotlin.uuid.Uuid
@@ -40,22 +39,8 @@ sealed class AndroidKey : Key() {
4039
val keyType: KeyType,
4140
val keyUseAuthorizationPolicy: KeyUseAuthorizationPolicy = KeyUseAuthorizationPolicy.None,
4241
val authorizationPrompt: KeyUseAuthorizationPrompt = KeyUseAuthorizationPrompt(),
43-
interactionContext: Any? = null,
42+
private val interactionContextProvider: () -> FragmentActivity? = { null },
4443
) {
45-
private val interactionContextProvider: () -> FragmentActivity? = when (interactionContext) {
46-
is FragmentActivity -> {
47-
val reference = WeakReference(interactionContext)
48-
val provider: () -> FragmentActivity? = { reference.get() }
49-
provider
50-
}
51-
is Function0<*> -> {
52-
{ interactionContext.invoke() as? FragmentActivity }
53-
}
54-
else -> {
55-
{ null }
56-
}
57-
}
58-
5944
internal val interactionContext: FragmentActivity?
6045
get() = interactionContextProvider().takeIf { it.canHostBiometricPrompt() }
6146
}

waltid-libraries/crypto/waltid-crypto/src/iosMain/kotlin/id/walt/crypto/IosKey.kt

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package id.walt.crypto
22

33
import at.asitplus.signum.indispensable.josef.io.joseCompliantSerializer
44
import at.asitplus.signum.indispensable.josef.toJsonWebKey
5+
import at.asitplus.signum.supreme.CFCryptoOperationFailed
56
import at.asitplus.signum.supreme.SignatureResult
67
import at.asitplus.signum.supreme.dsl.REQUIRED
78
import at.asitplus.signum.supreme.os.IosSigner
@@ -21,6 +22,7 @@ import kotlinx.serialization.json.Json
2122
import kotlinx.serialization.json.JsonElement
2223
import kotlinx.serialization.json.JsonObject
2324
import kotlinx.serialization.json.jsonObject
25+
import platform.Security.errSecItemNotFound
2426
import kotlin.io.encoding.Base64
2527
import kotlin.time.Duration
2628
import kotlin.uuid.Uuid
@@ -242,16 +244,27 @@ private fun IosKey.Options.requireSupportedProtectedCombination() {
242244
}
243245
}
244246

245-
private fun IosKey.Options.mapPlatformFailure(throwable: Throwable): Throwable {
247+
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
248+
internal fun IosKey.Options.mapPlatformFailure(throwable: Throwable): Throwable {
246249
if (keyUseAuthorizationPolicy == KeyUseAuthorizationPolicy.None) return throwable
247-
return if (generateSequence(throwable as Throwable?) { it.cause }.any { it is NoSuchElementException }) {
248-
KeyUseAuthorizationException(
250+
val causes = generateSequence(throwable as Throwable?) { it.cause }.toList()
251+
return when {
252+
causes.any { it is NoSuchElementException } -> KeyUseAuthorizationException(
249253
failure = KeyUseAuthorizationFailure.ProtectedKeyMissing,
250254
message = "The protected key is missing",
251255
cause = throwable,
252256
)
253-
} else {
254-
throwable
257+
258+
// BiometryCurrentSet invalidation can leave Signum's public-key metadata present while the
259+
// OS makes the protected private key inaccessible as an absent Keychain item.
260+
causes.filterIsInstance<CFCryptoOperationFailed>().any { it.osStatus == errSecItemNotFound } ->
261+
KeyUseAuthorizationException(
262+
failure = KeyUseAuthorizationFailure.ProtectedKeyInvalidated,
263+
message = "The protected key is no longer usable under its biometric current-set policy",
264+
cause = throwable,
265+
)
266+
267+
else -> throwable
255268
}
256269
}
257270

waltid-libraries/crypto/waltid-crypto/src/iosTest/kotlin/id/walt/crypto/MobilePlatformKeySupportTest.kt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,28 @@
11
package id.walt.crypto
22

3+
import at.asitplus.signum.supreme.CFCryptoOperationFailed
34
import at.asitplus.signum.supreme.SignatureResult
45
import at.asitplus.signum.supreme.UnlockFailed
56
import id.walt.crypto.keys.KeyUseAuthorizationException
67
import id.walt.crypto.keys.KeyUseAuthorizationFailure
8+
import id.walt.crypto.keys.KeyUseAuthorizationPolicy
9+
import id.walt.crypto.keys.KeyType
10+
import kotlinx.cinterop.ExperimentalForeignApi
11+
import platform.Security.errSecItemNotFound
712
import kotlin.test.Test
813
import kotlin.test.assertEquals
914
import kotlin.test.assertFailsWith
15+
import kotlin.test.assertIs
1016
import kotlin.test.assertSame
1117

1218
class MobilePlatformKeySupportTest {
1319

20+
private val protectedOptions = IosKey.Options(
21+
keyType = KeyType.secp256r1,
22+
inSecureElement = true,
23+
keyUseAuthorizationPolicy = KeyUseAuthorizationPolicy.BiometricCurrentSet,
24+
)
25+
1426
@Test
1527
fun signumUnlockFailureMapsToStableAuthorizationFailureWithoutParsingMessage() {
1628
val result = SignatureResult.Failure(
@@ -39,6 +51,29 @@ class MobilePlatformKeySupportTest {
3951
assertSame(expected, actual)
4052
}
4153

54+
@OptIn(ExperimentalForeignApi::class)
55+
@Test
56+
fun inaccessibleCurrentSetKeyMapsToStableInvalidatedFailure() {
57+
val failure = assertIs<KeyUseAuthorizationException>(
58+
protectedOptions.mapPlatformFailure(
59+
CFCryptoOperationFailed("retrieve private key", errSecItemNotFound)
60+
)
61+
)
62+
63+
assertEquals(KeyUseAuthorizationFailure.ProtectedKeyInvalidated, failure.failure)
64+
}
65+
66+
@Test
67+
fun absentProtectedKeyRemainsStableMissingFailure() {
68+
val failure = assertIs<KeyUseAuthorizationException>(
69+
protectedOptions.mapPlatformFailure(
70+
NoSuchElementException("No key for alias exists")
71+
)
72+
)
73+
74+
assertEquals(KeyUseAuthorizationFailure.ProtectedKeyMissing, failure.failure)
75+
}
76+
4277
@Test
4378
fun unprotectedSignumFailureKeepsLegacyCheckFailure() {
4479
val result = SignatureResult.Failure(UnlockFailed("legacy failure"))

waltid-libraries/protocols/waltid-openid4vc-wallet-persistence-mobile/src/androidMain/kotlin/id/walt/wallet2/persistence/keys/AndroidPlatformKeyProvider.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public class AndroidPlatformKeyProvider private constructor(
7979
keyType = request.keyType,
8080
keyUseAuthorizationPolicy = request.keyUseAuthorizationPolicy,
8181
authorizationPrompt = authorizationPrompt,
82-
interactionContext = interactionContextProvider,
82+
interactionContextProvider = interactionContextProvider,
8383
)
8484
return if (isPlatformBacked(request.keyType)) {
8585
AndroidKey.Platform.create(options)
@@ -104,7 +104,7 @@ public class AndroidPlatformKeyProvider private constructor(
104104
keyType = keyType,
105105
keyUseAuthorizationPolicy = keyUseAuthorizationPolicy,
106106
authorizationPrompt = authorizationPrompt,
107-
interactionContext = interactionContextProvider,
107+
interactionContextProvider = interactionContextProvider,
108108
)
109109
return if (keyUseAuthorizationPolicy == KeyUseAuthorizationPolicy.None) {
110110
runCatching { AndroidKey.Platform.load(options) }.getOrNull()

0 commit comments

Comments
 (0)