Skip to content

Commit 5cc09ef

Browse files
committed
fix(openid4vci): use trusted HTTPS nonce fixtures
1 parent 19deae7 commit 5cc09ef

25 files changed

Lines changed: 364 additions & 195 deletions

File tree

waltid-libraries/protocols/waltid-mobile-test-utils/src/commonMain/kotlin/id/walt/mobile/test/backend/EnterpriseMobileFixtureClient.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import io.ktor.client.request.header
88
import io.ktor.client.request.post
99
import io.ktor.client.request.setBody
1010
import io.ktor.client.statement.HttpResponse
11+
import io.ktor.client.statement.bodyAsBytes
1112
import io.ktor.client.statement.bodyAsText
1213
import io.ktor.http.ContentType
1314
import io.ktor.http.HttpHeaders
@@ -34,6 +35,13 @@ class EnterpriseMobileFixtureClient(
3435
return json.decodeFromString(ListSerializer(EnterpriseMobileScenario.serializer()), response.requireBody())
3536
}
3637

38+
suspend fun caCertificate(): ByteArray = client.get("$baseUrl/test-ca.cer").let { response ->
39+
if (!response.status.isSuccess()) {
40+
error("HTTP ${response.status.value} while obtaining the Enterprise test CA")
41+
}
42+
response.bodyAsBytes()
43+
}
44+
3745
suspend fun createOffer(
3846
scenario: EnterpriseMobileScenario,
3947
platform: EnterpriseMobilePlatform,

waltid-libraries/protocols/waltid-openid4vc-wallet-mobile/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ kotlin {
8484
implementation(identityLibs.androidx.test.runner)
8585
implementation(identityLibs.androidx.test.ext.junit)
8686
implementation(identityLibs.ktor.client.android)
87+
implementation(identityLibs.ktor.client.content.negotiation)
88+
implementation(identityLibs.ktor.serialization.kotlinx.json)
8789
}
8890
}
8991
}

waltid-libraries/protocols/waltid-openid4vc-wallet-mobile/src/androidDeviceTest/kotlin/id/walt/wallet2/mobile/test/EnterpriseMobileWalletIntegrationTest.kt

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,36 @@ import id.walt.mobile.test.backend.EnterpriseMobileScenario
99
import id.walt.wallet2.mobile.MobileWalletConfig
1010
import id.walt.wallet2.mobile.MobileWalletFactory
1111
import id.walt.wallet2.mobile.WalletAttestationConfig
12+
import io.ktor.client.HttpClient
13+
import io.ktor.client.engine.android.Android
14+
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
15+
import io.ktor.serialization.kotlinx.json.json
1216
import kotlinx.coroutines.runBlocking
17+
import kotlinx.serialization.json.Json
18+
import org.junit.After
1319
import org.junit.Test
20+
import java.io.ByteArrayInputStream
21+
import java.security.KeyStore
22+
import java.security.cert.CertificateFactory
1423
import java.util.UUID
24+
import javax.net.ssl.SSLContext
25+
import javax.net.ssl.TrustManagerFactory
1526
import kotlin.test.assertTrue
1627

