Skip to content

Commit

Permalink
Merge pull request #39 from niscy-eudiw/feature/holder-presentation
Browse files Browse the repository at this point in the history
Holder presentation
  • Loading branch information
dtsiflit authored Oct 21, 2024
2 parents d068fe2 + 0dceba3 commit 4e09ad5
Show file tree
Hide file tree
Showing 18 changed files with 640 additions and 199 deletions.
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

0 comments on commit 4e09ad5

Please sign in to comment.