mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-19 20:14:45 +00:00
## Summary - allow iOS to trust system-valid rotated gateway certificates - rebuild active gateway sessions after replacing the stored TLS pin - expose certificate trust recovery from gateway problem banners ## Verification - swift test --filter 'GatewayErrorsTests|GatewayNodeSessionTests/changedSessionBoxRebuildsExistingGatewayChannel' - xcodebuild build -scheme OpenClaw -destination 'platform=iOS,id=00008140-000848A92EE3001C' - installed and launched OpenClaw on attached iPhone with devicectl - verified iOS gateway log connected to wss://gutsy-home.tail06a72.ts.net:443 after trust/pairing recovery
203 lines
8.7 KiB
Swift
203 lines
8.7 KiB
Swift
import Foundation
|
|
import Network
|
|
import OpenClawKit
|
|
import Testing
|
|
@testable import OpenClaw
|
|
|
|
@Suite(.serialized) struct GatewayConnectionSecurityTests {
|
|
@MainActor
|
|
private func makeController() -> GatewayConnectionController {
|
|
GatewayConnectionController(appModel: NodeAppModel(), startDiscovery: false)
|
|
}
|
|
|
|
private func makeDiscoveredGateway(
|
|
stableID: String,
|
|
lanHost: String?,
|
|
tailnetDns: String?,
|
|
gatewayPort: Int?,
|
|
fingerprint: String?) -> GatewayDiscoveryModel.DiscoveredGateway
|
|
{
|
|
let endpoint: NWEndpoint = .service(name: "Test", type: "_openclaw-gw._tcp", domain: "local.", interface: nil)
|
|
return GatewayDiscoveryModel.DiscoveredGateway(
|
|
name: "Test",
|
|
endpoint: endpoint,
|
|
stableID: stableID,
|
|
debugID: "debug",
|
|
lanHost: lanHost,
|
|
tailnetDns: tailnetDns,
|
|
gatewayPort: gatewayPort,
|
|
canvasPort: nil,
|
|
tlsEnabled: true,
|
|
tlsFingerprintSha256: fingerprint,
|
|
cliPath: nil)
|
|
}
|
|
|
|
private func clearTLSFingerprint(stableID: String) {
|
|
GatewayTLSStore.clearFingerprint(stableID: stableID)
|
|
}
|
|
|
|
@Test @MainActor func discoveredTLSParams_prefersStoredPinOverAdvertisedTXT() async {
|
|
let stableID = "test|\(UUID().uuidString)"
|
|
defer { clearTLSFingerprint(stableID: stableID) }
|
|
clearTLSFingerprint(stableID: stableID)
|
|
|
|
GatewayTLSStore.saveFingerprint("11", stableID: stableID)
|
|
|
|
let gateway = makeDiscoveredGateway(
|
|
stableID: stableID,
|
|
lanHost: "evil.example.com",
|
|
tailnetDns: "evil.example.com",
|
|
gatewayPort: 12345,
|
|
fingerprint: "22")
|
|
let controller = makeController()
|
|
|
|
let params = controller._test_resolveDiscoveredTLSParams(gateway: gateway, allowTOFU: true)
|
|
#expect(params?.expectedFingerprint == "11")
|
|
#expect(params?.allowTOFU == false)
|
|
}
|
|
|
|
@Test @MainActor func discoveredTLSParams_doesNotTrustAdvertisedFingerprint() async {
|
|
let stableID = "test|\(UUID().uuidString)"
|
|
defer { clearTLSFingerprint(stableID: stableID) }
|
|
clearTLSFingerprint(stableID: stableID)
|
|
|
|
let gateway = makeDiscoveredGateway(
|
|
stableID: stableID,
|
|
lanHost: nil,
|
|
tailnetDns: nil,
|
|
gatewayPort: nil,
|
|
fingerprint: "22")
|
|
let controller = makeController()
|
|
|
|
let params = controller._test_resolveDiscoveredTLSParams(gateway: gateway, allowTOFU: true)
|
|
#expect(params?.expectedFingerprint == nil)
|
|
#expect(params?.allowTOFU == false)
|
|
}
|
|
|
|
@Test @MainActor func autoconnectRequiresStoredPinForDiscoveredGateways() async {
|
|
let stableID = "test|\(UUID().uuidString)"
|
|
defer { clearTLSFingerprint(stableID: stableID) }
|
|
clearTLSFingerprint(stableID: stableID)
|
|
|
|
let defaults = UserDefaults.standard
|
|
defaults.set(true, forKey: "gateway.autoconnect")
|
|
defaults.set(false, forKey: "gateway.manual.enabled")
|
|
defaults.removeObject(forKey: "gateway.last.host")
|
|
defaults.removeObject(forKey: "gateway.last.port")
|
|
defaults.removeObject(forKey: "gateway.last.tls")
|
|
defaults.removeObject(forKey: "gateway.last.stableID")
|
|
defaults.removeObject(forKey: "gateway.last.kind")
|
|
defaults.removeObject(forKey: "gateway.preferredStableID")
|
|
defaults.set(stableID, forKey: "gateway.lastDiscoveredStableID")
|
|
|
|
let gateway = makeDiscoveredGateway(
|
|
stableID: stableID,
|
|
lanHost: "test.local",
|
|
tailnetDns: nil,
|
|
gatewayPort: 18789,
|
|
fingerprint: nil)
|
|
let controller = makeController()
|
|
controller._test_setGateways([gateway])
|
|
controller._test_triggerAutoConnect()
|
|
|
|
#expect(controller._test_didAutoConnect() == false)
|
|
}
|
|
|
|
@Test @MainActor func manualConnectionsForceTLSForNonLoopbackHosts() async {
|
|
let controller = makeController()
|
|
|
|
#expect(controller._test_resolveManualUseTLS(host: "gateway.example.com", useTLS: false) == true)
|
|
#expect(controller._test_resolveManualUseTLS(host: "127.attacker.example", useTLS: false) == true)
|
|
#expect(controller._test_resolveManualUseTLS(host: "gateway.ts.net", useTLS: false) == true)
|
|
#expect(controller._test_resolveManualUseTLS(host: "100.64.0.9", useTLS: false) == true)
|
|
|
|
#expect(controller._test_resolveManualUseTLS(host: "localhost", useTLS: false) == false)
|
|
#expect(controller._test_resolveManualUseTLS(host: "127.0.0.1", useTLS: false) == false)
|
|
#expect(controller._test_resolveManualUseTLS(host: "::1", useTLS: false) == false)
|
|
#expect(controller._test_resolveManualUseTLS(host: "[::1]", useTLS: false) == false)
|
|
#expect(controller._test_resolveManualUseTLS(host: "::ffff:127.0.0.1", useTLS: false) == false)
|
|
#expect(controller._test_resolveManualUseTLS(host: "0.0.0.0", useTLS: false) == false)
|
|
}
|
|
|
|
@Test @MainActor func manualConnectionsAllowPrivateLanPlaintext() async {
|
|
let controller = makeController()
|
|
|
|
#expect(controller._test_resolveManualUseTLS(host: "openclaw.local", useTLS: false) == false)
|
|
#expect(controller._test_resolveManualUseTLS(host: "192.168.1.20", useTLS: false) == false)
|
|
#expect(controller._test_resolveManualUseTLS(host: "10.0.0.5", useTLS: false) == false)
|
|
#expect(controller._test_resolveManualUseTLS(host: "172.16.1.5", useTLS: false) == false)
|
|
#expect(controller._test_resolveManualUseTLS(host: "169.254.1.5", useTLS: false) == false)
|
|
#expect(controller._test_resolveManualUseTLS(host: "fd00::1", useTLS: false) == false)
|
|
}
|
|
|
|
@Test @MainActor func manualDefaultPortUses443OnlyForTailnetTLSHosts() async {
|
|
let controller = makeController()
|
|
|
|
#expect(controller._test_resolveManualPort(host: "gateway.example.com", port: 0, useTLS: true) == 18789)
|
|
#expect(controller._test_resolveManualPort(host: "device.sample.ts.net", port: 0, useTLS: true) == 443)
|
|
#expect(controller._test_resolveManualPort(host: "device.sample.ts.net.", port: 0, useTLS: true) == 443)
|
|
#expect(controller._test_resolveManualPort(host: "device.sample.ts.net", port: 18789, useTLS: true) == 18789)
|
|
}
|
|
|
|
@Test @MainActor func clearAllTLSFingerprints_removesStoredPins() async {
|
|
let stableID1 = "test|\(UUID().uuidString)"
|
|
let stableID2 = "test|\(UUID().uuidString)"
|
|
defer { GatewayTLSStore.clearAllFingerprints() }
|
|
|
|
GatewayTLSStore.saveFingerprint("11", stableID: stableID1)
|
|
GatewayTLSStore.saveFingerprint("22", stableID: stableID2)
|
|
|
|
#expect(GatewayTLSStore.loadFingerprint(stableID: stableID1) == "11")
|
|
#expect(GatewayTLSStore.loadFingerprint(stableID: stableID2) == "22")
|
|
|
|
GatewayTLSStore.clearAllFingerprints()
|
|
|
|
#expect(GatewayTLSStore.loadFingerprint(stableID: stableID1) == nil)
|
|
#expect(GatewayTLSStore.loadFingerprint(stableID: stableID2) == nil)
|
|
}
|
|
|
|
@Test func trustedPinMismatchCanBeRecoveredByReplacingStoredPin() {
|
|
let stableID = "test|\(UUID().uuidString)"
|
|
defer { GatewayTLSStore.clearFingerprint(stableID: stableID) }
|
|
GatewayTLSStore.saveFingerprint("old", stableID: stableID)
|
|
|
|
let error = GatewayTLSValidationError(
|
|
failure: GatewayTLSValidationFailure(
|
|
kind: .pinMismatch,
|
|
host: "gateway.tailnet.ts.net",
|
|
storeKey: stableID,
|
|
expectedFingerprint: "old",
|
|
observedFingerprint: "new",
|
|
systemTrustOk: true),
|
|
context: "connect to gateway")
|
|
|
|
let problem = GatewayConnectionProblemMapper.map(error: error)
|
|
|
|
#expect(problem?.kind == .tlsPinMismatch)
|
|
#expect(problem?.canTrustRotatedCertificate == true)
|
|
#expect(problem?.tlsStoreKey == stableID)
|
|
#expect(problem?.tlsExpectedFingerprint == "old")
|
|
#expect(problem?.tlsObservedFingerprint == "new")
|
|
|
|
#expect(GatewayTLSStore.replaceFingerprint(problem?.tlsObservedFingerprint ?? "", stableID: stableID))
|
|
#expect(GatewayTLSStore.loadFingerprint(stableID: stableID) == "new")
|
|
}
|
|
|
|
@Test func untrustedPinMismatchCannotBeRecoveredInApp() {
|
|
let error = GatewayTLSValidationError(
|
|
failure: GatewayTLSValidationFailure(
|
|
kind: .pinMismatch,
|
|
host: "gateway.tailnet.ts.net",
|
|
storeKey: "gateway",
|
|
expectedFingerprint: "old",
|
|
observedFingerprint: "new",
|
|
systemTrustOk: false),
|
|
context: "connect to gateway")
|
|
|
|
let problem = GatewayConnectionProblemMapper.map(error: error)
|
|
|
|
#expect(problem?.kind == .tlsPinMismatch)
|
|
#expect(problem?.canTrustRotatedCertificate == false)
|
|
}
|
|
}
|