forked from EWC-consortium/eudi-wallet-oid4vc-ios
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #12 from decentralised-dataexchange/main
Support for EdDSA, signature and expiry validation
- Loading branch information
Showing
13 changed files
with
466 additions
and
50 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
13 changes: 13 additions & 0 deletions
13
Sources/eudiWalletOidcIos/Helper/CryptographicAlgorithms.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
41 changes: 41 additions & 0 deletions
41
Sources/eudiWalletOidcIos/Helper/EncryptionKeyHandler/EDDSA/EDDSAKeyHandler.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
12 changes: 12 additions & 0 deletions
12
Sources/eudiWalletOidcIos/Service/CredentialValidation/CredentialValidaorProtocol.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
31 changes: 31 additions & 0 deletions
31
Sources/eudiWalletOidcIos/Service/CredentialValidation/CredentialValidatorService.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
|
||
} |
29 changes: 29 additions & 0 deletions
29
Sources/eudiWalletOidcIos/Service/CredentialValidation/ExpiryValidator.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
|
||
} |
160 changes: 160 additions & 0 deletions
160
Sources/eudiWalletOidcIos/Service/CredentialValidation/SignatureValidator.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
|
||
} |
Oops, something went wrong.