Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Eud flow prep #1

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,17 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-iso18013-data-model.git",
"state" : {
"revision" : "bf62cc73ae2cea61e98020d2d037c153500207e7",
"version" : "0.2.5"
"revision" : "12314daf45d637a1afeda321d605f328d422fe8d",
"version" : "0.2.6"
}
},
{
"identity" : "eudi-lib-ios-iso18013-data-transfer",
"kind" : "remoteSourceControl",
"location" : "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-iso18013-data-transfer.git",
"location" : "https://github.com/TICESoftware/eudi-lib-ios-iso18013-data-transfer",
"state" : {
"revision" : "4e18b4b5de1ab2e9c5b9495574667b1a27508a9e",
"version" : "0.2.8"
"branch" : "main",
"revision" : "468f789688cd320738cd5017b438d0f983dae5bc"
}
},
{
Expand Down Expand Up @@ -158,8 +158,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-certificates.git",
"state" : {
"revision" : "83640c8097acaec17c9835a083e89678cb0f2b66",
"version" : "1.3.0"
"revision" : "4688f242811d21a9c7a8ad669b3bc5b336759929",
"version" : "1.4.0"
}
},
{
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ let package = Package(
dependencies: [
.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", .upToNextMajor(from: "0.2.8")),
.package(url: "https://github.com/TICESoftware/eudi-lib-ios-iso18013-data-transfer", branch: "main"),
.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-siop-openid4vp-swift.git", exact: "0.1.1"),
.package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-openid4vci-swift.git", exact: "0.1.2"),
Expand Down
55 changes: 34 additions & 21 deletions Sources/EudiWalletKit/EudiWallet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,19 @@ public final class EudiWallet: ObservableObject {
/// - docType: document type
/// - promptMessage: Prompt message for biometric authentication (optional)
/// - Returns: (Issue request key pair, vci service, unique id)
func prepareIssuing(docType: String?, promptMessage: String? = nil) async throws -> (IssueRequest, OpenId4VCIService, String) {
func prepareIssuing(docType: String?, promptMessage: String? = nil, authorizationService: OpenId4VciUserAuthorizationService?) async throws -> (IssueRequest, OpenId4VCIService, String) {
guard let openID4VciIssuerUrl else { throw WalletError(description: "issuer Url not defined")}
guard let openID4VciClientId else { throw WalletError(description: "clientId not defined")}
let id: String = UUID().uuidString
let issueReq = try await Self.authorizedAction(action: {
return try await beginIssueDocument(id: id, privateKeyType: useSecureEnclave ? .secureEnclaveP256 : .x963EncodedP256, saveToStorage: false)
}, disabled: !userAuthenticationRequired || docType == nil, dismiss: {}, localizedReason: promptMessage ?? NSLocalizedString("issue_document", comment: "").replacingOccurrences(of: "{docType}", with: NSLocalizedString(docType ?? "", comment: "")))
guard let issueReq else { throw LAError(.userCancel)}
let openId4VCIService = OpenId4VCIService(issueRequest: issueReq, credentialIssuerURL: openID4VciIssuerUrl, clientId: openID4VciClientId, callbackScheme: openID4VciRedirectUri)
let openId4VCIService = OpenId4VCIService(issueRequest: issueReq,
credentialIssuerURL: openID4VciIssuerUrl,
clientId: openID4VciClientId,
callbackScheme: openID4VciRedirectUri,
authorizationService: authorizationService)
return (issueReq, openId4VCIService, id)
}

Expand All @@ -92,8 +96,8 @@ public final class EudiWallet: ObservableObject {
/// - format: Optional format type. Defaults to cbor
/// - promptMessage: Prompt message for biometric authentication (optional)
/// - Returns: The document issued. It is saved in storage.
@discardableResult public func issueDocument(docType: String, format: DataFormat = .cbor, promptMessage: String? = nil) async throws -> WalletStorage.Document {
let (issueReq, openId4VCIService, id) = try await prepareIssuing(docType: docType, promptMessage: promptMessage)
@discardableResult public func issueDocument(docType: String, format: DataFormat = .cbor, promptMessage: String? = nil, authorizationService: OpenId4VciUserAuthorizationService?) async throws -> WalletStorage.Document {
let (issueReq, openId4VCIService, id) = try await prepareIssuing(docType: docType, promptMessage: promptMessage, authorizationService: authorizationService)
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)
}
Expand Down Expand Up @@ -128,8 +132,8 @@ public final class EudiWallet: ObservableObject {
/// - format: data format
/// - useSecureEnclave: whether to use secure enclave (if supported)
/// - Returns: Offered document info model
public func resolveOfferUrlDocTypes(uriOffer: String, format: DataFormat = .cbor, useSecureEnclave: Bool = true) async throws -> [OfferedDocModel] {
let (_, openId4VCIService, _) = try await prepareIssuing(docType: nil)
public func resolveOfferUrlDocTypes(uriOffer: String, format: DataFormat = .cbor, useSecureEnclave: Bool = true, authorizationService: OpenId4VciUserAuthorizationService) async throws -> [OfferedDocModel] {
let (_, openId4VCIService, _) = try await prepareIssuing(docType: nil, authorizationService: authorizationService)
return try await openId4VCIService.resolveOfferDocTypes(uriOffer: uriOffer, format: format)
}

Expand All @@ -142,13 +146,13 @@ public final class EudiWallet: ObservableObject {
/// - useSecureEnclave: whether to use secure enclave (if supported)
/// - claimSet: claim set (optional)
/// - Returns: Array of issued and stored documents
public func issueDocumentsByOfferUrl(offerUri: String, docTypes: [OfferedDocModel], format: DataFormat = .cbor, promptMessage: String? = nil, useSecureEnclave: Bool = true, claimSet: ClaimSet? = nil) async throws -> [WalletStorage.Document] {
public func issueDocumentsByOfferUrl(offerUri: String, docTypes: [OfferedDocModel], format: DataFormat = .cbor, promptMessage: String? = nil, useSecureEnclave: Bool = true, claimSet: ClaimSet? = nil, authorizationService: OpenId4VciUserAuthorizationService?) async throws -> [WalletStorage.Document] {
guard format == .cbor else { throw fatalError("jwt format not implemented") }
var (issueReq, openId4VCIService, id) = try await prepareIssuing(docType: docTypes.map(\.docType).joined(separator: ", "), promptMessage: promptMessage)
var (issueReq, openId4VCIService, id) = try await prepareIssuing(docType: docTypes.map(\.docType).joined(separator: ", "), promptMessage: promptMessage, authorizationService: authorizationService)
let docsData = try await openId4VCIService.issueDocumentsByOfferUrl(offerUri: offerUri, docTypes: docTypes, format: format, useSecureEnclave: useSecureEnclave, claimSet: claimSet)
var documents = [WalletStorage.Document]()
for (i, docData) in docsData.enumerated() {
if i > 0 { (issueReq, openId4VCIService, id) = try await prepareIssuing(docType: nil) }
if i > 0 { (issueReq, openId4VCIService, id) = try await prepareIssuing(docType: nil, authorizationService: authorizationService) }
openId4VCIService.usedSecureEnclave = useSecureEnclave && SecureEnclave.isAvailable
documents.append(try await finalizeIssuing(id: id, data: docData, docType: nil, format: format, issueReq: issueReq, openId4VCIService: openId4VCIService))
}
Expand Down Expand Up @@ -213,22 +217,25 @@ 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] {
var parameters: [String: Any]
public func prepareServiceDataParameters(docType: String? = nil, dataFormat: DataFormat = .cbor ) throws -> MDocPresentationState {
switch dataFormat {
case .cbor:
guard var docs = try storageService.loadDocuments(), 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() }
guard cborsWithKeys.count > 0 else { throw WalletError(description: "Documents decode error") }
parameters = [InitializeKeys.document_signup_issuer_signed_obj.rawValue: Dictionary(uniqueKeysWithValues: cborsWithKeys.map(\.iss)), InitializeKeys.device_private_key_obj.rawValue: Dictionary(uniqueKeysWithValues: cborsWithKeys.map(\.dpk))]
if let trustedReaderCertificates { parameters[InitializeKeys.trusted_certificates.rawValue] = trustedReaderCertificates }
parameters[InitializeKeys.device_auth_method.rawValue] = deviceAuthMethod.rawValue


let signedObj = Dictionary(uniqueKeysWithValues: cborsWithKeys.map(\.iss))
let privateKeyObj = Dictionary(uniqueKeysWithValues: cborsWithKeys.map(\.dpk))

return MDocPresentationState(input: .documentSignupIssuerSignedObj(parameters: signedObj, devicePrivateKeyObj: privateKeyObj),
trustedCertificates: trustedReaderCertificates ?? [],
deviceAuthMethod: deviceAuthMethod)
default:
fatalError("jwt format not implemented")
}
return parameters
}

/// Begin attestation presentation to a verifier
Expand All @@ -239,17 +246,23 @@ public final class EudiWallet: ObservableObject {
/// - Returns: A presentation session instance,
public func beginPresentation(flow: FlowType, docType: String? = nil, dataFormat: DataFormat = .cbor) -> PresentationSession {
do {
let parameters = try prepareServiceDataParameters(docType: docType, dataFormat: dataFormat)
let docIdAndTypes = storage.getDocIdsToTypes()
switch flow {
case .ble:
let bleSvc = try BlePresentationService(parameters: parameters)
let parameters = try prepareServiceDataParameters(docType: docType, dataFormat: dataFormat)
let docIdAndTypes = storage.getDocIdsToTypes()
let mdocGattServer = try MdocGattServer(presentationState: parameters)
let bleSvc = try BlePresentationService(mdocGattServer: mdocGattServer)
return PresentationSession(presentationService: bleSvc, docIdAndTypes: docIdAndTypes, userAuthenticationRequired: userAuthenticationRequired)
case .openid4vp(let qrCode):
let openIdSvc = try OpenId4VpService(parameters: parameters, qrCode: qrCode, openId4VpVerifierApiUri: self.verifierApiUri, openId4VpVerifierLegalName: self.verifierLegalName)
case .openid4vp(let qrCode): // TODO: URL
let parameters = try prepareServiceDataParameters(docType: docType, dataFormat: dataFormat)
let docIdAndTypes = storage.getDocIdsToTypes()
let openIdSvc = try OpenId4VpService(state: parameters, openid4VPlinkData: qrCode, openId4VpVerifierApiUri: self.verifierApiUri, openId4VpVerifierLegalName: self.verifierLegalName)
return PresentationSession(presentationService: openIdSvc, docIdAndTypes: docIdAndTypes, userAuthenticationRequired: userAuthenticationRequired)
default:
return PresentationSession(presentationService: FaultPresentationService(error: PresentationSession.makeError(str: "Use beginPresentation(service:)")), docIdAndTypes: docIdAndTypes, userAuthenticationRequired: false)
let docIdAndTypes = storage.getDocIdsToTypes()
return PresentationSession(presentationService: FaultPresentationService(error: PresentationSession.makeError(str: "Use beginPresentation(service:)")),
docIdAndTypes: docIdAndTypes,
userAuthenticationRequired: false)
}
} catch {
return PresentationSession(presentationService: FaultPresentationService(error: error), docIdAndTypes: [:], userAuthenticationRequired: false)
Expand Down
10 changes: 5 additions & 5 deletions Sources/EudiWalletKit/Services/BlePresentationService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ public class BlePresentationService : PresentationService {
var deviceEngagement: String?
var request: [String: Any]?
public var flow: FlowType { .ble }

public init(parameters: [String: Any]) throws {
bleServerTransfer = try MdocGattServer(parameters: parameters)
bleServerTransfer.delegate = self
}
public init(mdocGattServer: MdocGattServer) throws {
bleServerTransfer = mdocGattServer
bleServerTransfer.delegate = self
}

/// Generate device engagement QR code

Expand Down
49 changes: 12 additions & 37 deletions Sources/EudiWalletKit/Services/OpenId4VciService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import CryptoKit
import Security
import WalletStorage

public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContextProviding {
public class OpenId4VCIService: NSObject {
let issueReq: IssueRequest
let credentialIssuerURL: String
var privateKey: SecKey!
Expand All @@ -34,13 +34,19 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext
let logger: Logger
let config: OpenId4VCIConfig
let alg = JWSAlgorithm(.ES256)
let authorizationService: OpenId4VciUserAuthorizationService
static var metadataCache = [String: CredentialOffer]()

init(issueRequest: IssueRequest, credentialIssuerURL: String, clientId: String, callbackScheme: String) {
init(issueRequest: IssueRequest,
credentialIssuerURL: String,
clientId: String,
callbackScheme: String,
authorizationService: OpenId4VciUserAuthorizationService?) {
self.issueReq = issueRequest
self.credentialIssuerURL = credentialIssuerURL
logger = Logger(label: "OpenId4VCI")
config = .init(clientId: clientId, authFlowRedirectionURI: URL(string: callbackScheme)!)
self.authorizationService = authorizationService ?? OpenId4VciUserAuthorizationServiceDefault(config: config)
}

fileprivate func initSecurityKeys(_ useSecureEnclave: Bool) throws {
Expand Down Expand Up @@ -189,7 +195,7 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext
}
}

private func authorizeRequestWithAuthCodeUseCase(issuer: Issuer, offer: CredentialOffer) async throws -> AuthorizedRequest {
private func authorizeRequestWithAuthCodeUseCase(issuer: Issuer, offer: CredentialOffer) async throws -> AuthorizedRequest {
var pushedAuthorizationRequestEndpoint = ""
if case let .oidc(metaData) = offer.authorizationServerMetadata, let endpoint = metaData.pushedAuthorizationRequestEndpoint {
pushedAuthorizationRequestEndpoint = endpoint
Expand All @@ -202,8 +208,9 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext

if case let .success(request) = parPlaced, case let .par(parRequested) = request {
logger.info("--> [AUTHORIZATION] Placed PAR. Get authorization code URL is: \(parRequested.getAuthorizationCodeURL)")
let authorizationCode = try await loginUserAndGetAuthCode(
getAuthorizationCodeUrl: parRequested.getAuthorizationCodeURL.url) ?? { throw WalletError(description: "Could not retrieve authorization code") }()

let authorizationCode = try await authorizationService.getAuthorizationCode(requestURL: parRequested.getAuthorizationCodeURL.url) ?? { throw WalletError(description: "Could not retrieve authorization code") }()

logger.info("--> [AUTHORIZATION] Authorization code retrieved")
let unAuthorized = await issuer.handleAuthorizationCode(parRequested: request, authorizationCode: .authorizationCode(authorizationCode: authorizationCode))
switch unAuthorized {
Expand Down Expand Up @@ -299,38 +306,6 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext
throw WalletError(description: error.localizedDescription)
}
}

@MainActor
private func loginUserAndGetAuthCode(getAuthorizationCodeUrl: URL) async throws -> String? {
return try await withCheckedThrowingContinuation { c in
let authenticationSession = ASWebAuthenticationSession(url: getAuthorizationCodeUrl, callbackURLScheme: config.authFlowRedirectionURI.scheme!) { optionalUrl, optionalError in
guard optionalError == nil else { c.resume(throwing: OpenId4VCIError.authRequestFailed(optionalError!)); return }
guard let url = optionalUrl else { c.resume(throwing: OpenId4VCIError.authorizeResponseNoUrl); return }
guard let code = url.getQueryStringParameter("code") else { c.resume(throwing: OpenId4VCIError.authorizeResponseNoCode); return }
c.resume(returning: code)
}
authenticationSession.prefersEphemeralWebBrowserSession = true
authenticationSession.presentationContextProvider = self
authenticationSession.start()
}
}

public func presentationAnchor(for session: ASWebAuthenticationSession)
-> ASPresentationAnchor {
#if os(iOS)
let window = UIApplication.shared.windows.first { $0.isKeyWindow }
return window ?? ASPresentationAnchor()
#else
return ASPresentationAnchor()
#endif
}
}

fileprivate extension URL {
func getQueryStringParameter(_ parameter: String) -> String? {
guard let url = URLComponents(string: self.absoluteString) else { return nil }
return url.queryItems?.first(where: { $0.name == parameter })?.value
}
}

extension SecureEnclave.P256.KeyAgreement.PrivateKey {
Expand Down
Loading