Skip to content

Commit

Permalink
Merge pull request #12 from decentralised-dataexchange/main
Browse files Browse the repository at this point in the history
Support for EdDSA, signature and expiry validation
  • Loading branch information
josmilan authored Aug 12, 2024
2 parents 5b7a506 + 20e9b2b commit f241f45
Show file tree
Hide file tree
Showing 13 changed files with 466 additions and 50 deletions.
13 changes: 11 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/attaswift/BigInt.git",
"state" : {
"revision" : "0ed110f7555c34ff468e72e1686e59721f2b0da6",
"version" : "5.3.0"
"revision" : "793a7fac0bfc318e85994bf6900652e827aef33e",
"version" : "5.4.1"
}
},
{
Expand Down Expand Up @@ -44,6 +44,15 @@
"revision" : "7038d35f98505f364d4cc50b65ab81de6a3d336c",
"version" : "2024.5.1"
}
},
{
"identity" : "swift-crypto",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-crypto.git",
"state" : {
"revision" : "46072478ca365fe48370993833cb22de9b41567f",
"version" : "3.5.2"
}
}
],
"version" : 2
Expand Down
8 changes: 5 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/keefertaylor/Base58Swift.git", branch: "master"),
.package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "1.8.1"),
.package(url: "https://github.com/decentralised-dataexchange/PresentationExchangeSdkiOS.git", .upToNextMajor(from: "2024.3.1")),
.package(url: "https://github.com/airsidemobile/JOSESwift.git", from: "2.3.0")
.package(url: "https://github.com/decentralised-dataexchange/PresentationExchangeSdkiOS.git", .upToNextMajor(from: "2024.8.1")),
.package(url: "https://github.com/airsidemobile/JOSESwift.git", from: "2.3.0"),
.package(url: "https://github.com/apple/swift-crypto.git", from:"3.5.2")
],
targets: [
.target(
name: "eudiWalletOidcIos",
dependencies: ["Base58Swift", "CryptoSwift", "PresentationExchangeSdkiOS", "JOSESwift"]),
dependencies: ["Base58Swift", "CryptoSwift", "PresentationExchangeSdkiOS", "JOSESwift", .product(name: "Crypto", package: "swift-crypto")],
path: "Sources"),
.testTarget(
name: "eudi-wallet-oidc-iosTests",
dependencies: ["eudiWalletOidcIos"]),
Expand Down
13 changes: 13 additions & 0 deletions Sources/eudiWalletOidcIos/Helper/CryptographicAlgorithms.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// File.swift
//
//
// Created by Milan on 25/07/24.
//

import Foundation

