Skip to content

Commit

Permalink
feat(pollux): add jwt credential revocation support
Browse files Browse the repository at this point in the history
Fixes ATL-7034

Signed-off-by: goncalo-frade-iohk <goncalo.frade@iohk.io>
  • Loading branch information
goncalo-frade-iohk committed Jul 1, 2024
1 parent 068acdd commit 3f2a698
Show file tree
Hide file tree
Showing 14 changed files with 241 additions and 10 deletions.
13 changes: 13 additions & 0 deletions Core/Sources/Helpers/JSONDecoder+Helper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,17 @@ public extension JSONDecoder {
decoder.keyDecodingStrategy = .convertFromSnakeCase
return decoder
}

static func backup() -> JSONDecoder {
let decoder = JSONDecoder()
decoder.dataDecodingStrategy = .base64
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .custom({ decoder in
let container = try decoder.singleValueContainer()
let seconds = try container.decode(Int.self)
let date = Date(timeIntervalSince1970: TimeInterval(seconds))
return date
})
return decoder
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Foundation

/// `RevocableCredential` is a protocol that defines the attributes and behaviors
/// of a credential that can be revoked or suspended.
public protocol RevocableCredential {
/// Indicates whether the credential can be revoked.
var canBeRevoked: Bool { get }

/// Indicates whether the credential can be suspended.
var canBeSuspended: Bool { get }

/// Checks if the credential is currently revoked.
///
/// - Returns: A Boolean value indicating whether the credential is revoked.
/// - Throws: An error if the status cannot be determined.
var isRevoked: Bool { get async throws }

/// Checks if the credential is currently suspended.
///
/// - Returns: A Boolean value indicating whether the credential is suspended.
/// - Throws: An error if the status cannot be determined.
var isSuspended: Bool { get async throws }
}

public extension Credential {
/// A Boolean value indicating whether the credential can verify revocability.
var isRevocable: Bool { self is RevocableCredential }

/// Returns the revocable representation of the credential.
var revocable: RevocableCredential? { self as? RevocableCredential }
}
18 changes: 18 additions & 0 deletions EdgeAgentSDK/Domain/Sources/Models/Errors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,14 @@ public enum PolluxError: KnownPrismError {
/// An error case indicating that the signature is invalid, with internal errors specified.
case invalidSignature(internalErrors: [Error] = [])

/// An error case indicating that the credential is revoked.
/// - Parameter jwtString: The JWT string representing the revoked credential.
case credentialIsRevoked(jwtString: String)

/// An error case indicating that the credential is suspended.
/// - Parameter jwtString: The JWT string representing the suspended credential.
case credentialIsSuspended(jwtString: String)

/// The error code returned by the server.
public var code: Int {
switch self {
Expand Down Expand Up @@ -862,6 +870,10 @@ public enum PolluxError: KnownPrismError {
return 76
case .invalidSignature:
return 77
case .credentialIsRevoked:
return 78
case .credentialIsSuspended:
return 79
}
}

Expand Down Expand Up @@ -942,6 +954,12 @@ Cannot verify input descriptor field \(name.map { "with name: \($0)"} ?? ""), wi
"""
case .invalidSignature:
return "Could not verify one or more JWT signatures"

case .credentialIsRevoked(let jwtString):
return "Credential (\(jwtString)) is revoked"

case .credentialIsSuspended(let jwtString):
return "Credential (\(jwtString)) is suspended"
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+Backup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ extension EdgeAgent {
let messages = messages.compactMap { messageStr -> (Message, Message.Direction)? in
guard
let messageData = Data(base64URLEncoded: messageStr),
let message = try? JSONDecoder.didComm().decode(Message.self, from: messageData)
let message = try? JSONDecoder.backup().decode(Message.self, from: messageData)
else {
return nil
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Domain
import Foundation

extension JWTCredential: RevocableCredential {
public var canBeRevoked: Bool {
self.jwtVerifiableCredential.verifiableCredential.credentialStatus?.statusPurpose == .revocation
}

public var canBeSuspended: Bool {
self.jwtVerifiableCredential.verifiableCredential.credentialStatus?.statusPurpose == .suspension
}

public var isRevoked: Bool {
get async throws {
guard canBeRevoked else { return false }
return try await JWTRevocationCheck(credential: self).checkIsRevoked()
}
}

public var isSuspended: Bool {
get async throws {
guard canBeSuspended else { return false }
return try await JWTRevocationCheck(credential: self).checkIsRevoked()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ extension JWTPayload.JWTVerfiableCredential: Codable {
}
let credentialSubject = try container.decode(AnyCodable.self, forKey: .credentialSubject)
let credentialStatus = try? container.decode(
VerifiableCredentialTypeContainer.self,
JWTRevocationStatus.self,
forKey: .credentialStatus
)
let credentialSchema = try? container.decode(
Expand Down
4 changes: 2 additions & 2 deletions EdgeAgentSDK/Pollux/Sources/Models/JWT/JWTPayload.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ struct JWTPayload {
let type: Set<String>
let credentialSchema: VerifiableCredentialTypeContainer?
let credentialSubject: AnyCodable
let credentialStatus: VerifiableCredentialTypeContainer?
let refreshService: VerifiableCredentialTypeContainer?
let evidence: VerifiableCredentialTypeContainer?
let termsOfUse: VerifiableCredentialTypeContainer?
let credentialStatus: JWTRevocationStatus?

/**
Initializes a new instance of `JWTVerifiableCredential`.
Expand All @@ -42,7 +42,7 @@ struct JWTPayload {
type: Set<String> = Set(),
credentialSchema: VerifiableCredentialTypeContainer? = nil,
credentialSubject: AnyCodable,
credentialStatus: VerifiableCredentialTypeContainer? = nil,
credentialStatus: JWTRevocationStatus? = nil,
refreshService: VerifiableCredentialTypeContainer? = nil,
evidence: VerifiableCredentialTypeContainer? = nil,
termsOfUse: VerifiableCredentialTypeContainer? = nil
Expand Down
76 changes: 76 additions & 0 deletions EdgeAgentSDK/Pollux/Sources/Models/JWT/JWTRevocationCheck.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import Domain
import Foundation
import Gzip
import JSONWebSignature

struct JWTRevocationCheck {
let credential: JWTCredential

init(credential: JWTCredential) {
self.credential = credential
}

func checkIsRevoked() async throws -> Bool {
guard let status = credential.jwtVerifiableCredential.verifiableCredential.credentialStatus else {
return false
}

guard status.type == "StatusList2021Entry" else {
throw UnknownError.somethingWentWrongError(customMessage: nil, underlyingErrors: nil)
}

let listData = try await DownloadDataWithResolver()
.downloadFromEndpoint(urlOrDID: status.statusListCredential)
let statusList = try JSONDecoder.didComm().decode(JWTRevocationStatusListCredential.self, from: listData)
let encodedList = statusList.credentialSubject.encodedList
let index = status.statusListIndex
return try verifyRevocationOnEncodedList(encodedList.tryToData(), index: index)
}

func verifyRevocationOnEncodedList(_ list: Data, index: Int) throws -> Bool {
let encodedListData = try list.gunzipped()
let bitList = encodedListData.bytes.flatMap { $0.toBits() }
guard index < bitList.count else {
throw UnknownError.somethingWentWrongError(customMessage: "Revocation index out of bounds", underlyingErrors: nil)
}
return bitList[index]
}
}

extension UInt8 {
func toBits() -> [Bool] {
var bits = [Bool](repeating: false, count: 8)
for i in 0..<8 {
bits[7 - i] = (self & (1 << i)) != 0
}
return bits
}
}

fileprivate struct DownloadDataWithResolver: Downloader {

public func downloadFromEndpoint(urlOrDID: String) async throws -> Data {
let url: URL

if let validUrl = URL(string: urlOrDID.replacingOccurrences(of: "host.docker.internal", with: "localhost")) {
url = validUrl
} else {
throw CommonError.invalidURLError(url: urlOrDID)
}

let (data, urlResponse) = try await URLSession.shared.data(from: url)

guard
let code = (urlResponse as? HTTPURLResponse)?.statusCode,
200...299 ~= code
else {
throw CommonError.httpError(
code: (urlResponse as? HTTPURLResponse)?.statusCode ?? 500,
message: String(data: data, encoding: .utf8) ?? ""
)
}

return data
}
}

32 changes: 32 additions & 0 deletions EdgeAgentSDK/Pollux/Sources/Models/JWT/JWTRevocationStatus.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Foundation

struct JWTRevocationStatus: Codable {
enum CredentialStatusListType: String, Codable {
case statusList2021Entry = "StatusList2021Entry"
}

enum CredentialStatusPurpose: String, Codable {
case revocation
case suspension
}

let id: String
let type: String
let statusPurpose: CredentialStatusPurpose
let statusListIndex: Int
let statusListCredential: String
}

struct JWTRevocationStatusListCredential: Codable {
struct StatusListCredentialSubject: Codable {
let type: String
let statusPurpose: String
let encodedList: String
}
let context: [String]
let type: [String]
let id: String
let issuer: String
let issuanceDate: String
let credentialSubject: StatusListCredentialSubject
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ extension PolluxImpl {
}

private func verifyJWT(jwtString: String) async throws -> Bool {
try await verifyJWTCredentialRevocation(jwtString: jwtString)
let payload: DefaultJWTClaimsImpl = try JWT.getPayload(jwtString: jwtString)
guard let issuer = payload.iss else {
throw PolluxError.requiresThatIssuerExistsAndIsAPrismDID
Expand All @@ -135,6 +136,20 @@ extension PolluxImpl {
return !validations.isEmpty
}

private func verifyJWTCredentialRevocation(jwtString: String) async throws {
guard let credential = try? JWTCredential(data: jwtString.tryToData()) else {
return
}
let isRevoked = try await credential.isRevoked
let isSuspended = try await credential.isSuspended
guard isRevoked else {
throw PolluxError.credentialIsRevoked(jwtString: jwtString)
}
guard isSuspended else {
throw PolluxError.credentialIsSuspended(jwtString: jwtString)
}
}

private func getDefinition(id: String) async throws -> PresentationExchangeRequest {
guard
let request = try await pluto.getMessage(id: id).first().await(),
Expand Down
8 changes: 8 additions & 0 deletions EdgeAgentSDK/Pollux/Tests/JWTTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,12 @@ final class JWTTests: XCTestCase {
XCTAssertEqual(credential.claims.map(\.key).sorted(), ["id", "test"].sorted())
XCTAssertEqual(credential.id, validJWTString)
}

func testRevoked() throws {
let validJWTString = try "eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6cHJpc206MmU0MGZkNjkyYjgzYzE5ZjlhNTUzNjRjMmNhNWJmNjkyOGI4ODU1NGE1YmYxMTc0YTc4ZjY4NDk4ZDgwZGZjNjpDcmNCQ3JRQkVqa0tCV3RsZVMweEVBSktMZ29KYzJWamNESTFObXN4RWlFQ1pDbDV4aUREb3ZsVFlNNVVSeXdHODZPWjc2RWNTY3NjSEplaHRnbWNKTlFTT2dvR1lYVjBhQzB4RUFSS0xnb0pjMlZqY0RJMU5tc3hFaUVDRUMzTUNPak4xb1lNZjU2ZVVBaTA3NkxGX2hRZDRwbFFib3JKcnBkOHdHY1NPd29IYldGemRHVnlNQkFCU2k0S0NYTmxZM0F5TlRack1SSWhBeTVqVkc4UTRWOHRYV0RoUWNvb2xPTmFIdTZHaW5ockJ6SEtfRXYySW9yNSIsInN1YiI6ImRpZDpwcmlzbTo4ODYwN2Y4YjE3ZWJhZmNhODgwNDdmZDQ0YTMyZTE4NGI1MGYwM2QyNWZhZWQ1ZGRiYWQyZGRjNGYyZjg5YWYzOkNzY0JDc1FCRW1RS0QyRjFkR2hsYm5ScFkyRjBhVzl1TUJBRVFrOEtDWE5sWTNBeU5UWnJNUklncnFDMVhaN2ZsOUpLSjBNT3pTa2hSZFhESHpnSVQzTGJ1MlNLdTJvZWxKVWFJT3gxSzFvY2NDRG14SS05Zm9jRm84emhpTm5BYXBPUGFXQXY0UGg0azZjWkVsd0tCMjFoYzNSbGNqQVFBVUpQQ2dselpXTndNalUyYXpFU0lLNmd0VjJlMzVmU1NpZEREczBwSVVYVnd4ODRDRTl5Mjd0a2lydHFIcFNWR2lEc2RTdGFISEFnNXNTUHZYNkhCYVBNNFlqWndHcVRqMmxnTC1ENGVKT25HUSIsIm5iZiI6MTY4ODA1ODcyNywiZXhwIjoxNjg4MDYyMzI3LCJ2YyI6eyJjcmVkZW50aWFsU2NoZW1hIjp7ImlkIjoiaHR0cHM6XC9cL2s4cy1kZXYuYXRhbGFwcmlzbS5pb1wvcHJpc20tYWdlbnRcL3NjaGVtYS1yZWdpc3RyeVwvc2NoZW1hc1wvMDIwMTY5M2ItNGQ2ZC0zNmVjLWEzN2QtODFkODhlODcyNTM5IiwidHlwZSI6IkNyZWRlbnRpYWxTY2hlbWEyMDIyIn0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7InRlc3QiOiJUZXN0MSIsImlkIjoiZGlkOnByaXNtOjg4NjA3ZjhiMTdlYmFmY2E4ODA0N2ZkNDRhMzJlMTg0YjUwZjAzZDI1ZmFlZDVkZGJhZDJkZGM0ZjJmODlhZjM6Q3NjQkNzUUJFbVFLRDJGMWRHaGxiblJwWTJGMGFXOXVNQkFFUWs4S0NYTmxZM0F5TlRack1SSWdycUMxWFo3Zmw5SktKME1PelNraFJkWERIemdJVDNMYnUyU0t1Mm9lbEpVYUlPeDFLMW9jY0NEbXhJLTlmb2NGbzh6aGlObkFhcE9QYVdBdjRQaDRrNmNaRWx3S0IyMWhjM1JsY2pBUUFVSlBDZ2x6WldOd01qVTJhekVTSUs2Z3RWMmUzNWZTU2lkRERzMHBJVVhWd3g4NENFOXkyN3RraXJ0cUhwU1ZHaURzZFN0YUhIQWc1c1NQdlg2SEJhUE00WWpad0dxVGoybGdMLUQ0ZUpPbkdRIn0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiQGNvbnRleHQiOlsiaHR0cHM6XC9cL3d3dy53My5vcmdcLzIwMThcL2NyZWRlbnRpYWxzXC92MSJdfX0.JZBqArVFvWgj2W0b7vVPSKR3mSH_X-VOC-YQ_jyLZSOEYUkortkRGi41xwA7SPFSqPdSCHl4iagpBir1tYMBOw".tryToData()
let credential = try JWTCredential(data: validJWTString)
let encodedList = Data(fromBase64URL: "H4sIAAAAAAAA_-3BMQ0AAAACIGf_0MbwARoAAAAAAAAAAAAAAAAAAADgbbmHB0sAQAAA")!
XCTAssertFalse(try JWTRevocationCheck(credential: credential)
.verifyRevocationOnEncodedList(encodedList, index: 94567))
}
}
6 changes: 4 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,14 @@ let package = Package(
),
.package(url: "https://github.com/apple/swift-protobuf.git", from: "1.7.0"),
.package(url: "https://github.com/beatt83/didcomm-swift.git", from: "0.1.8"),
.package(url: "https://github.com/beatt83/jose-swift.git", from: "3.1.0"),
.package(url: "https://github.com/beatt83/jose-swift.git", from: "3.2.0"),
.package(url: "https://github.com/beatt83/peerdid-swift.git", from: "3.0.1"),
.package(url: "https://github.com/input-output-hk/anoncreds-rs.git", exact: "0.4.1"),
.package(url: "https://github.com/input-output-hk/atala-prism-apollo.git", exact: "1.3.3"),
.package(url: "https://github.com/KittyMac/Sextant.git", exact: "0.4.31"),
.package(url: "https://github.com/kylef/JSONSchema.swift.git", exact: "0.6.0"),
.package(url: "https://github.com/goncalo-frade-iohk/eudi-lib-sdjwt-swift.git", from: "0.0.2")
.package(url: "https://github.com/goncalo-frade-iohk/eudi-lib-sdjwt-swift.git", from: "0.0.2"),
.package(url: "https://github.com/1024jp/GzipSwift.git", exact: "6.0.0")
],
targets: [
.target(
Expand Down Expand Up @@ -121,6 +122,7 @@ let package = Package(
"jose-swift",
"Sextant",
"eudi-lib-sdjwt-swift",
.product(name: "Gzip", package: "GzipSwift"),
.product(name: "AnoncredsSwift", package: "anoncreds-rs"),
.product(name: "JSONSchema", package: "JSONSchema.swift")
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ protocol BackupViewModel: ObservableObject {

struct BackupView<ViewModel: BackupViewModel>: View {
@StateObject var viewModel: ViewModel
@Environment(\.dismiss) var dismiss
@State private var jwe: String = ""
var body: some View {
VStack(spacing: 10) {
VStack(spacing: 8) {
VStack(spacing: 25) {
VStack(spacing: 10) {
AtalaButton(
configuration: .primary,
action: {
Expand All @@ -38,6 +39,9 @@ struct BackupView<ViewModel: BackupViewModel>: View {
action: {
Task {
try await self.viewModel.backupWith(jwe)
await MainActor.run {
self.dismiss()
}
}
},
label: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ final class BackupViewModelImpl: BackupViewModel {
@Published var newJWE: String? = nil

private let agent: EdgeAgent

init(agent: EdgeAgent) {
self.agent = agent
}
Expand All @@ -20,6 +20,12 @@ final class BackupViewModelImpl: BackupViewModel {
}

func backupWith(_ jwe: String) async throws {
try await agent.recoverWallet(encrypted: jwe)
do {
try await agent.recoverWallet(encrypted: jwe)
} catch {
print(error)
print()
throw error
}
}
}

0 comments on commit 3f2a698

Please sign in to comment.