Skip to content

Commit

Permalink
Merge pull request eu-digital-identity-wallet#73 from niscy-eudiw/pre…
Browse files Browse the repository at this point in the history
…authorized_flow_openid4vci

Preauthorized flow for openid4vci
  • Loading branch information
phisakel authored Jun 21, 2024
2 parents 4d80f55 + b222b51 commit 4e681e1
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 23 deletions.
15 changes: 6 additions & 9 deletions Sources/EudiWalletKit/EudiWallet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,9 @@ public final class EudiWallet: ObservableObject {

func finalizeIssuing(id: String, data: Data, docType: String?, format: DataFormat, issueReq: IssueRequest, openId4VCIService: OpenId4VCIService) async throws -> WalletStorage.Document {
let iss = IssuerSigned(data: [UInt8](data))
let deviceResponse = iss != nil ? nil : DeviceResponse(data: [UInt8](data))
guard let ddt = DocDataType(rawValue: format.rawValue) else { throw WalletError(description: "Invalid format \(format.rawValue)") }
let docTypeToSave = docType ?? (format == .cbor ? iss?.issuerAuth.mso.docType ?? deviceResponse?.documents?.first?.docType : nil)
let docTypeToSave = docType ?? (format == .cbor ? iss?.issuerAuth.mso.docType : nil)
var dataToSave: Data? = data
if let deviceResponse {
if let iss = deviceResponse.documents?.first?.issuerSigned { dataToSave = Data(iss.encode(options: CBOROptions())) } else { dataToSave = nil }
}
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
Expand All @@ -127,8 +123,8 @@ public final class EudiWallet: ObservableObject {
/// - uriOffer: url with offer
/// - 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] {
/// - Returns: Offered issue information model
public func resolveOfferUrlDocTypes(uriOffer: String, format: DataFormat = .cbor, useSecureEnclave: Bool = true) async throws -> OfferedIssueModel {
let (_, openId4VCIService, _) = try await prepareIssuing(docType: nil)
return try await openId4VCIService.resolveOfferDocTypes(uriOffer: uriOffer, format: format)
}
Expand All @@ -137,15 +133,16 @@ public final class EudiWallet: ObservableObject {
/// - Parameters:
/// - offerUri: url with offer
/// - docTypes: doc types to be issued
/// - txCodeValue: Transaction code given to user
/// - format: data format
/// - promptMessage: prompt message for biometric authentication (optional)
/// - 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], txCodeValue: String? = nil, format: DataFormat = .cbor, promptMessage: String? = nil, useSecureEnclave: Bool = true, claimSet: ClaimSet? = nil) 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)
let docsData = try await openId4VCIService.issueDocumentsByOfferUrl(offerUri: offerUri, docTypes: docTypes, format: format, useSecureEnclave: useSecureEnclave, claimSet: claimSet)
let docsData = try await openId4VCIService.issueDocumentsByOfferUrl(offerUri: offerUri, docTypes: docTypes, txCodeValue: txCodeValue, 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) }
Expand Down
24 changes: 15 additions & 9 deletions Sources/EudiWalletKit/Services/OpenId4VciService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext
}
publicKey = try KeyController.generateECDHPublicKey(from: privateKey)
let publicKeyJWK = try ECPublicKey(publicKey: publicKey,additionalParameters: ["alg": alg.name, "use": "sig", "kid": UUID().uuidString])
bindingKey = .jwk(algorithm: alg, jwk: publicKeyJWK, privateKey: privateKey)
bindingKey = .jwk(algorithm: alg, jwk: publicKeyJWK, privateKey: privateKey, issuer: config.clientId)
}

/// Issue a document with the given `docType` using OpenId4Vci protocol
Expand All @@ -75,30 +75,36 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext
/// - uriOffer: Uri of the offer (from a QR or a deep link)
/// - format: format of the exchanged data
/// - Returns: The data of the document
public func resolveOfferDocTypes(uriOffer: String, format: DataFormat = .cbor) async throws -> [OfferedDocModel] {
public func resolveOfferDocTypes(uriOffer: String, format: DataFormat = .cbor) async throws -> OfferedIssueModel {
let result = await CredentialOfferRequestResolver().resolve(source: try .init(urlString: uriOffer))
switch result {
case .success(let offer):
let code: Grants.PreAuthorizedCode? = switch offer.grants { case .preAuthorizedCode(let preAuthorizedCode): preAuthorizedCode; case .both(_, let preAuthorizedCode): preAuthorizedCode; case .authorizationCode(_), .none: nil }
Self.metadataCache[uriOffer] = offer
let credentialInfo = try getCredentialIdentifiers(issuerName: offer.credentialIssuerIdentifier.url.absoluteString, credentialsSupported: offer.credentialIssuerMetadata.credentialsSupported.filter { offer.credentialConfigurationIdentifiers.contains($0.key) }, format: format)
return credentialInfo.map(\.offered)
let credentialInfo = try getCredentialIdentifiers(credentialsSupported: offer.credentialIssuerMetadata.credentialsSupported.filter { offer.credentialConfigurationIdentifiers.contains($0.key) }, format: format)
return OfferedIssueModel(issuerName: offer.credentialIssuerIdentifier.url.absoluteString, docModels: credentialInfo.map(\.offered), txCodeSpec: code?.txCode)
case .failure(let error):
throw WalletError(description: "Unable to resolve credential offer: \(error.localizedDescription)")
}
}

public func issueDocumentsByOfferUrl(offerUri: String, docTypes: [OfferedDocModel], format: DataFormat, useSecureEnclave: Bool = true, claimSet: ClaimSet? = nil) async throws -> [Data] {
public func issueDocumentsByOfferUrl(offerUri: String, docTypes: [OfferedDocModel], txCodeValue: String?, format: DataFormat, useSecureEnclave: Bool = true, claimSet: ClaimSet? = nil) async throws -> [Data] {
guard format == .cbor else { throw fatalError("jwt format not implemented") }
try initSecurityKeys(useSecureEnclave)
guard let offer = Self.metadataCache[offerUri] else { throw WalletError(description: "offerUri not resolved. resolveOfferDocTypes must be called first")}
let credentialInfo = docTypes.compactMap { try? getCredentialIdentifier(credentialsSupported: offer.credentialIssuerMetadata.credentialsSupported, docType: $0.docType, format: format)
}
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 Issuer(authorizationServerMetadata: offer.authorizationServerMetadata, issuerMetadata: offer.credentialIssuerMetadata, config: config)
let authorized = try await authorizeRequestWithAuthCodeUseCase(issuer: issuer, offer: offer)
let data = try await credentialInfo.asyncCompactMap {
if preAuthorizedCode != nil && txCodeSpec != nil && txCodeValue == nil { throw WalletError(description: "A transaction code is required for this offer") }
let authorized = if let preAuthorizedCode, let txCodeValue, let authCode = try? IssuanceAuthorization(preAuthorizationCode: preAuthorizedCode, txCode: txCodeSpec) { try await issuer.authorizeWithPreAuthorizationCode(credentialOffer: offer, authorizationCode: authCode, clientId: config.clientId, transactionCode: txCodeValue).get() } else { try await authorizeRequestWithAuthCodeUseCase(issuer: issuer, offer: offer) }
let data = await credentialInfo.asyncCompactMap {
do {
logger.info("Starting issuing with identifer \($0.identifier.value) and scope \($0.scope)")
let str = try await issueOfferedCredentialWithProof(authorized, offer: offer, issuer: issuer, credentialConfigurationIdentifier: $0.identifier, claimSet: claimSet)
logger.info("Credential str:\n\(str)")
return Data(base64URLEncoded: str)
} catch {
logger.error("Failed to issue document with scope \($0.scope)")
Expand Down Expand Up @@ -176,11 +182,11 @@ public class OpenId4VCIService: NSObject, ASWebAuthenticationPresentationContext
}
}

func getCredentialIdentifiers(issuerName: String, credentialsSupported: [CredentialConfigurationIdentifier: CredentialSupported], format: DataFormat) throws -> [(identifier: CredentialConfigurationIdentifier, scope: String, offered: OfferedDocModel)] {
func getCredentialIdentifiers(credentialsSupported: [CredentialConfigurationIdentifier: CredentialSupported], format: DataFormat) throws -> [(identifier: CredentialConfigurationIdentifier, scope: String, offered: OfferedDocModel)] {
switch format {
case .cbor:
let credentialInfos = credentialsSupported.compactMap {
if case .msoMdoc(let msoMdocCred) = $0.value, let scope = msoMdocCred.scope, case let offered = OfferedDocModel(issuerName: issuerName, docType: msoMdocCred.docType, displayName: msoMdocCred.display.getName() ?? msoMdocCred.docType) { (identifier: $0.key, scope: scope, offered: offered) } else { nil } }
if case .msoMdoc(let msoMdocCred) = $0.value, let scope = msoMdocCred.scope, case let offered = OfferedDocModel(docType: msoMdocCred.docType, displayName: msoMdocCred.display.getName() ?? msoMdocCred.docType) { (identifier: $0.key, scope: scope,offered: offered) } else { nil } }
return credentialInfos
default:
throw WalletError(description: "Format \(format) not yet supported")
Expand Down
10 changes: 6 additions & 4 deletions Sources/EudiWalletKit/Services/StorageManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,13 @@ public class StorageManager: ObservableObject {

func toModel(doc: WalletStorage.Document) -> (any MdocDecodable)? {
guard let (iss, dpk) = doc.getCborData() else { return nil }
return switch doc.docType {
case EuPidModel.euPidDocType: EuPidModel(id: iss.0, createdAt: doc.createdAt, issuerSigned: iss.1, devicePrivateKey: dpk.1)!
case IsoMdlModel.isoDocType: IsoMdlModel(id: iss.0, createdAt: doc.createdAt, issuerSigned: iss.1, devicePrivateKey: dpk.1)!
default: GenericMdocModel(id: iss.0, createdAt: doc.createdAt, issuerSigned: iss.1, devicePrivateKey: dpk.1, docType: doc.docType, title: doc.docType.translated())
var retModel: (any MdocDecodable)? = switch doc.docType {
case EuPidModel.euPidDocType: EuPidModel(id: iss.0, createdAt: doc.createdAt, issuerSigned: iss.1, devicePrivateKey: dpk.1)
case IsoMdlModel.isoDocType: IsoMdlModel(id: iss.0, createdAt: doc.createdAt, issuerSigned: iss.1, devicePrivateKey: dpk.1)
default: nil
}
retModel = retModel ?? GenericMdocModel(id: iss.0, createdAt: doc.createdAt, issuerSigned: iss.1, devicePrivateKey: dpk.1, docType: doc.docType, title: doc.docType.translated())
return retModel
}

public func getDocIdsToTypes() -> [String: String] {
Expand Down
19 changes: 18 additions & 1 deletion Sources/EudiWalletKit/ViewModels/OfferedDocModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,26 @@ limitations under the License.
*/

import Foundation
import OpenID4VCI

public struct OfferedDocModel {
/// Offered issue model contains information gathered by resolving an issue offer URL.
///
/// This information is returned from ``EudiWallet/resolveOfferUrlDocTypes(uriOffer:format:useSecureEnclave:)``
public struct OfferedIssueModel {
/// Issuer name (currently the URL)
public let issuerName: String
/// Document types included in the offer
public let docModels: [OfferedDocModel]
/// Transaction code specification (in case of preauthorized flow)
public let txCodeSpec: TxCode?
/// Helper var for transaction code requirement
public var isTxCodeRequired: Bool { txCodeSpec != nil }
}

/// Information about an offered document type
public struct OfferedDocModel {
/// Document type
public let docType: String
/// Display name for document type
public let displayName: String
}
41 changes: 41 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,44 @@
## v0.5.2
### Support Pre-Authorized Code Flow

The flow is supported by existing methods:

1 - An issue offer url is scanned. The following method is called: `public func resolveOfferUrlDocTypes(uriOffer: String, format: DataFormat = .cbor, useSecureEnclave: Bool = true) async throws -> OfferedIssueModel`
### (Breaking change, the return value type is `OfferedIssueModel` instead of `[OfferedDocModel]`)

2 - If `OfferedIssueModel.isTxCodeRequired` is true, the call to `issueDocumentsByOfferUrl` must include the transaction code (parameter `txCodeValue`).

- Note: for the clientId value the `EudiWallet/openID4VciClientId` is used.

## v0.5.1
### Update eudi-lib-ios-openid4vci-swift dependency to version 0.1.5

- Update eudi-lib-ios-openid4vci-swift dependency to version 0.1.5
- Fixes iOS16 offer url parsing issue

## v0.5.0
- `EuPidModel` updated with new PID docType

## v0.4.9
### Openid4VP fixes and updates

- Update eudi-lib-ios-siop-openid4vp-swift to version 0.1.1
- Fix openid4vp certificate chain verification (PresentationSession's `readerCertIssuerValid` and `readerCertIssuer` properties)
- Add `readerLegalName` property to PresentationSession

## v0.4.8
- Update eudi-lib-ios-siop-openid4vp-swift to version 0.1.0
- Added wallet configuration parameter `public var verifierLegalName: String?` (used for Openid4VP preregistered clients)

## v0.4.7
###Update eudi-lib-ios-siop-openid4vp-swift to version 0.1.0

## v0.4.6
### Update openid4vci to version 0.1.2

##v0.4.5
### Update eudi-lib-ios-openid4vci-swift to version 0.0.9

## v0.4.4
### Breaking change - mdocModels contains not-nil items (SwiftUI breaks with nil items)
@Published public var mdocModels: [any MdocDecodable] = []
Expand Down

0 comments on commit 4e681e1

Please sign in to comment.