From 1f1e3306cd4735a23dabeffbe358bb6617c9bf35 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Wed, 23 Oct 2024 05:52:50 -0700 Subject: [PATCH] add sd-jwt to credential pack (#42) This PR adds support for SD-JWT credentials in credential pack. --- Sources/MobileSdk/Credential.swift | 65 ++++++++++++----- Sources/MobileSdk/CredentialPack.swift | 98 +++++++++++++++----------- Sources/MobileSdk/Credentials.swift | 10 +-- project.yml | 2 +- 4 files changed, 109 insertions(+), 66 deletions(-) diff --git a/Sources/MobileSdk/Credential.swift b/Sources/MobileSdk/Credential.swift index 5c6027b..b669472 100644 --- a/Sources/MobileSdk/Credential.swift +++ b/Sources/MobileSdk/Credential.swift @@ -10,28 +10,28 @@ open class Credential: Identifiable { open func get(keys: [String]) -> [String: GenericJSON] { if keys.contains("id") { - return ["id": GenericJSON.string(self.id)] + return ["id": GenericJSON.string(id)] } else { return [:] } } } -extension Mdoc { +public extension Mdoc { /// Access all of the elements in the mdoc, ignoring namespaces and missing elements that cannot be encoded as JSON. - public func jsonEncodedDetails() -> [String: GenericJSON] { - self.jsonEncodedDetailsInternal(containing: nil) + func jsonEncodedDetails() -> [String: GenericJSON] { + jsonEncodedDetailsInternal(containing: nil) } /// Access the specified elements in the mdoc, ignoring namespaces and missing elements that cannot be encoded as /// JSON. - public func jsonEncodedDetails(containing elementIdentifiers: [String]) -> [String: GenericJSON] { - self.jsonEncodedDetailsInternal(containing: elementIdentifiers) + func jsonEncodedDetails(containing elementIdentifiers: [String]) -> [String: GenericJSON] { + jsonEncodedDetailsInternal(containing: elementIdentifiers) } private func jsonEncodedDetailsInternal(containing elementIdentifiers: [String]?) -> [String: GenericJSON] { // Ignore the namespaces. - Dictionary(uniqueKeysWithValues: self.details().flatMap { + Dictionary(uniqueKeysWithValues: details().flatMap { $1.compactMap { let id = $0.identifier @@ -55,10 +55,10 @@ extension Mdoc { } } -extension JwtVc { +public extension JwtVc { /// Access the W3C VCDM credential (not including the JWT envelope). - public func credentialClaims() -> [String: GenericJSON] { - if let data = self.credentialAsJsonEncodedUtf8String().data(using: .utf8) { + func credentialClaims() -> [String: GenericJSON] { + if let data = credentialAsJsonEncodedUtf8String().data(using: .utf8) { do { let json = try JSONDecoder().decode(GenericJSON.self, from: data) if let object = json.dictValue { @@ -75,17 +75,17 @@ extension JwtVc { } /// Access the specified claims from the W3C VCDM credential (not including the JWT envelope). - public func credentialClaims(containing claimNames: [String]) -> [String: GenericJSON] { - self.credentialClaims().filter { (key, _) in + func credentialClaims(containing claimNames: [String]) -> [String: GenericJSON] { + credentialClaims().filter { key, _ in claimNames.contains(key) } } } -extension JsonVc { +public extension JsonVc { /// Access the W3C VCDM credential - public func credentialClaims() -> [String: GenericJSON] { - if let data = self.credentialAsJsonEncodedUtf8String().data(using: .utf8) { + func credentialClaims() -> [String: GenericJSON] { + if let data = credentialAsJsonEncodedUtf8String().data(using: .utf8) { do { let json = try JSONDecoder().decode(GenericJSON.self, from: data) if let object = json.dictValue { @@ -102,8 +102,39 @@ extension JsonVc { } /// Access the specified claims from the W3C VCDM credential. - public func credentialClaims(containing claimNames: [String]) -> [String: GenericJSON] { - self.credentialClaims().filter { (key, _) in + func credentialClaims(containing claimNames: [String]) -> [String: GenericJSON] { + credentialClaims().filter { key, _ in + claimNames.contains(key) + } + } +} + +public extension Vcdm2SdJwt { + /// Access the SD-JWT decoded credential + func credentialClaims() -> [String: GenericJSON] { + do { + if let data = try revealedClaimsAsJsonString().data(using: .utf8) { + do { + let json = try JSONDecoder().decode(GenericJSON.self, from: data) + if let object = json.dictValue { + return object + } else { + print("unexpected format for SD-JWT") + } + } catch let error as NSError { + print("failed to decode as JSON: \(error)") + } + } + } catch let error as NSError { + print("failed to decode SD-JWT data from UTF-8: \(error)") + } + + return [:] + } + + /// Access the specified claims from the SD-JWT credential. + func credentialClaims(containing claimNames: [String]) -> [String: GenericJSON] { + credentialClaims().filter { key, _ in claimNames.contains(key) } } diff --git a/Sources/MobileSdk/CredentialPack.swift b/Sources/MobileSdk/CredentialPack.swift index fc7afb9..2fdb863 100644 --- a/Sources/MobileSdk/CredentialPack.swift +++ b/Sources/MobileSdk/CredentialPack.swift @@ -1,14 +1,13 @@ +import CryptoKit import Foundation import SpruceIDMobileSdkRs -import CryptoKit public class CredentialPack { - private var credentials: [ParsedCredential] /// Initialize an empty CredentialPack. public init() { - self.credentials = [] + credentials = [] } /// Initialize a CredentialPack from existing credentials. @@ -18,73 +17,86 @@ public class CredentialPack { /// Add a JwtVc to the CredentialPack. public func addJwtVc(jwtVc: JwtVc) -> [ParsedCredential] { - self.credentials.append(ParsedCredential.newJwtVcJson(jwtVc: jwtVc)) - return self.credentials + credentials.append(ParsedCredential.newJwtVcJson(jwtVc: jwtVc)) + return credentials } /// Add a JsonVc to the CredentialPack. public func addJsonVc(jsonVc: JsonVc) -> [ParsedCredential] { - self.credentials.append(ParsedCredential.newLdpVc(jsonVc: jsonVc)) - return self.credentials + credentials.append(ParsedCredential.newLdpVc(jsonVc: jsonVc)) + return credentials + } + + /// Add an SD-JWT to the CredentialPack. + public func addSdJwt(sdJwt: Vcdm2SdJwt) -> [ParsedCredential] { + credentials.append(ParsedCredential.newSdJwt(sdJwtVc: sdJwt)) + return credentials } /// Add an Mdoc to the CredentialPack. public func addMDoc(mdoc: Mdoc) -> [ParsedCredential] { - self.credentials.append(ParsedCredential.newMsoMdoc(mdoc: mdoc)) - return self.credentials + credentials.append(ParsedCredential.newMsoMdoc(mdoc: mdoc)) + return credentials } /// Find credential claims from all credentials in this CredentialPack. public func findCredentialClaims(claimNames: [String]) -> [Uuid: [String: GenericJSON]] { - Dictionary(uniqueKeysWithValues: self.list() - .map { credential in - var claims: [String: GenericJSON] - if let mdoc = credential.asMsoMdoc() { - if claimNames.isEmpty { - claims = mdoc.jsonEncodedDetails() + Dictionary( + uniqueKeysWithValues: list() + .map { credential in + var claims: [String: GenericJSON] + if let mdoc = credential.asMsoMdoc() { + if claimNames.isEmpty { + claims = mdoc.jsonEncodedDetails() + } else { + claims = mdoc.jsonEncodedDetails(containing: claimNames) + } + } else if let jwtVc = credential.asJwtVc() { + if claimNames.isEmpty { + claims = jwtVc.credentialClaims() + } else { + claims = jwtVc.credentialClaims(containing: claimNames) + } + } else if let jsonVc = credential.asJsonVc() { + if claimNames.isEmpty { + claims = jsonVc.credentialClaims() + } else { + claims = jsonVc.credentialClaims(containing: claimNames) + } + } else if let sdJwt = credential.asSdJwt() { + if claimNames.isEmpty { + claims = sdJwt.credentialClaims() + } else { + claims = sdJwt.credentialClaims(containing: claimNames) + } } else { - claims = mdoc.jsonEncodedDetails(containing: claimNames) - } - } else if let jwtVc = credential.asJwtVc() { - if claimNames.isEmpty { - claims = jwtVc.credentialClaims() - } else { - claims = jwtVc.credentialClaims(containing: claimNames) - } - } else if let jsonVc = credential.asJsonVc() { - if claimNames.isEmpty { - claims = jsonVc.credentialClaims() - } else { - claims = jsonVc.credentialClaims(containing: claimNames) - } - } else { - var type: String - do { - type = try credential.intoGenericForm().type - } catch { - type = "unknown" + var type: String + do { + type = try credential.intoGenericForm().type + } catch { + type = "unknown" + } + print("unsupported credential type: \(type)") + claims = [:] } - print("unsupported credential type: \(type)") - claims = [:] - } - return (credential.id(), claims) - }) + return (credential.id(), claims) + }) } /// Get credentials by id. public func get(credentialsIds: [Uuid]) -> [ParsedCredential] { - return self.credentials.filter { + return credentials.filter { credentialsIds.contains($0.id()) } } /// Get a credential by id. public func get(credentialId: Uuid) -> ParsedCredential? { - return self.credentials.first(where: { $0.id() == credentialId }) + return credentials.first(where: { $0.id() == credentialId }) } /// List all of the credentials in the CredentialPack. public func list() -> [ParsedCredential] { - return self.credentials + return credentials } } diff --git a/Sources/MobileSdk/Credentials.swift b/Sources/MobileSdk/Credentials.swift index e26da22..c8cea5b 100644 --- a/Sources/MobileSdk/Credentials.swift +++ b/Sources/MobileSdk/Credentials.swift @@ -8,17 +8,17 @@ public class CredentialStore { self.credentials = credentials } - public func presentMdocBLE(deviceEngagement: DeviceEngagement, + public func presentMdocBLE(deviceEngagement _: DeviceEngagement, callback: BLESessionStateDelegate, useL2CAP: Bool = true // , trustedReaders: TrustedReaders ) async -> IsoMdlPresentation? { - if let firstMdoc = self.credentials.first(where: { $0.asMsoMdoc() != nil }) { + if let firstMdoc = credentials.first(where: { $0.asMsoMdoc() != nil }) { let mdoc = firstMdoc.asMsoMdoc()! return await IsoMdlPresentation(mdoc: MDoc(Mdoc: mdoc), - engagement: DeviceEngagement.QRCode, - callback: callback, - useL2CAP: useL2CAP) + engagement: DeviceEngagement.QRCode, + callback: callback, + useL2CAP: useL2CAP) } else { return nil } diff --git a/project.yml b/project.yml index 9259880..d76a2f0 100644 --- a/project.yml +++ b/project.yml @@ -4,7 +4,7 @@ options: packages: SpruceIDMobileSdkRs: url: https://github.com/spruceid/mobile-sdk-rs - revision: "d559d750dbd5945eace315a05ce05b2702ed2865" + from: 0.2.1 SwiftAlgorithms: url: https://github.com/apple/swift-algorithms from: 1.2.0