Skip to content

Commit b986f98

Browse files
authored
Merge pull request #310 from niscy-eudiw/main
Fix KB-JWT aud to use full client_id for decentralized_identifier scheme and add background reissue support
2 parents 366fd49 + 201c3d5 commit b986f98

File tree

6 files changed

+40
-31
lines changed

6 files changed

+40
-31
lines changed

Package.resolved

Lines changed: 9 additions & 9 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
@@ -13,10 +13,10 @@ let package = Package(
1313
targets: ["EudiWalletKit"])
1414
],
1515
dependencies: [
16-
.package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-iso18013-data-transfer.git", exact: "0.11.1"),
16+
.package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-iso18013-data-transfer.git", exact: "0.11.2"),
1717
.package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-wallet-storage.git", exact: "0.11.1"),
1818
.package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-sdjwt-swift.git", exact: "0.14.1"),
19-
.package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-siop-openid4vp-swift.git", exact: "0.30.0"),
19+
.package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-siop-openid4vp-swift.git", exact: "0.30.1"),
2020
.package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-openid4vci-swift.git", exact: "0.31.1"),
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/eu-digital-identity-wallet/SwiftCopyableMacro.git", from: "0.0.3")

Sources/EudiWalletKit/EudiWallet.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,17 +207,21 @@ public final class EudiWallet: ObservableObject, @unchecked Sendable {
207207
/// - credentialOptions: Credential options specifying batch size and credential policy. If nil, the options from the original issuance metadata are used.
208208
/// - keyOptions: Key options (secure area name and other options) for the document. If nil, the options from the original issuance metadata are used.
209209
/// - promptMessage: Prompt message for biometric authentication (optional).
210+
/// - backgroundOnly: When `true`, reissuance proceeds only if stored authorization data is available (no user interaction). Throws if authorization data is absent. Defaults to `false`.
210211
/// - Returns: The reissued document, saved in storage.
211-
/// - Throws: An error if the document metadata is not found or reissuance fails.
212-
@discardableResult public func reissueDocument(documentId: WalletStorage.Document.ID, credentialOptions: CredentialOptions? = nil, keyOptions: KeyOptions? = nil, promptMessage: String? = nil) async throws -> WalletStorage.Document {
212+
/// - Throws: An error if the document metadata is not found, if `backgroundOnly` is `true` and no stored authorization data exists, or if reissuance fails.
213+
@discardableResult public func reissueDocument(documentId: WalletStorage.Document.ID, credentialOptions: CredentialOptions? = nil, keyOptions: KeyOptions? = nil, promptMessage: String? = nil, backgroundOnly: Bool = false) async throws -> WalletStorage.Document {
213214
guard let docMetadata = try await storage.storageService.loadDocumentMetadata(id: documentId) else {
214215
throw PresentationSession.makeError(str: "Issued document metadata not found for id: \(documentId)", localizationKey: "issued_doc_not_found")
215216
}
216217
let vciService = try await resolveVCIService(issuerName: docMetadata.credentialIssuerIdentifier)
217218
let authorized: AuthorizedRequest? = docMetadata.authorizedRequestData
218219
.flatMap { try? JSONDecoder().decode(AuthorizedRequestData.self, from: $0) }
219220
.map { $0.toAuthorizedRequest() }
220-
let reissued = try await vciService.reissueDocument(documentId: documentId, docMetadata: docMetadata, authorized: authorized, credentialOptions: credentialOptions ?? docMetadata.credentialOptions, keyOptions: keyOptions ?? docMetadata.keyOptions, promptMessage: promptMessage)
221+
if backgroundOnly && authorized == nil {
222+
throw PresentationSession.makeError(str: "Background reissuance not possible: no stored authorization data for document \(documentId)", localizationKey: "background_reissue_not_possible")
223+
}
224+
let reissued = try await vciService.reissueDocument(documentId: documentId, docMetadata: docMetadata, authorized: authorized, credentialOptions: credentialOptions ?? docMetadata.credentialOptions, keyOptions: keyOptions ?? docMetadata.keyOptions, promptMessage: promptMessage, backgroundOnly: backgroundOnly)
221225
return reissued.first!
222226
}
223227

Sources/EudiWalletKit/Extensions.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,10 +199,10 @@ struct AuthorizedRequestData: Codable {
199199
}
200200

201201
extension CredentialConfiguration {
202-
func convertToDocMetadata(authorized: AuthorizedRequest? = nil, keyOptions: KeyOptions? = nil, credentialOptions: CredentialOptions? = nil) -> DocMetadata {
202+
func convertToDocMetadata(authorized: AuthorizedRequest? = nil, keyOptions: KeyOptions? = nil, credentialOptions: CredentialOptions? = nil, dpopKeyId: String? = nil) -> DocMetadata {
203203
let claimMetadata = claims.map(\.metadata)
204204
let authorizedRequestData: Data? = if let authorized { try? JSONEncoder().encode(AuthorizedRequestData(from: authorized)) } else { nil }
205-
return DocMetadata(credentialIssuerIdentifier: credentialIssuerIdentifier, configurationIdentifier: configurationIdentifier.value, docType: docType ?? vct ?? "", display: display, issuerDisplay: issuerDisplay, claims: claimMetadata, authorizedRequestData: authorizedRequestData, keyOptions: keyOptions, credentialOptions: credentialOptions)
205+
return DocMetadata(credentialIssuerIdentifier: credentialIssuerIdentifier, configurationIdentifier: configurationIdentifier.value, docType: docType ?? vct ?? "", display: display, issuerDisplay: issuerDisplay, claims: claimMetadata, authorizedRequestData: authorizedRequestData, keyOptions: keyOptions, credentialOptions: credentialOptions, dpopKeyId: dpopKeyId)
206206
}
207207
}
208208

Sources/EudiWalletKit/Services/OpenId4VciService.swift

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -176,10 +176,10 @@ public actor OpenId4VCIService {
176176
return CredentialOptions(credentialPolicy: .rotateUse, batchSize: metaData.batchCredentialIssuance?.batchSize ?? 1)
177177
}
178178

179-
func getIssuer(offer: CredentialOffer) async throws -> Issuer {
179+
func getIssuer(offer: CredentialOffer, dpopKeyId: String? = nil) async throws -> Issuer {
180180
var dpopConstructor: DPoPConstructorType? = nil
181181
if config.useDpopIfSupported {
182-
dpopConstructor = try await config.makePoPConstructor(popUsage: .dpop, privateKeyId: issueReq.dpopKeyId, algorithms: offer.authorizationServerMetadata.dpopSigningAlgValuesSupported, keyOptions: config.dpopKeyOptions)
182+
dpopConstructor = try await config.makePoPConstructor(popUsage: .dpop, privateKeyId: dpopKeyId ?? issueReq.dpopKeyId, algorithms: offer.authorizationServerMetadata.dpopSigningAlgValuesSupported, keyOptions: config.dpopKeyOptions)
183183
}
184184
let vciConfig = try await config.toOpenId4VCIConfig(credentialIssuerId: offer.credentialIssuerIdentifier.url.absoluteString, clientAttestationPopSigningAlgValuesSupported: offer.authorizationServerMetadata.clientAttestationPopSigningAlgValuesSupported)
185185
return try Issuer(authorizationServerMetadata: offer.authorizationServerMetadata, issuerMetadata: offer.credentialIssuerMetadata, config: vciConfig, parPoster: Poster(session: networking), tokenPoster: Poster(session: networking), requesterPoster: Poster(session: networking), deferredRequesterPoster: Poster(session: networking), notificationPoster: Poster(session: networking), noncePoster: Poster(session: networking), dpopConstructor: dpopConstructor)
@@ -200,7 +200,7 @@ public actor OpenId4VCIService {
200200
return try Issuer.createDeferredIssuer(deferredCredentialEndpoint: data.deferredCredentialEndpoint, deferredRequesterPoster: Poster(session: networking), config: vciConfig)
201201
}
202202

203-
func authorizeOffer(offerUri: String, docTypeModels: [OfferedDocModel], txCodeValue: String?, authorized: AuthorizedRequest?) async throws -> (AuthorizeRequestOutcome, Issuer, [CredentialConfiguration]) {
203+
func authorizeOffer(offerUri: String, docTypeModels: [OfferedDocModel], txCodeValue: String?, authorized: AuthorizedRequest?, backgroundOnly: Bool = false, dpopKeyId: String? = nil) async throws -> (AuthorizeRequestOutcome, Issuer, [CredentialConfiguration]) {
204204
guard let offer = Self.credentialOfferCache[offerUri] else {
205205
throw PresentationSession.makeError(str: "offerUri \(offerUri) not resolved. resolveOfferDocTypes must be called first")
206206
}
@@ -211,7 +211,7 @@ public actor OpenId4VCIService {
211211
let code: Grants.PreAuthorizedCode? = switch offer.grants { case .preAuthorizedCode(let preAuthorizedCode): preAuthorizedCode; case .both(_, let preAuthorizedCode): preAuthorizedCode; case .authorizationCode(_), .none: nil }
212212
let txCodeSpec: TxCode? = code?.txCode
213213
let preAuthorizedCode: String? = code?.preAuthorizedCode
214-
let issuer = try await getIssuer(offer: offer)
214+
let issuer = try await getIssuer(offer: offer, dpopKeyId: dpopKeyId)
215215
if preAuthorizedCode != nil && txCodeSpec != nil && txCodeValue == nil {
216216
throw PresentationSession.makeError(str: "A transaction code is required for this offer")
217217
}
@@ -227,6 +227,9 @@ public actor OpenId4VCIService {
227227
}
228228
catch {
229229
logger.error("Failed to refresh provided authorized request: \(error).")
230+
if backgroundOnly {
231+
throw PresentationSession.makeError(str: "Background reissuance not possible.", localizationKey: "background_reissue_not_possible")
232+
}
230233
}
231234
}
232235
if let preAuthorizedCode, let authCode = try? IssuanceAuthorization(preAuthorizationCode: preAuthorizedCode, txCode: txCodeSpec) {
@@ -302,13 +305,13 @@ public actor OpenId4VCIService {
302305
/// - keyOptions: Key options (secure area name and other options) for the document issuing (optional)
303306
/// - promptMessage: Prompt message for biometric authentication (optional)
304307
/// - Returns: Array of issued documents. They are saved in storage.
305-
@discardableResult func reissueDocument(documentId: WalletStorage.Document.ID, docMetadata: DocMetadata, authorized: AuthorizedRequest? = nil, credentialOptions: CredentialOptions? = nil, keyOptions: KeyOptions? = nil, promptMessage: String? = nil) async throws -> [WalletStorage.Document] {
308+
@discardableResult func reissueDocument(documentId: WalletStorage.Document.ID, docMetadata: DocMetadata, authorized: AuthorizedRequest? = nil, credentialOptions: CredentialOptions? = nil, keyOptions: KeyOptions? = nil, promptMessage: String? = nil, backgroundOnly: Bool = false) async throws -> [WalletStorage.Document] {
306309
let (credentialConfigurations, offer) = try await buildCredentialOffer(for: [.identifier(docMetadata.configurationIdentifier)])
307310
let credentialConfiguration = credentialConfigurations.first!
308311
let offerUri = UUID().uuidString
309312
Self.credentialOfferCache[offerUri] = offer
310313
let docTypes = [makeOfferedDocModel(from: credentialConfiguration, credentialOptions: credentialOptions, keyOptions: keyOptions)]
311-
return try await issueDocumentsByOfferUrl(offerUri: offerUri, docTypes: docTypes, authorized: authorized, documentId: documentId, txCodeValue: nil, promptMessage: promptMessage)
314+
return try await issueDocumentsByOfferUrl(offerUri: offerUri, docTypes: docTypes, authorized: authorized, documentId: documentId, txCodeValue: nil, promptMessage: promptMessage, backgroundOnly: backgroundOnly, dpopKeyId: docMetadata.dpopKeyId)
312315
}
313316

314317
/// Issue multiple documents using OpenId4Vci protocol
@@ -341,7 +344,7 @@ public actor OpenId4VCIService {
341344
/// - txCodeValue: Transaction code given to user (if available)
342345
/// - promptMessage: prompt message for biometric authentication (optional)
343346
/// - Returns: Array of issued and stored documents
344-
func issueDocumentsByOfferUrl(offerUri: String, docTypes: [OfferedDocModel], authorized: AuthorizedRequest?, documentId: String?, txCodeValue: String? = nil, promptMessage: String? = nil) async throws -> [WalletStorage.Document] {
347+
func issueDocumentsByOfferUrl(offerUri: String, docTypes: [OfferedDocModel], authorized: AuthorizedRequest?, documentId: String?, txCodeValue: String? = nil, promptMessage: String? = nil, backgroundOnly: Bool = false, dpopKeyId: String? = nil) async throws -> [WalletStorage.Document] {
345348
if docTypes.isEmpty { return [] }
346349
guard let offer = Self.credentialOfferCache[offerUri] else {
347350
throw PresentationSession.makeError(str: "Offer URI not resolved: \(offerUri)")
@@ -356,13 +359,13 @@ public actor OpenId4VCIService {
356359
try await svc.prepareIssuing(id: id, docTypeIdentifier: docTypeIdentifier, displayName: i > 0 ? nil : docTypes.map(\.displayName).joined(separator: ", "), credentialOptions: usedCredentialOptions, keyOptions: docTypeModel.keyOptions, disablePrompt: i > 0, promptMessage: promptMessage)
357360
openId4VCIServices.append(svc)
358361
}
359-
let (auth, issuer, credentialInfos) = try await openId4VCIServices.first!.authorizeOffer(offerUri: offerUri, docTypeModels: docTypes, txCodeValue: txCodeValue, authorized: authorized)
362+
let (auth, issuer, credentialInfos) = try await openId4VCIServices.first!.authorizeOffer(offerUri: offerUri, docTypeModels: docTypes, txCodeValue: txCodeValue, authorized: authorized, backgroundOnly: backgroundOnly, dpopKeyId: dpopKeyId)
360363
let documents = try await withThrowingTaskGroup(of: WalletStorage.Document.self) { group in
361364
for (i, openId4VCIService) in openId4VCIServices.enumerated() {
362365
group.addTask {
363366
let (bindingKeys, publicKeys) = try await openId4VCIService.initSecurityKeys(credentialInfos[i])
364367
let docData = try await openId4VCIService.issueDocumentByOfferUrl(issuer: issuer, offer: offer, authorizedOutcome: auth, configuration: credentialInfos[i], bindingKeys: bindingKeys, publicKeys: publicKeys, promptMessage: promptMessage)
365-
return try await self.finalizeIssuing(issueOutcome: docData, docType: docTypes[i].docTypeOrVct, format: credentialInfos[i].format, issueReq: openId4VCIService.issueReq, deleteId: documentId)
368+
return try await self.finalizeIssuing(issueOutcome: docData, docType: docTypes[i].docTypeOrVct, format: credentialInfos[i].format, issueReq: openId4VCIService.issueReq, deleteId: documentId, dpopKeyId: dpopKeyId)
366369
}
367370
}
368371
var result = [WalletStorage.Document]()
@@ -756,7 +759,8 @@ public actor OpenId4VCIService {
756759
}
757760
}
758761

759-
func finalizeIssuing(issueOutcome: IssuanceOutcome, docType: String?, format: DocDataFormat, issueReq: IssueRequest, deleteId: String?) async throws -> WalletStorage.Document {
762+
func finalizeIssuing(issueOutcome: IssuanceOutcome, docType: String?, format: DocDataFormat, issueReq: IssueRequest, deleteId: String?, dpopKeyId: String? = nil) async throws -> WalletStorage.Document {
763+
let savedDpopKeyId = dpopKeyId ?? issueReq.dpopKeyId
760764
var dataToSave: Data; var docTypeToSave = ""
761765
var docMetadata: DocMetadata; var displayName: String?
762766
let pds = issueOutcome.pendingOrDeferredStatus
@@ -767,7 +771,7 @@ public actor OpenId4VCIService {
767771
case .issued(let dataPair, let cc, let authorized):
768772
guard dataPair.first != nil else { throw PresentationSession.makeError(str: "Empty issued data array") }
769773
dataToSave = issueOutcome.getDataToSave(index: 0, format: format)
770-
docMetadata = cc.convertToDocMetadata(authorized: authorized, keyOptions: issueReq.keyOptions, credentialOptions: issueReq.credentialOptions)
774+
docMetadata = cc.convertToDocMetadata(authorized: authorized, keyOptions: issueReq.keyOptions, credentialOptions: issueReq.credentialOptions, dpopKeyId: savedDpopKeyId)
771775
let docTypeOrVctOrScope = docType ?? cc.docType ?? cc.scope ?? ""
772776
dkInfo.batchSize = dataPair.count
773777
docTypeToSave = if format == .cbor, dataToSave.count > 0 { (try IssuerSigned(data: [UInt8](dataToSave))).issuerAuth.mso.docType } else if format == .sdjwt, dataToSave.count > 0 { StorageManager.getVctFromSdJwt(docData: dataToSave) ?? docTypeOrVctOrScope } else { docTypeOrVctOrScope }
@@ -778,12 +782,12 @@ public actor OpenId4VCIService {
778782
}
779783
case .deferred(let deferredIssuanceModel):
780784
dataToSave = try JSONEncoder().encode(deferredIssuanceModel)
781-
docMetadata = deferredIssuanceModel.configuration.convertToDocMetadata()
785+
docMetadata = deferredIssuanceModel.configuration.convertToDocMetadata(dpopKeyId: savedDpopKeyId)
782786
docTypeToSave = docType ?? "DEFERRED"
783787
displayName = deferredIssuanceModel.configuration.display.getName(uiCulture)
784788
case .pending(let pendingAuthModel):
785789
dataToSave = try JSONEncoder().encode(pendingAuthModel)
786-
docMetadata = pendingAuthModel.configuration.convertToDocMetadata()
790+
docMetadata = pendingAuthModel.configuration.convertToDocMetadata(dpopKeyId: savedDpopKeyId)
787791
docTypeToSave = docType ?? "PENDING"
788792
displayName = pendingAuthModel.configuration.display.getName(uiCulture)
789793
}

Sources/EudiWalletKit/Services/OpenId4VpService.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,10 @@ public final class OpenId4VpService: @unchecked Sendable, PresentationService {
134134
// Add support for directPost.
135135
let responseUri = if case .directPostJWT(let uri) = vp.responseMode { uri.absoluteString } else if case .directPost(let uri) = vp.responseMode { uri.absoluteString } else { "" }
136136

137-
vpNonce = vp.nonce; vpClientId = vp.client.id.originalClientId
137+
let resolvedClientId = vp.client.id.clientId
138+
vpNonce = vp.nonce; vpClientId = resolvedClientId
138139
mdocGeneratedNonce = OpenId4VpUtils.generateMdocGeneratedNonce() // Not longer required for SessionTranscript, use the verifier (client) nonce i.e vpNonce
139-
sessionTranscript = SessionTranscript(handOver: OpenId4VpUtils.generateOpenId4VpHandover(clientId: vp.client.id.originalClientId, responseUri: responseUri, nonce: vpNonce, jwkThumbprint: jwkThumbprint?.byteArray))
140+
sessionTranscript = SessionTranscript(handOver: OpenId4VpUtils.generateOpenId4VpHandover(clientId: resolvedClientId, responseUri: responseUri, nonce: vpNonce, jwkThumbprint: jwkThumbprint?.byteArray))
140141

141142
logger.info("Session Transcript: \(sessionTranscript.encode().toHexString()), for clientId: \(vp.client.id), responseUri: \(responseUri), nonce: \(vp.nonce), mdocGeneratedNonce: \(mdocGeneratedNonce!)")
142143
var requestItems: RequestItems?; var deviceRequestBytes: Data?

0 commit comments

Comments
 (0)