Skip to content

Commit a9f2897

Browse files
committed
fix(wallet): restore mobile EHIC integration coverage
1 parent 9b728a6 commit a9f2897

3 files changed

Lines changed: 179 additions & 37 deletions

File tree

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

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,25 @@ import TestHelpers
66
actor EudiTestBackend {
77
static let shared = EudiTestBackend()
88

9-
/// Pinned `PID Issuer CA - UT 02` test-PKI anchor used by the EUDI verifier.
9+
/// Pinned `PID Issuer CA 02` test-PKI anchor used by the EUDI verifier.
1010
static let verifierTrustAnchorPEM = """
1111
-----BEGIN CERTIFICATE-----
12-
MIIC3TCCAoOgAwIBAgIUEwybFc9Jw+az3r188OiHDaxCfHEwCgYIKoZIzj0EAwMw
13-
XDEeMBwGA1UEAwwVUElEIElzc3VlciBDQSAtIFVUIDAyMS0wKwYDVQQKDCRFVURJ
14-
IFdhbGxldCBSZWZlcmVuY2UgSW1wbGVtZW50YXRpb24xCzAJBgNVBAYTAlVUMB4X
15-
DTI1MDMyNDIwMjYxNFoXDTM0MDYyMDIwMjYxM1owXDEeMBwGA1UEAwwVUElEIElz
16-
c3VlciBDQSAtIFVUIDAyMS0wKwYDVQQKDCRFVURJIFdhbGxldCBSZWZlcmVuY2Ug
17-
SW1wbGVtZW50YXRpb24xCzAJBgNVBAYTAlVUMFkwEwYHKoZIzj0CAQYIKoZIzj0D
18-
AQcDQgAEesDKj9rCIcrGj0wbSXYvCV953bOPSYLZH5TNmhTz2xa7VdlvQgQeGZRg
19-
1PrF5AFwt070wvL9qr1DUDdvLp6a1qOCASEwggEdMBIGA1UdEwEB/wQIMAYBAf8C
20-
AQAwHwYDVR0jBBgwFoAUYseURyi9D6IWIKeawkmURPEB08cwEwYDVR0lBAwwCgYI
21-
K4ECAgAAAQcwQwYDVR0fBDwwOjA4oDagNIYyaHR0cHM6Ly9wcmVwcm9kLnBraS5l
22-
dWRpdy5kZXYvY3JsL3BpZF9DQV9VVF8wMi5jcmwwHQYDVR0OBBYEFGLHlEcovQ+i
23-
FiCnmsJJlETxAdPHMA4GA1UdDwEB/wQEAwIBBjBdBgNVHRIEVjBUhlJodHRwczov
24-
L2dpdGh1Yi5jb20vZXUtZGlnaXRhbC1pZGVudGl0eS13YWxsZXQvYXJjaGl0ZWN0
25-
dXJlLWFuZC1yZWZlcmVuY2UtZnJhbWV3b3JrMAoGCCqGSM49BAMDA0gAMEUCIQCe
26-
4R9rO4JhFp821kO8Gkb8rXm4qGG/e5/Oi2XmnTQqOQIgfFs+LDbnP2/j1MB4rwZ1
27-
FgGdpr4oyrFB9daZyRIcP90=
12+
MIIC0zCCAnmgAwIBAgIUXRXxkLbUM6+njr/XT0IIw/HA/uowCgYIKoZIzj0EAwMw
13+
VzEZMBcGA1UEAwwQUElEIElzc3VlciBDQSAwMjEtMCsGA1UECgwkRVVESSBXYWxs
14+
ZXQgUmVmZXJlbmNlIEltcGxlbWVudGF0aW9uMQswCQYDVQQGEwJFVTAeFw0yNTA0
15+
MDkwMDAzMzBaFw0zNDA3MDYwMDAzMjlaMFcxGTAXBgNVBAMMEFBJRCBJc3N1ZXIg
16+
Q0EgMDIxLTArBgNVBAoMJEVVREkgV2FsbGV0IFJlZmVyZW5jZSBJbXBsZW1lbnRh
17+
dGlvbjELMAkGA1UEBhMCRVUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARkqdLm
18+
wIlv+SSWr00tAIrt7EAMztgd3w9qA6qEm16yVfsLcyx2f4oIWuH45wa37J9GoNWp
19+
deo27VoSoNMCzxOYo4IBITCCAR0wEgYDVR0TAQH/BAgwBgEB/wIBADAfBgNVHSME
20+
GDAWgBRCUFC+ELgQ8J1EXI2/qxAI7ifcSTATBgNVHSUEDDAKBggrgQICAAABBzBD
21+
BgNVHR8EPDA6MDigNqA0hjJodHRwczovL3ByZXByb2QucGtpLmV1ZGl3LmRldi9j
22+
cmwvcGlkX0NBX0VVXzAyLmNybDAdBgNVHQ4EFgQUQlBQvhC4EPCdRFyNv6sQCO4n
23+
3EkwDgYDVR0PAQH/BAQDAgEGMF0GA1UdEgRWMFSGUmh0dHBzOi8vZ2l0aHViLmNv
24+
bS9ldS1kaWdpdGFsLWlkZW50aXR5LXdhbGxldC9hcmNoaXRlY3R1cmUtYW5kLXJl
25+
ZmVyZW5jZS1mcmFtZXdvcmswCgYIKoZIzj0EAwMDSAAwRQIhAIavYfC5o0VVLKfg
26+
TKkzzWgc09hzDMsCl3O2le2sQfG7AiA2soqAN5gtUOLQKWK00DUz22EW79rvaV+V
27+
JPvfdQeokA==
2828
-----END CERTIFICATE-----
2929
"""
3030

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

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -204,18 +204,10 @@ final class MobileWalletIntegrationTests: XCTestCase {
204204
}
205205

206206
func testReceiveAndPresentEudiEhicSdJwtAgainstEudi() async throws {
207-
try XCTSkipIf(
208-
true,
209-
"Pending native iOS PKIX/x509_hash Request Object authentication and EUDI trust-anchor refresh; tracked by https://github.com/walt-id/waltid-identity/pull/1940"
210-
)
211207
try await receiveAndPresentEudiCredential(credentialID: Self.eudiEhicSdJwtCredentialID)
212208
}
213209

214210
func testPreviewAndSubmitEudiEhicSdJwtAgainstEudi() async throws {
215-
try XCTSkipIf(
216-
true,
217-
"Pending native iOS PKIX/x509_hash Request Object authentication and EUDI trust-anchor refresh; tracked by https://github.com/walt-id/waltid-identity/pull/1940"
218-
)
219211
try await previewAndSubmitEudiCredential(credentialID: Self.eudiEhicSdJwtCredentialID)
220212
}
221213

Lines changed: 162 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,42 @@
11
package id.walt.x509
22

3-
actual val platformSupportsPkixCertificatePathValidation: Boolean = false
3+
import kotlinx.cinterop.COpaquePointer
4+
import kotlinx.cinterop.ExperimentalForeignApi
5+
import kotlinx.cinterop.MemScope
6+
import kotlinx.cinterop.addressOf
7+
import kotlinx.cinterop.alloc
8+
import kotlinx.cinterop.memScoped
9+
import kotlinx.cinterop.ptr
10+
import kotlinx.cinterop.reinterpret
11+
import kotlinx.cinterop.usePinned
12+
import kotlinx.cinterop.value
13+
import platform.CoreFoundation.CFArrayAppendValue
14+
import platform.CoreFoundation.CFArrayCreateMutable
15+
import platform.CoreFoundation.CFArrayRef
16+
import platform.CoreFoundation.CFDataCreate
17+
import platform.CoreFoundation.CFErrorRefVar
18+
import platform.CoreFoundation.CFRelease
19+
import platform.CoreFoundation.kCFAllocatorDefault
20+
import platform.Foundation.CFBridgingRelease
21+
import platform.Foundation.NSError
22+
import platform.Security.SecCertificateCreateWithData
23+
import platform.Security.SecCertificateRef
24+
import platform.Security.SecPolicyCreateBasicX509
25+
import platform.Security.SecPolicyCreateRevocation
26+
import platform.Security.SecPolicyRef
27+
import platform.Security.SecTrustCreateWithCertificates
28+
import platform.Security.SecTrustEvaluateWithError
29+
import platform.Security.SecTrustRef
30+
import platform.Security.SecTrustRefVar
31+
import platform.Security.SecTrustSetAnchorCertificates
32+
import platform.Security.SecTrustSetAnchorCertificatesOnly
33+
import platform.Security.errSecSuccess
34+
import platform.Security.kSecRevocationUseAnyAvailableMethod
435

36+
@OptIn(ExperimentalForeignApi::class)
37+
actual val platformSupportsPkixCertificatePathValidation: Boolean = true
38+
39+
@OptIn(ExperimentalForeignApi::class)
540
@Throws(X509ValidationException::class)
641
actual fun validateCertificateChain(
742
leaf: CertificateDer,
@@ -10,18 +45,133 @@ actual fun validateCertificateChain(
1045
enableTrustedChainRoot: Boolean,
1146
enableSystemTrustAnchors: Boolean,
1247
enableRevocation: Boolean
13-
) {
14-
if (enableSystemTrustAnchors) {
15-
throw X509ValidationException("System trust anchors are not supported for iOS certificate validation.")
48+
) = memScoped {
49+
val certificateReferences = mutableListOf<SecCertificateRef>()
50+
val policyReferences = mutableListOf<SecPolicyRef>()
51+
val arrayReferences = mutableListOf<CFArrayRef>()
52+
var trustReference: SecTrustRef? = null
53+
54+
try {
55+
val anchorDers = buildList {
56+
addAll(trustAnchors.orEmpty())
57+
if (enableTrustedChainRoot) {
58+
addAll(chain.filter { certificate ->
59+
runCatching {
60+
PlatformX509Certificate.parse(certificate).isSelfSigned()
61+
}.getOrDefault(false)
62+
})
63+
}
64+
}.distinct()
65+
val certificateDers = (listOf(leaf) + chain)
66+
.distinct()
67+
68+
if (anchorDers.isEmpty() && !enableSystemTrustAnchors) {
69+
throw X509ValidationException(
70+
"No trust anchors available: provide trustAnchors, include a trusted root, or enable system trust anchors."
71+
)
72+
}
73+
74+
certificateReferences += certificateDers.mapIndexed { index, certificate ->
75+
certificate.toSecCertificate("certificate at position $index")
76+
}
77+
val certificates = if (certificateReferences.size == 1) {
78+
certificateReferences.single()
79+
} else {
80+
createCFArray(certificateReferences).also(arrayReferences::add)
81+
}
82+
83+
policyReferences += SecPolicyCreateBasicX509()
84+
?: throw X509ValidationException("Certificate validation failed: could not create the X.509 policy.")
85+
if (enableRevocation) {
86+
policyReferences += SecPolicyCreateRevocation(kSecRevocationUseAnyAvailableMethod)
87+
?: throw X509ValidationException("Certificate validation failed: could not create the revocation policy.")
88+
}
89+
val policies = if (policyReferences.size == 1) {
90+
policyReferences.single()
91+
} else {
92+
createCFArray(policyReferences).also(arrayReferences::add)
93+
}
94+
95+
val trust = alloc<SecTrustRefVar>().apply { value = null }
96+
checkStatus(
97+
operation = "create the certificate trust object",
98+
status = SecTrustCreateWithCertificates(certificates, policies, trust.ptr),
99+
)
100+
trustReference = trust.value
101+
?: throw X509ValidationException("Certificate validation failed: trust object was not created.")
102+
103+
if (anchorDers.isNotEmpty()) {
104+
certificateReferences += anchorDers.mapIndexed { index, certificate ->
105+
certificate.toSecCertificate("trust anchor at position $index")
106+
}
107+
val anchorsArray = createCFArray(
108+
certificateReferences.takeLast(anchorDers.size)
109+
).also(arrayReferences::add)
110+
111+
checkStatus(
112+
operation = "set certificate trust anchors",
113+
status = SecTrustSetAnchorCertificates(trustReference, anchorsArray),
114+
)
115+
checkStatus(
116+
operation = "configure certificate trust anchors",
117+
status = SecTrustSetAnchorCertificatesOnly(trustReference, !enableSystemTrustAnchors),
118+
)
119+
}
120+
121+
val error = alloc<CFErrorRefVar>().apply { value = null }
122+
if (!SecTrustEvaluateWithError(trustReference, error.ptr)) {
123+
val description = error.value?.let {
124+
(CFBridgingRelease(it) as NSError).localizedDescription
125+
} ?: "unknown Security framework error"
126+
throw X509ValidationException(
127+
"Certificate path invalid: $description"
128+
)
129+
}
130+
} catch (cause: X509ValidationException) {
131+
throw cause
132+
} catch (cause: Exception) {
133+
throw X509ValidationException("Certificate validation failed: ${cause.message}", cause)
134+
} finally {
135+
trustReference?.let(::CFRelease)
136+
arrayReferences.forEach(::CFRelease)
137+
policyReferences.forEach(::CFRelease)
138+
certificateReferences.forEach(::CFRelease)
16139
}
17-
if (enableRevocation) {
18-
throw X509ValidationException("Revocation checking is not supported for iOS certificate validation.")
140+
}
141+
142+
@OptIn(ExperimentalForeignApi::class)
143+
private fun CertificateDer.toSecCertificate(description: String): SecCertificateRef {
144+
val byteArray = bytes.toByteArray()
145+
val data = byteArray.usePinned { pinned ->
146+
CFDataCreate(
147+
allocator = kCFAllocatorDefault,
148+
bytes = pinned.addressOf(0).reinterpret(),
149+
length = byteArray.size.toLong(),
150+
)
151+
} ?: throw X509ValidationException("Certificate validation failed: could not create certificate data.")
152+
return try {
153+
SecCertificateCreateWithData(kCFAllocatorDefault, data)
154+
?: throw X509ValidationException(
155+
"Certificate chain validation failed: invalid X.509 DER in $description."
156+
)
157+
} finally {
158+
CFRelease(data)
19159
}
160+
}
20161

21-
validateCertificateChainWithExplicitTrust(
22-
leaf = leaf,
23-
chain = chain,
24-
trustAnchors = trustAnchors,
25-
enableTrustedChainRoot = enableTrustedChainRoot,
26-
)
162+
@OptIn(ExperimentalForeignApi::class)
163+
private fun MemScope.createCFArray(values: List<COpaquePointer>): CFArrayRef {
164+
val array = CFArrayCreateMutable(
165+
allocator = kCFAllocatorDefault,
166+
capacity = values.size.toLong(),
167+
callBacks = null,
168+
) ?: throw X509ValidationException("Certificate validation failed: could not create a Core Foundation array.")
169+
values.forEach { value -> CFArrayAppendValue(array, value) }
170+
return array
171+
}
172+
173+
private fun checkStatus(operation: String, status: Int) {
174+
if (status != errSecSuccess) {
175+
throw X509ValidationException("Certificate validation failed: could not $operation (OSStatus $status).")
176+
}
27177
}

0 commit comments

Comments
 (0)