1728
@EnterpriseMobileTest
1829
class EnterpriseMobileWalletIntegrationTest {
1930

31+
private var enterpriseHttpClient: HttpClient? = null
32+
2033
private val context: Context
2134
get() = InstrumentationRegistry.getInstrumentation().targetContext
2235

36+
@After
37+
fun closeEnterpriseHttpClient() {
38+
enterpriseHttpClient?.close()
39+
enterpriseHttpClient = null
40+
}
41+
2342
private val fixtureBaseUrl: String?
2443
get() = InstrumentationRegistry.getArguments().getString("enterprise_fixture_base_url")
2544

@@ -130,14 +149,45 @@ class EnterpriseMobileWalletIntegrationTest {
130149
private suspend fun createWallet(
131150
walletId: String,
132151
attestation: EnterpriseMobileAttestationConfig?,
133-
) = MobileWalletFactory(context).create(
134-
MobileWalletConfig(
152+
) = MobileWalletFactory(context).createWithHttpClient(
153+
config = MobileWalletConfig(
135154
walletId = walletId,
136155
attestationConfig = attestation?.toWalletAttestationConfig(),
137156
onEvent = { event -> println("WALLET EVENT: $event") },
138-
)
157+
),
158+
httpClient = enterpriseHttpClient(),
139159
)
140160

161+
private suspend fun enterpriseHttpClient(): HttpClient = enterpriseHttpClient ?: run {
162+
val certificate = CertificateFactory.getInstance("X.509").generateCertificate(
163+
ByteArrayInputStream(requireFixture().caCertificate())
164+
)
165+
val systemTrustStore = KeyStore.getInstance("AndroidCAStore").apply { load(null) }
166+
val trustStore = KeyStore.getInstance(KeyStore.getDefaultType()).apply {
167+
load(null, null)
168+
val aliases = systemTrustStore.aliases()
169+
while (aliases.hasMoreElements()) {
170+
val alias = aliases.nextElement()
171+
systemTrustStore.getCertificate(alias)?.let { setCertificateEntry("system-$alias", it) }
172+
}
173+
setCertificateEntry("waltid-enterprise-test-ca", certificate)
174+
}
175+
val trustManagers = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).apply {
176+
init(trustStore)
177+
}.trustManagers
178+
val sslContext = SSLContext.getInstance("TLS").apply {
179+
init(null, trustManagers, null)
180+
}
181+
HttpClient(Android) {
182+
engine {
183+
sslManager = { connection -> connection.sslSocketFactory = sslContext.socketFactory }
184+
}
185+
install(ContentNegotiation) {
186+
json(Json { ignoreUnknownKeys = true })
187+
}
188+
}.also { enterpriseHttpClient = it }
189+
}
190+
141191
private fun EnterpriseMobileAttestationConfig.toWalletAttestationConfig() =
142192
WalletAttestationConfig(
143193
baseUrl = baseUrl,

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.content.Context
44
import id.walt.wallet2.persistence.encryption.AndroidDatabaseEncryptionKeyProvider
55
import id.walt.wallet2.persistence.keys.AndroidPlatformKeyProvider
66
import id.walt.wallet2.persistence.stores.DriverFactory
7+
import io.ktor.client.HttpClient
78

89
/**
910
* Android [MobileWallet] factory backed by Android KeyStore and an app-private SQLDelight database.
@@ -18,9 +19,18 @@ public actual class MobileWalletFactory(private val context: Context) {
1819
* through the Android platform key provider.
1920
*/
2021
public actual suspend fun create(config: MobileWalletConfig): MobileWallet {
22+
return create(config, null)
23+
}
24+
25+
internal suspend fun createWithHttpClient(config: MobileWalletConfig, httpClient: HttpClient): MobileWallet {
26+
return create(config, httpClient)
27+
}
28+
29+
private suspend fun create(config: MobileWalletConfig, httpClient: HttpClient?): MobileWallet {
2130
val driverFactory = DriverFactory(context)
2231
return createEncryptedSqlDelightMobileWallet(
2332
config = config,
33+
httpClient = httpClient,
2434
managedDatabaseKeyProvider = AndroidDatabaseEncryptionKeyProvider(context),
2535
platformKeyProvider = AndroidPlatformKeyProvider(),
2636
openEncryptedDriver = driverFactory::createEncryptedDriver,

waltid-libraries/protocols/waltid-openid4vc-wallet-mobile/src/commonMain/kotlin/id/walt/wallet2/mobile/MobileWallet.kt

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import id.walt.wallet2.handlers.WalletPresentationHandler
2727
import id.waltid.openid4vci.wallet.attestation.ClientAttestationAssembler
2828
import id.waltid.openid4vci.wallet.attestation.HttpWalletAttestationProvider
2929
import id.waltid.openid4vp.wallet.WalletPresentFunctionality2.WalletPresentResult
30+
import io.ktor.client.HttpClient
3031
import io.ktor.http.Url
3132
import kotlinx.coroutines.flow.Flow
3233
import kotlinx.coroutines.flow.toList
@@ -131,6 +132,7 @@ public class MobileWallet internal constructor(
131132
private val keyGenerator: suspend (KeyType) -> Key,
132133
private val defaultKeyType: MobileWalletKeyType = MobileWalletKeyType.secp256r1,
133134
attestationConfig: WalletAttestationConfig? = null,
135+
private val httpClient: HttpClient? = null,
134136
private val transactionDataProfiles: List<MobileWalletTransactionDataProfile> = emptyList(),
135137
private val onEvent: suspend (MobileWalletEvent) -> Unit = {},
136138
private val deleteLocalPersistence: suspend () -> Unit = {},
@@ -143,13 +145,22 @@ public class MobileWallet internal constructor(
143145
public val events: Flow<MobileWalletEvent> = eventStream.events
144146

145147
private val attestationAssembler: ClientAttestationAssembler? = attestationConfig?.let { config ->
146-
ClientAttestationAssembler(
148+
val provider = httpClient?.let {
147149
HttpWalletAttestationProvider(
148150
baseUrl = config.baseUrl,
149151
attesterPath = config.attesterPath,
150152
bearerToken = config.bearerToken,
151153
hostHeader = config.hostHeader,
154+
httpClient = it,
152155
)
156+
} ?: HttpWalletAttestationProvider(
157+
baseUrl = config.baseUrl,
158+
attesterPath = config.attesterPath,
159+
bearerToken = config.bearerToken,
160+
hostHeader = config.hostHeader,
161+
)
162+
ClientAttestationAssembler(
163+
provider
153164
)
154165
}
155166

@@ -222,17 +233,17 @@ public class MobileWallet internal constructor(
222233
* @param offerUrl Credential offer URL, including `openid-credential-offer://` URLs.
223234
* @return Issuer, offered credential, and transaction-code metadata for app-side review.
224235
*/
225-
public suspend fun resolveOffer(offerUrl: String): MobileWalletOfferResolution =
226-
WalletIssuanceHandler.previewOffer(
227-
wallet = wallet,
228-
request = ResolveOfferRequest(offerUrl = Url(offerUrl.trim())),
229-
).let { result ->
230-
MobileWalletOfferResolution(
231-
transactionCodeRequired = result.txCodeRequired,
232-
credentialIssuer = result.credentialIssuer,
233-
offeredCredentials = result.offeredCredentials,
234-
)
235-
}
236+
public suspend fun resolveOffer(offerUrl: String): MobileWalletOfferResolution {
237+
val request = ResolveOfferRequest(offerUrl = Url(offerUrl.trim()))
238+
val result = httpClient?.let {
239+
WalletIssuanceHandler.previewOffer(wallet = wallet, request = request, httpClient = it)
240+
} ?: WalletIssuanceHandler.previewOffer(wallet = wallet, request = request)
241+
return MobileWalletOfferResolution(
242+
transactionCodeRequired = result.txCodeRequired,
243+
credentialIssuer = result.credentialIssuer,
244+
offeredCredentials = result.offeredCredentials,
245+
)
246+
}
236247

237248
/**
238249
* Receives credentials from an OpenID4VCI credential offer.
@@ -249,17 +260,28 @@ public class MobileWallet internal constructor(
249260
offerUrl: String,
250261
txCode: String? = null,
251262
clientId: String = "wallet-client",
252-
): List<String> =
253-
WalletIssuanceHandler.receiveCredential(
263+
): List<String> {
264+
val request = ReceiveCredentialRequest(
265+
offerUrl = Url(offerUrl.trim()),
266+
txCode = txCode?.ifBlank { null },
267+
clientId = clientId,
268+
)
269+
val result = httpClient?.let {
270+
WalletIssuanceHandler.receiveCredential(
271+
wallet = wallet,
272+
request = request,
273+
attestationAssembler = attestationAssembler,
274+
onEvent = ::emitSessionEvent,
275+
httpClient = it,
276+
)
277+
} ?: WalletIssuanceHandler.receiveCredential(
254278
wallet = wallet,
255-
request = ReceiveCredentialRequest(
256-
offerUrl = Url(offerUrl.trim()),
257-
txCode = txCode?.ifBlank { null },
258-
clientId = clientId,
259-
),
279+
request = request,
260280
attestationAssembler = attestationAssembler,
261281
onEvent = ::emitSessionEvent,
262-
).credentialIds
282+
)
283+
return result.credentialIds
284+
}
263285

264286
/**
265287
* Lists all credentials currently stored in the mobile wallet.

waltid-libraries/protocols/waltid-openid4vc-wallet-mobile/src/commonMain/kotlin/id/walt/wallet2/mobile/MobileWalletFactory.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import id.walt.wallet2.persistence.stores.PlatformKeyStore
1414
import id.walt.wallet2.persistence.stores.SqlDelightCredentialStore
1515
import id.walt.wallet2.persistence.stores.SqlDelightDidStore
1616
import id.walt.verifier.openid.transactiondata.TransactionDataTypeRegistry
17+
import io.ktor.client.HttpClient
1718

1819
/**
1920
* Configuration for creating a [MobileWallet].
@@ -129,6 +130,7 @@ public expect class MobileWalletFactory {
129130

130131
internal suspend fun createEncryptedSqlDelightMobileWallet(
131132
config: MobileWalletConfig,
133+
httpClient: HttpClient? = null,
132134
managedDatabaseKeyProvider: DatabaseEncryptionKeyProvider,
133135
platformKeyProvider: PlatformKeyProvider,
134136
openEncryptedDriver: (
@@ -156,6 +158,7 @@ internal suspend fun createEncryptedSqlDelightMobileWallet(
156158
config = config,
157159
db = db,
158160
keyProvider = platformKeyProvider,
161+
httpClient = httpClient,
159162
deleteLocalPersistence = {
160163
runCatching { driver.close() }
161164
deleteDatabase(databaseName)
@@ -168,6 +171,7 @@ private fun createSqlDelightMobileWallet(
168171
config: MobileWalletConfig,
169172
db: WalletPersistenceDatabase,
170173
keyProvider: PlatformKeyProvider,
174+
httpClient: HttpClient?,
171175
deleteLocalPersistence: suspend () -> Unit,
172176
): MobileWallet {
173177
val queries = db.walletPersistenceQueries
@@ -185,6 +189,7 @@ private fun createSqlDelightMobileWallet(
185189
keyGenerator = keyGenerator,
186190
defaultKeyType = config.defaultKeyType,
187191
attestationConfig = config.attestationConfig,
192+
httpClient = httpClient,
188193
transactionDataProfiles = config.transactionDataProfiles,
189194
onEvent = config.onEvent,
190195
deleteLocalPersistence = deleteLocalPersistence,

waltid-libraries/protocols/waltid-openid4vc-wallet-server/src/main/kotlin/id/walt/wallet2/server/handlers/Wallet2RouteHandler.kt

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -119,29 +119,12 @@ object Wallet2RouteHandler {
119119
*/
120120
getAccountId: (suspend RoutingCall.() -> String?)? = null,
121121
attestationAssembler: ClientAttestationAssembler? = null,
122-
) = registerWallet2Routes(
123-
resolver = resolver,
124-
getAccountId = getAccountId,
125-
attestationAssembler = attestationAssembler,
126-
allowInsecureHttpForTests = false,
127-
)
128-
129-
/**
130-
* Test/development variant for local issuer fixtures whose nonce endpoint cannot use HTTPS.
131-
* Production callers must use the strict overload above.
132-
*/
133-
fun Route.registerWallet2Routes(
134-
resolver: WalletResolver,
135-
getAccountId: (suspend RoutingCall.() -> String?)?,
136-
attestationAssembler: ClientAttestationAssembler?,
137-
allowInsecureHttpForTests: Boolean,
138122
) {
139123
route("/wallet") {
140124
registerWalletManagementRoutes(
141125
resolver = resolver,
142126
getAccountId = getAccountId,
143127
attestationAssembler = attestationAssembler,
144-
allowInsecureHttpForTests = allowInsecureHttpForTests,
145128
)
146129
}
147130
route("/stores", { tags = listOf("Named Store Management") }) {
@@ -157,7 +140,6 @@ object Wallet2RouteHandler {
157140
resolver: WalletResolver,
158141
getAccountId: (suspend RoutingCall.() -> String?)?,
159142
attestationAssembler: ClientAttestationAssembler?,
160-
allowInsecureHttpForTests: Boolean,
161143
) {
162144

163145
post("", Wallet2OpenApiDocs.createWallet()) {
@@ -568,7 +550,6 @@ object Wallet2RouteHandler {
568550
wallet = wallet,
569551
request = req,
570552
attestationAssembler = attestationAssembler,
571-
allowInsecureHttpForTests = allowInsecureHttpForTests,
572553
)
573554
call.respond(result)
574555
} catch (e: Exception) {
@@ -617,7 +598,6 @@ object Wallet2RouteHandler {
617598
call.respond(
618599
WalletIssuanceHandler.requestNonce(
619600
request = req,
620-
allowInsecureHttpForTests = allowInsecureHttpForTests,
621601
)
622602
)
623603
}

0 commit comments

Comments
 (0)