Skip to content

Commit ef8ab6d

Browse files
committed
fix(wallet): host iOS PKIX validation coverage
1 parent a9f2897 commit ef8ab6d

7 files changed

Lines changed: 113 additions & 7 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import Foundation
2+
3+
public enum X509RequestObjectFixture {
4+
public static let clientID = "x509_san_dns:verifier.example.com"
5+
6+
/// Test-only CA for the leaf in `signedRequestObject`, valid until July 2036.
7+
public static let trustAnchorPEM = """
8+
-----BEGIN CERTIFICATE-----
9+
MIIB4DCCAYegAwIBAgIUA/9XuKeSJXtWUSdtBGzaKnmVHrMwCgYIKoZIzj0EAwIw
10+
PjEqMCgGA1UEAwwhd2FsdC5pZCBBcHAtSG9zdGVkIFg1MDkgVGVzdCBSb290MRAw
11+
DgYDVQQKDAd3YWx0LmlkMB4XDTI2MDcyMTEwMjcwMVoXDTM2MDcxODEwMjcwMVow
12+
PjEqMCgGA1UEAwwhd2FsdC5pZCBBcHAtSG9zdGVkIFg1MDkgVGVzdCBSb290MRAw
13+
DgYDVQQKDAd3YWx0LmlkMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEP2Iu55PO
14+
zlwrg/MTYpLwbFwvtpdpx68WsA6l46EBHjfwquwQlS0Mqd5LvwBzspFULRY6nNi5
15+
RneWG6WhTiEQBKNjMGEwHQYDVR0OBBYEFAyoLc/HVdN+lzUziwIT+C4FNpBBMB8G
16+
A1UdIwQYMBaAFAyoLc/HVdN+lzUziwIT+C4FNpBBMA8GA1UdEwEB/wQFMAMBAf8w
17+
DgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMCA0cAMEQCIDbF5GL7Dk95vP7X+J3v
18+
X/4whS7wEaP7fK89tFgEGUTUAiByPxGxtjKLmEdPRyOUR3laRgXPmnV+HNyc8rL3
19+
3Fk2oA==
20+
-----END CERTIFICATE-----
21+
"""
22+
23+
/// Fixed ES256 Request Object with a matching `x5c` leaf, valid until January 2036.
24+
public static let signedRequestObject = [
25+
"eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QiLCJ4NWMiOlsiTUlJQjlEQ0NBWm1nQXdJQkFnSVVWQkF2WUFsczUxWlViaTZTN0d0S21jOWJ5S0l3Q2dZSUtvWkl6ajBFQXdJd1BqRXFNQ2dHQTFVRUF3d2hkMkZzZEM1cFpDQkJjSEF0U0c5emRHVmtJRmcxTURrZ1ZHVnpkQ0JTYjI5ME1SQXdEZ1lEVlFRS0RBZDNZV3gwTG1sa01CNFhEVEkyTURjeU1URXdNamN3TVZvWERUTTJNRGN4TnpFd01qY3dNVm93TVRFZE1Cc0dBMVVFQXd3VWRtVnlhV1pwWlhJdVpYaGhiWEJzWlM1amIyMHhFREFPQmdOVkJBb01CM2RoYkhRdWFXUXdXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBUzB3TnJ1QW4wS1FMQTR3Z0RqVHR0NmFBTTE0Z3Y4bmZhZEpLN0RGZzV0ZDRnSVNudG5DVGtDS3JIeU9TL08veGx0d0J5WXdlZ3ZQQlNwdklaa0UvTXJvNEdCTUg4d0RBWURWUjBUQVFIL0JBSXdBREFPQmdOVkhROEJBZjhFQkFNQ0I0QXdId1lEVlIwUkJCZ3dGb0lVZG1WeWFXWnBaWEl1WlhoaGJYQnNaUzVqYjIwd0hRWURWUjBPQkJZRUZGbEdUdXRub0laZTR3YkFlZ01adlpCb2d3TUxNQjhHQTFVZEl3UVlNQmFBRkF5b0xjL0hWZE4rbHpVeml3SVQrQzRGTnBCQk1Bb0dDQ3FHU000OUJBTUNBMGtBTUVZQ0lRQzFVaitQVmtJTnNxUDFZc21JWTY4a0lLc2ZzSjFsTUN3cFBSeFNUWVVmR1FJaEFKOENic0g3cTVKa0picHN5UTlWN3VZQlc4c21XSzg3T3JuRFJkZk0wUWVLIl19",
26+
".eyJhdWQiOiJodHRwczovL3NlbGYtaXNzdWVkLm1lL3YyIiwiZXhwIjoyMDgyNzU4NDAwLCJjbGllbnRfaWQiOiJ4NTA5X3Nhbl9kbnM6dmVyaWZpZXIuZXhhbXBsZS5jb20iLCJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJyZXNwb25zZV9tb2RlIjoiZGlyZWN0X3Bvc3QiLCJyZXNwb25zZV91cmkiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUuY29tL3Jlc3BvbnNlIiwibm9uY2UiOiJhcHAtaG9zdGVkLXg1MDktdGVzdCIsInN0YXRlIjoiYXBwLWhvc3RlZC14NTA5LXRlc3QiLCJkY3FsX3F1ZXJ5Ijp7ImNyZWRlbnRpYWxzIjpbeyJpZCI6InRlc3RfY3JlZGVudGlhbCIsImZvcm1hdCI6ImRjK3NkLWp3dCIsIm1ldGEiOnsidmN0X3ZhbHVlcyI6WyJ1cm46ZXhhbXBsZTp0ZXN0Il19fV0sImNyZWRlbnRpYWxfc2V0cyI6W3sib3B0aW9ucyI6W1sidGVzdF9jcmVkZW50aWFsIl1dLCJyZXF1aXJlZCI6ZmFsc2V9XX0sImNsaWVudF9tZXRhZGF0YSI6e319",
27+
".MscPu_lMJZURFAel1Myr2Lhi5SFgCKBjX0yO529dBsnVtV4tOq12ADgRz8twZ60KNgcToPcR2uuYse5ghgisrw",
28+
].joined()
29+
30+
public static let authorizationRequestURL: URL = {
31+
var components = URLComponents(string: "openid4vp://authorize")!
32+
components.queryItems = [
33+
URLQueryItem(name: "client_id", value: clientID),
34+
URLQueryItem(name: "request", value: signedRequestObject),
35+
]
36+
return components.url!
37+
}()
38+
}

