From 47e532886108f0bcf25d1fb294398923f60e5b19 Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Wed, 9 Oct 2024 10:06:48 +0300 Subject: [PATCH 01/14] [fix] added claims set to signed sd-jwt --- Sources/Issuer/SDJWT.swift | 17 ++++++++++++++--- Tests/Issuance/SignedJwtTest.swift | 3 +-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Sources/Issuer/SDJWT.swift b/Sources/Issuer/SDJWT.swift index 5b454c2..53be930 100644 --- a/Sources/Issuer/SDJWT.swift +++ b/Sources/Issuer/SDJWT.swift @@ -74,6 +74,7 @@ public struct SignedSDJWT { public let jwt: JWS public internal(set) var disclosures: [Disclosure] public internal(set) var kbJwt: JWS? + public internal(set) var claimSet: JSON var delineatedCompactSerialisation: String { let separator = "~" @@ -94,6 +95,7 @@ public struct SignedSDJWT { self.jwt = try JWS(jwsString: serializedJwt) self.disclosures = disclosures self.kbJwt = try? JWS(jwsString: serializedKbJwt ?? "") + self.claimSet = try jwt.payloadJSON() } init?(json: JSON) throws { @@ -101,20 +103,28 @@ public struct SignedSDJWT { self.jwt = triple.jwt self.disclosures = triple.disclosures self.kbJwt = triple.kbJwt + self.claimSet = try jwt.payloadJSON() } - private init?(sdJwt: SDJWT, issuersPrivateKey: KeyType) { + private init?( + sdJwt: SDJWT, + issuersPrivateKey: KeyType + ) throws { // Create a Signed SDJWT with no key binding guard let signedJwt = try? SignedSDJWT.createSignedJWT(key: issuersPrivateKey, jwt: sdJwt.jwt) else { return nil } - self.jwt = signedJwt self.disclosures = sdJwt.disclosures self.kbJwt = nil + self.claimSet = try jwt.payloadJSON() } - private init?(signedSDJWT: SignedSDJWT, kbJWT: JWT, holdersPrivateKey: KeyType) { + private init?( + signedSDJWT: SignedSDJWT, + kbJWT: JWT, + holdersPrivateKey: KeyType + ) throws { // Assume that we have a valid signed jwt from the issuer // And key exchange has been established // signed SDJWT might contain or not the cnf claim @@ -123,6 +133,7 @@ public struct SignedSDJWT { self.disclosures = signedSDJWT.disclosures let signedKBJwt = try? SignedSDJWT.createSignedJWT(key: holdersPrivateKey, jwt: kbJWT) self.kbJwt = signedKBJwt + self.claimSet = try jwt.payloadJSON() } // MARK: - Methods diff --git a/Tests/Issuance/SignedJwtTest.swift b/Tests/Issuance/SignedJwtTest.swift index 8cd0a30..3494935 100644 --- a/Tests/Issuance/SignedJwtTest.swift +++ b/Tests/Issuance/SignedJwtTest.swift @@ -48,7 +48,7 @@ final class SignedJwtTest: XCTestCase { CompactSerialiser(signedSDJWT: jwt) } - let verifier = try SDJWTVerifier( + _ = try SDJWTVerifier( serialisedString: serialised ).verifyIssuance { jws in try SignatureVerifier(signedJWT: jws, publicKey: keyPair.public) @@ -56,5 +56,4 @@ final class SignedJwtTest: XCTestCase { ClaimsVerifier() } } - } From 5815cdf1cbe2819afe23169f3f5b88ba43f24645 Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Wed, 9 Oct 2024 10:30:54 +0300 Subject: [PATCH 02/14] [fix] added json pointer to project --- Sources/Utilities/JSONPointer.swift | 64 +++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 Sources/Utilities/JSONPointer.swift diff --git a/Sources/Utilities/JSONPointer.swift b/Sources/Utilities/JSONPointer.swift new file mode 100644 index 0000000..9149949 --- /dev/null +++ b/Sources/Utilities/JSONPointer.swift @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2023 European Commission + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import SwiftyJSON + +/// A struct that implements JSON Pointer (RFC 6901) to navigate and extract values from JSON documents. +/// JSON Pointer defines a string syntax for identifying a specific value within a JSON document. +/// +/// More details about JSON Pointer can be found in the RFC specification: https://datatracker.ietf.org/doc/html/rfc6901 +public struct JSONPointer { + + /// The pointer string that represents the path in the JSON document. + public let pointer: String + + /// Initializes a `JSONPointer` instance with a pointer string. + /// + /// - Parameter pointer: A JSON Pointer string (must start with a `/`). + public init(pointer: String) { + self.pointer = pointer + } + + /// Applies the JSON Pointer to a given JSON object to retrieve the value at the specified path. + /// + /// - Parameter json: The `JSON` object to traverse. + /// - Returns: The `JSON` value found at the specified path, or `nil` if the path is invalid. + public func evaluate(on json: JSON) -> JSON? { + // Split the pointer into path components, ignoring the first empty token (because of leading `/`) + let components = pointer.split(separator: "/").map { component -> String in + // Unescape tokens according to RFC 6901 + return component.replacingOccurrences(of: "~1", with: "/").replacingOccurrences(of: "~0", with: "~") + } + + var currentJSON = json + + // Traverse through the components + for component in components { + // If the current part is an array index, convert it to Int + if let index = Int(component), currentJSON.type == .array { + currentJSON = currentJSON[index] + } else { + currentJSON = currentJSON[component] + } + + // If we encounter an invalid path, return nil + if currentJSON == JSON.null { + return nil + } + } + + return currentJSON + } +} From 6fb470b84361e6d133c013349d4b4785f00c38d2 Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Thu, 10 Oct 2024 10:31:43 +0300 Subject: [PATCH 03/14] [wip] holder presentation --- Sources/Claim/ClaimsExtractor.swift | 34 ++- Sources/Factory/SDJWTFactory.swift | 4 +- Sources/Issuer/SDJWT.swift | 173 +----------- Sources/Issuer/SignedSDJWT.swift | 253 ++++++++++++++++++ Sources/Types.swift | 2 +- .../SignatureAlgorithm+Extension.swift | 2 +- Sources/Utilities/JSONPointer.swift | 2 +- Tests/Presentation/PresentationTest.swift | 108 ++++++++ 8 files changed, 408 insertions(+), 170 deletions(-) create mode 100644 Sources/Issuer/SignedSDJWT.swift create mode 100644 Tests/Presentation/PresentationTest.swift diff --git a/Sources/Claim/ClaimsExtractor.swift b/Sources/Claim/ClaimsExtractor.swift index 3ec440d..8e90042 100644 --- a/Sources/Claim/ClaimsExtractor.swift +++ b/Sources/Claim/ClaimsExtractor.swift @@ -31,7 +31,12 @@ public class ClaimExtractor { // MARK: - Methods - public func findDigests(payload json: JSON, disclosures: [Disclosure]) throws -> ClaimExtractorResult { + public func findDigests( + payload json: JSON, + disclosures: [Disclosure], + visitor: Visitor? = nil, + currentPath: [String] = [] + ) throws -> ClaimExtractorResult { var json = json json.dictionaryObject?.removeValue(forKey: Keys.sdAlg.rawValue) var foundDigests: [DigestType] = [] @@ -51,6 +56,12 @@ public class ClaimExtractor { } json[foundDisclosure.key] = foundDisclosure.value + + if let d = digestsOfDisclosuresDict[foundDigest] { + let currentJsonPointer = "/" + (currentPath + [foundDisclosure.key]).joined(separator: "/") + // visitor?.call(key: foundDisclosure.key, disclosure: foundDisclosure.value.stringValue + " " + foundDigest + " " + d + " " + currentJsonPointer) + visitor?.call(key: currentJsonPointer, disclosure: d) + } foundDigests.append(.object(foundDigest)) } else { @@ -58,28 +69,42 @@ public class ClaimExtractor { break } } - } // Loop through the inner JSON data for (key, subJson): (String, JSON) in json { if !subJson.dictionaryValue.isEmpty { - let foundOnSubJSON = try self.findDigests(payload: subJson, disclosures: disclosures) + let newPath = currentPath + [key] // Update the path + let foundOnSubJSON = try self.findDigests( + payload: subJson, + disclosures: disclosures, + visitor: visitor, + currentPath: newPath // Pass the updated path + ) + // if found swap the disclosed value with the found value foundDigests += foundOnSubJSON.digestsFoundOnPayload json[key] = foundOnSubJSON.recreatedClaims } else if !subJson.arrayValue.isEmpty { for (index, object) in subJson.arrayValue.enumerated() { + let newPath = currentPath + [key, "\(index)"] // Update the path for array elements if object[Keys.dots.rawValue].exists() { if let foundDisclosedArrayElement = digestsOfDisclosuresDict[object[Keys.dots].stringValue]? .base64URLDecode()? .arrayProperty { foundDigests.appendOptional(.array(object[Keys.dots].stringValue)) + // If the object is a json we should further process it and replace // the element with the value found in the disclosure // Example https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-05.html#name-example-3-complex-structure - if let ifHasNested = try? findDigests(payload: foundDisclosedArrayElement, disclosures: disclosures), + if let ifHasNested = try? findDigests( + payload: foundDisclosedArrayElement, + disclosures: disclosures, + visitor: visitor, + currentPath: newPath // Pass the updated path for the nested JSON + + ), !ifHasNested.digestsFoundOnPayload.isEmpty { foundDigests += ifHasNested.digestsFoundOnPayload json[key].arrayObject?[index] = ifHasNested.recreatedClaims @@ -89,7 +114,6 @@ public class ClaimExtractor { } } } - return (foundDigests, json) } } diff --git a/Sources/Factory/SDJWTFactory.swift b/Sources/Factory/SDJWTFactory.swift index b2d5a14..b2bba3e 100644 --- a/Sources/Factory/SDJWTFactory.swift +++ b/Sources/Factory/SDJWTFactory.swift @@ -85,7 +85,9 @@ class SDJWTFactory { private func encodeObject(sdJwtObject: [String: SdElement]?) throws -> ClaimSet { // Check if the input object is of correct format guard let sdJwtObject else { - throw SDJWTError.nonObjectFormat(ofElement: sdJwtObject) + throw SDJWTError.nonObjectFormat( + ofElement: try sdJwtObject?.toJSONString() ?? "N/A" + ) } // Initialize arrays to store disclosures and JSON output diff --git a/Sources/Issuer/SDJWT.swift b/Sources/Issuer/SDJWT.swift index 53be930..96def1e 100644 --- a/Sources/Issuer/SDJWT.swift +++ b/Sources/Issuer/SDJWT.swift @@ -21,7 +21,7 @@ import SwiftyJSON public typealias KBJWT = JWT -struct SDJWT { +public struct SDJWT { // MARK: - Properties @@ -31,7 +31,11 @@ struct SDJWT { // MARK: - Lifecycle - init(jwt: JWT, disclosures: [Disclosure], kbJWT: KBJWT?) throws { + init( + jwt: JWT, + disclosures: [Disclosure], + kbJWT: KBJWT? = nil + ) throws { self.jwt = jwt self.disclosures = disclosures self.kbJwt = kbJWT @@ -62,166 +66,13 @@ struct SDJWT { } } - return try ClaimExtractor(digestsOfDisclosuresDict: digestsOfDisclosuresDict) - .findDigests(payload: jwt.payload, disclosures: disclosures) - } -} - -public struct SignedSDJWT { - - // MARK: - Properties - - public let jwt: JWS - public internal(set) var disclosures: [Disclosure] - public internal(set) var kbJwt: JWS? - public internal(set) var claimSet: JSON - - var delineatedCompactSerialisation: String { - let separator = "~" - let input = ([jwt.compactSerialization] + disclosures).reduce("") { $0.isEmpty ? $1 : $0 + separator + $1 } + separator - return DigestCreator() - .hashAndBase64Encode( - input: input - ) ?? "" - } - - // MARK: - Lifecycle - - init( - serializedJwt: String, - disclosures: [Disclosure], - serializedKbJwt: String? - ) throws { - self.jwt = try JWS(jwsString: serializedJwt) - self.disclosures = disclosures - self.kbJwt = try? JWS(jwsString: serializedKbJwt ?? "") - self.claimSet = try jwt.payloadJSON() - } - - init?(json: JSON) throws { - let triple = try JwsJsonSupport.parseJWSJson(unverifiedSdJwt: json) - self.jwt = triple.jwt - self.disclosures = triple.disclosures - self.kbJwt = triple.kbJwt - self.claimSet = try jwt.payloadJSON() - } - - private init?( - sdJwt: SDJWT, - issuersPrivateKey: KeyType - ) throws { - // Create a Signed SDJWT with no key binding - guard let signedJwt = try? SignedSDJWT.createSignedJWT(key: issuersPrivateKey, jwt: sdJwt.jwt) else { - return nil - } - self.jwt = signedJwt - self.disclosures = sdJwt.disclosures - self.kbJwt = nil - self.claimSet = try jwt.payloadJSON() - } - - private init?( - signedSDJWT: SignedSDJWT, - kbJWT: JWT, - holdersPrivateKey: KeyType - ) throws { - // Assume that we have a valid signed jwt from the issuer - // And key exchange has been established - // signed SDJWT might contain or not the cnf claim - - self.jwt = signedSDJWT.jwt - self.disclosures = signedSDJWT.disclosures - let signedKBJwt = try? SignedSDJWT.createSignedJWT(key: holdersPrivateKey, jwt: kbJWT) - self.kbJwt = signedKBJwt - self.claimSet = try jwt.payloadJSON() - } - - // MARK: - Methods - - // expose static func initializers to distinguish between 2 cases of - // signed SDJWT creation - - static func nonKeyBondedSDJWT(sdJwt: SDJWT, issuersPrivateKey: KeyType) throws -> SignedSDJWT { - try .init(sdJwt: sdJwt, issuersPrivateKey: issuersPrivateKey) ?? { - throw SDJWTVerifierError.invalidJwt - }() - } - - static func keyBondedSDJWT(signedSDJWT: SignedSDJWT, kbJWT: JWT, holdersPrivateKey: KeyType) throws -> SignedSDJWT { - try .init(signedSDJWT: signedSDJWT, kbJWT: kbJWT, holdersPrivateKey: holdersPrivateKey) ?? { - throw SDJWTVerifierError.invalidJwt - }() - } - - private static func createSignedJWT(key: KeyType, jwt: JWT) throws -> JWS { - try jwt.sign(key: key) - } - - func disclosuresToPresent(disclosures: [Disclosure]) -> Self { - var updated = self - updated.disclosures = disclosures - return updated - } - - func toSDJWT() throws -> SDJWT { - if let kbJwtHeader = kbJwt?.protectedHeader, - let kbJWtPayload = try? kbJwt?.payloadJSON() { - return try SDJWT( - jwt: JWT(header: jwt.protectedHeader, payload: jwt.payloadJSON()), - disclosures: disclosures, - kbJWT: JWT(header: kbJwtHeader, kbJwtPayload: kbJWtPayload)) - } - - return try SDJWT( - jwt: JWT(header: jwt.protectedHeader, payload: jwt.payloadJSON()), + let visitor = Visitor() + return try ClaimExtractor( + digestsOfDisclosuresDict: digestsOfDisclosuresDict + ).findDigests( + payload: jwt.payload, disclosures: disclosures, - kbJWT: nil) - } - - func extractHoldersPublicKey() throws -> JWK { - let payloadJson = try self.jwt.payloadJSON() - let jwk = payloadJson[Keys.cnf]["jwk"] - - guard jwk.exists() else { - throw SDJWTVerifierError.keyBindingFailed(description: "Failled to find holders public key") - } - - guard let jwkObject = try? JSONDecoder.jwt.decode(JWK.self, from: jwk.rawData()) else { - throw SDJWTVerifierError.keyBindingFailed(description: "failled to extract key type") - } - - return jwkObject - } -} - -extension SignedSDJWT { - func serialised(serialiser: (SignedSDJWT) -> (SerialiserProtocol)) throws -> Data { - serialiser(self).data - } - - func serialised(serialiser: (SignedSDJWT) -> (SerialiserProtocol)) throws -> String { - serialiser(self).serialised - } -} - -public extension SignedSDJWT { - func recreateClaims() throws -> ClaimExtractorResult { - return try self.toSDJWT().recreateClaims() - } - - - func asJwsJsonObject( - option: JwsJsonSupportOption = .flattened, - kbJwt: JWTString?, - getParts: (JWTString) throws -> (String, String, String) - ) throws -> JSON { - let (protected, payload, signature) = try getParts(jwt.compactSerialization) - return option.buildJwsJson( - protected: protected, - payload: payload, - signature: signature, - disclosures: Set(disclosures), - kbJwt: kbJwt + visitor: visitor ) } } diff --git a/Sources/Issuer/SignedSDJWT.swift b/Sources/Issuer/SignedSDJWT.swift new file mode 100644 index 0000000..9b6486f --- /dev/null +++ b/Sources/Issuer/SignedSDJWT.swift @@ -0,0 +1,253 @@ +/* + * Copyright (c) 2023 European Commission + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import Foundation +import SwiftyJSON +import JSONWebSignature +import JSONWebKey + +typealias DisclosuresPerClaim = Dictionary + +public struct SignedSDJWT { + + // MARK: - Properties + + public let jwt: JWS + public internal(set) var disclosures: [Disclosure] + public internal(set) var kbJwt: JWS? + public internal(set) var claimSet: JSON + + var delineatedCompactSerialisation: String { + let separator = "~" + let input = ([jwt.compactSerialization] + disclosures).reduce("") { $0.isEmpty ? $1 : $0 + separator + $1 } + separator + return DigestCreator() + .hashAndBase64Encode( + input: input + ) ?? "" + } + + // MARK: - Lifecycle + + init( + serializedJwt: String, + disclosures: [Disclosure], + serializedKbJwt: String? = nil + ) throws { + self.jwt = try JWS(jwsString: serializedJwt) + self.disclosures = disclosures + self.kbJwt = try? JWS(jwsString: serializedKbJwt ?? "") + self.claimSet = try jwt.payloadJSON() + } + + init?(json: JSON) throws { + let triple = try JwsJsonSupport.parseJWSJson(unverifiedSdJwt: json) + self.jwt = triple.jwt + self.disclosures = triple.disclosures + self.kbJwt = triple.kbJwt + self.claimSet = try jwt.payloadJSON() + } + + private init?( + sdJwt: SDJWT, + issuersPrivateKey: KeyType + ) throws { + // Create a Signed SDJWT with no key binding + guard let signedJwt = try? SignedSDJWT.createSignedJWT(key: issuersPrivateKey, jwt: sdJwt.jwt) else { + return nil + } + self.jwt = signedJwt + self.disclosures = sdJwt.disclosures + self.kbJwt = nil + self.claimSet = try jwt.payloadJSON() + } + + private init?( + signedSDJWT: SignedSDJWT, + kbJWT: JWT, + holdersPrivateKey: KeyType + ) throws { + // Assume that we have a valid signed jwt from the issuer + // And key exchange has been established + // signed SDJWT might contain or not the cnf claim + + self.jwt = signedSDJWT.jwt + self.disclosures = signedSDJWT.disclosures + let signedKBJwt = try? SignedSDJWT.createSignedJWT(key: holdersPrivateKey, jwt: kbJWT) + self.kbJwt = signedKBJwt + self.claimSet = try jwt.payloadJSON() + } + + // MARK: - Methods + + // expose static func initializers to distinguish between 2 cases of + // signed SDJWT creation + + static func nonKeyBondedSDJWT(sdJwt: SDJWT, issuersPrivateKey: KeyType) throws -> SignedSDJWT { + try .init(sdJwt: sdJwt, issuersPrivateKey: issuersPrivateKey) ?? { + throw SDJWTVerifierError.invalidJwt + }() + } + + static func keyBondedSDJWT(signedSDJWT: SignedSDJWT, kbJWT: JWT, holdersPrivateKey: KeyType) throws -> SignedSDJWT { + try .init(signedSDJWT: signedSDJWT, kbJWT: kbJWT, holdersPrivateKey: holdersPrivateKey) ?? { + throw SDJWTVerifierError.invalidJwt + }() + } + + private static func createSignedJWT(key: KeyType, jwt: JWT) throws -> JWS { + try jwt.sign(key: key) + } + + func disclosuresToPresent(disclosures: [Disclosure]) -> Self { + var updated = self + updated.disclosures = disclosures + return updated + } + + func toSDJWT() throws -> SDJWT { + if let kbJwtHeader = kbJwt?.protectedHeader, + let kbJWtPayload = try? kbJwt?.payloadJSON() { + return try SDJWT( + jwt: JWT(header: jwt.protectedHeader, payload: jwt.payloadJSON()), + disclosures: disclosures, + kbJWT: JWT(header: kbJwtHeader, kbJwtPayload: kbJWtPayload)) + } + + return try SDJWT( + jwt: JWT(header: jwt.protectedHeader, payload: jwt.payloadJSON()), + disclosures: disclosures, + kbJWT: nil) + } + + func extractHoldersPublicKey() throws -> JWK { + let payloadJson = try self.jwt.payloadJSON() + let jwk = payloadJson[Keys.cnf]["jwk"] + + guard jwk.exists() else { + throw SDJWTVerifierError.keyBindingFailed(description: "Failled to find holders public key") + } + + guard let jwkObject = try? JSONDecoder.jwt.decode(JWK.self, from: jwk.rawData()) else { + throw SDJWTVerifierError.keyBindingFailed(description: "failled to extract key type") + } + + return jwkObject + } +} + +public extension SignedSDJWT { + + func serialised(serialiser: (SignedSDJWT) -> (SerialiserProtocol)) throws -> Data { + serialiser(self).data + } + + func serialised(serialiser: (SignedSDJWT) -> (SerialiserProtocol)) throws -> String { + serialiser(self).serialised + } + + func recreateClaims() throws -> ClaimExtractorResult { + return try self.toSDJWT().recreateClaims() + } + + func asJwsJsonObject( + option: JwsJsonSupportOption = .flattened, + kbJwt: JWTString?, + getParts: (JWTString) throws -> (String, String, String) + ) throws -> JSON { + let (protected, payload, signature) = try getParts(jwt.compactSerialization) + return option.buildJwsJson( + protected: protected, + payload: payload, + signature: signature, + disclosures: Set(disclosures), + kbJwt: kbJwt + ) + } + + func present(query: Set) async throws -> SignedSDJWT? { + try await present(query: query) { jws in + return try jws.payloadJSON() + } + } + + func present( + query: Set, + claimsOf: (JWS) throws -> JSON + ) async throws -> SignedSDJWT? { + return try await present( + query: { jsonPointer in + return query.contains(jsonPointer) + }, + claimsOf: claimsOf + ) + } + + func present( + query: (JSONPointer) -> Bool, + claimsOf: (JWS) throws -> JSON + ) async throws -> SignedSDJWT? { + let (_, disclosuresPerClaim) = try recreateClaimsAndDisclosuresPerClaim( + claimsOf: claimsOf + ) + let keys = disclosuresPerClaim.keys.filter(query) + if !keys.isEmpty {//keys.isEmpty { + return nil + } else { + let disclosures = Set( + disclosuresPerClaim + .filter { + keys.contains($0.key) + } + .values + .flatMap { $0 } + ) + return try SignedSDJWT( + serializedJwt: jwt.compactSerialization, + disclosures: Array(disclosures) + ) + } + } +} + +private extension SignedSDJWT { + func recreateClaimsAndDisclosuresPerClaim( + claimsOf: (JWS) throws -> JSON + ) throws -> (JSON, DisclosuresPerClaim) { + + let claims = try recreateClaims() + print(claims) + + return (JSON.empty, [:]) + } +} + +public protocol ClaimVisitor { + func call(pointer: JSONPointer, disclosure: Disclosure?) + func call(key: String, disclosure: Disclosure?) +} + +public class Visitor: ClaimVisitor { + + public init() { + } + + public func call(pointer: JSONPointer, disclosure: Disclosure?) { + print("Visitor") + } + + public func call(key: String, disclosure: Disclosure?) { + print("Visitor: \(key) \(disclosure ?? "N/A")") + } +} diff --git a/Sources/Types.swift b/Sources/Types.swift index 7364a87..c1fdfba 100644 --- a/Sources/Types.swift +++ b/Sources/Types.swift @@ -25,7 +25,7 @@ public enum SDJWTError: Error { case encodingError case discloseError case serializationError - case nonObjectFormat(ofElement: Any) + case nonObjectFormat(ofElement: String) case keyCreation case algorithmMissMatch case noneAsAlgorithm diff --git a/Sources/Utilities/Extensions/SignatureAlgorithm+Extension.swift b/Sources/Utilities/Extensions/SignatureAlgorithm+Extension.swift index 96ce1cf..6c14117 100644 --- a/Sources/Utilities/Extensions/SignatureAlgorithm+Extension.swift +++ b/Sources/Utilities/Extensions/SignatureAlgorithm+Extension.swift @@ -16,7 +16,7 @@ import Foundation import JSONWebAlgorithms -extension SigningAlgorithm: CaseIterable { +extension SigningAlgorithm: @retroactive CaseIterable { public static var allCases: [SigningAlgorithm] { return [.HS256, .HS384, .HS512, .RS256, .RS384, .RS512, .ES256, .ES384, .ES512, .ES256K, .PS256, .PS384, .PS512, .EdDSA] } diff --git a/Sources/Utilities/JSONPointer.swift b/Sources/Utilities/JSONPointer.swift index 9149949..9e432b1 100644 --- a/Sources/Utilities/JSONPointer.swift +++ b/Sources/Utilities/JSONPointer.swift @@ -19,7 +19,7 @@ import SwiftyJSON /// JSON Pointer defines a string syntax for identifying a specific value within a JSON document. /// /// More details about JSON Pointer can be found in the RFC specification: https://datatracker.ietf.org/doc/html/rfc6901 -public struct JSONPointer { +public struct JSONPointer: Hashable { /// The pointer string that represents the path in the JSON document. public let pointer: String diff --git a/Tests/Presentation/PresentationTest.swift b/Tests/Presentation/PresentationTest.swift new file mode 100644 index 0000000..f0da223 --- /dev/null +++ b/Tests/Presentation/PresentationTest.swift @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2023 European Commission + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import Foundation +import SwiftyJSON +import JSONWebKey +import JSONWebSignature +import JSONWebToken +import XCTest + +@testable import eudi_lib_sdjwt_swift + +final class PresentationTest: XCTestCase { + + override func setUp() async throws { + try await super.setUp() + } + + override func tearDown() async throws { + try await super.tearDown() + } + + func test() async throws { + + + let issuersKey = issuersKeyPair.public + let issuerJwk = try issuersKey.jwk + + let holdersKey = holdersKeyPair.public + let holdersJwk = try holdersKey.jwk + + let jsonObject: JSON = [ + "issuer": "https://example.com/issuer", + "jwks": [ + "keys": [ + [ + "crv": "P-256", + "kid": "Ao50Swzv_uWu805LcuaTTysu_6GwoqnvJh9rnc44U48", + "kty": "EC", + "x": issuerJwk.x?.base64URLEncode(), + "y": issuerJwk.y?.base64URLEncode() + ] + ] + ] + ] + + let issuerSignedSDJWT = try SDJWTIssuer.issue( + issuersPrivateKey: issuersKeyPair.private, + header: DefaultJWSHeaderImpl( + algorithm: .ES256, + keyID: "Ao50Swzv_uWu805LcuaTTysu_6GwoqnvJh9rnc44U48" + ) + ) { + ConstantClaims.iat(time: Date()) + ConstantClaims.exp(time: Date() + 3600) + ConstantClaims.iss(domain: "https://example.com/issuer") + FlatDisclosedClaim("sub", "6c5c0a49-b589-431d-bae7-219122a9ec2c") + FlatDisclosedClaim("given_name", "太郎") + FlatDisclosedClaim("family_name", "山田") + FlatDisclosedClaim("email", "\"unusual email address\"@example.jp") + FlatDisclosedClaim("phone_number", "+81-80-1234-5678") + ObjectClaim("address") { + FlatDisclosedClaim("street_address", "東京都港区芝公園4丁目2−8") + FlatDisclosedClaim("locality", "東京都") + FlatDisclosedClaim("region", "港区") + FlatDisclosedClaim("country", "JP") + } + FlatDisclosedClaim("birthdate", "1940-01-01") + ObjectClaim("cnf") { + ObjectClaim("jwk") { + PlainClaim("kid", "Ao50Swzv_uWu805LcuaTTysu_6GwoqnvJh9rnc44U48") + PlainClaim("kty", "EC") + PlainClaim("y", holdersJwk.y!.base64URLEncode()) + PlainClaim("x", holdersJwk.x!.base64URLEncode()) + PlainClaim("crv", "P-256") + } + } + } + + let query: Set = Set( + ["/address/region", "/address/country"] + .compactMap { + JSONPointer(pointer: $0) + } + ) + + + let presentedSdJwt = try await issuerSignedSDJWT.present( + query: query + ) + + // po CompactSerialiser(signedSDJWT: presentedSdJwt!).serialised +// print(presentedSdJwt) + } +} + From a75753b01ed9cf00943faab42761075250d92f9c Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Thu, 10 Oct 2024 12:21:44 +0300 Subject: [PATCH 04/14] [fix] updated json pointer --- Sources/Utilities/JSONPointer.swift | 51 +++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/Sources/Utilities/JSONPointer.swift b/Sources/Utilities/JSONPointer.swift index 9e432b1..cc8f15a 100644 --- a/Sources/Utilities/JSONPointer.swift +++ b/Sources/Utilities/JSONPointer.swift @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import Foundation import SwiftyJSON /// A struct that implements JSON Pointer (RFC 6901) to navigate and extract values from JSON documents. @@ -26,26 +27,36 @@ public struct JSONPointer: Hashable { /// Initializes a `JSONPointer` instance with a pointer string. /// - /// - Parameter pointer: A JSON Pointer string (must start with a `/`). - public init(pointer: String) { + /// - Parameter pointer: A JSON Pointer string (must start with a `/` or be empty for root). + public init(pointer: String = "/") { self.pointer = pointer } + + /// Initializes a `JSONPointer` instance with a token array. + /// + /// - Parameter tokens: An array of tokens representing the path in the JSON document. + public init(tokens: [String]) { + // Join the tokens with `/`, handling root correctly + self.pointer = "/" + tokens.map { $0.replacingOccurrences(of: "/", with: "~1").replacingOccurrences(of: "~", with: "~0") }.joined(separator: "/") + } + + /// Splits the pointer string into path components (tokens), handling RFC 6901 unescaping. + public var tokenArray: [String] { + return pointer.split(separator: "/").map { component -> String in + // Unescape tokens according to RFC 6901 + return component.replacingOccurrences(of: "~1", with: "/").replacingOccurrences(of: "~0", with: "~") + } + } /// Applies the JSON Pointer to a given JSON object to retrieve the value at the specified path. /// /// - Parameter json: The `JSON` object to traverse. /// - Returns: The `JSON` value found at the specified path, or `nil` if the path is invalid. public func evaluate(on json: JSON) -> JSON? { - // Split the pointer into path components, ignoring the first empty token (because of leading `/`) - let components = pointer.split(separator: "/").map { component -> String in - // Unescape tokens according to RFC 6901 - return component.replacingOccurrences(of: "~1", with: "/").replacingOccurrences(of: "~0", with: "~") - } - var currentJSON = json // Traverse through the components - for component in components { + for component in tokenArray { // If the current part is an array index, convert it to Int if let index = Int(component), currentJSON.type == .array { currentJSON = currentJSON[index] @@ -58,7 +69,27 @@ public struct JSONPointer: Hashable { return nil } } - return currentJSON } + + /// Returns the parent JSON Pointer for the current pointer. + /// If the pointer is root, it returns `nil`. + public func parent() -> JSONPointer? { + guard !isRoot else { return nil } + + // Remove the last token to get the parent path + let parentTokens = tokenArray.dropLast() + let parentPointer = JSONPointer(tokens: Array(parentTokens)) + return parentPointer + } + + /// Indicates if the pointer refers to the root of the JSON document. + public var isRoot: Bool { + return pointer == "/" + } + + /// Compares two JSONPointer objects for equality by comparing their pointer strings. + public static func ==(lhs: JSONPointer, rhs: JSONPointer) -> Bool { + return lhs.pointer == rhs.pointer + } } From ad69a75fdf72b82ec931d3e39532c8327a50c49b Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Thu, 10 Oct 2024 13:04:27 +0300 Subject: [PATCH 05/14] [fix] updated present interface --- Sources/Issuer/SignedSDJWT.swift | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/Sources/Issuer/SignedSDJWT.swift b/Sources/Issuer/SignedSDJWT.swift index 9b6486f..5ee037d 100644 --- a/Sources/Issuer/SignedSDJWT.swift +++ b/Sources/Issuer/SignedSDJWT.swift @@ -177,32 +177,19 @@ public extension SignedSDJWT { } func present(query: Set) async throws -> SignedSDJWT? { - try await present(query: query) { jws in - return try jws.payloadJSON() - } - } - - func present( - query: Set, - claimsOf: (JWS) throws -> JSON - ) async throws -> SignedSDJWT? { return try await present( query: { jsonPointer in return query.contains(jsonPointer) - }, - claimsOf: claimsOf + } ) } func present( - query: (JSONPointer) -> Bool, - claimsOf: (JWS) throws -> JSON + query: (JSONPointer) -> Bool ) async throws -> SignedSDJWT? { - let (_, disclosuresPerClaim) = try recreateClaimsAndDisclosuresPerClaim( - claimsOf: claimsOf - ) + let (_, disclosuresPerClaim) = try recreateClaimsAndDisclosuresPerClaim() let keys = disclosuresPerClaim.keys.filter(query) - if !keys.isEmpty {//keys.isEmpty { + if keys.isEmpty { return nil } else { let disclosures = Set( @@ -222,9 +209,7 @@ public extension SignedSDJWT { } private extension SignedSDJWT { - func recreateClaimsAndDisclosuresPerClaim( - claimsOf: (JWS) throws -> JSON - ) throws -> (JSON, DisclosuresPerClaim) { + func recreateClaimsAndDisclosuresPerClaim() throws -> (JSON, DisclosuresPerClaim) { let claims = try recreateClaims() print(claims) From 34b7c25f99fe8ba41dc95fc8374d51dc05c4ec3e Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Thu, 10 Oct 2024 16:38:38 +0300 Subject: [PATCH 06/14] [fix] added claim visitor --- Sources/Utilities/ClaimVisitor.swift | 50 ++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 Sources/Utilities/ClaimVisitor.swift diff --git a/Sources/Utilities/ClaimVisitor.swift b/Sources/Utilities/ClaimVisitor.swift new file mode 100644 index 0000000..b1de072 --- /dev/null +++ b/Sources/Utilities/ClaimVisitor.swift @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023 European Commission + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import Foundation + +public protocol ClaimVisitor: Sendable { + func call(pointer: JSONPointer, disclosure: Disclosure) +} + +public final class Visitor: ClaimVisitor { + + nonisolated(unsafe) var disclosuresPerClaim: [JSONPointer: [Disclosure]] = [:] + + public init() { + } + + public func call(pointer: JSONPointer, disclosure: Disclosure) { + // Ensure that the path (pointer) does not already exist in disclosuresPerClaim + guard disclosuresPerClaim[pointer] == nil else { + fatalError("Disclosures for \(pointer.pointer) have already been calculated.") + } + + // Calculate claimDisclosures + let claimDisclosures: [Disclosure] = { + let containerPath = pointer.parent() // Assuming pointer has a parent() method + let containerDisclosures = containerPath.flatMap { disclosuresPerClaim[$0] } ?? [] + + return containerDisclosures + [disclosure] + }() + + // Insert the claimDisclosures only if the pointer doesn't already exist + disclosuresPerClaim[pointer] = disclosuresPerClaim[pointer] ?? claimDisclosures + + + print("Visitor: \(pointer.pointer) \(disclosure)") + } +} + From bf8d1aef60d258d3287536dff31cab9eb4b108ff Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Thu, 10 Oct 2024 17:13:50 +0300 Subject: [PATCH 07/14] [fix] json pointer sendable --- Sources/Utilities/JSONPointer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Utilities/JSONPointer.swift b/Sources/Utilities/JSONPointer.swift index cc8f15a..a52a5c5 100644 --- a/Sources/Utilities/JSONPointer.swift +++ b/Sources/Utilities/JSONPointer.swift @@ -20,7 +20,7 @@ import SwiftyJSON /// JSON Pointer defines a string syntax for identifying a specific value within a JSON document. /// /// More details about JSON Pointer can be found in the RFC specification: https://datatracker.ietf.org/doc/html/rfc6901 -public struct JSONPointer: Hashable { +public struct JSONPointer: Hashable, Sendable { /// The pointer string that represents the path in the JSON document. public let pointer: String From 4c57907b78865847ac688265a81167e994fe302b Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Thu, 10 Oct 2024 17:16:05 +0300 Subject: [PATCH 08/14] [fix] visitor can get disclosures --- Sources/Utilities/ClaimVisitor.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/Utilities/ClaimVisitor.swift b/Sources/Utilities/ClaimVisitor.swift index b1de072..81c32d8 100644 --- a/Sources/Utilities/ClaimVisitor.swift +++ b/Sources/Utilities/ClaimVisitor.swift @@ -22,6 +22,9 @@ public protocol ClaimVisitor: Sendable { public final class Visitor: ClaimVisitor { nonisolated(unsafe) var disclosuresPerClaim: [JSONPointer: [Disclosure]] = [:] + nonisolated(unsafe) var disclosures: [Disclosure] { + disclosuresPerClaim.flatMap { $0.value } + } public init() { } From 8daa42193b3695cb7c610f057090db28a44cd8c6 Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Mon, 14 Oct 2024 10:46:05 +0300 Subject: [PATCH 09/14] [fix] holder presentation and kbjwt --- Sources/Claim/ClaimsExtractor.swift | 29 +++++--- Sources/Issuer/SDJWT.swift | 3 +- Sources/Issuer/SignedSDJWT.swift | 54 +++++++------- Sources/Utilities/ClaimVisitor.swift | 5 +- Sources/Verifier/KeyBindingVerifier.swift | 4 ++ Tests/Helpers/Utilities.swift | 2 +- Tests/Issuance/BuilderTest.swift | 6 +- Tests/Presentation/PresentationTest.swift | 88 ++++++++++++++++------- 8 files changed, 118 insertions(+), 73 deletions(-) diff --git a/Sources/Claim/ClaimsExtractor.swift b/Sources/Claim/ClaimsExtractor.swift index 8e90042..1c86d14 100644 --- a/Sources/Claim/ClaimsExtractor.swift +++ b/Sources/Claim/ClaimsExtractor.swift @@ -15,18 +15,22 @@ */ import SwiftyJSON -public typealias ClaimExtractorResult = (digestsFoundOnPayload: [DigestType], recreatedClaims: JSON) +public typealias ClaimExtractorResult = ( + digestsFoundOnPayload: [DigestType], + recreatedClaims: JSON, + disclosuresPerClaim: DisclosuresPerClaim? +) public class ClaimExtractor { // MARK: - Properties - var digestsOfDisclosuresDict: [DisclosureDigest: Disclosure] + var digestsOfDisclosures: [DisclosureDigest: Disclosure] // MARK: - Lifecycle public init(digestsOfDisclosuresDict: [DisclosureDigest: Disclosure]) { - self.digestsOfDisclosuresDict = digestsOfDisclosuresDict + self.digestsOfDisclosures = digestsOfDisclosuresDict } // MARK: - Methods @@ -46,9 +50,9 @@ public class ClaimExtractor { var sdArray = sdArray.compactMap(\.string) // try to find matching digests in order to be replaced with the value while true { - let (updatedSdArray, foundDigest) = sdArray.findAndRemoveFirst(from: digestsOfDisclosuresDict.compactMap({$0.key})) + let (updatedSdArray, foundDigest) = sdArray.findAndRemoveFirst(from: digestsOfDisclosures.compactMap({$0.key})) if let foundDigest, - let foundDisclosure = digestsOfDisclosuresDict[foundDigest]?.base64URLDecode()?.objectProperty { + let foundDisclosure = digestsOfDisclosures[foundDigest]?.base64URLDecode()?.objectProperty { json[Keys.sd.rawValue].arrayObject = updatedSdArray guard !json[foundDisclosure.key].exists() else { @@ -57,10 +61,15 @@ public class ClaimExtractor { json[foundDisclosure.key] = foundDisclosure.value - if let d = digestsOfDisclosuresDict[foundDigest] { + if let disclosure = digestsOfDisclosures[foundDigest] { let currentJsonPointer = "/" + (currentPath + [foundDisclosure.key]).joined(separator: "/") // visitor?.call(key: foundDisclosure.key, disclosure: foundDisclosure.value.stringValue + " " + foundDigest + " " + d + " " + currentJsonPointer) - visitor?.call(key: currentJsonPointer, disclosure: d) + visitor?.call( + pointer: .init( + pointer: currentJsonPointer + ), + disclosure: disclosure + ) } foundDigests.append(.object(foundDigest)) @@ -89,7 +98,7 @@ public class ClaimExtractor { for (index, object) in subJson.arrayValue.enumerated() { let newPath = currentPath + [key, "\(index)"] // Update the path for array elements if object[Keys.dots.rawValue].exists() { - if let foundDisclosedArrayElement = digestsOfDisclosuresDict[object[Keys.dots].stringValue]? + if let foundDisclosedArrayElement = digestsOfDisclosures[object[Keys.dots].stringValue]? .base64URLDecode()? .arrayProperty { @@ -105,7 +114,7 @@ public class ClaimExtractor { currentPath: newPath // Pass the updated path for the nested JSON ), - !ifHasNested.digestsFoundOnPayload.isEmpty { + !ifHasNested.digestsFoundOnPayload.isEmpty { foundDigests += ifHasNested.digestsFoundOnPayload json[key].arrayObject?[index] = ifHasNested.recreatedClaims } @@ -114,6 +123,6 @@ public class ClaimExtractor { } } } - return (foundDigests, json) + return (foundDigests, json, visitor?.disclosuresPerClaim) } } diff --git a/Sources/Issuer/SDJWT.swift b/Sources/Issuer/SDJWT.swift index 96def1e..86f9f4a 100644 --- a/Sources/Issuer/SDJWT.swift +++ b/Sources/Issuer/SDJWT.swift @@ -54,7 +54,7 @@ public struct SDJWT { } } - func recreateClaims() throws -> ClaimExtractorResult { + func recreateClaims(visitor: Visitor? = nil) throws -> ClaimExtractorResult { let digestCreator = try extractDigestCreator() var digestsOfDisclosuresDict = [DisclosureDigest: Disclosure]() for disclosure in self.disclosures { @@ -66,7 +66,6 @@ public struct SDJWT { } } - let visitor = Visitor() return try ClaimExtractor( digestsOfDisclosuresDict: digestsOfDisclosuresDict ).findDigests( diff --git a/Sources/Issuer/SignedSDJWT.swift b/Sources/Issuer/SignedSDJWT.swift index 5ee037d..07e5510 100644 --- a/Sources/Issuer/SignedSDJWT.swift +++ b/Sources/Issuer/SignedSDJWT.swift @@ -18,7 +18,7 @@ import SwiftyJSON import JSONWebSignature import JSONWebKey -typealias DisclosuresPerClaim = Dictionary +public typealias DisclosuresPerClaim = Dictionary public struct SignedSDJWT { @@ -157,8 +157,11 @@ public extension SignedSDJWT { serialiser(self).serialised } - func recreateClaims() throws -> ClaimExtractorResult { - return try self.toSDJWT().recreateClaims() + func recreateClaims(visitor: Visitor? = nil) throws -> ClaimExtractorResult { + return try self.toSDJWT() + .recreateClaims( + visitor: visitor + ) } func asJwsJsonObject( @@ -176,18 +179,25 @@ public extension SignedSDJWT { ) } - func present(query: Set) async throws -> SignedSDJWT? { + func present( + query: Set, + visitor: Visitor? = Visitor() + ) async throws -> SignedSDJWT? { return try await present( query: { jsonPointer in return query.contains(jsonPointer) - } + }, + visitor: visitor ) } - func present( - query: (JSONPointer) -> Bool + private func present( + query: (JSONPointer) -> Bool, + visitor: Visitor? ) async throws -> SignedSDJWT? { - let (_, disclosuresPerClaim) = try recreateClaimsAndDisclosuresPerClaim() + let (_, disclosuresPerClaim) = try recreateClaimsAndDisclosuresPerClaim( + visitor: visitor + ) let keys = disclosuresPerClaim.keys.filter(query) if keys.isEmpty { return nil @@ -209,30 +219,14 @@ public extension SignedSDJWT { } private extension SignedSDJWT { - func recreateClaimsAndDisclosuresPerClaim() throws -> (JSON, DisclosuresPerClaim) { + func recreateClaimsAndDisclosuresPerClaim(visitor: Visitor?) throws -> (JSON, DisclosuresPerClaim) { - let claims = try recreateClaims() + let claims = try recreateClaims(visitor: visitor) print(claims) - return (JSON.empty, [:]) - } -} - -public protocol ClaimVisitor { - func call(pointer: JSONPointer, disclosure: Disclosure?) - func call(key: String, disclosure: Disclosure?) -} - -public class Visitor: ClaimVisitor { - - public init() { - } - - public func call(pointer: JSONPointer, disclosure: Disclosure?) { - print("Visitor") - } - - public func call(key: String, disclosure: Disclosure?) { - print("Visitor: \(key) \(disclosure ?? "N/A")") + return ( + claims.recreatedClaims, + claims.disclosuresPerClaim ?? [:] + ) } } diff --git a/Sources/Utilities/ClaimVisitor.swift b/Sources/Utilities/ClaimVisitor.swift index 81c32d8..16261cd 100644 --- a/Sources/Utilities/ClaimVisitor.swift +++ b/Sources/Utilities/ClaimVisitor.swift @@ -29,7 +29,10 @@ public final class Visitor: ClaimVisitor { public init() { } - public func call(pointer: JSONPointer, disclosure: Disclosure) { + public func call( + pointer: JSONPointer, + disclosure: Disclosure + ) { // Ensure that the path (pointer) does not already exist in disclosuresPerClaim guard disclosuresPerClaim[pointer] == nil else { fatalError("Disclosures for \(pointer.pointer) have already been calculated.") diff --git a/Sources/Verifier/KeyBindingVerifier.swift b/Sources/Verifier/KeyBindingVerifier.swift index 5508494..ec5e822 100644 --- a/Sources/Verifier/KeyBindingVerifier.swift +++ b/Sources/Verifier/KeyBindingVerifier.swift @@ -53,6 +53,8 @@ public class KeyBindingVerifier: VerifierProtocol { try verifyIat(iatOffset: iatOffset, iat: Date(timeIntervalSince1970: TimeInterval(timeInterval))) try verifyAud(aud: aud, expectedAudience: expectedAudience) + + try verify() } public func verify( @@ -70,6 +72,8 @@ public class KeyBindingVerifier: VerifierProtocol { } self.signatureVerifier = try SignatureVerifier(signedJWT: challenge, publicKey: extractedKey) + + try verify() } @discardableResult diff --git a/Tests/Helpers/Utilities.swift b/Tests/Helpers/Utilities.swift index 32aa23d..548c570 100644 --- a/Tests/Helpers/Utilities.swift +++ b/Tests/Helpers/Utilities.swift @@ -72,7 +72,7 @@ func validateObjectResults(factoryResult result: Result, expect } XCTAssert(expectedDigests + numberOfDecoys <= expectedDigests + decoysLimit) return (json, disclosures) - case .failure(let err): + case .failure: XCTFail("Failed to Create SDJWT") return(.empty, []) } diff --git a/Tests/Issuance/BuilderTest.swift b/Tests/Issuance/BuilderTest.swift index 654c536..89a1237 100644 --- a/Tests/Issuance/BuilderTest.swift +++ b/Tests/Issuance/BuilderTest.swift @@ -54,9 +54,9 @@ final class BuilderTest: XCTestCase { let unsignedJwt = factory.createSDJWTPayload(sdJwtObject: sdObject.asObject) switch unsignedJwt { - case .success((let json, let disclosures)): + case .success: XCTAssert(true) - case .failure(let err): + case .failure: XCTFail("Failed to Create SDJWT") } @@ -112,8 +112,6 @@ final class BuilderTest: XCTestCase { FlatDisclosedClaim("array", ["GR", "DE"]) } - let json = plainJWT.asJSON - @SDJWTBuilder var objects: SdElement { FlatDisclosedClaim("Flat Object", plainJWT.asJSON) diff --git a/Tests/Presentation/PresentationTest.swift b/Tests/Presentation/PresentationTest.swift index f0da223..73eaef0 100644 --- a/Tests/Presentation/PresentationTest.swift +++ b/Tests/Presentation/PresentationTest.swift @@ -32,29 +32,13 @@ final class PresentationTest: XCTestCase { try await super.tearDown() } - func test() async throws { - - - let issuersKey = issuersKeyPair.public - let issuerJwk = try issuersKey.jwk + func testSDJWTPresentationWithSelectiveDisclosures() async throws { + // Given + let visitor = Visitor() let holdersKey = holdersKeyPair.public let holdersJwk = try holdersKey.jwk - - let jsonObject: JSON = [ - "issuer": "https://example.com/issuer", - "jwks": [ - "keys": [ - [ - "crv": "P-256", - "kid": "Ao50Swzv_uWu805LcuaTTysu_6GwoqnvJh9rnc44U48", - "kty": "EC", - "x": issuerJwk.x?.base64URLEncode(), - "y": issuerJwk.y?.base64URLEncode() - ] - ] - ] - ] + var verifier: KeyBindingVerifier = KeyBindingVerifier() let issuerSignedSDJWT = try SDJWTIssuer.issue( issuersPrivateKey: issuersKeyPair.private, @@ -87,22 +71,76 @@ final class PresentationTest: XCTestCase { PlainClaim("crv", "P-256") } } + RecursiveObject("test_recursive") { + FlatDisclosedClaim("recursive_address", "東京都港区芝公園4丁目2−8") + } } + // When let query: Set = Set( - ["/address/region", "/address/country"] + ["/address/region", "/address/country", "/dimitri_recursive/recursive_address"] .compactMap { JSONPointer(pointer: $0) } ) - let presentedSdJwt = try await issuerSignedSDJWT.present( - query: query + query: query, + visitor: visitor ) - // po CompactSerialiser(signedSDJWT: presentedSdJwt!).serialised -// print(presentedSdJwt) + guard let presentedSdJwt = presentedSdJwt else { + XCTFail("Expected presentedSdJwt value to be non-nil but it was nil") + return + } + + let sdHash = DigestCreator() + .hashAndBase64Encode( + input: CompactSerialiser( + signedSDJWT: presentedSdJwt + ).serialised + )! + + var holderPresentation: SignedSDJWT? + XCTAssertNoThrow( + holderPresentation = try SDJWTIssuer + .presentation( + holdersPrivateKey: holdersKeyPair.private, + signedSDJWT: issuerSignedSDJWT, + disclosuresToPresent: presentedSdJwt.disclosures, + keyBindingJWT: KBJWT( + header: DefaultJWSHeaderImpl(algorithm: .ES256), + kbJwtPayload: .init([ + Keys.nonce.rawValue: "123456789", + Keys.aud.rawValue: "example.com", + Keys.iat.rawValue: 1694600000, + Keys.sdHash.rawValue: sdHash + ]) + ) + ) + ) + + let kbJwt = holderPresentation?.kbJwt + + // Then + XCTAssertNoThrow( + try verifier.verify( + iatOffset: .init( + startTime: Date(timeIntervalSince1970: 1694600000 - 1000), + endTime: Date(timeIntervalSince1970: 1694600000) + )!, + expectedAudience: "example.com", + challenge: kbJwt!, + extractedKey: holdersJwk + ) + ) + + XCTAssertNotNil(kbJwt) + XCTAssertEqual(presentedSdJwt.disclosures.count, 4) + + let presentedDisclosures = Set(presentedSdJwt.disclosures) + let visitedDisclosures = Set(visitor.disclosures) + XCTAssertTrue(presentedDisclosures.isSubset(of: visitedDisclosures)) } } From c4166561afdb4dfddc60d0aaed0884103e7999e6 Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Mon, 14 Oct 2024 10:59:33 +0300 Subject: [PATCH 10/14] [fix] minor test fix --- Tests/Presentation/PresentationTest.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Presentation/PresentationTest.swift b/Tests/Presentation/PresentationTest.swift index 73eaef0..3ca3a96 100644 --- a/Tests/Presentation/PresentationTest.swift +++ b/Tests/Presentation/PresentationTest.swift @@ -78,7 +78,7 @@ final class PresentationTest: XCTestCase { // When let query: Set = Set( - ["/address/region", "/address/country", "/dimitri_recursive/recursive_address"] + ["/address/region", "/address/country", "/test_recursive/recursive_address"] .compactMap { JSONPointer(pointer: $0) } From 93fc8b75b6f317b71f10e2df1481746ec4b285d8 Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Mon, 14 Oct 2024 11:15:38 +0300 Subject: [PATCH 11/14] [fix] readme updates for holder presentation --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f500dbb..1e45c7f 100644 --- a/README.md +++ b/README.md @@ -225,8 +225,11 @@ All examples assume that we have the following claim set ## SD-JWT VC support The library supports verifying [SD-JWT-based Verifiable Credentials](https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-04.html). -More specifically, Issuer-signed JWT Verification Key Validation support is provided by [SDJWTVerifier](Sources/Verifier/SDJWTVerifier.swift). Please check [VcVerifierTest](Tests/Verification/VcVerifierTest.swift) for code examples of verifying an Issuance SD-JWT VC and a Presentation SD-JWT VC (including verification of the Key Binding JWT). +More specifically, Issuer-signed JWT Verification Key Validation support is provided by [SDJWTVerifier](Sources/Verifier/SDJWTVerifier.swift). +Please check [PresentationTest](Tests/Presentation/PresentationTest.swift) for code examples on creating a holder presentation. + +Please check [VcVerifierTest](Tests/Verification/VcVerifierTest.swift) for code examples on verifying an Issuance SD-JWT VC and a Presentation SD-JWT VC (including verification of the Key Binding JWT). ## How to contribute From cfe40ff29958bded329f9531e816a0ed54cb7a4f Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Mon, 14 Oct 2024 13:15:10 +0300 Subject: [PATCH 12/14] [fix] added array claim to presentation tests --- Sources/Factory/SDJWTFactory.swift | 5 ----- Sources/Issuer/SignedSDJWT.swift | 12 +++++++++++ Tests/Presentation/PresentationTest.swift | 26 +++++++++++++++++------ Tests/SpecExamples.swift | 2 +- 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/Sources/Factory/SDJWTFactory.swift b/Sources/Factory/SDJWTFactory.swift index b2bba3e..1f76777 100644 --- a/Sources/Factory/SDJWTFactory.swift +++ b/Sources/Factory/SDJWTFactory.swift @@ -119,7 +119,6 @@ class SDJWTFactory { switch element { case .plain(let json): partialResult.arrayObject?.append(json) - // //............ case .object(let object): let claimSet = try self.encodeObject(sdJwtObject: object) disclosures.append(contentsOf: claimSet.disclosures) @@ -127,21 +126,17 @@ class SDJWTFactory { let dottedKeyJson: JSON = [Keys.dots.rawValue: digest] partialResult.arrayObject?.append(dottedKeyJson) disclosures.append(disclosure) - // //............ case .array(let array): let claims = try encodeClaim(key: Keys.dots.rawValue, value: .array(array)) partialResult.arrayObject?.append(claims.value) disclosures.append(contentsOf: claims.disclosures) - // //............ default: let (disclosure, digest) = try self.discloseArrayElement(value: element.asJSON) let dottedKeyJson: JSON = [Keys.dots.rawValue: digest] partialResult.arrayObject?.append(dottedKeyJson) disclosures.append(disclosure) } - // //............ } - return (output, disclosures) } diff --git a/Sources/Issuer/SignedSDJWT.swift b/Sources/Issuer/SignedSDJWT.swift index 07e5510..7483d47 100644 --- a/Sources/Issuer/SignedSDJWT.swift +++ b/Sources/Issuer/SignedSDJWT.swift @@ -29,6 +29,18 @@ public struct SignedSDJWT { public internal(set) var kbJwt: JWS? public internal(set) var claimSet: JSON + public var serialisation: String { + let separator = "~" + let kbJwtSerialization = kbJwt?.compactSerialization ?? "" + let jwtAndDisclosures: [String] = ([jwt.compactSerialization] + disclosures) + return jwtAndDisclosures + .reduce("") { + $0.isEmpty ? $1 : $0 + separator + $1 + } + + separator + + kbJwtSerialization + } + var delineatedCompactSerialisation: String { let separator = "~" let input = ([jwt.compactSerialization] + disclosures).reduce("") { $0.isEmpty ? $1 : $0 + separator + $1 } + separator diff --git a/Tests/Presentation/PresentationTest.swift b/Tests/Presentation/PresentationTest.swift index 3ca3a96..e64f46a 100644 --- a/Tests/Presentation/PresentationTest.swift +++ b/Tests/Presentation/PresentationTest.swift @@ -38,7 +38,14 @@ final class PresentationTest: XCTestCase { let visitor = Visitor() let holdersKey = holdersKeyPair.public let holdersJwk = try holdersKey.jwk - var verifier: KeyBindingVerifier = KeyBindingVerifier() + let verifier: KeyBindingVerifier = KeyBindingVerifier() + + @SDJWTBuilder + var evidenceObject: SdElement { + FlatDisclosedClaim("type", "document") + FlatDisclosedClaim("method", "pipp") + FlatDisclosedClaim("time", "2012-04-22T11:30Z") + } let issuerSignedSDJWT = try SDJWTIssuer.issue( issuersPrivateKey: issuersKeyPair.private, @@ -74,14 +81,21 @@ final class PresentationTest: XCTestCase { RecursiveObject("test_recursive") { FlatDisclosedClaim("recursive_address", "東京都港区芝公園4丁目2−8") } + ArrayClaim("evidence", array: [ + evidenceObject + ]) } // When let query: Set = Set( - ["/address/region", "/address/country", "/test_recursive/recursive_address"] - .compactMap { - JSONPointer(pointer: $0) - } + [ + "/address/region", + "/address/country", + "/test_recursive/recursive_address", + "/evidence/0/time" + ].compactMap { + JSONPointer(pointer: $0) + } ) let presentedSdJwt = try await issuerSignedSDJWT.present( @@ -136,7 +150,7 @@ final class PresentationTest: XCTestCase { ) XCTAssertNotNil(kbJwt) - XCTAssertEqual(presentedSdJwt.disclosures.count, 4) + XCTAssertEqual(presentedSdJwt.disclosures.count, 5) let presentedDisclosures = Set(presentedSdJwt.disclosures) let visitedDisclosures = Set(visitor.disclosures) diff --git a/Tests/SpecExamples.swift b/Tests/SpecExamples.swift index a03c456..0d46e8d 100644 --- a/Tests/SpecExamples.swift +++ b/Tests/SpecExamples.swift @@ -84,8 +84,8 @@ final class SpecExamples: XCTestCase { PlainClaim("date_of_issuance", "2010-03-23") PlainClaim("date_of_expiry", "2020-03-22") } - } + // ....... @SDJWTBuilder var complex: SdElement { From f9941239e7254f7d806e8073756cde77c58ac7fe Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Wed, 16 Oct 2024 10:46:45 +0300 Subject: [PATCH 13/14] [fix] updated readme with draft version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1e45c7f..009d2cf 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ the [EUDI Wallet Reference Implementation project description](https://github.co This is a library offering a DSL (domain-specific language) for defining how a set of claims should be made selectively disclosable. -Library implements [SD-JWT draft8](https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-08.html) +Library implements [SD-JWT draft 12](https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-12.html) is implemented in Swift. ## Use cases supported From 0dceba3f7f4cd07233eeef0b3ca10592e8d4d51f Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Wed, 16 Oct 2024 16:31:48 +0300 Subject: [PATCH 14/14] [fix] presentation updates --- Sources/Claim/ClaimsExtractor.swift | 4 ++-- Sources/Issuer/SignedSDJWT.swift | 1 - Sources/{ => Types}/Types.swift | 0 Sources/Utilities/ClaimVisitor.swift | 15 ++++++++------- .../Utilities/Extensions/String+Extension.swift | 7 ------- Tests/Helpers/Utilities.swift | 6 ++---- Tests/Issuance/IssuerTests.swift | 2 +- Tests/SpecExamples.swift | 6 +++--- Tests/Verification/SerializerTest.swift | 4 ++-- 9 files changed, 18 insertions(+), 27 deletions(-) rename Sources/{ => Types}/Types.swift (100%) diff --git a/Sources/Claim/ClaimsExtractor.swift b/Sources/Claim/ClaimsExtractor.swift index 1c86d14..2fbc44f 100644 --- a/Sources/Claim/ClaimsExtractor.swift +++ b/Sources/Claim/ClaimsExtractor.swift @@ -63,12 +63,12 @@ public class ClaimExtractor { if let disclosure = digestsOfDisclosures[foundDigest] { let currentJsonPointer = "/" + (currentPath + [foundDisclosure.key]).joined(separator: "/") - // visitor?.call(key: foundDisclosure.key, disclosure: foundDisclosure.value.stringValue + " " + foundDigest + " " + d + " " + currentJsonPointer) visitor?.call( pointer: .init( pointer: currentJsonPointer ), - disclosure: disclosure + disclosure: disclosure, + value: foundDisclosure.value.string ) } foundDigests.append(.object(foundDigest)) diff --git a/Sources/Issuer/SignedSDJWT.swift b/Sources/Issuer/SignedSDJWT.swift index 7483d47..2211f93 100644 --- a/Sources/Issuer/SignedSDJWT.swift +++ b/Sources/Issuer/SignedSDJWT.swift @@ -234,7 +234,6 @@ private extension SignedSDJWT { func recreateClaimsAndDisclosuresPerClaim(visitor: Visitor?) throws -> (JSON, DisclosuresPerClaim) { let claims = try recreateClaims(visitor: visitor) - print(claims) return ( claims.recreatedClaims, diff --git a/Sources/Types.swift b/Sources/Types/Types.swift similarity index 100% rename from Sources/Types.swift rename to Sources/Types/Types.swift diff --git a/Sources/Utilities/ClaimVisitor.swift b/Sources/Utilities/ClaimVisitor.swift index 16261cd..8b71636 100644 --- a/Sources/Utilities/ClaimVisitor.swift +++ b/Sources/Utilities/ClaimVisitor.swift @@ -16,7 +16,11 @@ import Foundation public protocol ClaimVisitor: Sendable { - func call(pointer: JSONPointer, disclosure: Disclosure) + func call( + pointer: JSONPointer, + disclosure: Disclosure, + value: String? + ) } public final class Visitor: ClaimVisitor { @@ -31,7 +35,8 @@ public final class Visitor: ClaimVisitor { public func call( pointer: JSONPointer, - disclosure: Disclosure + disclosure: Disclosure, + value: String? = nil ) { // Ensure that the path (pointer) does not already exist in disclosuresPerClaim guard disclosuresPerClaim[pointer] == nil else { @@ -40,17 +45,13 @@ public final class Visitor: ClaimVisitor { // Calculate claimDisclosures let claimDisclosures: [Disclosure] = { - let containerPath = pointer.parent() // Assuming pointer has a parent() method + let containerPath = pointer.parent() let containerDisclosures = containerPath.flatMap { disclosuresPerClaim[$0] } ?? [] - return containerDisclosures + [disclosure] }() // Insert the claimDisclosures only if the pointer doesn't already exist disclosuresPerClaim[pointer] = disclosuresPerClaim[pointer] ?? claimDisclosures - - - print("Visitor: \(pointer.pointer) \(disclosure)") } } diff --git a/Sources/Utilities/Extensions/String+Extension.swift b/Sources/Utilities/Extensions/String+Extension.swift index 47d572d..de830f4 100644 --- a/Sources/Utilities/Extensions/String+Extension.swift +++ b/Sources/Utilities/Extensions/String+Extension.swift @@ -81,26 +81,22 @@ extension String { // Decode the Base64-encoded string guard let keyData = Data(base64Encoded: keyString) else { - print("Invalid Base64 string") return nil } // First, try RSA if let secKey = String.createSecKey(from: keyData, keyType: kSecAttrKeyTypeRSA) { - print("Key identified as RSA.") return secKey } // If RSA fails, try EC if let secKey = String.createSecKey(from: keyData, keyType: kSecAttrKeyTypeEC) { - print("Key identified as EC.") return secKey } // Add more key types if needed (e.g., DSA, etc.) // If neither RSA nor EC works, return nil - print("Unable to identify key type.") return nil } @@ -122,9 +118,6 @@ extension String { if let secKey = SecKeyCreateWithData(keyData as CFData, attributes as CFDictionary, &error) { return secKey } else { - if let err = error?.takeRetainedValue() { - print("Error creating SecKey: \(err.localizedDescription)") - } return nil } } diff --git a/Tests/Helpers/Utilities.swift b/Tests/Helpers/Utilities.swift index 548c570..f95cfa8 100644 --- a/Tests/Helpers/Utilities.swift +++ b/Tests/Helpers/Utilities.swift @@ -65,8 +65,8 @@ func validateObjectResults(factoryResult result: Result, expect TestLogger.log("==============================") disclosures .compactMap { $0.base64URLDecode()} - .forEach {print($0)} - print("==============================") + .forEach { TestLogger.log($0) } + TestLogger.log("==============================") if numberOfDecoys == 0 && decoysLimit == 0 { XCTAssert(disclosures.count == expectedDigests) } @@ -131,9 +131,7 @@ class MockSaltProvider: SaltProvider { class TestLogger { static func log(_ message: String) { #if DEBUG -// if isRunningTests() { print(message) -// } #endif } diff --git a/Tests/Issuance/IssuerTests.swift b/Tests/Issuance/IssuerTests.swift index cd83691..bbd61ba 100644 --- a/Tests/Issuance/IssuerTests.swift +++ b/Tests/Issuance/IssuerTests.swift @@ -66,6 +66,6 @@ final class IssuerTest: XCTestCase { jwTpayload: payload) let jwt = try JWT(header: DefaultJWSHeaderImpl(algorithm: .ES256), payload: JSON(envelopedFormat.data)) - print(jwt.payload) + TestLogger.log((try? jwt.payload.toJSONString()) ?? "") } } diff --git a/Tests/SpecExamples.swift b/Tests/SpecExamples.swift index 0d46e8d..b8e22fb 100644 --- a/Tests/SpecExamples.swift +++ b/Tests/SpecExamples.swift @@ -124,12 +124,12 @@ final class SpecExamples: XCTestCase { } let output = factory.createSDJWTPayload(sdJwtObject: complex.asObject) - let digestCount = try XCTUnwrap(try? output.get().value.findDigestCount()) + let _ = try XCTUnwrap(try? output.get().value.findDigestCount()) validateObjectResults(factoryResult: output, expectedDigests: 16) try output.get().disclosures.forEach { disclosure in - print(disclosure.base64URLDecode()) + TestLogger.log(disclosure.base64URLDecode() ?? "") } - let findDigest = try? XCTUnwrap(output.get()) + let _ = try? XCTUnwrap(output.get()) } } diff --git a/Tests/Verification/SerializerTest.swift b/Tests/Verification/SerializerTest.swift index e8e75bc..212afa4 100644 --- a/Tests/Verification/SerializerTest.swift +++ b/Tests/Verification/SerializerTest.swift @@ -36,7 +36,7 @@ final class SerialiserTest: XCTestCase { let serialisedString = try testSerializerWhenSerializedFormatIsSelected_ThenExpectSerialisedFormattedSignedSDJWT() let parser = CompactParser() let jwt = try parser.getSignedSdJwt(serialisedString: serialisedString).toSDJWT() - print(jwt.disclosures) + TestLogger.log(jwt.disclosures.joined(separator: ", ")) } func testSerialiseWhenChosingEnvelopeFormat_AppylingNoKeyBinding_ThenExpectACorrectJWT() throws { @@ -57,7 +57,7 @@ final class SerialiserTest: XCTestCase { ClaimsVerifier() }.get() - print(verifier) + XCTAssertNotNil(verifier) } }