Skip to content

Commit

Permalink
Merge pull request #34 from niscy-eudiw/feature/json-serialization
Browse files Browse the repository at this point in the history
JSON serialization of SD-JWT
  • Loading branch information
dtsiflit authored Sep 26, 2024
2 parents edb8b4f + ec6392a commit 7715625
Show file tree
Hide file tree
Showing 6 changed files with 384 additions and 53 deletions.
74 changes: 74 additions & 0 deletions Sources/Builders/JSONBuilder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* 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

@resultBuilder
internal struct JSONBuilder {

// Handle key-value pairs
static func buildBlock(_ components: [String: JSON?]...) -> [String: JSON?] {
var result: [String: JSON?] = [:]
for component in components {
result.merge(component) { (_, new) in new }
}
return result
}

// Handle individual expressions
static func buildExpression(_ expression: [String: JSON?]) -> [String: JSON?] {
return expression
}

static func buildExpression(_ expression: [JSON]) -> [String: JSON?] {
// This expression now returns empty dictionary, as array should be handled in JSON context
return [:]
}

// Handle inline JSON objects
static func buildExpression(_ expression: JSON) -> [String: JSON?] {
// Assuming this JSON is a dictionary, we merge it
guard let dictionary = expression.dictionary else {
return [:] // Ignore if it's not an object (could handle arrays differently)
}
return dictionary.mapValues { $0 }
}

// Handle building arrays of JSON objects
static func buildArray(_ components: [[String: JSON?]]) -> [String: JSON?] {
var result: [String: JSON?] = [:]
for component in components {
result.merge(component) { (_, new) in new }
}
return result
}
}

// Function to create JSON objects
internal func JSONObject(@JSONBuilder _ content: () -> [String: JSON?]) -> JSON {
var result: [String: JSON] = [:]
for (key, value) in content() {
if let unwrappedValue = value {
result[key] = unwrappedValue
}
}
return JSON(result)
}

// DSL for JSON array construction
internal func JSONArray(_ build: () -> [JSON]) -> JSON {
return JSON(build())
}
83 changes: 49 additions & 34 deletions Sources/Issuer/SDJWT.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,21 @@ import SwiftyJSON
public typealias KBJWT = JWT

struct SDJWT {

// MARK: - Properties

public internal(set) var jwt: JWT
public internal(set) var disclosures: [Disclosure]
public internal(set) var kbJwt: JWT?

// MARK: - Lifecycle

init(jwt: JWT, disclosures: [Disclosure], kbJWT: KBJWT?) throws {
self.jwt = jwt
self.disclosures = disclosures
self.kbJwt = kbJWT
}

func extractDigestCreator() throws -> DigestCreator {
if jwt.payload[Keys.sdAlg.rawValue].exists() {
let stringValue = jwt.payload[Keys.sdAlg.rawValue].stringValue
Expand All @@ -49,7 +49,7 @@ struct SDJWT {
throw SDJWTVerifierError.missingOrUnknownHashingAlgorithm
}
}

func recreateClaims() throws -> ClaimExtractorResult {
let digestCreator = try extractDigestCreator()
var digestsOfDisclosuresDict = [DisclosureDigest: Disclosure]()
Expand All @@ -61,31 +61,31 @@ struct SDJWT {
throw SDJWTVerifierError.failedToCreateVerifier
}
}

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?

var delineatedCompactSerialisation: String {
let separator = "~"
let input = ([jwt.compactSerialization] + disclosures).reduce("") { $0.isEmpty ? $1 : $0 + separator + $1 } + 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],
Expand All @@ -95,83 +95,83 @@ public struct SignedSDJWT {
self.disclosures = disclosures
self.kbJwt = try? JWS(jwsString: serializedKbJwt ?? "")
}

private init?<KeyType>(sdJwt: SDJWT, issuersPrivateKey: KeyType) {
// 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
}

private init?<KeyType>(signedSDJWT: SignedSDJWT, kbJWT: JWT, holdersPrivateKey: KeyType) {
// 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
}

// MARK: - Methods

// expose static func initializers to distinguish between 2 cases of
// signed SDJWT creation

static func nonKeyBondedSDJWT<KeyType>(sdJwt: SDJWT, issuersPrivateKey: KeyType) throws -> SignedSDJWT {
try .init(sdJwt: sdJwt, issuersPrivateKey: issuersPrivateKey) ?? {
throw SDJWTVerifierError.invalidJwt
}()
}

static func keyBondedSDJWT<KeyType>(signedSDJWT: SignedSDJWT, kbJWT: JWT, holdersPrivateKey: KeyType) throws -> SignedSDJWT {
try .init(signedSDJWT: signedSDJWT, kbJWT: kbJWT, holdersPrivateKey: holdersPrivateKey) ?? {
throw SDJWTVerifierError.invalidJwt
}()
}

private static func createSignedJWT<KeyType>(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,
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
}
}
Expand All @@ -180,14 +180,29 @@ extension SignedSDJWT {
func serialised(serialiser: (SignedSDJWT) -> (SerialiserProtocol)) throws -> Data {
serialiser(self).data
}

func serialised(serialiser: (SignedSDJWT) -> (SerialiserProtocol)) throws -> String {
serialiser(self).serialised
}
}

extension SignedSDJWT {
public func recreateClaims() throws -> ClaimExtractorResult {
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
)
}
}
80 changes: 61 additions & 19 deletions Sources/Parser/CompactParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,47 +22,89 @@ public enum SerialisationFormat {
}