waltid-applications/waltid-wallet-demo-ios/iosApp/iosAppTests/MobileWalletIntegrationTests.swift

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,48 @@ final class MobileWalletIntegrationTests: XCTestCase {
9393

9494
// MARK: - Tests (mirror Android MobileWalletIntegrationTest.kt)
9595

96+
func testAppHostedWalletValidatesSignedRequestObjectCertificateChain() async throws {
97+
let wallet = try await Wallet(
98+
configuration: WalletConfiguration(
99+
walletID: testWalletId,
100+
requestObjectTrustAnchorPEMCertificates: [X509RequestObjectFixture.trustAnchorPEM]
101+
)
102+
)
103+
104+
let preview = try await wallet.previewPresentation(
105+
request: X509RequestObjectFixture.authorizationRequestURL
106+
)
107+
108+
XCTAssertEqual(preview.request.clientID, X509RequestObjectFixture.clientID)
109+
XCTAssertEqual(preview.request.nonce, "app-hosted-x509-test")
110+
XCTAssertEqual(preview.credentialOptions, [])
111+
}
112+
113+
func testAppHostedWalletRejectsUntrustedSignedRequestObjectCertificateChain() async throws {
114+
let wallet = try await makeWallet()
115+
116+
do {
117+
_ = try await wallet.previewPresentation(
118+
request: X509RequestObjectFixture.authorizationRequestURL
119+
)
120+
XCTFail("Expected the signed Request Object to be rejected with an unrelated trust anchor")
121+
} catch {
122+
XCTAssertTrue(
123+
String(describing: error).contains("UntrustedCertificateChain"),
124+
"Expected an untrusted certificate-chain error, got: \(error)"
125+
)
126+
}
127+
}
128+
129+
func testWalletAcceptsSystemRequestObjectTrustAnchors() async throws {
130+
_ = try await Wallet(
131+
configuration: WalletConfiguration(
132+
walletID: testWalletId,
133+
requestObjectEnableSystemTrustAnchors: true
134+
)
135+
)
136+
}
137+
96138
func testBootstrapCreatesKeyAndDid() async throws {
97139
let wallet = try await makeWallet()
98140

waltid-libraries/crypto/waltid-x509/build.gradle.kts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
@file:OptIn(ExperimentalKotlinGradlePluginApi::class)
22

33
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
4+
import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeTest
45

56
plugins {
67
id("waltid.multiplatform.library")
@@ -96,3 +97,14 @@ mavenPublishing {
9697
description.set("walt.id Kotlin/Java library X.509")
9798
}
9899
}
100+
101+
if (enableIosBuild) {
102+
tasks.withType<KotlinNativeTest>().configureEach {
103+
if (name.startsWith("ios")) {
104+
// SecTrust returns OSStatus -26276 from the standalone Kotlin/Native
105+
// test executable. The iOS implementation is covered in the app-hosted XCTest
106+
// MobileWalletIntegrationTests.testAppHostedWalletValidatesSignedRequestObjectCertificateChain.
107+
filter.excludeTestsMatching("id.walt.x509.X509ValidationMPTest.*")
108+
}
109+
}
110+
}

waltid-libraries/protocols/waltid-openid4vc-wallet-mobile/src/iosMain/kotlin/id/walt/wallet2/mobile/swiftinterop/WalletSdkBridgeModels.kt

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ import kotlin.time.Instant
4545
* @property attestation Optional client-attestation configuration for issuers that require it.
4646
* @property requestObjectTrustAnchorPemCertificates Wallet-controlled PEM trust anchors for
4747
* signed OID4VP Request Objects.
48-
* @property requestObjectEnableSystemTrustAnchors Retained for source compatibility. iOS system
49-
* trust anchors are not supported for Request Object validation and `true` is rejected.
48+
* @property requestObjectEnableSystemTrustAnchors Whether iOS system trust anchors are accepted
49+
* for signed Request Object certificate validation.
5050
* @property requestObjectAudience Expected Request Object audience.
5151
* @property transactionDataProfiles Transaction data profiles this wallet accepts.
5252
*/
@@ -63,9 +63,6 @@ public data class WalletBridgeConfiguration(
6363
)
6464

6565
internal fun WalletBridgeConfiguration.toMobileWalletConfig(): MobileWalletConfig {
66-
require(!requestObjectEnableSystemTrustAnchors) {
67-
"iOS system trust anchors are not supported for OID4VP Request Object validation"
68-
}
6966
val x509Trust = if (requestObjectTrustAnchorPemCertificates.isNotEmpty() || requestObjectEnableSystemTrustAnchors) {
7067
WalletX509TrustConfig(
7168
trustAnchorPemCertificates = requestObjectTrustAnchorPemCertificates,

waltid-libraries/protocols/waltid-openid4vc-wallet-mobile/src/iosTest/kotlin/id/walt/wallet2/mobile/swiftinterop/WalletSdkBridgeTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ class WalletSdkBridgeTest {
239239
hostHeader = "attestation.example",
240240
),
241241
requestObjectTrustAnchorPemCertificates = listOf("test-anchor-pem"),
242+
requestObjectEnableSystemTrustAnchors = true,
242243
requestObjectAudience = "https://wallet.example",
243244
transactionDataProfiles = listOf(
244245
MobileWalletTransactionDataProfile(
@@ -265,6 +266,7 @@ class WalletSdkBridgeTest {
265266
listOf("test-anchor-pem"),
266267
capturedConfig?.requestObjectX509Trust?.trustAnchorPemCertificates,
267268
)
269+
assertEquals(true, capturedConfig?.requestObjectX509Trust?.enableSystemTrustAnchors)
268270
assertEquals("https://wallet.example", capturedConfig?.requestObjectAudience)
269271
assertEquals(
270272
listOf(

waltid-libraries/protocols/waltid-openid4vci/build.gradle.kts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeTest
2+
13
plugins {
24
id("waltid.full.library")
35
id("waltid.publish.maven")
@@ -70,3 +72,16 @@ mavenPublishing {
7072
description.set("walt.id Kotlin/Java OpenID4VCI library")
7173
}
7274
}
75+
76+
if (enableIosBuild) {
77+
tasks.withType<KotlinNativeTest>().configureEach {
78+
if (name.startsWith("ios")) {
79+
// This common test reaches SecTrust through client-attestation validation, which
80+
// returns OSStatus -26276 from the standalone Kotlin/Native executable.
81+
// App-hosted positive and negative PKIX coverage lives in MobileWalletIntegrationTests.
82+
filter.excludeTestsMatching(
83+
"id.walt.openid4vci.clientauth.attestation.verifier.X509ChainClientAttestationVerifierTest.*"
84+
)
85+
}
86+
}
87+
}

waltid-libraries/protocols/waltid-wallet-sdk-ios/Sources/WalletSDK/WalletModels.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ public struct WalletConfiguration: Sendable {
3838
/// - persistence: Local persistence configuration for wallet-owned state.
3939
/// - requestObjectTrustAnchorPEMCertificates: Wallet-controlled PEM trust
4040
/// anchors used to validate signed OID4VP Request Objects.
41-
/// - requestObjectEnableSystemTrustAnchors: Retained for compatibility. Passing `true`
42-
/// is rejected because iOS Request Object PKIX validation is not yet supported.
41+
/// - requestObjectEnableSystemTrustAnchors: Whether iOS system trust anchors are accepted
42+
/// for signed Request Object certificate validation.
4343
/// - requestObjectAudience: Expected audience of signed OID4VP Request Objects.
4444
/// - transactionDataProfiles: OpenID4VP transaction data profiles this
4545
/// wallet accepts before previewing or submitting a presentation.

0 commit comments

Comments
 (0)