Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Holder presentation #39

Merged
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
59 changes: 46 additions & 13 deletions Sources/Claim/ClaimsExtractor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,32 @@
*/
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

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] = []
Expand All @@ -41,46 +50,71 @@ 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 {
throw SDJWTVerifierError.nonUniqueDisclosures
}

json[foundDisclosure.key] = foundDisclosure.value

if let disclosure = digestsOfDisclosures[foundDigest] {
let currentJsonPointer = "/" + (currentPath + [foundDisclosure.key]).joined(separator: "/")
visitor?.call(
pointer: .init(
pointer: currentJsonPointer
),
disclosure: disclosure,
value: foundDisclosure.value.string
)
}
foundDigests.append(.object(foundDigest))

} else {
json.dictionaryObject?.removeValue(forKey: Keys.sd.rawValue)
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]?
if let foundDisclosedArrayElement = digestsOfDisclosures[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),
!ifHasNested.digestsFoundOnPayload.isEmpty {
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
}
Expand All @@ -89,7 +123,6 @@ public class ClaimExtractor {
}
}
}

return (foundDigests, json)
return (foundDigests, json, visitor?.disclosuresPerClaim)
}
}
9 changes: 3 additions & 6 deletions Sources/Factory/SDJWTFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -117,29 +119,24 @@ 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)
let (disclosure, digest) = try self.discloseArrayElement(value: claimSet.value)
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)
}

Expand Down
163 changes: 12 additions & 151 deletions Sources/Issuer/SDJWT.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import SwiftyJSON

public typealias KBJWT = JWT

struct SDJWT {
public struct SDJWT {

// MARK: - Properties

Expand All @@ -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
Expand All @@ -50,7 +54,7 @@ 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 {
Expand All @@ -62,155 +66,12 @@ 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?

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 ?? "")
}

init?(json: JSON) throws {
let triple = try JwsJsonSupport.parseJWSJson(unverifiedSdJwt: json)
self.jwt = triple.jwt
self.disclosures = triple.disclosures
self.kbJwt = triple.kbJwt
}

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,
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()),
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
)
}
}
Loading