Skip to content

Commit

Permalink
Merge pull request #74 from niscy-eudiw/deferred_issue_openid4vci
Browse files Browse the repository at this point in the history
Refactor to support Deferred document issuing
  • Loading branch information
phisakel authored Jul 16, 2024
2 parents 52fa378 + 8227fc5 commit 1a8febc
Show file tree
Hide file tree
Showing 10 changed files with 252 additions and 121 deletions.
8 changes: 4 additions & 4 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-openid4vci-swift.git",
"state" : {
"revision" : "727bc48dcbdb7cd1cd942c0086fb8e9b7dc06879",
"version" : "0.3.1"
"revision" : "b926a5dce8bd372efb496eb48e7a9f90db8d5843",
"version" : "0.4.1"
}
},
{
Expand All @@ -77,8 +77,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-wallet-storage.git",
"state" : {
"revision" : "ba9ba8b2b889638d3a4b62358c741b34291848da",
"version" : "0.2.0"
"revision" : "8ebf1f0cf0edb2854b7b098cd90c32e0da5c9c34",
"version" : "0.2.2"
}
},
{
Expand Down
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ let package = Package(
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.5.3"),
.package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-iso18013-data-transfer.git", exact: "0.2.9"),
.package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-wallet-storage.git", .upToNextMajor(from: "0.2.0")),
.package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-wallet-storage.git", exact: "0.2.2"),
.package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-siop-openid4vp-swift.git", exact: "0.3.2"),
.package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-openid4vci-swift.git", exact: "0.3.1"),
.package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-openid4vci-swift.git", exact: "0.4.1"),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,13 +194,13 @@ catch {
#### Resolving Credential offer

The library provides the `resolveOfferUrlDocTypes(uriOffer:)` method that resolves the credential offer URI.
The method returns the resolved `OfferedIssueModel` object that contains the offer's data (offered document types, issuer name and transaction code specification for pre-authorized flow). The offer's data can be displayed to the
The method returns the resolved `OfferedIssuanceModel` object that contains the offer's data (offered document types, issuer name and transaction code specification for pre-authorized flow). The offer's data can be displayed to the
user.

The following example shows how to resolve a credential offer:

```swift
func resolveOfferUrlDocTypes(uriOffer: String) async throws -> OfferedIssueModel {
func resolveOfferUrlDocTypes(uriOffer: String) async throws -> OfferedIssuanceModel {
return try await wallet.resolveOfferUrlDocTypes(uriOffer: uriOffer)
}
```
Expand All @@ -220,7 +220,7 @@ The user is redirected in an authorization web view to the issuer's authorizatio
#### Pre-Authorization code flow

When Issuer supports the pre-authorization code flow, the resolved offer will also contain the corresponding
information. Specifically, the `txCodeSpec` field in the `OfferedIssueModel` object will contain:
information. Specifically, the `txCodeSpec` field in the `OfferedIssuanceModel` object will contain:

- The input mode, whether it is NUMERIC or TEXT
- The expected length of the input
Expand Down
97 changes: 64 additions & 33 deletions Sources/EudiWalletKit/EudiWallet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ public final class EudiWallet: ObservableObject {
public private(set) var storage: StorageManager
var storageService: any WalletStorage.DataStorageService { storage.storageService }
/// Instance of the wallet initialized with default parameters
public static private(set) var standard: EudiWallet = EudiWallet()
public static private(set) var standard: EudiWallet = try! EudiWallet()
/// The [access group](https://developer.apple.com/documentation/security/ksecattraccessgroup) that documents are stored in.
public var accessGroup: String? { didSet { storage.storageService.accessGroup = accessGroup } }
/// Whether user authentication via biometrics or passcode is required before sending user data
public var userAuthenticationRequired: Bool
/// Trusted root certificates to validate the reader authentication certificate included in the proximity request
Expand All @@ -53,7 +55,8 @@ public final class EudiWallet: ObservableObject {
public var urlSession: URLSession

/// Initialize a wallet instance. All parameters are optional.
public init(storageType: StorageType = .keyChain, serviceName: String = "eudiw", accessGroup: String? = nil, trustedReaderCertificates: [Data]? = nil, userAuthenticationRequired: Bool = true, verifierApiUri: String? = nil, openID4VciIssuerUrl: String? = nil, openID4VciClientId: String? = nil, openID4VciRedirectUri: String? = nil, urlSession: URLSession? = nil) {
public init(storageType: StorageType = .keyChain, serviceName: String = "eudiw", accessGroup: String? = nil, trustedReaderCertificates: [Data]? = nil, userAuthenticationRequired: Bool = true, verifierApiUri: String? = nil, openID4VciIssuerUrl: String? = nil, openID4VciClientId: String? = nil, openID4VciRedirectUri: String? = nil, urlSession: URLSession? = nil) throws {
guard !serviceName.isEmpty, !serviceName.contains(":") else { throw WalletError(description: "Not allowed service name, remove : character") }
let keyChainObj = KeyChainStorageService(serviceName: serviceName, accessGroup: accessGroup)
let storageService = switch storageType { case .keyChain:keyChainObj }
storage = StorageManager(storageService: storageService)
Expand Down Expand Up @@ -100,25 +103,52 @@ public final class EudiWallet: ObservableObject {
let data = try await openId4VCIService.issueDocument(docType: docType, format: format, useSecureEnclave: useSecureEnclave)
return try await finalizeIssuing(id: id, data: data, docType: docType, format: format, issueReq: issueReq, openId4VCIService: openId4VCIService)
}

/// Request a deferred issuance based on a stored deferred document. On success, the deferred document is updated with the issued document.
///
/// The caller does not need to reload documents, storage manager collections are updated.
/// - Parameter deferredDoc: A stored document with deferred status
/// - Returns: The issued document in case it was approved in the backend and the deferred data are valid, otherwise a deferred status document
@discardableResult public func requestDeferredIssuance(deferredDoc: WalletStorage.Document) async throws -> WalletStorage.Document {
guard deferredDoc.status == .deferred else { throw WalletError(description: "Invalid document status") }
guard let pkt = deferredDoc.privateKeyType, let pk = deferredDoc.privateKey, let format = DataFormat(deferredDoc.docDataType) else { throw WalletError(description: "Invalid document") }
let issueReq = try IssueRequest(id: deferredDoc.id, docType: deferredDoc.docType, privateKeyType: pkt, keyData: pk)
let openId4VCIService = OpenId4VCIService(issueRequest: issueReq, credentialIssuerURL: "", clientId: "", callbackScheme: openID4VciRedirectUri, urlSession: urlSession)
openId4VCIService.usedSecureEnclave = deferredDoc.privateKeyType == .secureEnclaveP256
let data = try await openId4VCIService.requestDeferredIssuance(deferredDoc: deferredDoc)
guard case .issued(_) = data else { return deferredDoc }
return try await finalizeIssuing(id: deferredDoc.id, data: data, docType: deferredDoc.docType, format: format, issueReq: issueReq, openId4VCIService: openId4VCIService)
}

func finalizeIssuing(id: String, data: Data, docType: String?, format: DataFormat, issueReq: IssueRequest, openId4VCIService: OpenId4VCIService) async throws -> WalletStorage.Document {
let iss = IssuerSigned(data: [UInt8](data))
func finalizeIssuing(id: String, data: IssuanceOutcome, docType: String?, format: DataFormat, issueReq: IssueRequest, openId4VCIService: OpenId4VCIService) async throws -> WalletStorage.Document {
var dataToSave: Data
var docTypeToSave: String
guard let ddt = DocDataType(rawValue: format.rawValue) else { throw WalletError(description: "Invalid format \(format.rawValue)") }
let docTypeToSave = docType ?? (format == .cbor ? iss?.issuerAuth.mso.docType : nil)
var dataToSave: Data? = data
guard let docTypeToSave else { throw WalletError(description: "Unknown document type") }
guard let dataToSave else { throw WalletError(description: "Issued data cannot be recognized") }
var issued: WalletStorage.Document
switch data {
case .issued(let data):
dataToSave = data
let dt = if format == .cbor { IssuerSigned(data: [UInt8](data))?.issuerAuth.mso.docType ?? docType } else { docType }
guard let dt else { throw WalletError(description: "Unknown document type") }
docTypeToSave = dt
case .deferred(let deferredIssuanceModel):
dataToSave = try JSONEncoder().encode(deferredIssuanceModel)
docTypeToSave = docType ?? "DEFERRED"
}
var newDocument: WalletStorage.Document
let newDocStatus: WalletStorage.DocumentStatus = data.isDeferred ? .deferred : .issued
if !openId4VCIService.usedSecureEnclave {
issued = WalletStorage.Document(id: id, docType: docTypeToSave, docDataType: ddt, data: dataToSave, privateKeyType: .x963EncodedP256, privateKey: issueReq.keyData, createdAt: Date())
newDocument = WalletStorage.Document(id: id, docType: docTypeToSave, docDataType: ddt, data: dataToSave, privateKeyType: .x963EncodedP256, privateKey: issueReq.keyData, createdAt: Date(), status: newDocStatus)
} else {
issued = WalletStorage.Document(id: id, docType: docTypeToSave, docDataType: ddt, data: dataToSave, privateKeyType: .secureEnclaveP256, privateKey: issueReq.keyData, createdAt: Date())
newDocument = WalletStorage.Document(id: id, docType: docTypeToSave, docDataType: ddt, data: dataToSave, privateKeyType: .secureEnclaveP256, privateKey: issueReq.keyData, createdAt: Date(), status: newDocStatus)
}
try issueReq.saveToStorage(storage.storageService)
try endIssueDocument(issued)
await storage.appendDocModel(issued)
try issueReq.saveToStorage(storage.storageService, status: newDocStatus)
try endIssueDocument(newDocument)
await storage.appendDocModel(newDocument)
await storage.refreshPublishedVars()
return issued
if !data.isDeferred, storage.deferredDocuments.first(where: { $0.id == id }) != nil {
try await storage.deleteDocument(id: id, status: .deferred)
}
return newDocument
}

/// Resolve OpenID4VCI offer URL document types. Resolved offer metadata are cached
Expand All @@ -127,7 +157,7 @@ public final class EudiWallet: ObservableObject {
/// - format: data format
/// - useSecureEnclave: whether to use secure enclave (if supported)
/// - Returns: Offered issue information model
public func resolveOfferUrlDocTypes(uriOffer: String, format: DataFormat = .cbor, useSecureEnclave: Bool = true) async throws -> OfferedIssueModel {
public func resolveOfferUrlDocTypes(uriOffer: String, format: DataFormat = .cbor, useSecureEnclave: Bool = true) async throws -> OfferedIssuanceModel {
let (_, openId4VCIService, _) = try await prepareIssuing(docType: nil)
return try await openId4VCIService.resolveOfferDocTypes(uriOffer: uriOffer, format: format)
}
Expand All @@ -150,58 +180,59 @@ public final class EudiWallet: ObservableObject {
for (i, docData) in docsData.enumerated() {
if i > 0 { (issueReq, openId4VCIService, id) = try await prepareIssuing(docType: nil) }
openId4VCIService.usedSecureEnclave = useSecureEnclave && SecureEnclave.isAvailable
documents.append(try await finalizeIssuing(id: id, data: docData, docType: nil, format: format, issueReq: issueReq, openId4VCIService: openId4VCIService))
documents.append(try await finalizeIssuing(id: id, data: docData, docType: docData.isDeferred ? docTypes[i].docType : nil, format: format, issueReq: issueReq, openId4VCIService: openId4VCIService))
}
return documents
}

/// Begin issuing a document by generating an issue request
///
/// - Parameters:
/// - id: Document identifier
/// - issuer: Issuer function
public func beginIssueDocument(id: String, privateKeyType: PrivateKeyType = .secureEnclaveP256, saveToStorage: Bool = true) async throws -> IssueRequest {
public func beginIssueDocument(id: String, privateKeyType: PrivateKeyType = .secureEnclaveP256, saveToStorage: Bool = true, bDeferred: Bool = false) async throws -> IssueRequest {
let request = try IssueRequest(id: id, privateKeyType: privateKeyType)
if saveToStorage { try request.saveToStorage(storage.storageService) }
if saveToStorage { try request.saveToStorage(storage.storageService, status: bDeferred ? .deferred : .issued) }
return request
}

/// End issuing by saving the issuing document (and its private key) in storage
/// - Parameter issued: The issued document
public func endIssueDocument(_ issued: WalletStorage.Document) throws {
try storage.storageService.saveDocumentData(issued, dataToSaveType: .doc, dataType: issued.docDataType.rawValue, allowOverwrite: true)
try storage.storageService.saveDocumentData(issued, dataToSaveType: .key, dataType: issued.privateKeyType!.rawValue, allowOverwrite: true)
try storage.storageService.saveDocument(issued, allowOverwrite: true)
}

/// Load documents from storage
///
/// Calls ``storage`` loadDocuments
/// - Returns: An array of ``WalletStorage.Document`` objects
@discardableResult public func loadDocuments() async throws -> [WalletStorage.Document]? {
return try await storage.loadDocuments()
@discardableResult public func loadDocuments(status: WalletStorage.DocumentStatus = .issued) async throws -> [WalletStorage.Document]? {
return try await storage.loadDocuments(status: status)
}


/// Delete all documents from storage
///
/// Calls ``storage`` loadDocuments
/// - Returns: An array of ``WalletStorage.Document`` objects
public func deleteDocuments() async throws {
return try await storage.deleteDocuments()
public func deleteDocuments(status: WalletStorage.DocumentStatus = .issued) async throws {
return try await storage.deleteDocuments(status: status)
}

/// Load sample data from json files
///
/// The mdoc data are stored in wallet storage as documents
/// - Parameter sampleDataFiles: Names of sample files provided in the app bundle
public func loadSampleData(sampleDataFiles: [String]? = nil) async throws {
try? storageService.deleteDocuments()
try? storageService.deleteDocuments(status: .issued)
let docSamples = (sampleDataFiles ?? ["EUDI_sample_data"]).compactMap { Data(name:$0) }
.compactMap(SignUpResponse.decomposeCBORSignupResponse(data:)).flatMap {$0}
.map { Document(docType: $0.docType, docDataType: .cbor, data: $0.issData, privateKeyType: .x963EncodedP256, privateKey: $0.pkData, createdAt: Date.distantPast, modifiedAt: nil) }
.map { Document(docType: $0.docType, docDataType: .cbor, data: $0.issData, privateKeyType: .x963EncodedP256, privateKey: $0.pkData, createdAt: Date.distantPast, modifiedAt: nil, status: .issued) }
do {
for docSample in docSamples {
try storageService.saveDocument(docSample, allowOverwrite: true)
}
try await storage.loadDocuments()
for docSample in docSamples {
try storageService.saveDocument(docSample, allowOverwrite: true)
}
try await storage.loadDocuments(status: .issued)
} catch {
await storage.setError(error)
throw WalletError(description: error.localizedDescription, code: (error as NSError).code)
Expand All @@ -213,11 +244,11 @@ public final class EudiWallet: ObservableObject {
/// - docType: docType of documents to present (optional)
/// - dataFormat: Exchanged data ``Format`` type
/// - Returns: A data dictionary that can be used to initialize a presentation service
public func prepareServiceDataParameters(docType: String? = nil, dataFormat: DataFormat = .cbor ) throws -> [String : Any] {
func prepareServiceDataParameters(docType: String? = nil, dataFormat: DataFormat = .cbor ) throws -> [String: Any] {
var parameters: [String: Any]
switch dataFormat {
case .cbor:
guard var docs = try storageService.loadDocuments(), docs.count > 0 else { throw WalletError(description: "No documents found") }
guard var docs = try storageService.loadDocuments(status: .issued), docs.count > 0 else { throw WalletError(description: "No documents found") }
if let docType { docs = docs.filter { $0.docType == docType} }
if let docType { guard docs.count > 0 else { throw WalletError(description: "No documents of type \(docType) found") } }
let cborsWithKeys = docs.compactMap { $0.getCborData() }
Expand Down
11 changes: 11 additions & 0 deletions Sources/EudiWalletKit/Services/Enumerations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
// TransferStatus.swift

import Foundation
import WalletStorage

/// Data exchange flow type
public enum FlowType: Codable, Hashable {
Expand All @@ -35,6 +36,16 @@ public enum DataFormat: String {
case sdjwt = "sdjwt"
}

public extension DataFormat {
init?(_ docDataType: DocDataType) {
switch docDataType {
case .cbor: self = .cbor
case .sjwt: self = .sdjwt
default: return nil
}
}
}

public enum StorageType {
case keyChain
}
Expand Down
Loading

0 comments on commit 1a8febc

Please sign in to comment.