public enum CryptographicAlgorithms: String {
case ES256 = "ES256"
case EdDSA = "EdDSA"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// File.swift
//
//
// Created by Milan on 25/07/24.
//

import Foundation
import Crypto

public class EDDSAKeyHandler:NSObject, SecureKeyProtocol{

public var keyStorageType: SecureKeyTypes = .eddsa

public func generateSecureKey() -> SecureKeyData?{
let privateKey = Curve25519.Signing.PrivateKey()
return SecureKeyData(publicKey: privateKey.publicKey.rawRepresentation, privateKey: privateKey.rawRepresentation)
}


public func sign(payload: String, header: Data, withKey privateKey: Data?) -> String?{
if let privateKeyData = privateKey{
do{

let payloadData = Data(payload.utf8)
let unsignedToken = "\(header.base64URLEncodedString()).\(payloadData.base64URLEncodedString())"
if let data = unsignedToken.data(using: .utf8){
let privateKey = try Curve25519.Signing.PrivateKey(rawRepresentation: privateKeyData)
let signedData = try privateKey.signature(for: data)
let idToken = "\(unsignedToken).\(signedData.urlSafeBase64EncodedString()))"
return idToken
}

}
catch{
return nil
}
}
return nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Foundation
//Enum to identify the type of key generation class used like CryptoKitHandler, SecureEnclaveHandler
public enum SecureKeyTypes{
case cryptoKit
case eddsa
case secureEnclave
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// File.swift
//
//
// Created by iGrant on 25/07/24.
//

import Foundation

protocol CredentialValidaorProtocol {
func validateCredential(jwt: String?, jwksURI: String?) async throws
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// File.swift
//
//
// Created by iGrant on 24/07/24.
//

import Foundation
import Base58Swift

public enum ValidationError: Error {
case JWTExpired
case signatureExpired
}

public class CredentialValidatorService: CredentialValidaorProtocol {
public static var shared = CredentialValidatorService()
public init() {}

public func validateCredential(jwt: String?, jwksURI: String?) async throws {
let isJWTExpired = ExpiryValidator.validateExpiryDate(jwt: jwt) ?? false
let isSignatureExpied = await SignatureValidator.validateSign(jwt: jwt, jwksURI: jwksURI) ?? false
if isJWTExpired {
throw ValidationError.JWTExpired
}
if !isSignatureExpied {
throw ValidationError.signatureExpired
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// File.swift
//
//
// Created by iGrant on 25/07/24.
//

import Foundation

class ExpiryValidator {

static func validateExpiryDate(jwt: String?) -> Bool? {
guard let split = jwt?.split(separator: "."),
let jsonString = "\(split[1])".decodeBase64(),
let jsonObject = UIApplicationUtils.shared.convertStringToDictionary(text: jsonString) else { return false }
guard let vc = jsonObject["vc"] as? [String: Any], let expirationDate = vc["expirationDate"] as? String else { return false }
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
guard let expiryDate = dateFormatter.date(from: expirationDate) else { return false}
let currentDate = Date()
if currentDate <= expiryDate {
return false
} else {
return true
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
//
// File.swift
//
//
// Created by iGrant on 25/07/24.
//

import Foundation
import Base58Swift
import Security
import CryptoKit

class SignatureValidator {

static func validateSign(jwt: String?, jwksURI: String?) async -> Bool? {
var jwk: [String: Any] = [:]
guard let split = jwt?.split(separator: "."),
let jsonString = "\(split[0])".decodeBase64(),
let jsonObject = UIApplicationUtils.shared.convertStringToDictionary(text: jsonString) else { return false }
if let kid = jsonObject["kid"] as? String {
if kid.hasPrefix("did:key:z") {
jwk = processJWKfromKid(did: kid)
} else if kid.hasPrefix("did:ebsi:z") {
jwk = await processJWKforEBSI(did: kid)
} else {
jwk = await processJWKFromJwksURI2(kid: kid, jwksURI: jwksURI)
}
} else {
let kid = jsonObject["kid"] as? String
jwk = await processJWKFromJwksURI2(kid: kid, jwksURI: jwksURI)
}
return validateSignature(jwt: jwt, jwk: jwk)
}


static func processJWKfromKid(did: String?) -> [String: Any] {
guard let did = did else { return [:]}
let components = did.split(separator: "#")
guard let didPart = components.first else {
return [:]
}
return DidService.shared.createJWKfromDID(did: String(didPart))
}

static func processJWKforEBSI(did: String?) async -> [String: Any]{
guard let did = did else { return [:]}
let ebsiEndPoint = "https://api-conformance.ebsi.eu/did-registry/v5/identifiers/\(did)"
do {
guard let url = URL(string: ebsiEndPoint) else { return [:]}
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { return [:]}
guard let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], let verificationMethods = jsonObject["verificationMethod"] as? [[String: Any]] else { return [:]}
for data in verificationMethods {
if let publicKeyJwk = data["publicKeyJwk"] as? [String: Any], let crv = publicKeyJwk["crv"] as? String, crv == "P-256" {
return publicKeyJwk
}
}
} catch {
print("error")
}
return [:]
}

static func processJWKFromJwksURI2(kid: String?, jwksURI: String?) async -> [String: Any] {
guard let jwksURI = jwksURI else {return [:]}
return await fetchJwkData(kid: kid, jwksUri: jwksURI)
}

static func fetchJwkData(kid: String?, jwksUri: String)async -> [String: Any] {
guard let url = URL(string: jwksUri) else {
return [:]
}
do {
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { return [:]}
guard let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], let keys = jsonObject["keys"] as? [[String: Any]] else { return [:]}

var jwkKey: [String: Any]? = keys.first { $0["use"] as? String == "sig" }

if jwkKey == nil, let kid = kid {
jwkKey = keys.first { $0["kid"] as? String == kid }
}
return jwkKey ?? [:]

} catch {
print("error")
}
return [:]
}

static private func validateSignature(jwt: String?, jwk: [String: Any]) -> Bool? {
let segments = jwt?.split(separator: ".")
guard segments?.count == 3 else {
return true
}
let headerData = String(segments?[0] ?? "")
let payloadData = String(segments?[1] ?? "")
var sigatureData = String(segments?[2] ?? "")
if sigatureData.contains("~") {
let splitData = sigatureData.split(separator: "~")
sigatureData = String(splitData[0])
}
guard let headerEncoded = Data(base64URLEncoded: headerData) else { return false }
guard let signatureEncoded = Data(base64URLEncoded: sigatureData) else { return false }
guard let headerJson = try? JSONSerialization.jsonObject(with: headerEncoded, options: []) as? [String: Any], let alg = headerJson["alg"] as? String else {
return false
}
guard let crv = jwk["crv"] as? String else {
return false
}
let algToCrvMap: [String: String] = [
"ES256": "P-256",
"ES384": "P-384",
"ES512": "P-521"
]
if let expectedCrv = algToCrvMap[alg], expectedCrv != crv {
return false
}
guard let publicKey = extractPublicKey(from: jwk) else {
return false
}

let signedData = "\(headerData).\(payloadData)".data(using: .utf8)!
let isVerified = verifySignature(signature: signatureEncoded, for: signedData, using: publicKey)

return isVerified
}

static private func extractPublicKey(from jwk: [String: Any]) -> P256.Signing.PublicKey? {
guard let crv = jwk["crv"] as? String, crv == "P-256",
let x = jwk["x"] as? String,
let y = jwk["y"] as? String,
let xData = Data(base64URLEncoded: x),
let yData = Data(base64URLEncoded: y) else {
return nil
}

var publicKeyData = Data()
publicKeyData.append(0x04)
publicKeyData.append(xData)
publicKeyData.append(yData)

do {
let publicKey = try P256.Signing.PublicKey(x963Representation: publicKeyData)
return publicKey
} catch {
print("Error creating public key: \(error)")
return nil
}
}

static private func verifySignature(signature: Data, for data: Data, using publicKey: P256.Signing.PublicKey) -> Bool {
guard let ecdsaSignature = try? P256.Signing.ECDSASignature(rawRepresentation: signature) else {
print("Error converting signature to ECDSASignature")
return false
}
return publicKey.isValidSignature(ecdsaSignature, for: data)
}

}
Loading

0 comments on commit f241f45

Please sign in to comment.