diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index cf70d6ce0..498b0aa9a 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -2783,11 +2783,9 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = DApp/DApp.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 173; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = W5R8AG9K22; + DEVELOPMENT_TEAM = W5R8AG9K22; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = DApp/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = dApp; @@ -2807,7 +2805,6 @@ PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.dapp; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore com.walletconnect.dapp"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -2822,11 +2819,9 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = DApp/DAppRelease.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 173; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = W5R8AG9K22; + DEVELOPMENT_TEAM = W5R8AG9K22; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = DApp/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = dApp; @@ -2846,7 +2841,6 @@ PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.dapp; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore com.walletconnect.dapp"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -2859,11 +2853,9 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = PNDecryptionService/PNDecryptionService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 173; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = W5R8AG9K22; + DEVELOPMENT_TEAM = W5R8AG9K22; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = PNDecryptionService/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = PNDecryptionService; @@ -2878,7 +2870,6 @@ PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.walletapp.PNDecryptionService; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore com.walletconnect.walletapp.PNDecryptionService"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -2892,11 +2883,9 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = PNDecryptionService/PNDecryptionServiceRelease.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 173; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = W5R8AG9K22; + DEVELOPMENT_TEAM = W5R8AG9K22; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = PNDecryptionService/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = PNDecryptionService; @@ -2911,7 +2900,6 @@ PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.walletapp.PNDecryptionService; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore com.walletconnect.walletapp.PNDecryptionService"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -3075,11 +3063,9 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = WalletApp/WalletApp.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 173; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = W5R8AG9K22; + DEVELOPMENT_TEAM = W5R8AG9K22; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = WalletApp/Other/Info.plist; @@ -3098,7 +3084,6 @@ PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.walletapp; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore com.walletconnect.walletapp"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -3114,11 +3099,9 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = WalletApp/WalletAppRelease.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 173; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = W5R8AG9K22; + DEVELOPMENT_TEAM = W5R8AG9K22; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = WalletApp/Other/Info.plist; @@ -3137,7 +3120,6 @@ PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.walletapp; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore com.walletconnect.walletapp"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift index e3888e786..e52094049 100644 --- a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift +++ b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift @@ -115,12 +115,12 @@ final class RelayClientEndToEndTests: XCTestCase { expectationA.assertForOverFulfill = false expectationB.assertForOverFulfill = false - relayA.messagePublisher.sink { topic, payload, _ in + relayA.messagePublisher.sink { topic, payload, _, _ in (subscriptionATopic, subscriptionAPayload) = (topic, payload) expectationA.fulfill() }.store(in: &publishers) - relayB.messagePublisher.sink { topic, payload, _ in + relayB.messagePublisher.sink { topic, payload, _, _ in (subscriptionBTopic, subscriptionBPayload) = (topic, payload) Task(priority: .high) { sleep(1) diff --git a/Example/WalletApp/PresentationLayer/Wallet/AuthRequest/AuthRequestPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/AuthRequest/AuthRequestPresenter.swift index fdd623c2f..ce57ddd3d 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/AuthRequest/AuthRequestPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/AuthRequest/AuthRequestPresenter.swift @@ -87,7 +87,7 @@ final class AuthRequestPresenter: ObservableObject { /* Redirect */ if let uri = request.requester.redirect?.native { -// WalletConnectRouter.goBack(uri: uri) + WalletConnectRouter.goBack(uri: uri) router.dismiss() } else { showSignedSheet.toggle() diff --git a/Package.swift b/Package.swift index 7e5c3133c..797d5622c 100644 --- a/Package.swift +++ b/Package.swift @@ -121,7 +121,7 @@ let package = Package( path: "Sources/WalletConnectRouter/Router"), .target( name: "WalletConnectVerify", - dependencies: ["WalletConnectUtils", "WalletConnectNetworking"], + dependencies: ["WalletConnectUtils", "WalletConnectNetworking", "WalletConnectJWT"], resources: [.process("Resources/PrivacyInfo.xcprivacy")]), .target( name: "Database", diff --git a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift index 7270871d7..c8697ec5d 100644 --- a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift +++ b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift @@ -44,12 +44,12 @@ class WalletRequestSubscriber { Task(priority: .high) { let assertionId = payload.decryptedPayload.sha256().toHexString() do { - let response = try await verifyClient.verifyOrigin(assertionId: assertionId) - let verifyContext = verifyClient.createVerifyContext(origin: response.origin, domain: payload.request.payloadParams.domain, isScam: response.isScam) + let response = try await verifyClient.verify(.v1(assertionId: assertionId)) + let verifyContext = verifyClient.createVerifyContext(origin: response.origin, domain: payload.request.payloadParams.domain, isScam: response.isScam, isVerified: nil) verifyContextStore.set(verifyContext, forKey: request.id.string) onRequest?((request, verifyContext)) } catch { - let verifyContext = verifyClient.createVerifyContext(origin: nil, domain: payload.request.payloadParams.domain, isScam: nil) + let verifyContext = verifyClient.createVerifyContext(origin: nil, domain: payload.request.payloadParams.domain, isScam: nil, isVerified: nil) verifyContextStore.set(verifyContext, forKey: request.id.string) onRequest?((request, verifyContext)) return diff --git a/Sources/WalletConnectJWT/JWTValidator.swift b/Sources/WalletConnectJWT/JWTValidator.swift index e94a72bdf..e328c9cec 100644 --- a/Sources/WalletConnectJWT/JWTValidator.swift +++ b/Sources/WalletConnectJWT/JWTValidator.swift @@ -1,10 +1,15 @@ import Foundation +import CryptoKit -struct JWTValidator { +public struct JWTValidator { - let jwtString: String + private let jwtString: String - func isValid(publicKey: SigningPublicKey) throws -> Bool { + public init(jwtString: String) { + self.jwtString = jwtString + } + + public func isValid(publicKey: SigningPublicKey) throws -> Bool { var components = jwtString.components(separatedBy: ".") guard components.count == 3 else { throw JWTError.undefinedFormat } @@ -20,3 +25,32 @@ struct JWTValidator { return publicKey.isValid(signature: signatureData, for: unsignedData) } } + + +public struct P256JWTValidator { + + private let jwtString: String + + public init(jwtString: String) { + self.jwtString = jwtString + } + + public func isValid(publicKey: P256.Signing.PublicKey) throws -> Bool { + var components = jwtString.components(separatedBy: ".") + + guard components.count == 3 else { throw JWTError.undefinedFormat } + + let signature = components.removeLast() + + guard let unsignedData = components + .joined(separator: ".") + .data(using: .utf8) + else { throw JWTError.invalidJWTString } + + let signatureData = try JWTEncoder.base64urlDecodedData(string: signature) + + let P256Signature = try P256.Signing.ECDSASignature(rawRepresentation: signatureData) + + return publicKey.isValidSignature(P256Signature, for: unsignedData) + } +} diff --git a/Sources/WalletConnectNetworking/NetworkInteracting.swift b/Sources/WalletConnectNetworking/NetworkInteracting.swift index e916c3f62..bfef71546 100644 --- a/Sources/WalletConnectNetworking/NetworkInteracting.swift +++ b/Sources/WalletConnectNetworking/NetworkInteracting.swift @@ -5,7 +5,7 @@ public protocol NetworkInteracting { var isSocketConnected: Bool { get } var socketConnectionStatusPublisher: AnyPublisher { get } var networkConnectionStatusPublisher: AnyPublisher { get } - var requestPublisher: AnyPublisher<(topic: String, request: RPCRequest, decryptedPayload: Data, publishedAt: Date, derivedTopic: String?), Never> { get } + var requestPublisher: AnyPublisher<(topic: String, request: RPCRequest, decryptedPayload: Data, publishedAt: Date, derivedTopic: String?, encryptedMessage: String, attestation: String?), Never> { get } func subscribe(topic: String) async throws func unsubscribe(topic: String) func batchSubscribe(topics: [String]) async throws diff --git a/Sources/WalletConnectNetworking/NetworkingInteractor.swift b/Sources/WalletConnectNetworking/NetworkingInteractor.swift index e7c8eb90d..5b106cdb8 100644 --- a/Sources/WalletConnectNetworking/NetworkingInteractor.swift +++ b/Sources/WalletConnectNetworking/NetworkingInteractor.swift @@ -9,10 +9,10 @@ public class NetworkingInteractor: NetworkInteracting { private let rpcHistory: RPCHistory private let logger: ConsoleLogging - private let requestPublisherSubject = PassthroughSubject<(topic: String, request: RPCRequest, decryptedPayload: Data, publishedAt: Date, derivedTopic: String?), Never>() + private let requestPublisherSubject = PassthroughSubject<(topic: String, request: RPCRequest, decryptedPayload: Data, publishedAt: Date, derivedTopic: String?, encryptedMessage: String, attestation: String?), Never>() private let responsePublisherSubject = PassthroughSubject<(topic: String, request: RPCRequest, response: RPCResponse, publishedAt: Date, derivedTopic: String?), Never>() - public var requestPublisher: AnyPublisher<(topic: String, request: RPCRequest, decryptedPayload: Data, publishedAt: Date, derivedTopic: String?), Never> { + public var requestPublisher: AnyPublisher<(topic: String, request: RPCRequest, decryptedPayload: Data, publishedAt: Date, derivedTopic: String?, encryptedMessage: String, attestation: String?), Never> { requestPublisherSubject.eraseToAnyPublisher() } @@ -51,8 +51,8 @@ public class NetworkingInteractor: NetworkInteracting { private func setupRelaySubscribtion() { relayClient.messagePublisher - .sink { [unowned self] (topic, message, publishedAt) in - manageSubscription(topic, message, publishedAt) + .sink { [unowned self] (topic, message, publishedAt, attestation) in + manageSubscription(topic, message, publishedAt, attestation) }.store(in: &publishers) } @@ -123,19 +123,29 @@ public class NetworkingInteractor: NetworkInteracting { }.store(in: &publishers) } + public func requestSubscription(on request: ProtocolMethod) -> AnyPublisher, Never> { return requestPublisher .filter { rpcRequest in return rpcRequest.request.method == request.method } - .compactMap { [weak self] topic, rpcRequest, decryptedPayload, publishedAt, derivedTopic in + .compactMap { [weak self] topic, rpcRequest, decryptedPayload, publishedAt, derivedTopic, encryptedMessage, attestation in do { guard let id = rpcRequest.id, let request = try rpcRequest.params?.get(RequestParams.self) else { return nil } - return RequestSubscriptionPayload(id: id, topic: topic, request: request, decryptedPayload: decryptedPayload, publishedAt: publishedAt, derivedTopic: derivedTopic) + return RequestSubscriptionPayload( + id: id, + topic: topic, + request: request, + decryptedPayload: decryptedPayload, + publishedAt: publishedAt, + derivedTopic: derivedTopic, + encryptedMessage: encryptedMessage, + attestation: attestation + ) } catch { self?.logger.debug("Networking Interactor - \(error)") + return nil } - return nil } .eraseToAnyPublisher() } @@ -245,11 +255,11 @@ public class NetworkingInteractor: NetworkInteracting { try await respond(topic: topic, response: response, protocolMethod: protocolMethod, envelopeType: envelopeType) } - private func manageSubscription(_ topic: String, _ encodedEnvelope: String, _ publishedAt: Date) { + private func manageSubscription(_ topic: String, _ encodedEnvelope: String, _ publishedAt: Date, _ attestation: String?) { if let result = serializer.tryDeserializeRequestOrResponse(topic: topic, codingType: .base64Encoded, envelopeString: encodedEnvelope) { switch result { case .left(let result): - handleRequest(topic: topic, request: result.request, decryptedPayload: result.decryptedPayload, publishedAt: publishedAt, derivedTopic: result.derivedTopic) + handleRequest(topic: topic, request: result.request, decryptedPayload: result.decryptedPayload, publishedAt: publishedAt, derivedTopic: result.derivedTopic, encryptedMessage: encodedEnvelope, attestation: attestation) case .right(let result): handleResponse(topic: topic, response: result.response, publishedAt: publishedAt, derivedTopic: result.derivedTopic) } @@ -259,13 +269,13 @@ public class NetworkingInteractor: NetworkInteracting { } public func handleHistoryRequest(topic: String, request: RPCRequest) { - requestPublisherSubject.send((topic, request, Data(), Date(), nil)) + requestPublisherSubject.send((topic, request, Data(), Date(), nil, "", nil )) } - private func handleRequest(topic: String, request: RPCRequest, decryptedPayload: Data, publishedAt: Date, derivedTopic: String?) { + private func handleRequest(topic: String, request: RPCRequest, decryptedPayload: Data, publishedAt: Date, derivedTopic: String?, encryptedMessage: String, attestation: String?) { do { try rpcHistory.set(request, forTopic: topic, emmitedBy: .remote, transportType: .relay) - requestPublisherSubject.send((topic, request, decryptedPayload, publishedAt, derivedTopic)) + requestPublisherSubject.send((topic, request, decryptedPayload, publishedAt, derivedTopic, encryptedMessage, attestation)) } catch { logger.debug(error) } diff --git a/Sources/WalletConnectNetworking/RequestSubscriptionPayload.swift b/Sources/WalletConnectNetworking/RequestSubscriptionPayload.swift index 135378a99..9a532441b 100644 --- a/Sources/WalletConnectNetworking/RequestSubscriptionPayload.swift +++ b/Sources/WalletConnectNetworking/RequestSubscriptionPayload.swift @@ -2,18 +2,22 @@ import Foundation public struct RequestSubscriptionPayload: Codable, SubscriptionPayload { public let id: RPCID + public let encryptedMessage: String + public let attestation: String? public let topic: String public let request: Request public let decryptedPayload: Data public let publishedAt: Date public let derivedTopic: String? - public init(id: RPCID, topic: String, request: Request, decryptedPayload: Data, publishedAt: Date, derivedTopic: String?) { + public init(id: RPCID, topic: String, request: Request, decryptedPayload: Data, publishedAt: Date, derivedTopic: String?, encryptedMessage: String, attestation: String?) { self.id = id self.topic = topic self.request = request self.decryptedPayload = decryptedPayload self.publishedAt = publishedAt self.derivedTopic = derivedTopic + self.encryptedMessage = encryptedMessage + self.attestation = attestation } } diff --git a/Sources/WalletConnectPairing/PairingRequestsSubscriber.swift b/Sources/WalletConnectPairing/PairingRequestsSubscriber.swift index c3327e482..089eed549 100644 --- a/Sources/WalletConnectPairing/PairingRequestsSubscriber.swift +++ b/Sources/WalletConnectPairing/PairingRequestsSubscriber.swift @@ -28,7 +28,7 @@ public class PairingRequestsSubscriber { .filter { [unowned self] in !pairingProtocolMethods.contains($0.request.method)} .filter { [unowned self] in pairingStorage.hasPairing(forTopic: $0.topic)} .filter { [unowned self] in !registeredProtocolMethods.contains($0.request.method)} - .sink { [unowned self] topic, request, _, _, _ in + .sink { [unowned self] topic, request, _, _, _, _, _ in Task(priority: .high) { let protocolMethod = UnsupportedProtocolMethod(method: request.method) logger.debug("PairingRequestsSubscriber: responding unregistered request method") diff --git a/Sources/WalletConnectRelay/RPC/Methods/Subscription.swift b/Sources/WalletConnectRelay/RPC/Methods/Subscription.swift index c4e4b1831..be2b25259 100644 --- a/Sources/WalletConnectRelay/RPC/Methods/Subscription.swift +++ b/Sources/WalletConnectRelay/RPC/Methods/Subscription.swift @@ -8,21 +8,24 @@ struct Subscription: RelayRPC { let topic: String let message: String let publishedAt: Date + let attestation: String? enum CodingKeys: String, CodingKey { - case topic, message, publishedAt + case topic, message, publishedAt, attestation } - internal init(topic: String, message: String, publishedAt: Date) { + internal init(topic: String, message: String, publishedAt: Date, attestation: String?) { self.topic = topic self.message = message self.publishedAt = publishedAt + self.attestation = attestation } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) topic = try container.decode(String.self, forKey: .topic) message = try container.decode(String.self, forKey: .message) + attestation = try? container.decode(String.self, forKey: .attestation) let publishedAtMiliseconds = try container.decode(UInt64.self, forKey: .publishedAt) publishedAt = Date(milliseconds: publishedAtMiliseconds) } @@ -45,7 +48,9 @@ struct Subscription: RelayRPC { "irn_subscription" } - init(id: String, topic: String, message: String) { - self.params = Params(id: id, data: Params.Contents(topic: topic, message: message, publishedAt: Date())) + #if DEBUG + init(id: String, topic: String, message: String, attestation: String? = nil) { + self.params = Params(id: id, data: Params.Contents(topic: topic, message: message, publishedAt: Date(), attestation: attestation)) } + #endif } diff --git a/Sources/WalletConnectRelay/RelayClient.swift b/Sources/WalletConnectRelay/RelayClient.swift index 93b33f1c7..f51f69c84 100644 --- a/Sources/WalletConnectRelay/RelayClient.swift +++ b/Sources/WalletConnectRelay/RelayClient.swift @@ -26,7 +26,7 @@ public final class RelayClient { return dispatcher.isSocketConnected } - public var messagePublisher: AnyPublisher<(topic: String, message: String, publishedAt: Date), Never> { + public var messagePublisher: AnyPublisher<(topic: String, message: String, publishedAt: Date, attestation: String?), Never> { messagePublisherSubject.eraseToAnyPublisher() } @@ -38,7 +38,7 @@ public final class RelayClient { dispatcher.networkConnectionStatusPublisher } - private let messagePublisherSubject = PassthroughSubject<(topic: String, message: String, publishedAt: Date), Never>() + private let messagePublisherSubject = PassthroughSubject<(topic: String, message: String, publishedAt: Date, attestation: String?), Never>() private let subscriptionResponsePublisherSubject = PassthroughSubject<(RPCID?, [String]), Never>() private var subscriptionResponsePublisher: AnyPublisher<(RPCID?, [String]), Never> { @@ -238,7 +238,7 @@ public final class RelayClient { try acknowledgeRequest(request) try rpcHistory.set(request, forTopic: params.data.topic, emmitedBy: .remote, transportType: .relay) logger.debug("received message: \(params.data.message) on topic: \(params.data.topic)") - messagePublisherSubject.send((params.data.topic, params.data.message, params.data.publishedAt)) + messagePublisherSubject.send((params.data.topic, params.data.message, params.data.publishedAt, params.data.attestation)) } catch { logger.error("RPC History 'set()' error: \(error)") } diff --git a/Sources/WalletConnectSign/Auth/Link/ApproveSessionAuthenticateUtil.swift b/Sources/WalletConnectSign/Auth/Link/ApproveSessionAuthenticateUtil.swift index c69461c8a..73f14e765 100644 --- a/Sources/WalletConnectSign/Auth/Link/ApproveSessionAuthenticateUtil.swift +++ b/Sources/WalletConnectSign/Auth/Link/ApproveSessionAuthenticateUtil.swift @@ -14,6 +14,8 @@ class ApproveSessionAuthenticateUtil { private let logger: ConsoleLogging private let sessionStore: WCSessionStorage private let sessionNamespaceBuilder: SessionNamespaceBuilder + private let verifyContextStore: CodableStore + private let verifyClient: VerifyClientProtocol init( logger: ConsoleLogging, @@ -23,7 +25,9 @@ class ApproveSessionAuthenticateUtil { messageFormatter: SIWEFromCacaoFormatting, sessionStore: WCSessionStorage, sessionNamespaceBuilder: SessionNamespaceBuilder, - networkingInteractor: NetworkInteracting + networkingInteractor: NetworkInteracting, + verifyContextStore: CodableStore, + verifyClient: VerifyClientProtocol ) { self.logger = logger self.kms = kms @@ -33,6 +37,8 @@ class ApproveSessionAuthenticateUtil { self.signatureVerifier = signatureVerifier self.messageFormatter = messageFormatter self.networkingInteractor = networkingInteractor + self.verifyContextStore = verifyContextStore + self.verifyClient = verifyClient } func getsessionAuthenticateRequestParams(requestId: RPCID) throws -> (request: SessionAuthenticateRequestParams, topic: String) { @@ -60,13 +66,13 @@ class ApproveSessionAuthenticateUtil { return (topic, keys) } - func createSession( response: SessionAuthenticateResponseParams, pairingTopic: String, request: SessionAuthenticateRequestParams, sessionTopic: String, - transportType: WCSession.TransportType + transportType: WCSession.TransportType, + verifyContext: VerifyContext ) throws -> Session? { @@ -101,7 +107,8 @@ class ApproveSessionAuthenticateUtil { settleParams: settleParams, requiredNamespaces: [:], acknowledged: true, - transportType: transportType + transportType: transportType, + verifyContext: verifyContext ) logger.debug("created a session with topic: \(sessionTopic)") @@ -114,6 +121,15 @@ class ApproveSessionAuthenticateUtil { return session.publicRepresentation() } + + func getVerifyContext(requestId: RPCID, domain: String) -> VerifyContext { + guard let context = try? verifyContextStore.get(key: requestId.string) else { + return verifyClient.createVerifyContext(origin: nil, domain: domain, isScam: false, isVerified: nil) + } + return context + } + + func recoverAndVerifySignature(cacaos: [Cacao]) async throws { try await cacaos.asyncForEach { [unowned self] cacao in guard diff --git a/Sources/WalletConnectSign/Auth/Link/LinkEnvelopesDispatcher.swift b/Sources/WalletConnectSign/Auth/Link/LinkEnvelopesDispatcher.swift index 2ac026b5a..987a8aae8 100644 --- a/Sources/WalletConnectSign/Auth/Link/LinkEnvelopesDispatcher.swift +++ b/Sources/WalletConnectSign/Auth/Link/LinkEnvelopesDispatcher.swift @@ -152,7 +152,7 @@ final class LinkEnvelopesDispatcher { guard let id = rpcRequest.id, let request = try rpcRequest.params?.get(RequestParams.self) else { return nil } - return RequestSubscriptionPayload(id: id, topic: topic, request: request, decryptedPayload: Data(), publishedAt: Date(), derivedTopic: nil) + return RequestSubscriptionPayload(id: id, topic: topic, request: request, decryptedPayload: Data(), publishedAt: Date(), derivedTopic: nil, encryptedMessage: "", attestation: nil) } catch { self?.logger.debug(error) } diff --git a/Sources/WalletConnectSign/Auth/Link/LinkSessionAuthenticateResponder.swift b/Sources/WalletConnectSign/Auth/Link/LinkSessionAuthenticateResponder.swift index e6116d4ef..31d6ba9fe 100644 --- a/Sources/WalletConnectSign/Auth/Link/LinkSessionAuthenticateResponder.swift +++ b/Sources/WalletConnectSign/Auth/Link/LinkSessionAuthenticateResponder.swift @@ -11,6 +11,7 @@ actor LinkSessionAuthenticateResponder { private let metadata: AppMetadata private let util: ApproveSessionAuthenticateUtil private let walletErrorResponder: WalletErrorResponder + private let verifyContextStore: CodableStore init( linkEnvelopesDispatcher: LinkEnvelopesDispatcher, @@ -18,10 +19,12 @@ actor LinkSessionAuthenticateResponder { kms: KeyManagementService, metadata: AppMetadata, approveSessionAuthenticateUtil: ApproveSessionAuthenticateUtil, - walletErrorResponder: WalletErrorResponder + walletErrorResponder: WalletErrorResponder, + verifyContextStore: CodableStore ) { self.linkEnvelopesDispatcher = linkEnvelopesDispatcher self.logger = logger + self.verifyContextStore = verifyContextStore self.kms = kms self.metadata = metadata self.util = approveSessionAuthenticateUtil @@ -57,20 +60,23 @@ actor LinkSessionAuthenticateResponder { let url = try await linkEnvelopesDispatcher.respond(topic: responseTopic, response: response, peerUniversalLink: peerUniversalLink, envelopeType: .type1(pubKey: responseKeys.publicKey.rawRepresentation)) - let session = try util.createSession( response: responseParams, pairingTopic: pairingTopic, request: sessionAuthenticateRequestParams, sessionTopic: sessionTopic, - transportType: .linkMode + transportType: .linkMode, + verifyContext: util.getVerifyContext(requestId: requestId, domain: sessionAuthenticateRequestParams.requester.metadata.url) ) + verifyContextStore.delete(forKey: requestId.string) + return (session, url) } func respondError(requestId: RPCID) async throws { - try await walletErrorResponder.respondError(AuthError.userRejeted, requestId: requestId) + _ = try await walletErrorResponder.respondError(AuthError.userRejeted, requestId: requestId) + verifyContextStore.delete(forKey: requestId.string) } } diff --git a/Sources/WalletConnectSign/Auth/Services/App/AuthResponseSubscriber.swift b/Sources/WalletConnectSign/Auth/Services/App/AuthResponseSubscriber.swift index 03f0456be..602827661 100644 --- a/Sources/WalletConnectSign/Auth/Services/App/AuthResponseSubscriber.swift +++ b/Sources/WalletConnectSign/Auth/Services/App/AuthResponseSubscriber.swift @@ -200,7 +200,8 @@ class AuthResponseSubscriber { settleParams: settleParams, requiredNamespaces: [:], acknowledged: true, - transportType: transportType + transportType: transportType, + verifyContext: nil ) sessionStore.setSession(session) diff --git a/Sources/WalletConnectSign/Auth/Services/Wallet/AuthRequestSubscriber.swift b/Sources/WalletConnectSign/Auth/Services/Wallet/AuthRequestSubscriber.swift index a4389ee9f..d8e4372b6 100644 --- a/Sources/WalletConnectSign/Auth/Services/Wallet/AuthRequestSubscriber.swift +++ b/Sources/WalletConnectSign/Auth/Services/Wallet/AuthRequestSubscriber.swift @@ -54,14 +54,20 @@ class AuthRequestSubscriber { let request = AuthenticationRequest(id: payload.id, topic: payload.topic, payload: payload.request.authPayload, requester: payload.request.requester.metadata) Task(priority: .high) { - let assertionId = payload.decryptedPayload.sha256().toHexString() do { - let response = try await verifyClient.verifyOrigin(assertionId: assertionId) - let verifyContext = verifyClient.createVerifyContext(origin: response.origin, domain: payload.request.authPayload.domain, isScam: response.isScam) + let response: VerifyResponse + if let attestation = payload.attestation, + let messageId = payload.encryptedMessage.data(using: .utf8)?.sha256().toHexString() { + response = try await verifyClient.verify(.v2(attestationJWT: attestation, messageId: messageId)) + } else { + let assertionId = payload.decryptedPayload.sha256().toHexString() + response = try await verifyClient.verify(.v1(assertionId: assertionId)) + } + let verifyContext = verifyClient.createVerifyContext(origin: response.origin, domain: payload.request.authPayload.domain, isScam: response.isScam, isVerified: response.isVerified) verifyContextStore.set(verifyContext, forKey: request.id.string) onRequest?((request, verifyContext)) } catch { - let verifyContext = verifyClient.createVerifyContext(origin: nil, domain: payload.request.authPayload.domain, isScam: nil) + let verifyContext = verifyClient.createVerifyContext(origin: nil, domain: payload.request.authPayload.domain, isScam: nil, isVerified: nil) verifyContextStore.set(verifyContext, forKey: request.id.string) onRequest?((request, verifyContext)) return diff --git a/Sources/WalletConnectSign/Auth/Services/Wallet/SessionAuthenticateResponder.swift b/Sources/WalletConnectSign/Auth/Services/Wallet/SessionAuthenticateResponder.swift index dea7e0e9e..37e89a07d 100644 --- a/Sources/WalletConnectSign/Auth/Services/Wallet/SessionAuthenticateResponder.swift +++ b/Sources/WalletConnectSign/Auth/Services/Wallet/SessionAuthenticateResponder.swift @@ -125,7 +125,8 @@ actor SessionAuthenticateResponder { pairingTopic: pairingTopic, request: sessionAuthenticateRequestParams, sessionTopic: sessionTopic, - transportType: .relay + transportType: .relay, + verifyContext: util.getVerifyContext(requestId: requestId, domain: sessionAuthenticateRequestParams.requester.metadata.url) ) pairingRegisterer.activate( pairingTopic: pairingTopic, diff --git a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift index cd127d81a..fbe9a1a56 100644 --- a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift @@ -226,6 +226,9 @@ final class ApproveEngine { expiry: Int64(expiry) ) + let verifyContext = (try? verifyContextStore.get(key: proposal.proposer.publicKey)) ?? verifyClient.createVerifyContext(origin: nil, domain: proposal.proposer.metadata.url, isScam: false, isVerified: nil) + + let session = WCSession( topic: topic, pairingTopic: pairingTopic, @@ -235,7 +238,8 @@ final class ApproveEngine { settleParams: settleParams, requiredNamespaces: proposal.requiredNamespaces, acknowledged: false, - transportType: .relay + transportType: .relay, + verifyContext: verifyContext ) logger.debug("Sending session settle request") @@ -395,18 +399,25 @@ private extension ApproveEngine { } Task(priority: .high) { - let assertionId = payload.decryptedPayload.sha256().toHexString() do { - let response = try await verifyClient.verifyOrigin(assertionId: assertionId) + let response: VerifyResponse + if let attestation = payload.attestation, + let messageId = payload.encryptedMessage.data(using: .utf8)?.sha256().toHexString() { + response = try await verifyClient.verify(.v2(attestationJWT: attestation, messageId: messageId)) + } else { + let assertionId = payload.decryptedPayload.sha256().toHexString() + response = try await verifyClient.verify(.v1(assertionId: assertionId)) + } let verifyContext = verifyClient.createVerifyContext( origin: response.origin, domain: payload.request.proposer.metadata.url, - isScam: response.isScam + isScam: response.isScam, + isVerified: response.isVerified ) verifyContextStore.set(verifyContext, forKey: proposal.proposer.publicKey) onSessionProposal?(proposal.publicRepresentation(pairingTopic: payload.topic), verifyContext) } catch { - let verifyContext = verifyClient.createVerifyContext(origin: nil, domain: payload.request.proposer.metadata.url, isScam: nil) + let verifyContext = verifyClient.createVerifyContext(origin: nil, domain: payload.request.proposer.metadata.url, isScam: nil, isVerified: nil) onSessionProposal?(proposal.publicRepresentation(pairingTopic: payload.topic), verifyContext) return } @@ -460,7 +471,8 @@ private extension ApproveEngine { settleParams: params, requiredNamespaces: proposedNamespaces, acknowledged: true, - transportType: .relay + transportType: .relay, + verifyContext: nil ) sessionStore.setSession(session) diff --git a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift index 2933da364..a18c922c8 100644 --- a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift @@ -20,7 +20,6 @@ final class SessionEngine { private let networkingInteractor: NetworkInteracting private let historyService: HistoryServiceProtocol private let verifyContextStore: CodableStore - private let verifyClient: VerifyClientProtocol private let kms: KeyManagementServiceProtocol private var publishers = [AnyCancellable]() private let logger: ConsoleLogging @@ -31,7 +30,6 @@ final class SessionEngine { networkingInteractor: NetworkInteracting, historyService: HistoryServiceProtocol, verifyContextStore: CodableStore, - verifyClient: VerifyClientProtocol, kms: KeyManagementServiceProtocol, sessionStore: WCSessionStorage, logger: ConsoleLogging, @@ -41,7 +39,6 @@ final class SessionEngine { self.networkingInteractor = networkingInteractor self.historyService = historyService self.verifyContextStore = verifyContextStore - self.verifyClient = verifyClient self.kms = kms self.sessionStore = sessionStore self.logger = logger @@ -214,20 +211,9 @@ private extension SessionEngine { guard !request.isExpired() else { return respondError(payload: payload, reason: .sessionRequestExpired, protocolMethod: protocolMethod) } - Task(priority: .high) { - let assertionId = payload.decryptedPayload.sha256().toHexString() - do { - let response = try await verifyClient.verifyOrigin(assertionId: assertionId) - let verifyContext = verifyClient.createVerifyContext(origin: response.origin, domain: session.peerParticipant.metadata.url, isScam: response.isScam) - verifyContextStore.set(verifyContext, forKey: request.id.string) - - sessionRequestsProvider.emitRequestIfPending() - } catch { - let verifyContext = verifyClient.createVerifyContext(origin: nil, domain: session.peerParticipant.metadata.url, isScam: nil) - verifyContextStore.set(verifyContext, forKey: request.id.string) - sessionRequestsProvider.emitRequestIfPending() - } - } + let verifyContext = session.verifyContext ?? VerifyContext(origin: nil, validation: .unknown) + verifyContextStore.set(verifyContext, forKey: request.id.string) + sessionRequestsProvider.emitRequestIfPending() } func onSessionPing(payload: SubscriptionPayload) { diff --git a/Sources/WalletConnectSign/LinkAndRelayDispatchers/LinkSessionRequester.swift b/Sources/WalletConnectSign/LinkAndRelayDispatchers/LinkSessionRequester.swift index 015f261ee..684e5522a 100644 --- a/Sources/WalletConnectSign/LinkAndRelayDispatchers/LinkSessionRequester.swift +++ b/Sources/WalletConnectSign/LinkAndRelayDispatchers/LinkSessionRequester.swift @@ -18,7 +18,7 @@ final class LinkSessionRequester { func request(_ request: Request) async throws -> String? { logger.debug("will request on session topic: \(request.topic)") - guard let session = sessionStore.getSession(forTopic: request.topic), session.acknowledged else { + guard let session = sessionStore.getSession(forTopic: request.topic) else { logger.debug("Could not find session for topic \(request.topic)") throw WalletConnectError.noSessionMatchingTopic(request.topic) } diff --git a/Sources/WalletConnectSign/LinkAndRelayDispatchers/SessionResponderDispatcher.swift b/Sources/WalletConnectSign/LinkAndRelayDispatchers/SessionResponderDispatcher.swift index fa61d3971..de13ac389 100644 --- a/Sources/WalletConnectSign/LinkAndRelayDispatchers/SessionResponderDispatcher.swift +++ b/Sources/WalletConnectSign/LinkAndRelayDispatchers/SessionResponderDispatcher.swift @@ -21,7 +21,7 @@ class SessionResponderDispatcher { func respondSessionRequest(topic: String, requestId: RPCID, response: RPCResult) async throws -> String? { - guard let session = sessionStore.getSession(forTopic: topic), session.acknowledged else { + guard let session = sessionStore.getSession(forTopic: topic) else { logger.debug("Could not find session for topic \(topic)") throw WalletConnectError.noSessionMatchingTopic(topic) } diff --git a/Sources/WalletConnectSign/Services/HistoryService.swift b/Sources/WalletConnectSign/Services/HistoryService.swift index c3ac80a3a..4963b8028 100644 --- a/Sources/WalletConnectSign/Services/HistoryService.swift +++ b/Sources/WalletConnectSign/Services/HistoryService.swift @@ -110,7 +110,7 @@ final class MockHistoryService: HistoryServiceProtocol { removePendingRequestCalled(topic) } - func getSessionRequest(id: JSONRPC.RPCID) -> (request: Request, context: VerifyContext?)? { + func getSessionRequest(id: RPCID) -> (request: Request, context: VerifyContext?)? { fatalError("Unimplemented") } diff --git a/Sources/WalletConnectSign/Sign/SignClientFactory.swift b/Sources/WalletConnectSign/Sign/SignClientFactory.swift index 6a9989112..98bdb3239 100644 --- a/Sources/WalletConnectSign/Sign/SignClientFactory.swift +++ b/Sources/WalletConnectSign/Sign/SignClientFactory.swift @@ -65,7 +65,7 @@ public struct SignClientFactory { let verifyClient = VerifyClientFactory.create() let sessionRequestsProvider = SessionRequestsProvider(historyService: historyService) let invalidRequestsSanitiser = InvalidRequestsSanitiser(historyService: historyService, history: rpcHistory) - let sessionEngine = SessionEngine(networkingInteractor: networkingClient, historyService: historyService, verifyContextStore: verifyContextStore, verifyClient: verifyClient, kms: kms, sessionStore: sessionStore, logger: logger, sessionRequestsProvider: sessionRequestsProvider, invalidRequestsSanitiser: invalidRequestsSanitiser) + let sessionEngine = SessionEngine(networkingInteractor: networkingClient, historyService: historyService, verifyContextStore: verifyContextStore, kms: kms, sessionStore: sessionStore, logger: logger, sessionRequestsProvider: sessionRequestsProvider, invalidRequestsSanitiser: invalidRequestsSanitiser) let nonControllerSessionStateMachine = NonControllerSessionStateMachine(networkingInteractor: networkingClient, kms: kms, sessionStore: sessionStore, logger: logger) let controllerSessionStateMachine = ControllerSessionStateMachine(networkingInteractor: networkingClient, kms: kms, sessionStore: sessionStore, logger: logger) let sessionExtendRequester = SessionExtendRequester(sessionStore: sessionStore, networkingInteractor: networkingClient) @@ -121,7 +121,7 @@ public struct SignClientFactory { let walletErrorResponder = WalletErrorResponder(networkingInteractor: networkingClient, logger: logger, kms: kms, rpcHistory: rpcHistory, linkEnvelopesDispatcher: linkEnvelopesDispatcher) let authRequestSubscriber = AuthRequestSubscriber(networkingInteractor: networkingClient, logger: logger, kms: kms, walletErrorResponder: walletErrorResponder, pairingRegisterer: pairingClient, verifyClient: verifyClient, verifyContextStore: verifyContextStore, pairingStore: pairingStore) - let approveSessionAuthenticateUtil = ApproveSessionAuthenticateUtil(logger: logger, kms: kms, rpcHistory: rpcHistory, signatureVerifier: signatureVerifier, messageFormatter: messageFormatter, sessionStore: sessionStore, sessionNamespaceBuilder: sessionNameSpaceBuilder, networkingInteractor: networkingClient) + let approveSessionAuthenticateUtil = ApproveSessionAuthenticateUtil(logger: logger, kms: kms, rpcHistory: rpcHistory, signatureVerifier: signatureVerifier, messageFormatter: messageFormatter, sessionStore: sessionStore, sessionNamespaceBuilder: sessionNameSpaceBuilder, networkingInteractor: networkingClient, verifyContextStore: verifyContextStore, verifyClient: verifyClient) let pendingRequestsProvider = PendingRequestsProvider(rpcHistory: rpcHistory, verifyContextStore: verifyContextStore) let authResponseTopicResubscriptionService = AuthResponseTopicResubscriptionService(networkingInteractor: networkingClient, logger: logger, authResponseTopicRecordsStore: authResponseTopicRecordsStore) @@ -131,7 +131,7 @@ public struct SignClientFactory { let relaySessionAuthenticateResponder = SessionAuthenticateResponder(networkingInteractor: networkingClient, logger: logger, kms: kms, verifyContextStore: verifyContextStore, walletErrorResponder: walletErrorResponder, pairingRegisterer: pairingClient, metadata: metadata, approveSessionAuthenticateUtil: approveSessionAuthenticateUtil, eventsClient: eventsClient) - let linkSessionAuthenticateResponder = LinkSessionAuthenticateResponder(linkEnvelopesDispatcher: linkEnvelopesDispatcher, logger: logger, kms: kms, metadata: metadata, approveSessionAuthenticateUtil: approveSessionAuthenticateUtil, walletErrorResponder: walletErrorResponder) + let linkSessionAuthenticateResponder = LinkSessionAuthenticateResponder(linkEnvelopesDispatcher: linkEnvelopesDispatcher, logger: logger, kms: kms, metadata: metadata, approveSessionAuthenticateUtil: approveSessionAuthenticateUtil, walletErrorResponder: walletErrorResponder, verifyContextStore: verifyContextStore) let approveSessionAuthenticateDispatcher = ApproveSessionAuthenticateDispatcher(relaySessionAuthenticateResponder: relaySessionAuthenticateResponder, logger: logger, rpcHistory: rpcHistory, approveSessionAuthenticateUtil: approveSessionAuthenticateUtil, linkSessionAuthenticateResponder: linkSessionAuthenticateResponder) diff --git a/Sources/WalletConnectSign/Types/Session/WCSession.swift b/Sources/WalletConnectSign/Types/Session/WCSession.swift index c5224bb00..3e40af9b2 100644 --- a/Sources/WalletConnectSign/Types/Session/WCSession.swift +++ b/Sources/WalletConnectSign/Types/Session/WCSession.swift @@ -17,6 +17,7 @@ struct WCSession: SequenceObject, Equatable { let peerParticipant: Participant let controller: AgreementPeer var transportType: TransportType + var verifyContext: VerifyContext? private(set) var acknowledged: Bool private(set) var expiryDate: Date @@ -41,7 +42,8 @@ struct WCSession: SequenceObject, Equatable { settleParams: SessionType.SettleParams, requiredNamespaces: [String: ProposalNamespace], acknowledged: Bool, - transportType: TransportType + transportType: TransportType, + verifyContext: VerifyContext? ) { self.topic = topic self.pairingTopic = pairingTopic @@ -56,6 +58,7 @@ struct WCSession: SequenceObject, Equatable { self.acknowledged = acknowledged self.expiryDate = Date(timeIntervalSince1970: TimeInterval(settleParams.expiry)) self.transportType = transportType + self.verifyContext = verifyContext } #if DEBUG @@ -74,7 +77,8 @@ struct WCSession: SequenceObject, Equatable { accounts: Set, acknowledged: Bool, expiryTimestamp: Int64, - transportType: TransportType + transportType: TransportType, + verifyContext: VerifyContext ) { self.topic = topic self.pairingTopic = pairingTopic @@ -89,6 +93,7 @@ struct WCSession: SequenceObject, Equatable { self.acknowledged = acknowledged self.expiryDate = Date(timeIntervalSince1970: TimeInterval(expiryTimestamp)) self.transportType = transportType + self.verifyContext = verifyContext } #endif @@ -192,7 +197,7 @@ struct WCSession: SequenceObject, Equatable { extension WCSession { enum CodingKeys: String, CodingKey { - case topic, pairingTopic, relay, selfParticipant, peerParticipant, expiryDate, acknowledged, controller, namespaces, timestamp, requiredNamespaces, sessionProperties, transportType + case topic, pairingTopic, relay, selfParticipant, peerParticipant, expiryDate, acknowledged, controller, namespaces, timestamp, requiredNamespaces, sessionProperties, transportType, verifyContext } init(from decoder: Decoder) throws { @@ -210,6 +215,7 @@ extension WCSession { self.requiredNamespaces = try container.decode([String: ProposalNamespace].self, forKey: .requiredNamespaces) self.pairingTopic = try container.decode(String.self, forKey: .pairingTopic) self.transportType = (try? container.decode(TransportType.self, forKey: .transportType)) ?? .relay + self.verifyContext = try? container.decode(VerifyContext.self, forKey: .verifyContext) } func encode(to encoder: Encoder) throws { @@ -227,5 +233,6 @@ extension WCSession { try container.encode(timestamp, forKey: .timestamp) try container.encode(requiredNamespaces, forKey: .requiredNamespaces) try container.encode(transportType, forKey: .transportType) + try container.encode(verifyContext, forKey: .verifyContext) } } diff --git a/Sources/WalletConnectVerify/AttestationJWTVerifier.swift b/Sources/WalletConnectVerify/AttestationJWTVerifier.swift new file mode 100644 index 000000000..424cf8fc4 --- /dev/null +++ b/Sources/WalletConnectVerify/AttestationJWTVerifier.swift @@ -0,0 +1,72 @@ + +import Foundation +import CryptoKit + +struct AttestationJWTClaims: Codable { + + var exp: UInt64 + + var isScam: Bool? + + var id: String + + var origin: String + + var isVerified: Bool +} + +class AttestationJWTVerifier { + + enum Errors: Error { + case issuerDoesNotMatchVerifyServerPubKey + case messageIdMismatch + case invalidJWT + } + + let verifyServerPubKeyManager: VerifyServerPubKeyManagerProtocol + + init(verifyServerPubKeyManager: VerifyServerPubKeyManagerProtocol) { + self.verifyServerPubKeyManager = verifyServerPubKeyManager + } + + // messageId - hash of the encrypted message supplied in the request + func verify(attestationJWT: String, messageId: String) async throws -> VerifyResponse { + do { + let verifyServerPubKey = try await verifyServerPubKeyManager.getPublicKey() + try verifyJWTAgainstPubKey(attestationJWT, signingPubKey: verifyServerPubKey) + } catch { + let refreshedVerifyServerPubKey = try await verifyServerPubKeyManager.refreshKey() + try verifyJWTAgainstPubKey(attestationJWT, signingPubKey: refreshedVerifyServerPubKey) + } + + let claims = try decodeJWTClaims(jwtString: attestationJWT) + guard messageId == claims.id else { + throw Errors.messageIdMismatch + } + + return VerifyResponse(origin: claims.origin, isScam: claims.isScam, isVerified: claims.isVerified) + } + + func verifyJWTAgainstPubKey(_ jwtString: String, signingPubKey: P256.Signing.PublicKey) throws { + let validator = P256JWTValidator(jwtString: jwtString) + guard let isValid = try? validator.isValid(publicKey: signingPubKey), + isValid else { + throw Errors.invalidJWT + } + } + + private func decodeJWTClaims(jwtString: String) throws -> AttestationJWTClaims { + let components = jwtString.components(separatedBy: ".") + + guard components.count == 3 else { throw Errors.invalidJWT } + + let payload = components[1] + guard let payloadData = Data(base64urlEncoded: payload) else { + throw Errors.invalidJWT + } + + let claims = try JSONDecoder().decode(AttestationJWTClaims.self, from: payloadData) + return claims + } +} + diff --git a/Sources/WalletConnectVerify/OriginVerifier.swift b/Sources/WalletConnectVerify/OriginVerifier.swift index 0689088d2..54d4452f0 100644 --- a/Sources/WalletConnectVerify/OriginVerifier.swift +++ b/Sources/WalletConnectVerify/OriginVerifier.swift @@ -5,10 +5,7 @@ public final class OriginVerifier { case registrationFailed } - private var verifyHost = "verify.walletconnect.com" - /// The property is used to determine whether verify.walletconnect.org will be used - /// in case verify.walletconnect.com doesn't respond for some reason (most likely due to being blocked in the user's location). - private var fallback = false + private var verifyHost = "verify.walletconnect.org" func verifyOrigin(assertionId: String) async throws -> VerifyResponse { let sessionConfiguration = URLSessionConfiguration.default @@ -28,17 +25,8 @@ public final class OriginVerifier { } return response } catch { - if (error as? HTTPError) == .couldNotConnect && !fallback { - fallback = true - verifyHostFallback() - return try await verifyOrigin(assertionId: assertionId) - } throw error } } - - func verifyHostFallback() { - verifyHost = "verify.walletconnect.org" - } } diff --git a/Sources/WalletConnectVerify/PublicKeyFetcher.swift b/Sources/WalletConnectVerify/PublicKeyFetcher.swift new file mode 100644 index 000000000..b178673b2 --- /dev/null +++ b/Sources/WalletConnectVerify/PublicKeyFetcher.swift @@ -0,0 +1,115 @@ +import Foundation +import CryptoKit + +// PublicKeyFetcher class +protocol PublicKeyFetching { + func fetchPublicKey() async throws -> VerifyServerPublicKey +} +struct VerifyServerPublicKey: Codable { + enum Errors: Error { + case invalisXCoordinateData + case invalidYCoordinateData + } + let publicKey: JWK + let expiresAt: TimeInterval + + enum CodingKeys: String, CodingKey { + case publicKey = "publicKey" + case expiresAt = "expiresAt" + } + + struct JWK: Codable { + let crv: String + let ext: Bool + let keyOps: [String] + let kty: String + let x: String + let y: String + + enum CodingKeys: String, CodingKey { + case crv + case ext + case keyOps = "key_ops" + case kty + case x + case y + } + + func P256SigningPublicKey() throws -> P256.Signing.PublicKey { + let jwk = self + // Convert the x and y values from base64url to Data + guard let xData = Data(base64urlEncoded: jwk.x), xData.count == 32 else { + print("Invalid x-coordinate data.") + throw Errors.invalisXCoordinateData + } + guard let yData = Data(base64urlEncoded: jwk.y), yData.count == 32 else { + print("Invalid y-coordinate data.") + throw Errors.invalidYCoordinateData + } + + // Concatenate the coordinates with the uncompressed point prefix 0x04 + let rawKeyData = xData + yData + + return try P256.Signing.PublicKey(rawRepresentation: rawKeyData) + } + } + +} + + +class PublicKeyFetcher: PublicKeyFetching { + enum Errors: Error, LocalizedError { + case invalidURL + case httpError(statusCode: Int, message: String) + case decodingError(Error) + } + + private let urlString = "https://verify.walletconnect.org/v2/public-key" + + + func fetchPublicKey() async throws -> VerifyServerPublicKey { + guard let url = URL(string: urlString) else { + throw Errors.invalidURL + } + + let (data, response) = try await URLSession.shared.data(from: url) + if let httpResponse = response as? HTTPURLResponse, !(200...299).contains(httpResponse.statusCode) { + let errorMessage = String(data: data, encoding: .utf8) ?? "Unknown error" + throw Errors.httpError(statusCode: httpResponse.statusCode, message: errorMessage) + } + + do { + let publicKeyResponse = try JSONDecoder().decode(VerifyServerPublicKey.self, from: data) + return publicKeyResponse + } catch { + throw Errors.decodingError(error) + } + } +} + +extension PublicKeyFetcher.Errors { + var errorDescription: String? { + switch self { + case .invalidURL: + return "The URL provided is invalid." + case .httpError(let statusCode, let message): + return "HTTP Error \(statusCode): \(message)" + case .decodingError: + return "Failed to decode the JSON response." + } + } +} + +#if DEBUG +class MockPublicKeyFetcher: PublicKeyFetching { + var publicKey: VerifyServerPublicKey? + var error: Error? + + func fetchPublicKey() async throws -> VerifyServerPublicKey { + if let error = error { + throw error + } + return publicKey! + } +} +#endif diff --git a/Sources/WalletConnectVerify/Register/VerifyResponse.swift b/Sources/WalletConnectVerify/Register/VerifyResponse.swift index 24fba83eb..5ea509b46 100644 --- a/Sources/WalletConnectVerify/Register/VerifyResponse.swift +++ b/Sources/WalletConnectVerify/Register/VerifyResponse.swift @@ -3,4 +3,5 @@ import Foundation public struct VerifyResponse: Decodable { public let origin: String? public let isScam: Bool? + public let isVerified: Bool? } diff --git a/Sources/WalletConnectVerify/Register/VerifyService.swift b/Sources/WalletConnectVerify/Register/VerifyService.swift index 5cf720118..dec10faee 100644 --- a/Sources/WalletConnectVerify/Register/VerifyService.swift +++ b/Sources/WalletConnectVerify/Register/VerifyService.swift @@ -20,7 +20,7 @@ enum VerifyAPI: HTTPService { } var queryParameters: [String: String]? { - return nil + return ["v2Supported": "true"] } var scheme: String { diff --git a/Sources/WalletConnectVerify/VerifyClient.swift b/Sources/WalletConnectVerify/VerifyClient.swift index 9e834d7ed..93fb7dbb4 100644 --- a/Sources/WalletConnectVerify/VerifyClient.swift +++ b/Sources/WalletConnectVerify/VerifyClient.swift @@ -2,11 +2,16 @@ import DeviceCheck import Foundation public protocol VerifyClientProtocol { - func verifyOrigin(assertionId: String) async throws -> VerifyResponse - func createVerifyContext(origin: String?, domain: String, isScam: Bool?) -> VerifyContext + func verify(_ verificationType: VerificationType) async throws -> VerifyResponse + func createVerifyContext(origin: String?, domain: String, isScam: Bool?, isVerified: Bool?) -> VerifyContext func createVerifyContextForLinkMode(redirectUniversalLink: String, domain: String) -> VerifyContext } +public enum VerificationType { + case v1(assertionId: String) + case v2(attestationJWT: String, messageId: String) +} + public actor VerifyClient: VerifyClientProtocol { enum Errors: Error { case attestationNotSupported @@ -16,29 +21,40 @@ public actor VerifyClient: VerifyClientProtocol { let assertionRegistrer: AssertionRegistrer let appAttestationRegistrer: AppAttestationRegistrer let verifyContextFactory: VerifyContextFactory + let attestationVerifier: AttestationJWTVerifier init( originVerifier: OriginVerifier, assertionRegistrer: AssertionRegistrer, appAttestationRegistrer: AppAttestationRegistrer, - verifyContextFactory: VerifyContextFactory = VerifyContextFactory() + verifyContextFactory: VerifyContextFactory = VerifyContextFactory(), + attestationVerifier: AttestationJWTVerifier ) { self.originVerifier = originVerifier self.assertionRegistrer = assertionRegistrer self.appAttestationRegistrer = appAttestationRegistrer self.verifyContextFactory = verifyContextFactory + self.attestationVerifier = attestationVerifier } public func registerAttestationIfNeeded() async throws { try await appAttestationRegistrer.registerAttestationIfNeeded() } - public func verifyOrigin(assertionId: String) async throws -> VerifyResponse { - return try await originVerifier.verifyOrigin(assertionId: assertionId) + /// Verify V2 attestation JWT + /// messageId - hash of the encrypted message supplied in the request + /// assertionId - hash of decrytped message + public func verify(_ verificationType: VerificationType) async throws -> VerifyResponse { + switch verificationType { + case .v1(let assertionId): + return try await originVerifier.verifyOrigin(assertionId: assertionId) + case .v2(let attestationJWT, let messageId): + return try await attestationVerifier.verify(attestationJWT: attestationJWT, messageId: messageId) + } } - - nonisolated public func createVerifyContext(origin: String?, domain: String, isScam: Bool?) -> VerifyContext { - verifyContextFactory.createVerifyContext(origin: origin, domain: domain, isScam: isScam) + + nonisolated public func createVerifyContext(origin: String?, domain: String, isScam: Bool?, isVerified: Bool?) -> VerifyContext { + verifyContextFactory.createVerifyContext(origin: origin, domain: domain, isScam: isScam, isVerified: isVerified) } nonisolated public func createVerifyContextForLinkMode(redirectUniversalLink: String, domain: String) -> VerifyContext { @@ -53,14 +69,13 @@ public actor VerifyClient: VerifyClientProtocol { #if DEBUG public struct VerifyClientMock: VerifyClientProtocol { - public init() {} - - public func verifyOrigin(assertionId: String) async throws -> VerifyResponse { - return VerifyResponse(origin: "domain.com", isScam: nil) + + public func verify(_ verificationType: VerificationType) async throws -> VerifyResponse { + return VerifyResponse(origin: "domain.com", isScam: nil, isVerified: nil) } - - public func createVerifyContext(origin: String?, domain: String, isScam: Bool?) -> VerifyContext { + + public func createVerifyContext(origin: String?, domain: String, isScam: Bool?, isVerified: Bool?) -> VerifyContext { return VerifyContext(origin: "domain.com", validation: .valid) } diff --git a/Sources/WalletConnectVerify/VerifyClientFactory.swift b/Sources/WalletConnectVerify/VerifyClientFactory.swift index 8b230fc5f..a0fe4fbb6 100644 --- a/Sources/WalletConnectVerify/VerifyClientFactory.swift +++ b/Sources/WalletConnectVerify/VerifyClientFactory.swift @@ -17,10 +17,15 @@ public class VerifyClientFactory { attestChallengeProvider: attestChallengeProvider, keyAttestationService: keyAttestationService ) + let verifyServerPubKeyManagerStore: CodableStore = CodableStore(defaults: keyValueStorage, identifier: "com.walletconnect.verify") + + let verifyServerPubKeyManager = VerifyServerPubKeyManager(store: verifyServerPubKeyManagerStore) + let attestationVerifier = AttestationJWTVerifier(verifyServerPubKeyManager: verifyServerPubKeyManager) return VerifyClient( originVerifier: originVerifier, assertionRegistrer: assertionRegistrer, - appAttestationRegistrer: appAttestationRegistrer + appAttestationRegistrer: appAttestationRegistrer, + attestationVerifier: attestationVerifier ) } } diff --git a/Sources/WalletConnectVerify/VerifyContextFactory.swift b/Sources/WalletConnectVerify/VerifyContextFactory.swift index 0cdec63fb..629973e38 100644 --- a/Sources/WalletConnectVerify/VerifyContextFactory.swift +++ b/Sources/WalletConnectVerify/VerifyContextFactory.swift @@ -2,13 +2,23 @@ import Foundation class VerifyContextFactory { - public func createVerifyContext(origin: String?, domain: String, isScam: Bool?) -> VerifyContext { + public func createVerifyContext(origin: String?, domain: String, isScam: Bool?, isVerified: Bool?) -> VerifyContext { + guard isScam != true else { return VerifyContext( origin: origin, validation: .scam ) } + + // If isVerified is provided and is false, return unknown + if let isVerified = isVerified, !isVerified { + return VerifyContext( + origin: origin, + validation: .unknown + ) + } + if let origin, let originUrl = URL(string: origin), let domainUrl = URL(string: domain) { return VerifyContext( origin: origin, diff --git a/Sources/WalletConnectVerify/VerifyImports.swift b/Sources/WalletConnectVerify/VerifyImports.swift index 065c96db2..c47a6a9ba 100644 --- a/Sources/WalletConnectVerify/VerifyImports.swift +++ b/Sources/WalletConnectVerify/VerifyImports.swift @@ -1,4 +1,6 @@ #if !CocoaPods @_exported import WalletConnectUtils @_exported import WalletConnectNetworking +@_exported import WalletConnectJWT +@_exported import WalletConnectKMS #endif diff --git a/Sources/WalletConnectVerify/VerifyServerPubKeyManager.swift b/Sources/WalletConnectVerify/VerifyServerPubKeyManager.swift new file mode 100644 index 000000000..49e2d3051 --- /dev/null +++ b/Sources/WalletConnectVerify/VerifyServerPubKeyManager.swift @@ -0,0 +1,96 @@ +import Foundation +import CryptoKit + +protocol VerifyServerPubKeyManagerProtocol { + func getPublicKey() async throws -> P256.Signing.PublicKey + func refreshKey() async throws -> P256.Signing.PublicKey +} + +class VerifyServerPubKeyManager: VerifyServerPubKeyManagerProtocol { + + static let publicKeyStorageKey = "verify_server_pub_key" + private let store: CodableStore + private let fetcher: PublicKeyFetching + + init(store: CodableStore, fetcher: PublicKeyFetching = PublicKeyFetcher()) { + self.store = store + self.fetcher = fetcher + + // Check if there is a cached, non-expired key on initialization + Task { + do { + if let localKey = try getPublicKeyFromLocalStorage(), !isKeyExpired(localKey) { + // Key is valid, no action needed + } else { + // No valid key, fetch and store a new one + let serverKey = try await fetcher.fetchPublicKey() + savePublicKeyToLocalStorage(publicKey: serverKey) + } + } catch { + print("Failed to initialize public key: \(error)") + } + } + } + + func getPublicKey() async throws -> P256.Signing.PublicKey { + if let localKey = try getPublicKeyFromLocalStorage(), !isKeyExpired(localKey) { + return try localKey.publicKey.P256SigningPublicKey() + } else { + let serverKey = try await fetcher.fetchPublicKey() + savePublicKeyToLocalStorage(publicKey: serverKey) + return try serverKey.publicKey.P256SigningPublicKey() + } + } + + func refreshKey() async throws -> P256.Signing.PublicKey { + let serverKey = try await fetcher.fetchPublicKey() + savePublicKeyToLocalStorage(publicKey: serverKey) + return try serverKey.publicKey.P256SigningPublicKey() + } + + private func getPublicKeyFromLocalStorage() throws -> VerifyServerPublicKey? { + return try store.get(key: Self.publicKeyStorageKey) + } + + private func isKeyExpired(_ key: VerifyServerPublicKey) -> Bool { + let currentTime = Date().timeIntervalSince1970 + return currentTime >= key.expiresAt + } + + private func savePublicKeyToLocalStorage(publicKey: VerifyServerPublicKey) { + store.set(publicKey, forKey: Self.publicKeyStorageKey) + } +} + +#if DEBUG +class VerifyServerPubKeyManagerMock: VerifyServerPubKeyManagerProtocol { + var mockPublicKey: P256.Signing.PublicKey? + var error: Error? + + init() { + let jwk = VerifyServerPublicKey.JWK( + crv: "P-256", + ext: true, + keyOps: ["verify"], + kty: "EC", + x: "CbL4DOYOb1ntd-8OmExO-oS0DWCMC00DntrymJoB8tk", + y: "KTFwjHtQxGTDR91VsOypcdBfvbo6sAMj5p4Wb-9hRA0" + ) + mockPublicKey = try? jwk.P256SigningPublicKey() + } + + func getPublicKey() async throws -> P256.Signing.PublicKey { + if let error = error { + throw error + } + return mockPublicKey! + } + + func refreshKey() async throws -> P256.Signing.PublicKey { + if let error = error { + throw error + } + return mockPublicKey! + } +} +#endif diff --git a/Tests/RelayerTests/RelayClientTests.swift b/Tests/RelayerTests/RelayClientTests.swift index 6442757cb..d767623e4 100644 --- a/Tests/RelayerTests/RelayClientTests.swift +++ b/Tests/RelayerTests/RelayClientTests.swift @@ -29,10 +29,10 @@ final class RelayClientTests: XCTestCase { let topic = "0987" let message = "qwerty" let subscriptionId = "sub-id" - let subscription = Subscription(id: subscriptionId, topic: topic, message: message) + let subscription = Subscription(id: subscriptionId, topic: topic, message: message, attestation: nil) let request = subscription.asRPCRequest() - sut.messagePublisher.sink { (subscriptionTopic, subscriptionMessage, _) in + sut.messagePublisher.sink { (subscriptionTopic, subscriptionMessage, _, _) in XCTAssertEqual(subscriptionMessage, message) XCTAssertEqual(subscriptionTopic, topic) expectation.fulfill() @@ -62,7 +62,7 @@ final class RelayClientTests: XCTestCase { let expectation = expectation(description: "Duplicate Subscription requests must notify only the first time") let request = Subscription.init(id: "sub_id", topic: "topic", message: "message").asRPCRequest() - sut.messagePublisher.sink { (_, _, _) in + sut.messagePublisher.sink { (_, _, _, _) in expectation.fulfill() }.store(in: &publishers) diff --git a/Tests/TestingUtils/NetworkingInteractorMock.swift b/Tests/TestingUtils/NetworkingInteractorMock.swift index 65b73ab0c..c08cde49c 100644 --- a/Tests/TestingUtils/NetworkingInteractorMock.swift +++ b/Tests/TestingUtils/NetworkingInteractorMock.swift @@ -40,10 +40,10 @@ public class NetworkingInteractorMock: NetworkInteracting { networkConnectionStatusPublisherSubject.eraseToAnyPublisher() } - public let requestPublisherSubject = PassthroughSubject<(topic: String, request: RPCRequest, decryptedPayload: Data, publishedAt: Date, derivedTopic: String?), Never>() + public let requestPublisherSubject = PassthroughSubject<(topic: String, request: RPCRequest, decryptedPayload: Data, publishedAt: Date, derivedTopic: String?, encryptedMessage: String, attestation: String?), Never>() public let responsePublisherSubject = PassthroughSubject<(topic: String, request: RPCRequest, response: RPCResponse, publishedAt: Date, derivedTopic: String?), Never>() - public var requestPublisher: AnyPublisher<(topic: String, request: JSONRPC.RPCRequest, decryptedPayload: Data, publishedAt: Date, derivedTopic: String?), Never> { + public var requestPublisher: AnyPublisher<(topic: String, request: JSONRPC.RPCRequest, decryptedPayload: Data, publishedAt: Date, derivedTopic: String?, encryptedMessage: String, attestation: String?), Never> { requestPublisherSubject.eraseToAnyPublisher() } @@ -63,9 +63,9 @@ public class NetworkingInteractorMock: NetworkInteracting { .filter { rpcRequest in return rpcRequest.request.method == request.method } - .compactMap { topic, rpcRequest, decryptedPayload, publishedAt, derivedTopic in + .compactMap { topic, rpcRequest, decryptedPayload, publishedAt, derivedTopic, encryptedMessage, attestation in guard let id = rpcRequest.id, let request = try? rpcRequest.params?.get(Request.self) else { return nil } - return RequestSubscriptionPayload(id: id, topic: topic, request: request, decryptedPayload: decryptedPayload, publishedAt: publishedAt, derivedTopic: derivedTopic) + return RequestSubscriptionPayload(id: id, topic: topic, request: request, decryptedPayload: decryptedPayload, publishedAt: publishedAt, derivedTopic: derivedTopic, encryptedMessage: encryptedMessage, attestation: attestation) } .eraseToAnyPublisher() } diff --git a/Tests/VerifyTests/AttestationJWTVerifier.swift b/Tests/VerifyTests/AttestationJWTVerifier.swift new file mode 100644 index 000000000..2f1a0e7ab --- /dev/null +++ b/Tests/VerifyTests/AttestationJWTVerifier.swift @@ -0,0 +1,36 @@ +import XCTest +import CryptoKit +@testable import WalletConnectVerify + +class AttestationJWTVerifierTests: XCTestCase { + var verifier: AttestationJWTVerifier! + var mockManager: VerifyServerPubKeyManagerMock! + + override func setUp() { + super.setUp() + mockManager = VerifyServerPubKeyManagerMock() + verifier = AttestationJWTVerifier(verifyServerPubKeyManager: mockManager) + } + + func testVerifyValidJWT() async throws { + let jwt = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MjM0NzE3MjMsImlkIjoiYjNmYmZhMDUxZDJhNjdkNGRmNTYzM2IyMjc0NDAyNTUxMTg1NzQwZGQwMjA3YWM0OWI1M2RiYTcxOTc0YTgzNCIsIm9yaWdpbiI6Imh0dHBzOi8vcmVhY3QtZGFwcC12Mi1naXQtY2hvcmUtdmVyaWZ5LXYyLXNhbXBsZXMtd2FsbGV0Y29ubmVjdDEudmVyY2VsLmFwcCIsImlzU2NhbSI6bnVsbCwiaXNWZXJpZmllZCI6dHJ1ZX0.8RQwiEEfTGn8p3INRdHpi88dpzetKCp3nscfLtWG2cVE2dU0dWgV2ncqnh_RWmygqEnWCPUlH1RMwS1nWbZzrQ" + let messageId = "b3fbfa051d2a67d4df5633b2274402551185740dd0207ac49b53dba71974a834" + + let response = try await verifier.verify(attestationJWT: jwt, messageId: messageId) + XCTAssertEqual(response.origin, "https://react-dapp-v2-git-chore-verify-v2-samples-walletconnect1.vercel.app") + } + + func testVerifyJWTWithInvalidMessageId() async throws { + let jwt = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MjM0NzE3MjMsImlkIjoiYjNmYmZhMDUxZDJhNjdkNGRmNTYzM2IyMjc0NDAyNTUxMTg1NzQwZGQwMjA3YWM0OWI1M2RiYTcxOTc0YTgzNCIsIm9yaWdpbiI6Imh0dHBzOi8vcmVhY3QtZGFwcC12Mi1naXQtY2hvcmUtdmVyaWZ5LXYyLXNhbXBsZXMtd2FsbGV0Y29ubmVjdDEudmVyY2VsLmFwcCIsImlzU2NhbSI6bnVsbCwiaXNWZXJpZmllZCI6dHJ1ZX0.8RQwiEEfTGn8p3INRdHpi88dpzetKCp3nscfLtWG2cVE2dU0dWgV2ncqnh_RWmygqEnWCPUlH1RMwS1nWbZzrQ" + let invalidMessageId = "InvalidMessageId" + + do { + _ = try await verifier.verify(attestationJWT: jwt, messageId: invalidMessageId) + XCTFail("Expected to throw messageIdMismatch error") + } catch AttestationJWTVerifier.Errors.messageIdMismatch { + // Expected error + } catch { + XCTFail("Unexpected error: \(error)") + } + } +} diff --git a/Tests/VerifyTests/VerifyContextFactoryTests.swift b/Tests/VerifyTests/VerifyContextFactoryTests.swift index 2806b10c3..0d43c2aed 100644 --- a/Tests/VerifyTests/VerifyContextFactoryTests.swift +++ b/Tests/VerifyTests/VerifyContextFactoryTests.swift @@ -17,25 +17,38 @@ class VerifyContextFactoryTests: XCTestCase { } func testScamValidation() { - let context = factory.createVerifyContext(origin: "http://example.com", domain: "http://example.com", isScam: true) + let context = factory.createVerifyContext(origin: "http://example.com", domain: "http://example.com", isScam: true, isVerified: nil) XCTAssertEqual(context.validation, .scam) } func testValidOriginAndDomain() { - let context = factory.createVerifyContext(origin: "http://example.com", domain: "http://example.com", isScam: false) + let context = factory.createVerifyContext(origin: "http://example.com", domain: "http://example.com", isScam: false, isVerified: nil) XCTAssertEqual(context.validation, .valid) } func testInvalidOriginAndDomain() { - let context = factory.createVerifyContext(origin: "http://example.com", domain: "http://different.com", isScam: false) + let context = factory.createVerifyContext(origin: "http://example.com", domain: "http://different.com", isScam: false, isVerified: nil) XCTAssertEqual(context.validation, .invalid) } func testUnknownValidation() { - let context = factory.createVerifyContext(origin: nil, domain: "http://example.com", isScam: false) + let context = factory.createVerifyContext(origin: nil, domain: "http://example.com", isScam: false, isVerified: nil) XCTAssertEqual(context.validation, .unknown) } + func testVerifyContextIsMarkedAsUnknownWhenIsVerifiedIsFalse() { + let context = factory.createVerifyContext(origin: "http://example.com", domain: "http://example.com", isScam: false, isVerified: false) + XCTAssertEqual(context.validation, .unknown) + } + + func testVerifyContextIsMarkedAsScamWhenIsScamIsTrueRegardlessOfIsVerified() { + let context = factory.createVerifyContext(origin: "http://example.com", domain: "http://example.com", isScam: true, isVerified: true) + XCTAssertEqual(context.validation, .scam) + + let contextWithFalseVerification = factory.createVerifyContext(origin: "http://example.com", domain: "http://example.com", isScam: true, isVerified: false) + XCTAssertEqual(contextWithFalseVerification.validation, .scam) + } + // tests for createVerifyContextForLinkMode func testValidUniversalLink() { diff --git a/Tests/VerifyTests/VerifyServerPubKeyManagerTests.swift b/Tests/VerifyTests/VerifyServerPubKeyManagerTests.swift new file mode 100644 index 000000000..1a997465a --- /dev/null +++ b/Tests/VerifyTests/VerifyServerPubKeyManagerTests.swift @@ -0,0 +1,79 @@ +import XCTest +import CryptoKit +@testable import WalletConnectVerify + +class VerifyServerPubKeyManagerTests: XCTestCase { + var manager: VerifyServerPubKeyManager! + var store: CodableStore! + var fetcher: MockPublicKeyFetcher! + + override func setUp() { + super.setUp() + let storage = RuntimeKeyValueStorage() + store = CodableStore(defaults: storage, identifier: "test") + fetcher = MockPublicKeyFetcher() + } + + func testGetPublicKeyFromServer() async throws { + let expectedJWK = VerifyServerPublicKey.JWK.stub() + let expectedPublicKey = try expectedJWK.P256SigningPublicKey() + let expiresAt = Date().timeIntervalSince1970 + 3600 // 1 hour from now + fetcher.publicKey = VerifyServerPublicKey(publicKey: expectedJWK, expiresAt: expiresAt) + manager = VerifyServerPubKeyManager(store: store, fetcher: fetcher) + + let publicKey = try await manager.getPublicKey() + + XCTAssertEqual(publicKey.rawRepresentation, expectedPublicKey.rawRepresentation) + } + + func testGetPublicKeyFromLocalStorage() async throws { + let expectedJWK = VerifyServerPublicKey.JWK.stub() + let expectedPublicKey = try expectedJWK.P256SigningPublicKey() + let expiresAt = Date().timeIntervalSince1970 + 3600 // 1 hour from now + let storedKey = VerifyServerPublicKey(publicKey: expectedJWK, expiresAt: expiresAt) + store.set(storedKey, forKey: VerifyServerPubKeyManager.publicKeyStorageKey) + manager = VerifyServerPubKeyManager(store: store, fetcher: fetcher) + + let publicKey = try await manager.getPublicKey() + + XCTAssertEqual(publicKey.rawRepresentation, expectedPublicKey.rawRepresentation) + } + + func testGetExpiredPublicKeyFromLocalStorage() async throws { + let oldJWK = VerifyServerPublicKey.JWK.stub() + let newJWK = VerifyServerPublicKey.JWK( + crv: "P-256", + ext: true, + keyOps: ["verify"], + kty: "EC", + x: "MKl2ZQXTZsL10tK3nDXJZUJTTkGaxgPtg42lC5VxW9c", + y: "IcIsyFf6M5XzUjxwK9ujYB69TUMzIYGTkUyrvjoB3UM" + ) + let oldPublicKey = try oldJWK.P256SigningPublicKey() + let newPublicKey = try newJWK.P256SigningPublicKey() + let expiredTime = Date().timeIntervalSince1970 - 3600 // 1 hour ago + let validTime = Date().timeIntervalSince1970 + 3600 // 1 hour from now + let storedKey = VerifyServerPublicKey(publicKey: oldJWK, expiresAt: expiredTime) + store.set(storedKey, forKey: VerifyServerPubKeyManager.publicKeyStorageKey) + + fetcher.publicKey = VerifyServerPublicKey(publicKey: newJWK, expiresAt: validTime) + manager = VerifyServerPubKeyManager(store: store, fetcher: fetcher) + + let publicKey = try await manager.getPublicKey() + + XCTAssertEqual(publicKey.rawRepresentation, newPublicKey.rawRepresentation) + } +} + +extension VerifyServerPublicKey.JWK { + static func stub() -> VerifyServerPublicKey.JWK { + return VerifyServerPublicKey.JWK( + crv: "P-256", + ext: true, + keyOps: ["verify"], + kty: "EC", + x: "CbL4DOYOb1ntd-8OmExO-oS0DWCMC00DntrymJoB8tk", + y: "KTFwjHtQxGTDR91VsOypcdBfvbo6sAMj5p4Wb-9hRA0" + ) + } +} diff --git a/Tests/WalletConnectSignTests/ApproveEngineTests.swift b/Tests/WalletConnectSignTests/ApproveEngineTests.swift index 79b2f7996..1d36d7902 100644 --- a/Tests/WalletConnectSignTests/ApproveEngineTests.swift +++ b/Tests/WalletConnectSignTests/ApproveEngineTests.swift @@ -73,8 +73,8 @@ final class ApproveEngineTests: XCTestCase { pairingStorageMock.setPairing(pairing) let proposerPubKey = AgreementPrivateKey().publicKey.hexRepresentation let proposal = SessionProposal.stub(proposerPubKey: proposerPubKey) - pairingRegisterer.subject.send(RequestSubscriptionPayload(id: RPCID("id"), topic: topicA, request: proposal, decryptedPayload: Data(), publishedAt: Date(), derivedTopic: nil)) - + pairingRegisterer.subject.send(RequestSubscriptionPayload(id: RPCID("id"), topic: topicA, request: proposal, decryptedPayload: Data(), publishedAt: Date(), derivedTopic: nil, encryptedMessage: "", attestation: nil)) + _ = try await engine.approveProposal(proposerPubKey: proposal.proposer.publicKey, validating: SessionNamespace.stubDictionary()) let topicB = networkingInteractor.subscriptions.last! @@ -98,7 +98,7 @@ final class ApproveEngineTests: XCTestCase { sessionProposed = true } - pairingRegisterer.subject.send(RequestSubscriptionPayload(id: RPCID("id"), topic: topicA, request: proposal, decryptedPayload: Data(), publishedAt: Date(), derivedTopic: nil)) + pairingRegisterer.subject.send(RequestSubscriptionPayload(id: RPCID("id"), topic: topicA, request: proposal, decryptedPayload: Data(), publishedAt: Date(), derivedTopic: nil, encryptedMessage: "", attestation: nil)) XCTAssertNotNil(try! proposalPayloadsStore.get(key: proposal.proposer.publicKey), "Proposer must store proposal payload") XCTAssertTrue(sessionProposed) } @@ -121,7 +121,7 @@ final class ApproveEngineTests: XCTestCase { didCallBackOnSessionApproved = true } sessionTopicToProposal.set(SessionProposal.stub().publicRepresentation(pairingTopic: ""), forKey: sessionTopic) - networkingInteractor.requestPublisherSubject.send((sessionTopic, RPCRequest.stubSettle(), Data(), Date(), "")) + networkingInteractor.requestPublisherSubject.send((sessionTopic, RPCRequest.stubSettle(), Data(), Date(), "", "", nil)) usleep(100) @@ -172,7 +172,7 @@ final class ApproveEngineTests: XCTestCase { engine.onSessionProposal = { _, _ in proposalReceivedExpectation.fulfill() } - pairingRegisterer.subject.send(RequestSubscriptionPayload(id: RPCID("id"), topic: topicA, request: proposal, decryptedPayload: Data(), publishedAt: Date(), derivedTopic: nil)) + pairingRegisterer.subject.send(RequestSubscriptionPayload(id: RPCID("id"), topic: topicA, request: proposal, decryptedPayload: Data(), publishedAt: Date(), derivedTopic: nil, encryptedMessage: "", attestation: nil)) wait(for: [proposalReceivedExpectation], timeout: 0.1) @@ -191,8 +191,8 @@ final class ApproveEngineTests: XCTestCase { engine.onSessionProposal = { _, _ in proposalReceivedExpectation.fulfill() } - pairingRegisterer.subject.send(RequestSubscriptionPayload(id: RPCID("id"), topic: topicA, request: proposal, decryptedPayload: Data(), publishedAt: Date(), derivedTopic: nil)) - + pairingRegisterer.subject.send(RequestSubscriptionPayload(id: RPCID("id"), topic: topicA, request: proposal, decryptedPayload: Data(), publishedAt: Date(), derivedTopic: nil, encryptedMessage: "", attestation: nil)) + wait(for: [proposalReceivedExpectation], timeout: 0.1) XCTAssertTrue(verifyContextStore.getAll().count == 1) @@ -214,7 +214,7 @@ final class ApproveEngineTests: XCTestCase { engine.onSessionProposal = { _, _ in proposalReceivedExpectation.fulfill() } - pairingRegisterer.subject.send(RequestSubscriptionPayload(id: RPCID("id"), topic: topicA, request: proposal, decryptedPayload: Data(), publishedAt: Date(), derivedTopic: nil)) + pairingRegisterer.subject.send(RequestSubscriptionPayload(id: RPCID("id"), topic: topicA, request: proposal, decryptedPayload: Data(), publishedAt: Date(), derivedTopic: nil, encryptedMessage: "", attestation: nil)) wait(for: [proposalReceivedExpectation], timeout: 0.1) diff --git a/Tests/WalletConnectSignTests/NonControllerSessionStateMachineTests.swift b/Tests/WalletConnectSignTests/NonControllerSessionStateMachineTests.swift index 7304ea2c8..5f9007b37 100644 --- a/Tests/WalletConnectSignTests/NonControllerSessionStateMachineTests.swift +++ b/Tests/WalletConnectSignTests/NonControllerSessionStateMachineTests.swift @@ -35,7 +35,7 @@ class NonControllerSessionStateMachineTests: XCTestCase { didCallbackUpdatMethods = true XCTAssertEqual(topic, session.topic) } - networkingInteractor.requestPublisherSubject.send((session.topic, RPCRequest.stubUpdateNamespaces(), Data(), Date(), nil)) + networkingInteractor.requestPublisherSubject.send((session.topic, RPCRequest.stubUpdateNamespaces(), Data(), Date(), nil, "", nil)) XCTAssertTrue(didCallbackUpdatMethods) usleep(100) XCTAssertTrue(networkingInteractor.didRespondSuccess) @@ -51,7 +51,7 @@ class NonControllerSessionStateMachineTests: XCTestCase { // } func testUpdateMethodPeerErrorSessionNotFound() { - networkingInteractor.requestPublisherSubject.send(("", RPCRequest.stubUpdateNamespaces(), Data(), Date(), nil)) + networkingInteractor.requestPublisherSubject.send(("", RPCRequest.stubUpdateNamespaces(), Data(), Date(), nil, "", nil)) usleep(100) XCTAssertFalse(networkingInteractor.didRespondSuccess) XCTAssertEqual(networkingInteractor.lastErrorCode, 7001) @@ -60,7 +60,7 @@ class NonControllerSessionStateMachineTests: XCTestCase { func testUpdateMethodPeerErrorUnauthorized() { let session = WCSession.stub(isSelfController: true) // Peer is not a controller storageMock.setSession(session) - networkingInteractor.requestPublisherSubject.send((session.topic, RPCRequest.stubUpdateNamespaces(), Data(), Date(), nil)) + networkingInteractor.requestPublisherSubject.send((session.topic, RPCRequest.stubUpdateNamespaces(), Data(), Date(), nil, "", nil)) usleep(100) XCTAssertFalse(networkingInteractor.didRespondSuccess) XCTAssertEqual(networkingInteractor.lastErrorCode, 3003) @@ -73,7 +73,7 @@ class NonControllerSessionStateMachineTests: XCTestCase { storageMock.setSession(session) let twoDaysFromNowTimestamp = Int64(TimeTraveler.dateByAdding(days: 2).timeIntervalSince1970) - networkingInteractor.requestPublisherSubject.send((session.topic, RPCRequest.stubUpdateExpiry(expiry: twoDaysFromNowTimestamp), Data(), Date(), nil)) + networkingInteractor.requestPublisherSubject.send((session.topic, RPCRequest.stubUpdateExpiry(expiry: twoDaysFromNowTimestamp), Data(), Date(), nil, "", nil)) let potentiallyExtendedSession = storageMock.getAll().first {$0.topic == session.topic}! XCTAssertEqual(potentiallyExtendedSession.expiryDate.timeIntervalSinceReferenceDate, tomorrow.timeIntervalSinceReferenceDate, accuracy: 1, "expiry date has been extended for peer non controller request ") @@ -84,7 +84,7 @@ class NonControllerSessionStateMachineTests: XCTestCase { let session = WCSession.stub(isSelfController: false, expiryDate: tomorrow) storageMock.setSession(session) let tenDaysFromNowTimestamp = Int64(TimeTraveler.dateByAdding(days: 10).timeIntervalSince1970) - networkingInteractor.requestPublisherSubject.send((session.topic, RPCRequest.stubUpdateExpiry(expiry: tenDaysFromNowTimestamp), Data(), Date(), nil)) + networkingInteractor.requestPublisherSubject.send((session.topic, RPCRequest.stubUpdateExpiry(expiry: tenDaysFromNowTimestamp), Data(), Date(), nil, "", nil)) let potentaillyExtendedSession = storageMock.getAll().first {$0.topic == session.topic}! XCTAssertEqual(potentaillyExtendedSession.expiryDate.timeIntervalSinceReferenceDate, tomorrow.timeIntervalSinceReferenceDate, accuracy: 1, "expiry date has been extended despite ttl to high") @@ -96,7 +96,7 @@ class NonControllerSessionStateMachineTests: XCTestCase { storageMock.setSession(session) let oneDayFromNowTimestamp = Int64(TimeTraveler.dateByAdding(days: 10).timeIntervalSince1970) - networkingInteractor.requestPublisherSubject.send((session.topic, RPCRequest.stubUpdateExpiry(expiry: oneDayFromNowTimestamp), Data(), Date(), nil)) + networkingInteractor.requestPublisherSubject.send((session.topic, RPCRequest.stubUpdateExpiry(expiry: oneDayFromNowTimestamp), Data(), Date(), nil, "", nil)) let potentaillyExtendedSession = storageMock.getAll().first {$0.topic == session.topic}! XCTAssertEqual(potentaillyExtendedSession.expiryDate.timeIntervalSinceReferenceDate, tomorrow.timeIntervalSinceReferenceDate, accuracy: 1, "expiry date has been extended despite ttl to low") } diff --git a/Tests/WalletConnectSignTests/SessionEngineTests.swift b/Tests/WalletConnectSignTests/SessionEngineTests.swift index 0f2828805..63bff095f 100644 --- a/Tests/WalletConnectSignTests/SessionEngineTests.swift +++ b/Tests/WalletConnectSignTests/SessionEngineTests.swift @@ -30,7 +30,6 @@ final class SessionEngineTests: XCTestCase { networkingInteractor: networkingInteractor, historyService: historyService, verifyContextStore: verifyContextStore, - verifyClient: VerifyClientMock(), kms: KeyManagementServiceMock(), sessionStore: sessionStorage, logger: ConsoleLoggerMock(), @@ -59,7 +58,7 @@ final class SessionEngineTests: XCTestCase { expiry: UInt64(Date().timeIntervalSince1970) ) - networkingInteractor.requestPublisherSubject.send(("topic", request, Data(), Date(), "")) + networkingInteractor.requestPublisherSubject.send(("topic", request, Data(), Date(), "", "", nil)) wait(for: [expectation], timeout: 0.5) } @@ -75,7 +74,6 @@ final class SessionEngineTests: XCTestCase { networkingInteractor: networkingInteractor, historyService: historyService, verifyContextStore: verifyContextStore, - verifyClient: VerifyClientMock(), kms: KeyManagementServiceMock(), sessionStore: sessionStorage, logger: ConsoleLoggerMock(), diff --git a/Tests/WalletConnectSignTests/Stub/Session+Stub.swift b/Tests/WalletConnectSignTests/Stub/Session+Stub.swift index 4e332022d..b023226ce 100644 --- a/Tests/WalletConnectSignTests/Stub/Session+Stub.swift +++ b/Tests/WalletConnectSignTests/Stub/Session+Stub.swift @@ -32,7 +32,8 @@ extension WCSession { accounts: Account.stubSet(), acknowledged: acknowledged, expiryTimestamp: Int64(expiryDate.timeIntervalSince1970), - transportType: .relay) + transportType: .relay, + verifyContext: VerifyContext(origin: nil, validation: .unknown)) } }