Skip to content

Commit 68ccf3d

Browse files
committed
fix(openid4vci): use trusted HTTPS nonce fixtures
1 parent ff5386f commit 68ccf3d

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
@@ -10,18 +10,37 @@ import id.walt.wallet2.mobile.MobileWalletConfig
1010
import id.walt.wallet2.mobile.MobileWalletFactory
1111
import id.walt.wallet2.mobile.MobileWalletPresentationResult
1212
import id.walt.wallet2.mobile.WalletAttestationConfig
13+
import io.ktor.client.HttpClient
14+
import io.ktor.client.engine.android.Android
15+
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
16+
import io.ktor.serialization.kotlinx.json.json
1317
import kotlinx.coroutines.runBlocking
18+
import kotlinx.serialization.json.Json
19+
import org.junit.After
1420
import org.junit.Test
21+
import java.io.ByteArrayInputStream
22+
import java.security.KeyStore
23+
import java.security.cert.CertificateFactory
1524
import java.util.UUID
25+
import javax.net.ssl.SSLContext
26+
import javax.net.ssl.TrustManagerFactory
1627
import kotlin.test.assertIs
1728
import kotlin.test.assertTrue
1829

1930
@EnterpriseMobileTest
2031
class EnterpriseMobileWalletIntegrationTest {
2132

33+
private var enterpriseHttpClient: HttpClient? = null
34+
2235
private val context: Context
2336
get() = InstrumentationRegistry.getInstrumentation().targetContext
2437

38+
@After
39+
fun closeEnterpriseHttpClient() {
40+
enterpriseHttpClient?.close()
41+
enterpriseHttpClient = null
42+
}
43+
2544
private val fixtureBaseUrl: String?
2645
get() = InstrumentationRegistry.getArguments().getString("enterprise_fixture_base_url")
2746

@@ -132,14 +151,45 @@ class EnterpriseMobileWalletIntegrationTest {
132151
private suspend fun createWallet(
133152
walletId: String,
134153
attestation: EnterpriseMobileAttestationConfig?,
135-
) = MobileWalletFactory(context).create(
136-
MobileWalletConfig(
154+
) = MobileWalletFactory(context).createWithHttpClient(
155+
config = MobileWalletConfig(
137156
walletId = walletId,
138157
attestationConfig = attestation?.toWalletAttestationConfig(),
139158
onEvent = { event -> println("WALLET EVENT: $event") },
140-
)
159+
),
160+
httpClient = enterpriseHttpClient(),
141161
)
142162

163+
private suspend fun enterpriseHttpClient(): HttpClient = enterpriseHttpClient ?: run {
164+
val certificate = CertificateFactory.getInstance("X.509").generateCertificate(
165+
ByteArrayInputStream(requireFixture().caCertificate())
166+
)
167+
val systemTrustStore = KeyStore.getInstance("AndroidCAStore").apply { load(null) }
168+
val trustStore = KeyStore.getInstance(KeyStore.getDefaultType()).apply {
169+
load(null, null)
170+
val aliases = systemTrustStore.aliases()
171+
while (aliases.hasMoreElements()) {
172+
val alias = aliases.nextElement()
173+
systemTrustStore.getCertificate(alias)?.let { setCertificateEntry("system-$alias", it) }
174+
}
175+
setCertificateEntry("waltid-enterprise-test-ca", certificate)
176+
}
177+
val trustManagers = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).apply {
178+
init(trustStore)
179+
}.trustManagers
180+
val sslContext = SSLContext.getInstance("TLS").apply {
181+
init(null, trustManagers, null)
182+
}
183+
HttpClient(Android) {
184+
engine {
185+
sslManager = { connection -> connection.sslSocketFactory = sslContext.socketFactory }
186+
}
187+
install(ContentNegotiation) {
188+
json(Json { ignoreUnknownKeys = true })
189+
}
190+
}.also { enterpriseHttpClient = it }
191+
}
192+
143193
private fun EnterpriseMobileAttestationConfig.toWalletAttestationConfig() =
144194
WalletAttestationConfig(
145195
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
@@ -31,6 +31,7 @@ import id.waltid.openid4vci.wallet.attestation.ClientAttestationAssembler
3131
import id.waltid.openid4vci.wallet.attestation.HttpWalletAttestationProvider
3232
import id.waltid.openid4vp.wallet.WalletPresentFunctionality2
3333
import id.waltid.openid4vp.wallet.WalletPresentFunctionality2.WalletPresentResult
34+
import io.ktor.client.HttpClient
3435
import io.ktor.http.Url
3536
import kotlinx.coroutines.flow.Flow
3637
import kotlinx.coroutines.flow.toList
@@ -163,6 +164,7 @@ public class MobileWallet internal constructor(
163164
private val keyGenerator: suspend (KeyType) -> Key,
164165
private val defaultKeyType: MobileWalletKeyType = MobileWalletKeyType.secp256r1,
165166
attestationConfig: WalletAttestationConfig? = null,
167+
private val httpClient: HttpClient? = null,
166168
private val transactionDataProfiles: List<MobileWalletTransactionDataProfile> = emptyList(),
167169
private val onEvent: suspend (MobileWalletEvent) -> Unit = {},
168170
private val deleteLocalPersistence: suspend () -> Unit = {},
@@ -175,13 +177,22 @@ public class MobileWallet internal constructor(
175177
public val events: Flow<MobileWalletEvent> = eventStream.events
176178

177179
private val attestationAssembler: ClientAttestationAssembler? = attestationConfig?.let { config ->
178-
ClientAttestationAssembler(
180+
val provider = httpClient?.let {
179181
HttpWalletAttestationProvider(
180182
baseUrl = config.baseUrl,
181183
attesterPath = config.attesterPath,
182184
bearerToken = config.bearerToken,
183185
hostHeader = config.hostHeader,
186+
httpClient = it,
184187
)
188+
} ?: HttpWalletAttestationProvider(
189+
baseUrl = config.baseUrl,
190+
attesterPath = config.attesterPath,
191+
bearerToken = config.bearerToken,
192+
hostHeader = config.hostHeader,
193+
)
194+
ClientAttestationAssembler(
195+
provider
185196
)
186197
}
187198

@@ -254,17 +265,17 @@ public class MobileWallet internal constructor(
254265
* @param offerUrl Credential offer URL, including `openid-credential-offer://` URLs.
255266
* @return Issuer, offered credential, and transaction-code metadata for app-side review.
256267
*/
257-
public suspend fun resolveOffer(offerUrl: String): MobileWalletOfferResolution =
258-
WalletIssuanceHandler.previewOffer(
259-
wallet = wallet,
260-
request = ResolveOfferRequest(offerUrl = Url(offerUrl.trim())),
261-
).let { result ->
262-
MobileWalletOfferResolution(
263-
transactionCodeRequired = result.txCodeRequired,
264-
credentialIssuer = result.credentialIssuer,
265-
offeredCredentials = result.offeredCredentials,
266-
)
267-
}
268+
public suspend fun resolveOffer(offerUrl: String): MobileWalletOfferResolution {
269+
val request = ResolveOfferRequest(offerUrl = Url(offerUrl.trim()))
270+
val result = httpClient?.let {
271+
WalletIssuanceHandler.previewOffer(wallet = wallet, request = request, httpClient = it)
272+
} ?: WalletIssuanceHandler.previewOffer(wallet = wallet, request = request)
273+
return MobileWalletOfferResolution(
274+
transactionCodeRequired = result.txCodeRequired,
275+
credentialIssuer = result.credentialIssuer,
276+
offeredCredentials = result.offeredCredentials,
277+
)
278+
}
268279

269280
/**
270281
* Receives credentials from an OpenID4VCI credential offer.
@@ -281,17 +292,28 @@ public class MobileWallet internal constructor(
281292
offerUrl: String,
282293
txCode: String? = null,
283294
clientId: String = "wallet-client",
284-
): List<String> =
285-
WalletIssuanceHandler.receiveCredential(
295+
): List<String> {
296+
val request = ReceiveCredentialRequest(
297+
offerUrl = Url(offerUrl.trim()),
298+
txCode = txCode?.ifBlank { null },
299+
clientId = clientId,
300+
)
301+
val result = httpClient?.let {
302+
WalletIssuanceHandler.receiveCredential(
303+
wallet = wallet,
304+
request = request,
305+
attestationAssembler = attestationAssembler,
306+
onEvent = ::emitSessionEvent,
307+
httpClient = it,
308+
)
309+
} ?: WalletIssuanceHandler.receiveCredential(
286310
wallet = wallet,
287-
request = ReceiveCredentialRequest(
288-
offerUrl = Url(offerUrl.trim()),
289-
txCode = txCode?.ifBlank { null },
290-
clientId = clientId,
291-
),
311+
request = request,
292312
attestationAssembler = attestationAssembler,
293313
onEvent = ::emitSessionEvent,
294-
).credentialIds
314+
)
315+
return result.credentialIds
316+
}
295317

296318
/**
297319
* 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)