public class CompactParser: ParserProtocol {

// MARK: - Properties


private static let TILDE = "~"

var serialisedString: String
var serialisationFormat: SerialisationFormat = .serialised
// MARK: - Lifecycle

public required init(serialiserProtocol: SerialiserProtocol) {
self.serialisedString = serialiserProtocol.serialised
}

public init(serialisedString: String) {
self.serialisedString = serialisedString
}

// MARK: - Methods

public func getSignedSdJwt() throws -> SignedSDJWT {
let (serialisedJWT, disclosuresInBase64, serialisedKBJWT) = try self.parseCombined()
return try SignedSDJWT(serializedJwt: serialisedJWT, disclosures: disclosuresInBase64, serializedKbJwt: serialisedKBJWT)
}


func extractJWTParts(_ jwt: String) throws -> (String, String, String) {
// Split the JWT string into its components: header, payload, signature
let parts = jwt.split(separator: ".")

// Ensure that we have exactly 3 parts (header, payload, signature)
guard parts.count == 3 else {
throw SDJWTVerifierError.parsingError
}

var header: Substring?
var payload: Substring?
var signature: Substring?

// Iterate over the components and assign them to respective variables
for (index, part) in parts.enumerated() {
switch index {
case 0:
header = part // Assigning Substring
case 1:
payload = part // Assigning Substring
case 2:
signature = part // Assigning Substring
default:
break
}
}

// Ensure that all components are properly assigned
guard let unwrappedHeader = header,
let unwrappedPayload = payload,
let unwrappedSignature = signature else {
throw SDJWTVerifierError.parsingError
}

// Convert Substring to String just before returning
return (String(unwrappedHeader), String(unwrappedPayload), String(unwrappedSignature))

}

private func parseCombined() throws -> (String, [Disclosure], String?) {
let parts = self.serialisedString
.split(separator: "~")
.map {String($0)}

guard parts.count > 1 else {
throw SDJWTVerifierError.parsingError
}

let jwt = String(parts[0])
if serialisedString.hasSuffix("~") == true {
// means no key binding is present
let disclosures = parts[safe: 1..<parts.count]?.compactMap({String($0)})

return (jwt, disclosures ?? [], nil)
} else {
// means we have key binding jwt
let disclosures = parts[safe: 1..<parts.count-1]?.compactMap({String($0)})
let kbJwt = String(parts[parts.count - 1])
return (jwt, disclosures ?? [], kbJwt)
}

if serialisedString.hasSuffix("~") == true {
// means no key binding is present
let disclosures = parts[safe: 1..<parts.count]?.compactMap({String($0)})

return (jwt, disclosures ?? [], nil)
} else {
// means we have key binding jwt
let disclosures = parts[safe: 1..<parts.count-1]?.compactMap({String($0)})
let kbJwt = String(parts[parts.count - 1])
return (jwt, disclosures ?? [], kbJwt)
}
}
}
Loading

0 comments on commit 7715625

Please sign in to comment.