A Swift library implementing the W3C Digital Credentials API (DcApi) with ISO 18013-5 Annex C compliance for secure mobile document verification on iOS platforms.
This library enables iOS applications to participate in online mobile document (mdoc) verification processes using the W3C's Digital Credentials API protocol. Currently, it is compatible with applications using the Eudi Wallet Kit with minimum version v0.17.0.
- Add an "Identity Document Provider" extension target to your iOS app project.
- Add an SPM dependency to
DcApi18013AnnexCfrom XCode "Package Dependencies" tab. The package URL ishttps://github.com/eu-digital-identity-wallet/av-lib-ios-w3c-dc-api.git. Add the package to the extension target. - Add the "Keychain Sharing" capability in your iOS app from the "Sign and Capabilities" tab and configure a keychain access group. Use the access group in your main app when initializing the EudiWallet class.
- Register your age verification document in you main app by using the
IdentityDocumentProviderRegistrationStore. Use an empty array for thesupportedAuthorityKeyIdentifiersparameter. - Add the "Keychain Sharing" capability to your Identity Document Provider extension target and use the same access group as your main app.
- Add the "Digital Credentials API - Mobile Document Provider" capability to your main app and check the "EU Age verification" option.
- In your Identity Document Provider extension target, import the
DcApi18013AnnexClibrary and initialize theDcApiHandlerwith the same access group as your main app.
@main
struct ProviderExtension: IdentityDocumentProvider {
let dcApiHandler = DcApiHandler(serviceName: "myService",
accessGroup: "AppStoreTeamID.groupName")
var body: some IdentityDocumentRequestScene {
ISO18013MobileDocumentRequestScene { context in
// Insert your view here
RequestAuthorizationView(context: context, dcApiHandler: dcApiHandler)
}
}
A sample implementation of the RequestAuthorizationView is the following:
import SwiftUI
import IdentityDocumentServices
import IdentityDocumentServicesUI
import DcApi18013AnnexC
import MdocDataModel18013
import WalletStorage
struct RequestAuthorizationView: View {
let context: ISO18013MobileDocumentRequestContext
let dcApiHandler: DcApiHandler
@State var availableClaims: [DocClaimsModel] = []
@State var websiteName: String?
@State var requestSet: ISO18013MobileDocumentRequest.DocumentRequestSet?
@State var errorMessage: String?
@State var selectedDocumentIds: Set<String>?
@State var selectedClaimsByDocumentId: [String: [String: [String]]]?
var body: some View {
VStack(alignment: .center) {
if let requestSet, let websiteName {
Text(websiteName).font(.headline).padding(.bottom, 6)
Text("Matching credentials: \(availableClaims.count)")
.font(.subheadline)
.foregroundStyle(.secondary)
List {
VStack(alignment: .leading) {
ForEach(requestSet.requests, id: \.documentType) { rs in
Text(rs.documentType).font(.title)
let namespaces = Array(rs.namespaces.keys)
ForEach(namespaces, id: \.self) { ns in
Text(ns).font(.title2)
let elements = Array(rs.namespaces[ns]!.keys)
ForEach(elements, id: \.self) { el in
VStack(alignment: .leading, spacing: 2) {
Text(el).fontWeight(
rs.namespaces[ns]![el]!.isRetaining ? .bold : .thin)
if let claim = claim(for: rs.documentType, namespace: ns, element: el) {
Text("\(claim.displayName ?? claim.name): \(claim.stringValue)")
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
}
}
}
}
}
if let errorMessage { Text(verbatim: errorMessage).foregroundStyle(.red) }
HStack(alignment: .bottom, spacing: 40) {
Button {
context.cancel()
} label: {
Label("Cancel", systemImage: "x.circle")
}.buttonStyle(.bordered)
if errorMessage == nil {
Button {
Task { try await self.acceptVerification() }
} label: {
Label("Accept", systemImage: "checkmark.seal")
}.buttonStyle(.borderedProminent).glassEffect(.regular)
}
}
} else {
ContentUnavailableView("Cannot validate request",
image: "externaldrive.fill.trianglebadge.exclamationmark")
}
}.padding() // vstack
.task {
do {
let (claims, set, _, readerName) = try await dcApiHandler.validateRequest(context.request)
availableClaims = claims
requestSet = set
websiteName = context.requestingWebsiteOrigin?.absoluteString ?? readerName ??
"Website name not available"
} catch {
errorMessage = String(describing: error)
}
}
} // body
func acceptVerification() async throws {
try await context.sendResponse { rawRequest in
try await dcApiHandler.validateConsistency(request: context.request, rawRequest: rawRequest)
let responseData = try await dcApiHandler.buildAndEncryptResponse(
rawRequest: rawRequest,
originUrl: context.requestingWebsiteOrigin?.absoluteString,
selectedDocumentIds: selectedDocumentIds,
selectedClaimsByDocumentId: selectedClaimsByDocumentId)
return ISO18013MobileDocumentResponse(responseData: responseData)
}
}
private func claim(for documentType: String, namespace: String, element: String) -> DocClaim? {
availableClaims
.first(where: { $0.docType == documentType })?
.docClaims
.first(where: { $0.namespace == namespace && $0.name == element })
}
} // end viewvalidateRequest(_:) returns four values in this order: filtered DocClaimsModel values for locally available credentials, the matched DocumentRequestSet, the authority key identifier extracted from the reader certificate chain when present, and the reader name derived from that chain. The example above uses the filtered claims and matched request set, and ignores the authority key identifier.
When the verifier requests multiple document types across separate request sets (e.g. an mDL combined with a PID), validateRequest(_:) unions the requested elements from all matching sets so that all relevant credentials are returned.
buildAndEncryptResponse accepts two optional parameters that let the user choose which documents and claims to disclose:
selectedDocumentIds: Set<String>?— When set, only the documents with the given IDs are included in the response. Passnilto include all matching documents (default behaviour).selectedClaimsByDocumentId: [String: [String: [String]]]?— When set, only the specified elements are disclosed per document. The structure is a dictionary mapping document ID → namespace → array of element identifiers. Passnilto disclose all requested elements (default behaviour).
Both parameters default to nil, so existing callers are unaffected.
- EUDI Libraries: European Digital Identity Wallet standard libraries
eudi-lib-ios-iso18013-data-transfereudi-lib-ios-wallet-storage
- Swift-Log: Structured logging support
- ISO 18013-5: Mobile driving licence standard (mDL)
- ISO 18013-7 Annex C: Mobile driving licence (mDL) addon functions, Digital Credentials API integration
- W3C Digital Credentials API: Web standard for credential requests
- RFC 9180: HPKE encryption standard
| Package Name | Package URL | License |
|---|---|---|
| swift-log | https://github.com/apple/swift-log.git | Apache-2.0 License |
| eudi-lib-ios-iso18013-data-transfer | https://github.com/eu-digital-identity-wallet/eudi-lib-ios-iso18013-data-transfer.git | Apache-2.0 License |
| eudi-lib-ios-wallet-storage | https://github.com/eu-digital-identity-wallet/eudi-lib-ios-wallet-storage.git | Apache-2.0 License |
| SwiftHPKE | https://github.com/leif-ibsen/SwiftHPKE | MIT License |