Skip to content

Commit

Permalink
Merge pull request #134 from niscy-eudiw/main
Browse files Browse the repository at this point in the history
Add CredentialConfiguration struct and refactor issuance logic in EudiWallet to authorize via Web once
  • Loading branch information
phisakel authored Dec 10, 2024
2 parents 1ce560c + 0dc1211 commit 5aa8e3a
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 31 deletions.
18 changes: 9 additions & 9 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"originHash" : "8fcf479635d61188a4fb8fcfce50e451e8721e48de01744dcb206e08824e8559",
"originHash" : "0cadf5d34094304873169a948165b2e0c9a9b38d371b26dabc88c7f9eb3c774a",
"pins" : [
{
"identity" : "blueecc",
Expand Down Expand Up @@ -51,26 +51,26 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-openid4vci-swift.git",
"state" : {
"revision" : "176842b11ccd7cf5cc14cc12464ea431d12a2a5c",
"version" : "0.9.0"
"revision" : "a2d316ac7fd9b010ed528f41b3992be2eea98881",
"version" : "0.9.1"
}
},
{
"identity" : "eudi-lib-ios-presentation-exchange-swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-presentation-exchange-swift.git",
"state" : {
"revision" : "4e1ac57b6772f7f6eac5ef24d977c42e94bc6565",
"version" : "0.2.0"
"revision" : "21e03b18e4ea349edd21c016b9e0b3a2063285bf",
"version" : "0.2.1"
}
},
{
"identity" : "eudi-lib-ios-siop-openid4vp-swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-siop-openid4vp-swift.git",
"state" : {
"revision" : "1049277d9b5818416b313f58a5eaf139de9881b3",
"version" : "0.6.1"
"revision" : "c72b0f972f765bd106b52c953de0827096398763",
"version" : "0.6.2"
}
},
{
Expand Down Expand Up @@ -213,8 +213,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/SwiftyJSON/SwiftyJSON.git",
"state" : {
"revision" : "2b6054efa051565954e1d2b9da831680026cd768",
"version" : "4.3.0"
"revision" : "af76cf3ef710b6ca5f8c05f3a31307d44a3c5828",
"version" : "5.0.2"
}
},
{
Expand Down
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ let package = Package(
.package(url: "https://github.com/crspybits/swift-log-file", from: "0.1.0"),
.package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-iso18013-data-transfer.git", exact: "0.4.1"),
.package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-wallet-storage.git", exact: "0.4.1"),
.package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-siop-openid4vp-swift.git", exact: "0.6.1"),
.package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-openid4vci-swift.git", exact: "0.9.0"),
.package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-siop-openid4vp-swift.git", exact: "0.6.2"),
.package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-openid4vci-swift.git", exact: "0.9.1"),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
Expand Down
10 changes: 8 additions & 2 deletions Sources/EudiWalletKit/EudiWallet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -290,9 +290,15 @@ public final class EudiWallet: ObservableObject, @unchecked Sendable {
guard format == .cbor else { fatalError("jwt format not implemented") }
if docTypes.isEmpty { return [] }
var documents = [WalletStorage.Document]()
var openId4VCIServices = [OpenId4VCIService]()
for (i, docTypeModel) in docTypes.enumerated() {
let openId4VCIService = try await prepareIssuing(id: UUID().uuidString, docType: i > 0 ? "" : docTypes.map(\.docType).joined(separator: ", "), displayName: i > 0 ? nil : docTypes.map(\.displayName).joined(separator: ", "), keyOptions: docTypeKeyOptions?[docTypeModel.docType], disablePrompt: i > 0, promptMessage: promptMessage)
guard let docData = try await openId4VCIService.issueDocumentByOfferUrl(offerUri: offerUri, docTypeModel: docTypeModel, txCodeValue: txCodeValue, format: format, promptMessage: promptMessage, claimSet: claimSet) else { continue }
openId4VCIServices.append(try await prepareIssuing(id: UUID().uuidString, docType: i > 0 ? "" : docTypes.map(\.docType).joined(separator: ", "), displayName: i > 0 ? nil : docTypes.map(\.displayName).joined(separator: ", "), keyOptions: docTypeKeyOptions?[docTypeModel.docType], disablePrompt: i > 0, promptMessage: promptMessage))
}
let (auth, credentialInfos) = try await openId4VCIServices.first!.authorizeOffer(offerUri: offerUri, docTypeModels: docTypes, txCodeValue: txCodeValue, format: format)
for (i, openId4VCIService) in openId4VCIServices.enumerated() {
openId4VCIServices[i].bindingKey = openId4VCIServices.first!.bindingKey
guard let offer = OpenId4VCIService.metadataCache[offerUri] else { throw WalletError(description: "offerUri not resolved. resolveOfferDocTypes must be called first")}
guard let docData = try await openId4VCIService.issueDocumentByOfferUrl(offer: offer, authorizedOutcome: auth, credentialInfo: credentialInfos[i], promptMessage: promptMessage, claimSet: claimSet) else { continue }
documents.append(try await finalizeIssuing(data: docData, docType: docData.isDeferred ? docTypes[i].docType : nil, format: format, issueReq: openId4VCIService.issueReq, openId4VCIService: openId4VCIService))
}
OpenId4VCIService.metadataCache.removeValue(forKey: offerUri)
Expand Down
32 changes: 19 additions & 13 deletions Sources/EudiWalletKit/Services/OpenId4VciService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public class OpenId4VCIService: NSObject, @unchecked Sendable, ASWebAuthenticati
self.config = config
}

func initSecurityKeys(algSupported: [String]) async throws {
func initSecurityKeys(algSupported: Set<String>) async throws {
let crvType = issueReq.keyOptions?.curve ?? type(of: issueReq.secureArea).defaultEcCurve
let secureAreaSigningAlg: SigningAlgorithm = crvType.defaultSigningAlgorithm
let algTypes = algSupported.compactMap { JWSAlgorithm.AlgorithmType(rawValue: $0) }
Expand Down Expand Up @@ -96,18 +96,23 @@ public class OpenId4VCIService: NSObject, @unchecked Sendable, ASWebAuthenticati
func getIssuerForDeferred(data: DeferredIssuanceModel) throws -> Issuer {
try Issuer.createDeferredIssuer(deferredCredentialEndpoint: data.deferredCredentialEndpoint, deferredRequesterPoster: Poster(session: urlSession), config: config)
}
func issueDocumentByOfferUrl(offerUri: String, docTypeModel: OfferedDocModel, txCodeValue: String?, format: DataFormat, promptMessage: String? = nil, claimSet: ClaimSet? = nil) async throws -> IssuanceOutcome? {

func authorizeOffer(offerUri: String, docTypeModels: [OfferedDocModel], txCodeValue: String?, format: DataFormat) async throws -> (AuthorizeRequestOutcome, [CredentialConfiguration]) {
guard format == .cbor else { fatalError("jwt format not implemented") }
guard let offer = Self.metadataCache[offerUri] else { throw WalletError(description: "offerUri not resolved. resolveOfferDocTypes must be called first")}
guard let credentialInfo = try? getCredentialIdentifier(credentialsSupported: offer.credentialIssuerMetadata.credentialsSupported, docType: docTypeModel.docType, format: format) else { return nil }
try await initSecurityKeys(algSupported: credentialInfo.algValuesSupported)
let credentialInfos = docTypeModels.compactMap { try? getCredentialIdentifier(credentialsSupported: offer.credentialIssuerMetadata.credentialsSupported, docType: $0.docType, format: format) }
guard credentialInfos.count > 0, credentialInfos.count == docTypeModels.count else { throw WalletError(description: "Missing Credential identifiers") }
try await initSecurityKeys(algSupported: Set(credentialInfos.flatMap { $0.algValuesSupported }))
let code: Grants.PreAuthorizedCode? = switch offer.grants { case .preAuthorizedCode(let preAuthorizedCode): preAuthorizedCode; case .both(_, let preAuthorizedCode): preAuthorizedCode; case .authorizationCode(_), .none: nil }
let txCodeSpec: TxCode? = code?.txCode
let preAuthorizedCode: String? = code?.preAuthorizedCode
let issuer = try getIssuer(offer: offer)
if preAuthorizedCode != nil && txCodeSpec != nil && txCodeValue == nil { throw WalletError(description: "A transaction code is required for this offer") }
let authorizedOutcome = if let preAuthorizedCode, let authCode = try? IssuanceAuthorization(preAuthorizationCode: preAuthorizedCode, txCode: txCodeSpec) { AuthorizeRequestOutcome.authorized(try await issuer.authorizeWithPreAuthorizationCode(credentialOffer: offer, authorizationCode: authCode, clientId: config.clientId, transactionCode: txCodeValue).get()) } else { try await authorizeRequestWithAuthCodeUseCase(issuer: issuer, offer: offer) }
return (authorizedOutcome, credentialInfos)
}

func issueDocumentByOfferUrl(offer: CredentialOffer, authorizedOutcome: AuthorizeRequestOutcome, credentialInfo: CredentialConfiguration, promptMessage: String? = nil, claimSet: ClaimSet? = nil) async throws -> IssuanceOutcome? {
if case .presentation_request(let url) = authorizedOutcome, let parRequested {
logger.info("Dynamic issuance request with url: \(url)")
let uuid = UUID().uuidString
Expand All @@ -118,6 +123,7 @@ public class OpenId4VCIService: NSObject, @unchecked Sendable, ASWebAuthenticati
do {
let id = credentialInfo.identifier.value; let sc = credentialInfo.scope; let dn = credentialInfo.displayName ?? ""
logger.info("Starting issuing with identifer \(id), scope \(sc), displayName: \(dn)")
let issuer = try getIssuer(offer: offer)
let res = try await issueOfferedCredentialInternalValidated(authorized, offer: offer, issuer: issuer, credentialConfigurationIdentifier: credentialInfo.identifier, displayName: credentialInfo.displayName, claimSet: claimSet)
// logger.info("Credential str:\n\(str)")
return res
Expand All @@ -135,20 +141,20 @@ public class OpenId4VCIService: NSObject, @unchecked Sendable, ASWebAuthenticati
case .success(let metaData):
if let authorizationServer = metaData.authorizationServers?.first {
let authServerMetadata = await AuthorizationServerMetadataResolver(oidcFetcher: Fetcher(session: urlSession), oauthFetcher: Fetcher(session: urlSession)).resolve(url: authorizationServer)
let (credentialConfigurationIdentifier, _, displayName, algValues) = try getCredentialIdentifier(credentialsSupported: metaData.credentialsSupported, docType: docType, format: format)
try await initSecurityKeys(algSupported: algValues)
let offer = try CredentialOffer(credentialIssuerIdentifier: credentialIssuerIdentifier, credentialIssuerMetadata: metaData, credentialConfigurationIdentifiers: [credentialConfigurationIdentifier], grants: nil, authorizationServerMetadata: try authServerMetadata.get())
let credentialConfiguration = try getCredentialIdentifier(credentialsSupported: metaData.credentialsSupported, docType: docType, format: format)
try await initSecurityKeys(algSupported: Set(credentialConfiguration.algValuesSupported))
let offer = try CredentialOffer(credentialIssuerIdentifier: credentialIssuerIdentifier, credentialIssuerMetadata: metaData, credentialConfigurationIdentifiers: [credentialConfiguration.identifier], grants: nil, authorizationServerMetadata: try authServerMetadata.get())
// Authorize with auth code flow
let issuer = try getIssuer(offer: offer)
let authorizedOutcome = try await authorizeRequestWithAuthCodeUseCase(issuer: issuer, offer: offer)
if case .presentation_request(let url) = authorizedOutcome, let parRequested {
logger.info("Dynamic issuance request with url: \(url)")
let uuid = UUID().uuidString
Self.metadataCache[uuid] = offer
return .pending(PendingIssuanceModel(pendingReason: .presentation_request_url(url.absoluteString), identifier: credentialConfigurationIdentifier, displayName: displayName ?? "", algValuesSupported: algValues, metadataKey: uuid, pckeCodeVerifier: parRequested.pkceVerifier.codeVerifier, pckeCodeVerifierMethod: parRequested.pkceVerifier.codeVerifierMethod ))
return .pending(PendingIssuanceModel(pendingReason: .presentation_request_url(url.absoluteString), identifier: credentialConfiguration.identifier, displayName: credentialConfiguration.displayName ?? "", algValuesSupported:credentialConfiguration.algValuesSupported, metadataKey: uuid, pckeCodeVerifier: parRequested.pkceVerifier.codeVerifier, pckeCodeVerifierMethod: parRequested.pkceVerifier.codeVerifierMethod ))
}
guard case .authorized(let authorized) = authorizedOutcome else { throw WalletError(description: "Invalid authorized request outcome") }
return try await issueOfferedCredentialInternal(authorized, issuer: issuer, credentialConfigurationIdentifier: credentialConfigurationIdentifier, displayName: displayName, claimSet: claimSet)
return try await issueOfferedCredentialInternal(authorized, issuer: issuer, credentialConfigurationIdentifier: credentialConfiguration.identifier, displayName: credentialConfiguration.displayName, claimSet: claimSet)
} else {
throw WalletError(description: "Invalid authorization server")
}
Expand All @@ -175,7 +181,7 @@ public class OpenId4VCIService: NSObject, @unchecked Sendable, ASWebAuthenticati
return try await issueOfferedCredentialInternal(authorized, issuer: issuer, credentialConfigurationIdentifier: credentialConfigurationIdentifier, displayName: displayName, claimSet: claimSet)
}

func getCredentialIdentifier(credentialsSupported: [CredentialConfigurationIdentifier: CredentialSupported], docType: String, format: DataFormat) throws -> (identifier: CredentialConfigurationIdentifier, scope: String, displayName: String?, algValuesSupported: [String]) {
func getCredentialIdentifier(credentialsSupported: [CredentialConfigurationIdentifier: CredentialSupported], docType: String, format: DataFormat) throws -> CredentialConfiguration {
switch format {
case .cbor:
guard let credential = credentialsSupported.first(where: { if case .msoMdoc(let msoMdocCred) = $0.value, msoMdocCred.docType == docType { true } else { false } }), case let .msoMdoc(msoMdocConf) = credential.value, let scope = msoMdocConf.scope else {
Expand All @@ -185,7 +191,7 @@ public class OpenId4VCIService: NSObject, @unchecked Sendable, ASWebAuthenticati
logger.info("Currently supported cryptographic suites: \(msoMdocConf.credentialSigningAlgValuesSupported)")
//return (identifier: credential.key, scope: scope, docType: docType)
let displayName = msoMdocConf.display.getName()
return (identifier: credential.key, scope: scope, displayName: displayName, algValuesSupported: msoMdocConf.credentialSigningAlgValuesSupported)
return CredentialConfiguration(identifier: credential.key, scope: scope, displayName: displayName, algValuesSupported: msoMdocConf.credentialSigningAlgValuesSupported)
default:
throw WalletError(description: "Format \(format) not yet supported")
}
Expand Down Expand Up @@ -337,7 +343,7 @@ public class OpenId4VCIService: NSObject, @unchecked Sendable, ASWebAuthenticati
logger.info("Starting issuing with identifer \(model.identifier.value)")
let pkceVerifier = try PKCEVerifier(codeVerifier: model.pckeCodeVerifier, codeVerifierMethod: model.pckeCodeVerifierMethod)
let authorized = try await issuer.authorizeWithAuthorizationCode(authorizationCode: .authorizationCode(AuthorizationCodeRetrieved(credentials: [.init(value: model.identifier.value)], authorizationCode: IssuanceAuthorization(authorizationCode: authorizationCode), pkceVerifier: pkceVerifier, configurationIds: [model.identifier]))).get()
try await initSecurityKeys(algSupported: model.algValuesSupported)
try await initSecurityKeys(algSupported: Set(model.algValuesSupported))
let res = try await issueOfferedCredentialInternalValidated(authorized, offer: offer, issuer: issuer, credentialConfigurationIdentifier: model.identifier, displayName: model.displayName, claimSet: nil)
Self.metadataCache.removeValue(forKey: model.metadataKey)
return res
Expand Down
31 changes: 26 additions & 5 deletions Sources/EudiWalletKit/ViewModels/InternalssuanceModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ import Foundation
@preconcurrency import OpenID4VCI
import WalletStorage

struct CredentialConfiguration: Sendable {
let identifier: CredentialConfigurationIdentifier
let scope: String
let displayName: String?
let algValuesSupported: [String]
}

struct DeferredIssuanceModel: Codable, Sendable {
let deferredCredentialEndpoint: CredentialIssuerEndpoint
let accessToken: IssuanceAccessToken
Expand Down Expand Up @@ -48,10 +55,24 @@ enum IssuanceOutcome {
}

extension IssuanceOutcome {
var isDeferred: Bool { switch self { case .deferred(_): true; default: false } }
var isPending: Bool { switch self { case .pending(_): true; default: false } }
var pendingOrDeferredStatus: DocumentStatus? { switch self { case .deferred(_): .deferred; case .pending(_): .pending; default: nil } }
var isDeferred: Bool {
switch self {
case .deferred(_): true
default: false
}
}
var isPending: Bool {
switch self {
case .pending(_): true
default: false
}
}
var pendingOrDeferredStatus: DocumentStatus? {
switch self {
case .deferred(_): .deferred
case .pending(_): .pending
default: nil
}
}
}



0 comments on commit 5aa8e3a

Please sign in to comment.