diff --git a/Sources/AriesFramework/agent/Agent.swift b/Sources/AriesFramework/agent/Agent.swift index 5292fa9..ca5910c 100644 --- a/Sources/AriesFramework/agent/Agent.swift +++ b/Sources/AriesFramework/agent/Agent.swift @@ -13,6 +13,7 @@ public class Agent { public var connectionService: ConnectionService! public var didExchangeService: DidExchangeService! public var peerDIDService: PeerDIDService! + public var jwsService: JwsService! var messageSender: MessageSender! var messageReceiver: MessageReceiver! public var dispatcher: Dispatcher! @@ -46,6 +47,7 @@ public class Agent { self.connectionService = ConnectionService(agent: self) self.didExchangeService = DidExchangeService(agent: self) self.peerDIDService = PeerDIDService(agent: self) + self.jwsService = JwsService(agent: self) self.messageSender = MessageSender(agent: self) self.messageReceiver = MessageReceiver(agent: self) self.dispatcher = Dispatcher(agent: self) diff --git a/Sources/AriesFramework/agent/decorators/Attachment.swift b/Sources/AriesFramework/agent/decorators/Attachment.swift index f2a2f12..a427255 100644 --- a/Sources/AriesFramework/agent/decorators/Attachment.swift +++ b/Sources/AriesFramework/agent/decorators/Attachment.swift @@ -51,7 +51,7 @@ public struct Attachment: Codable { } } - public static func fromData(_ data: Data, id: String) -> Attachment { + public static func fromData(_ data: Data, id: String = UUID().uuidString) -> Attachment { return Attachment( id: id, mimetype: "application/json", diff --git a/Sources/AriesFramework/connection/DidExchangeService.swift b/Sources/AriesFramework/connection/DidExchangeService.swift index 197f403..0d6f008 100644 --- a/Sources/AriesFramework/connection/DidExchangeService.swift +++ b/Sources/AriesFramework/connection/DidExchangeService.swift @@ -119,6 +119,13 @@ public class DidExchangeService { let message = DidExchangeResponseMessage(threadId: threadId, did: peerDid) message.thread = ThreadDecorator(threadId: threadId) + let payload = peerDid.data(using: .utf8)! + let signingKey = connectionRecord.getTags()["invitationKey"] ?? connectionRecord.verkey + let jws = try await agent.jwsService.createJws(payload: payload, verkey: signingKey) + var attachment = Attachment.fromData(payload) + attachment.addJws(jws) + message.didRotate = attachment + try await updateState(connectionRecord: &connectionRecord, newState: .Responded) return OutboundMessage(payload: message, connection: connectionRecord) @@ -150,15 +157,38 @@ public class DidExchangeService { throw AriesFrameworkError.frameworkError("Invalid or missing thread ID") } + try verifyDidRotate(message: message, connectionRecord: connectionRecord) + let didDoc = try agent.peerDIDService.parsePeerDID(message.did) connectionRecord.theirDid = didDoc.id connectionRecord.theirDidDoc = didDoc - // TODO: Verify the signature of message.didRotate attachment try await updateState(connectionRecord: &connectionRecord, newState: ConnectionState.Responded) return connectionRecord } + func verifyDidRotate(message: DidExchangeResponseMessage, connectionRecord: ConnectionRecord) throws { + guard let didRotateAttachment = message.didRotate, + let jws = didRotateAttachment.data.jws, + let base64Payload = didRotateAttachment.data.base64, + let payload = Data(base64Encoded: base64Payload) else { + throw AriesFrameworkError.frameworkError("Missing valid did_rotate in response: \(String(describing: message.didRotate))") + } + + let signedDid = String(data: payload, encoding: .utf8) + if message.did != signedDid { + throw AriesFrameworkError.frameworkError("DID Rotate attachment's did \(String(describing: signedDid)) does not correspond to message did \(message.did)") + } + + let (isValid, signer) = try agent.jwsService.verifyJws(jws: jws, payload: payload) + let senderKeys = try connectionRecord.outOfBandInvitation!.fingerprints().map { + try DIDParser.ConvertFingerprintToVerkey(fingerprint: $0) + } + if !isValid || !senderKeys.contains(signer) { + throw AriesFrameworkError.frameworkError("Failed to verify did rotate signature. isValid: \(isValid), signer: \(signer), senderKeys: \(senderKeys)") + } + } + /** Create a DID exchange complete message for the connection with the specified connection id. diff --git a/Sources/AriesFramework/connection/JwsService.swift b/Sources/AriesFramework/connection/JwsService.swift new file mode 100644 index 0000000..afb87a1 --- /dev/null +++ b/Sources/AriesFramework/connection/JwsService.swift @@ -0,0 +1,88 @@ + +import Foundation +import os +import Base58Swift + +public class JwsService { + let agent: Agent + let logger = Logger(subsystem: "AriesFramework", category: "JwsService") + + init(agent: Agent) { + self.agent = agent + } + + /** + Creates a JWS using the given payload and verkey. + + - Parameters: + - payload: The payload to sign. + - verkey: The verkey to sign the payload for. The verkey should be created using ``Wallet.createDid(seed:)``. + - Returns: A JWS object. + */ + public func createJws(payload: Data, verkey: String) async throws -> JwsGeneralFormat { + guard let keyEntry = try await agent.wallet.session!.fetchKey(name: verkey, forUpdate: false) else { + throw AriesFrameworkError.frameworkError("Unable to find key for verkey: \(verkey)") + } + let key = try keyEntry.loadLocalKey() + let jwkJson = try key.toJwkPublic(alg: nil).data(using: .utf8)! + guard let jwk = try JSONSerialization.jsonObject(with: jwkJson) as? [String: Any] else { + throw AriesFrameworkError.frameworkError("Unable to parse JWK JSON: \(jwkJson)") + } + let protectedHeader = [ + "alg": "EdDSA", + "jwk": jwk + ] as [String: Any] + let protectedHeaderJson = try JSONSerialization.data(withJSONObject: protectedHeader) + let base64ProtectedHeader = protectedHeaderJson.base64EncodedString().base64ToBase64url() + let base64Payload = payload.base64EncodedString().base64ToBase64url() + + let message = "\(base64ProtectedHeader).\(base64Payload)".data(using: .utf8)! + let signature = try key.signMessage(message: message, sigType: nil) + let base64Signature = signature.base64EncodedString().base64ToBase64url() + let header = [ + "kid": try DIDParser.ConvertVerkeyToDidKey(verkey: verkey) + ] + + return JwsGeneralFormat(header: header, signature: base64Signature, protected: base64ProtectedHeader) + } + + /** + Verifies the given JWS against the given payload. + + - Parameters: + - jws: The JWS to verify. + - payload: The payload to verify the JWS against. + - Returns: A tuple containing the validity of the JWS and the signer's verkey. + */ + public func verifyJws(jws: Jws, payload: Data) throws -> (isValid: Bool, signer: String) { + logger.debug("Verifying JWS...") + var firstSig: JwsGeneralFormat! + switch jws { + case let .flattened(list): + if list.signatures.count == 0 { + throw AriesFrameworkError.frameworkError("No signatures found in JWS") + } + firstSig = list.signatures.first! + case let .general(jws): + firstSig = jws + } + guard let protectedJson = Data(base64Encoded: firstSig.protected.base64urlToBase64()), + let protected = try JSONSerialization.jsonObject(with: protectedJson) as? [String: Any], + let signature = Data(base64Encoded: firstSig.signature.base64urlToBase64()), + let jwk = protected["jwk"] else { + throw AriesFrameworkError.frameworkError("Invalid Jws: \(firstSig)") + } + let jwkData = try JSONSerialization.data(withJSONObject: jwk) + let jwkString = String(data: jwkData, encoding: .utf8)! + logger.debug("jwk: \(jwkString)") + let key = try agent.wallet.keyFactory.fromJwk(jwk: jwkString) + let publicBytes = try key.toPublicBytes() + let signer = Base58.base58Encode([UInt8](publicBytes)) + + let base64Payload = payload.base64EncodedString().base64ToBase64url() + let message = "\(firstSig.protected).\(base64Payload)".data(using: .utf8)! + let isValid = try key.verifySignature(message: message, signature: signature, sigType: nil) + + return (isValid, signer) + } +} diff --git a/Tests/AriesFrameworkTests/connection/JwsServiceTest.swift b/Tests/AriesFrameworkTests/connection/JwsServiceTest.swift new file mode 100644 index 0000000..f1613aa --- /dev/null +++ b/Tests/AriesFrameworkTests/connection/JwsServiceTest.swift @@ -0,0 +1,54 @@ +import XCTest +@testable import AriesFramework + +class JwsServiceTest: XCTestCase { + var agent: Agent! + let seed = "00000000000000000000000000000My2" + let verkey = "kqa2HyagzfMAq42H5f9u3UMwnSBPQx2QfrSyXbUPxMn" + let payload = "hello".data(using: .utf8)! + + override func setUp() async throws { + try await super.setUp() + let config = try TestHelper.getBaseConfig(name: "alice") + agent = Agent(agentConfig: config, agentDelegate: nil) + try await agent.initialize() + + let (_, newKey) = try await agent.wallet.createDid(seed: seed) + XCTAssertEqual(newKey, verkey) + } + + override func tearDown() async throws { + try await agent.reset() + try await super.tearDown() + } + + func testCreateAndVerify() async throws { + let jws = try await agent.jwsService.createJws(payload: payload, verkey: verkey) + XCTAssertEqual("did:key:z6MkfD6ccYE22Y9pHKtixeczk92MmMi2oJCP6gmNooZVKB9A", jws.header?["kid"]) + let protectedJson = Data(base64Encoded: jws.protected.base64urlToBase64())! + let protected = try JSONSerialization.jsonObject(with: protectedJson) as? [String: Any] + XCTAssertEqual("EdDSA", protected?["alg"] as? String) + XCTAssertNotNil(protected?["jwk"]) + + let (isValid, signer) = try agent.jwsService.verifyJws(jws: .general(jws), payload: payload) + XCTAssertTrue(isValid) + XCTAssertEqual(signer, verkey) + } + + func testFlattenedJws() async throws { + let jws = try await agent.jwsService.createJws(payload: payload, verkey: verkey) + let list: Jws = .flattened(JwsFlattenedFormat(signatures: [jws])) + + let (isValid, signer) = try agent.jwsService.verifyJws(jws: list, payload: payload) + XCTAssertTrue(isValid) + XCTAssertEqual(signer, verkey) + } + + func testVerifyFail() async throws { + let wrongPayload = "world".data(using: .utf8)! + let jws = try await agent.jwsService.createJws(payload: payload, verkey: verkey) + let (isValid, signer) = try agent.jwsService.verifyJws(jws: .general(jws), payload: wrongPayload) + XCTAssertFalse(isValid) + XCTAssertEqual(signer, verkey) + } +}