Skip to content

Commit

Permalink
Verify did_rotate attachment signature (#107)
Browse files Browse the repository at this point in the history
Signed-off-by: conanoc <[email protected]>
  • Loading branch information
conanoc authored Apr 26, 2024
1 parent 3dd1084 commit 6f9b179
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 2 deletions.
2 changes: 2 additions & 0 deletions Sources/AriesFramework/agent/Agent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion Sources/AriesFramework/agent/decorators/Attachment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
32 changes: 31 additions & 1 deletion Sources/AriesFramework/connection/DidExchangeService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
88 changes: 88 additions & 0 deletions Sources/AriesFramework/connection/JwsService.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
54 changes: 54 additions & 0 deletions Tests/AriesFrameworkTests/connection/JwsServiceTest.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}

0 comments on commit 6f9b179

Please sign in to comment.