Skip to content

Commit e6b747d

Browse files
authored
Merge pull request #389 from beyonkuhre/feat/mso_mdoc_transaction_data
Implement transaction data for mso_mdoc credentials
2 parents 06cfb4e + c116a3f commit e6b747d

12 files changed

Lines changed: 184 additions & 47 deletions

Package.resolved

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ let package = Package(
1717
.package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-wallet-storage.git", exact: "0.22.0"),
1818
.package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-sdjwt-swift.git", exact: "0.14.6"),
1919
.package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-openid4vci-swift.git", exact: "0.40.2"),
20-
.package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-siop-openid4vp-swift.git", exact: "0.35.0"),
20+
.package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-openid4vp-swift.git", exact: "0.35.0"),
2121
.package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-statium-swift.git", exact: "0.4.0"),
2222
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.5.0"),
2323
],
@@ -29,7 +29,7 @@ let package = Package(
2929
dependencies: [
3030
.product(name: "MdocDataTransfer18013", package: "eudi-lib-ios-iso18013-data-transfer"),
3131
.product(name: "WalletStorage", package: "eudi-lib-ios-wallet-storage"),
32-
.product(name: "OpenID4VP", package: "eudi-lib-ios-siop-openid4vp-swift"),
32+
.product(name: "OpenID4VP", package: "eudi-lib-ios-openid4vp-swift"),
3333
.product(name: "OpenID4VCI", package: "eudi-lib-ios-openid4vci-swift"),
3434
.product(name: "eudi-lib-sdjwt-swift", package: "eudi-lib-sdjwt-swift"),
3535
.product(name: "StatiumSwift", package: "eudi-lib-ios-statium-swift"),

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,7 @@ On view appearance the attestations are presented with the receiveRequest method
586586
_ = await presentationSession.receiveRequest()
587587
}
588588
```
589-
After the request is received the ``presentationSession.disclosedDocumentSets`` contains an array of credential selection options. Each element is a `[DocElements]` representing one valid combination of credentials that satisfies the query. The selected state of the items can be modified via UI binding. Finally, the response is sent with the following code:
589+
After the request is received the ``presentationSession.disclosedDocumentSets`` contains an array of credential selection options. Each element is a `[DocElements]` representing one valid combination of credentials that satisfies the query. The selected state of the items can be modified via UI binding. Finally, the response is sent with the following code. The optional `deviceNameSpacesToSend` parameter can be used to include device-signed namespaces in the response:
590590

591591
```swift
592592
// Use the first credential selection option (or let the user choose)

Sources/EudiWalletKit/EudiWalletKit.docc/PresentationService.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ After the request is received, ``PresentationSession/disclosedDocumentSets`` con
7979

8080
When partial-claim presentation is enabled, each option includes only the claims that are both requested and available. The selected state of the items can be modified via UI binding.
8181

82+
The `deviceNameSpacesToSend` parameter allows including device-signed namespaces in the response. Pass a ``RequestDeviceNameSpaces`` value when device namespaces are needed, or omit it to send only issuer-signed data.
83+
8284
```swift
8385
// Example: use the first credential selection option
8486
let selectedOption = presentationSession.disclosedDocumentSets.first ?? []

Sources/EudiWalletKit/Models/OpenId4VpConfiguration.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import struct OpenID4VP.PreregisteredClient
2020
import class OpenID4VP.JWSAlgorithm
2121
import enum OpenID4VP.WebKeySource
2222
import enum OpenID4VP.ResponseEncryptionConfiguration
23+
import struct OpenID4VP.SupportedTransactionDataType
2324
import enum OpenID4VP.ResponseMode
2425

2526
/// Client identifier scheme for verifier authentication
@@ -99,21 +100,27 @@ public struct OpenId4VpConfiguration: Sendable {
99100
/// from the Verifier's request. The response URI is always taken from the request.
100101
/// When `nil`, the library uses the mode from the request (default behavior).
101102
public let preferredResponseMode: PreferredResponseMode?
103+
/// Configuration for supported transaction data types
104+
///
105+
/// When provided all request with transaction data will be validated against the list
106+
public let supportedTransactionDataTypes: [SupportedTransactionDataType]
102107

103108
public static let defaultClientIdSchemes: [ClientIdScheme] = [.x509SanDns, .x509Hash, .redirectUri]
104109

105110
public init() {
106111
self.clientIdSchemes = Self.defaultClientIdSchemes
107112
self.responseEncryptionConfiguration = nil
108113
self.allowPresentingPartialClaims = false
114+
self.supportedTransactionDataTypes = []
109115
self.preferredResponseMode = nil
110116
}
111117

112-
public init(clientIdSchemes: [ClientIdScheme]? = nil, responseEncryptionConfiguration: ResponseEncryptionConfiguration? = nil, allowPresentingPartialClaims: Bool = false, preferredResponseMode: PreferredResponseMode? = nil) {
118+
public init(clientIdSchemes: [ClientIdScheme]? = nil, responseEncryptionConfiguration: ResponseEncryptionConfiguration? = nil, allowPresentingPartialClaims: Bool = false, preferredResponseMode: PreferredResponseMode? = nil, supportedTransactionDataTypes: [SupportedTransactionDataType] = []) {
113119
self.clientIdSchemes = clientIdSchemes ?? Self.defaultClientIdSchemes
114120
self.responseEncryptionConfiguration = responseEncryptionConfiguration
115121
self.allowPresentingPartialClaims = allowPresentingPartialClaims
116122
self.preferredResponseMode = preferredResponseMode
123+
self.supportedTransactionDataTypes = supportedTransactionDataTypes
117124
}
118125
}
119126

Sources/EudiWalletKit/Services/BlePresentationService.swift

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public final class BlePresentationService: @unchecked Sendable, PresentationServ
3535
var continuationPowerOn: CheckedContinuation<Void, Error>?
3636
var continuationRequest: CheckedContinuation<UserRequestInfo, Error>?
3737
var continuationDisconnect: CheckedContinuation<Void, Error>?
38-
var handleSelected: ((Bool, RequestItems?) async -> Void)?
38+
var handleSelected: ((Bool, RequestItems?, RequestDeviceNameSpaces?) async -> Void)?
3939
var request: UserRequestInfo?
4040
var readBuffer = Data()
4141
public var transactionLog: TransactionLog
@@ -151,7 +151,7 @@ public final class BlePresentationService: @unchecked Sendable, PresentationServ
151151
continuationRequest?.resume(returning: decoded.userRequestInfo)
152152
continuationRequest = nil
153153
} else {
154-
await userSelected(false, nil)
154+
await userSelected(false, nil, nil)
155155
let notAvailableMessage = "The requested document is not available in your EUDI Wallet. Please contact the authorised issuer for further information."
156156
let userInfo = [NSLocalizedDescriptionKey: notAvailableMessage]
157157
didFinishedWithError(NSError(domain: "\(MdocGattServer.self)", code: 0, userInfo: userInfo))
@@ -189,7 +189,7 @@ public final class BlePresentationService: @unchecked Sendable, PresentationServ
189189
}
190190
}
191191

192-
public func userSelected(_ b: Bool, _ items: RequestItems?) async {
192+
public func userSelected(_ b: Bool, _ items: RequestItems?, _ deviceNameSpaces: RequestDeviceNameSpaces? = nil) async {
193193
status = .userSelected
194194
let resError = await MdocHelpers.getSessionDataToSend(sessionEncryption: sessionEncryption, status: .error, docToSend: DeviceResponse(status: 0))
195195
var bytesToSend = try! resError.get()
@@ -207,7 +207,18 @@ public final class BlePresentationService: @unchecked Sendable, PresentationServ
207207
let docTypeReq = deviceRequest?.docRequests.first?.itemsRequest.docType ?? ""
208208
let compactDocMetadata = docMetadata.compactMapValues { $0 }
209209
let eReaderKey = sessionEncryption!.sessionKeys.publicKey
210-
guard let (drToSend, _, _, resMetadata, resDocIds, resZkpDocIds) = try await MdocHelpers.getDeviceResponseToSend(deviceRequest: deviceRequest!, issuerSigned: docs, docMetadata: compactDocMetadata, selectedItems: items, sessionEncryption: sessionEncryption, eReaderKey: eReaderKey, privateKeyObjects: privateKeyObjects, dauthMethod: dauthMethod, unlockData: unlockData, zkSystemRepository: zkSystemRepository) else {
210+
guard let (drToSend, _, _, resMetadata, resDocIds, resZkpDocIds) = try await MdocHelpers.getDeviceResponseToSend(
211+
deviceRequest: deviceRequest!,
212+
issuerSigned: docs,
213+
docMetadata: compactDocMetadata,
214+
selectedItems: items,
215+
sessionEncryption: sessionEncryption,
216+
eReaderKey: eReaderKey,
217+
privateKeyObjects: privateKeyObjects,
218+
dauthMethod: dauthMethod,
219+
unlockData: unlockData,
220+
zkSystemRepository: zkSystemRepository,
221+
deviceNameSpacesRequested: deviceNameSpaces) else {
211222
errorToSend = MdocHelpers.getErrorNoDocuments(docTypeReq)
212223
return
213224
}
@@ -258,8 +269,10 @@ public final class BlePresentationService: @unchecked Sendable, PresentationServ
258269
/// - Parameters:
259270
/// - userAccepted: True if user accepted to send the response
260271
/// - itemsToSend: The selected items to send organized in document types and namespaces
261-
public func sendResponse(userAccepted: Bool, itemsToSend: RequestItems, onSuccess: (@Sendable (URL?) -> Void)?) async throws {
262-
await handleSelected?(userAccepted, itemsToSend)
272+
/// - deviceNameSpacesToSend: Optional device-signed namespaces to include in the response
273+
/// - onSuccess: Callback invoked on successful response with an optional redirect URL
274+
public func sendResponse(userAccepted: Bool, itemsToSend: RequestItems, deviceNameSpacesToSend: RequestDeviceNameSpaces? = nil, onSuccess: (@Sendable (URL?) -> Void)?) async throws {
275+
await handleSelected?(userAccepted, itemsToSend, deviceNameSpacesToSend)
263276
handleSelected = nil
264277
TransactionLogUtils.setCborTransactionLogResponseInfo(self, transactionLog: &transactionLog)
265278
}

Sources/EudiWalletKit/Services/FaultPresentationService.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public final class FaultPresentationService: @unchecked Sendable, PresentationSe
4747
throw error
4848
}
4949

50-
public func sendResponse(userAccepted: Bool, itemsToSend: RequestItems, onSuccess: ((URL?) -> Void)?) async throws{
50+
public func sendResponse(userAccepted: Bool, itemsToSend: RequestItems, deviceNameSpacesToSend: RequestDeviceNameSpaces? = nil, onSuccess: ((URL?) -> Void)?) async throws{
5151
throw error
5252
}
5353

Sources/EudiWalletKit/Services/OpenId4VpService.swift

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import eudi_lib_sdjwt_swift
2828
import JOSESwift
2929
import Logging
3030
import X509
31+
import SwiftyJSON
3132
import struct OpenID4VP.ClaimPath
3233
import enum OpenID4VP.ClaimPathElement
3334
import struct WalletStorage.Document
@@ -66,6 +67,7 @@ public final class OpenId4VpService: @unchecked Sendable, PresentationService {
6667
var zkSpecsRequested: [DocType: [ZkSystemSpec]]?
6768
var networking: Networking
6869
var unlockData: [String: Data]!
70+
var verifierInfo: [VerifierInfo]?
6971
public var transactionLog: TransactionLog
7072
public var zkpDocumentIds: [WalletStorage.Document.ID]?
7173
public var flow: FlowType
@@ -144,6 +146,8 @@ public final class OpenId4VpService: @unchecked Sendable, PresentationService {
144146
vpNonce = vp.nonce; vpClientId = resolvedClientId
145147
mdocGeneratedNonce = OpenId4VpUtils.generateMdocGeneratedNonce() // Not longer required for SessionTranscript, use the verifier (client) nonce i.e vpNonce
146148
sessionTranscript = SessionTranscript(handOver: OpenId4VpUtils.generateOpenId4VpHandover(clientId: resolvedClientId, responseUri: responseUri, nonce: vpNonce, jwkThumbprint: jwkThumbprint?.byteArray))
149+
transactionData = vp.transactionData
150+
verifierInfo = vp.verifierInfo
147151

148152
logger.info("Session Transcript: \(sessionTranscript.encode().toHexString()), for clientId: \(vp.client.id), responseUri: \(responseUri), nonce: \(vp.nonce), mdocGeneratedNonce: \(mdocGeneratedNonce!)")
149153
// Only DCQL supported now
@@ -156,13 +160,29 @@ public final class OpenId4VpService: @unchecked Sendable, PresentationService {
156160
let credentialSelectionSets = try OpenId4VpUtils.resolveDcql(
157161
dcql, queryable: dcqlQueryable, allowPresentingPartialClaims: openID4VpConfig.allowPresentingPartialClaims)
158162
let requestItemsArray = OpenId4VpUtils.getRequestItems(credentialSelectionSets, idsToDocTypes: transferInfo.idsToDocTypes, formatsRequested: formatsRequested)
159-
self.transactionData = vp.transactionData
163+
let transactionDataRequestedArray = transactionData != nil
164+
? try OpenId4VpUtils.getTransactionDataRequested(
165+
credentialSelectionSets,
166+
transactionDataList: transactionData!)
167+
: nil
168+
let verifierInfoRequestedArray = verifierInfo != nil
169+
? OpenId4VpUtils.getVerifierInfoRequested(credentialSelectionSets, verifierInfoList: verifierInfo!)
170+
: nil
160171
let certificateIssuerName = readerCertificateIssuer.map(MdocHelpers.getCN(from:))
161172
let rar = ReaderAuthenticationResult(isValidated: readerAuthValidated, certificateIssuer: certificateIssuerName, validationMessage: readerCertificateValidationMessage, legalName: rrd.legalName, authBytes: nil, certificateChain: certificateChain)
162173
var results = [UserRequestInfo]()
163174
for (requestName, requestItems) in requestItemsArray {
175+
let transactionDataRequested = transactionDataRequestedArray?.first(where: { $0.0 == requestName })
176+
let verifierInfoRequested = verifierInfoRequestedArray?.first(where: { $0.0 == requestName })
164177
//guard let requestItems, let formatsRequested else { throw PresentationSession.makeError(str: "Invalid request query") }
165-
var result = UserRequestInfo(docDataFormats: formatsRequested, itemsRequested: requestItems, deviceRequestBytes: deviceRequestBytes, requestName: requestName)
178+
var result = UserRequestInfo(
179+
docDataFormats: formatsRequested,
180+
itemsRequested: requestItems,
181+
deviceRequestBytes: deviceRequestBytes,
182+
transactionDataRequested: transactionDataRequested?.1,
183+
verifierInfo: verifierInfoRequested?.1,
184+
requestName: requestName
185+
)
166186
logger.info("Verifier requested items: \(requestItems.mapValues { $0.mapValues { ar in ar.map(\.elementIdentifier) } })")
167187
result.readerAuthResults = ["": rar]
168188
TransactionLogUtils.setCborTransactionLogRequestInfo(result, transactionLog: &transactionLog)
@@ -176,11 +196,23 @@ public final class OpenId4VpService: @unchecked Sendable, PresentationService {
176196
docsCbor = transferInfo.documentObjects.filter { k,v in Self.filterFormat(transferInfo.dataFormats[k]!, fmt: .cbor)} .mapValues { try? IssuerSigned(data: $0.bytes) }.compactMapValues { $0 }
177197
}
178198

179-
func generateCborVpToken(itemsToSend: RequestItems) async throws -> (VerifiablePresentation, Data, [Data?], [String]) {
199+
func generateCborVpToken(itemsToSend: RequestItems, deviceNameSpacesToSend: RequestDeviceNameSpaces?) async throws -> (VerifiablePresentation, Data, [Data?], [String]) {
180200
let docMetadata = transferInfo.docMetadata
181201
let privateKeyObjects = transferInfo.privateKeyObjects
182202
let zkSystemRepository = transferInfo.zkSystemRepository
183-
let resp = try await MdocHelpers.getDeviceResponseToSend(deviceRequest: nil, issuerSigned: docsCbor, docMetadata: docMetadata, selectedItems: itemsToSend, eReaderKey: eReaderPub, privateKeyObjects: privateKeyObjects, sessionTranscript: sessionTranscript, dauthMethod: .deviceSignature, unlockData: unlockData, zkSpecsRequested: zkSpecsRequested, zkSystemRepository: zkSystemRepository)
203+
let resp = try await MdocHelpers.getDeviceResponseToSend(
204+
deviceRequest: nil,
205+
issuerSigned: docsCbor,
206+
docMetadata: docMetadata,
207+
selectedItems: itemsToSend,
208+
eReaderKey: eReaderPub,
209+
privateKeyObjects: privateKeyObjects,
210+
sessionTranscript: sessionTranscript,
211+
dauthMethod: .deviceSignature,
212+
unlockData: unlockData,
213+
zkSpecsRequested: zkSpecsRequested,
214+
zkSystemRepository: zkSystemRepository,
215+
deviceNameSpacesRequested: deviceNameSpacesToSend)
184216
guard let resp else { throw PresentationSession.makeError(str: "DOCUMENT_ERROR") }
185217
let vpTokenData = Data(resp.deviceResponse.toCBOR(options: CBOROptions()).encode())
186218
let vpTokenStr = vpTokenData.base64URLEncodedString()
@@ -209,7 +241,9 @@ public final class OpenId4VpService: @unchecked Sendable, PresentationService {
209241
/// - Parameters:
210242
/// - userAccepted: True if user accepted to send the response
211243
/// - itemsToSend: The selected items to send organized in document types and namespaces
212-
public func sendResponse(userAccepted: Bool, itemsToSend: RequestItems, onSuccess: ((URL?) -> Void)?) async throws {
244+
/// - deviceNameSpacesToSend: Optional device-signed namespaces to include in the response
245+
/// - onSuccess: Callback invoked on successful response with an optional redirect URL
246+
public func sendResponse(userAccepted: Bool, itemsToSend: RequestItems, deviceNameSpacesToSend: RequestDeviceNameSpaces? = nil, onSuccess: ((URL?) -> Void)?) async throws {
213247
guard dcql != nil, let resolved = resolvedRequestData else {
214248
throw PresentationSession.makeError(str: "Unexpected error")
215249
}
@@ -229,7 +263,7 @@ public final class OpenId4VpService: @unchecked Sendable, PresentationService {
229263
if transferInfo.dataFormats[docId] == .cbor {
230264
if docsCbor == nil { makeCborDocs() }
231265
let itemsToSend1 = Dictionary(uniqueKeysWithValues: [(docId, nsItems)])
232-
let vpToken = try await generateCborVpToken(itemsToSend: itemsToSend1)
266+
let vpToken = try await generateCborVpToken(itemsToSend: itemsToSend1, deviceNameSpacesToSend: deviceNameSpacesToSend)
233267
zkpDocumentIds!.append(contentsOf: vpToken.3)
234268
inputToPresentations.append((inputDescrId, docId, vpToken.0))
235269
} else if transferInfo.dataFormats[docId] == .sdjwt {
@@ -341,7 +375,18 @@ public final class OpenId4VpService: @unchecked Sendable, PresentationService {
341375
case .preregistered(let clients): .preregistered(clients: Dictionary(uniqueKeysWithValues: clients.map { ($0.clientId, $0) }))
342376
}
343377
}
344-
let res = OpenId4VPConfiguration(privateKey: privateKey, publicWebKeySet: keySet, supportedClientIdSchemes: supportedClientIdPrefixes, vpFormatsSupported: [], jarConfiguration: .encryptionOption, vpConfiguration: .default(), errorDispatchPolicy: .allClients, session: networking, responseEncryptionConfiguration: openID4VpConfig.responseEncryptionConfiguration ?? .default())
378+
let res = OpenId4VPConfiguration(
379+
privateKey: privateKey,
380+
publicWebKeySet: keySet,
381+
supportedClientIdSchemes: supportedClientIdPrefixes,
382+
vpFormatsSupported: [],
383+
jarConfiguration: .encryptionOption,
384+
vpConfiguration: try! .init(
385+
vpFormatsSupported: .default(),
386+
supportedTransactionDataTypes: openID4VpConfig.supportedTransactionDataTypes),
387+
errorDispatchPolicy: .allClients,
388+
session: networking,
389+
responseEncryptionConfiguration: openID4VpConfig.responseEncryptionConfiguration ?? .default())
345390
return res
346391
}
347392

0 commit comments

Comments
 (0)