feat: M4a host key 校验(TOFU pin,关闭 MITM 敞口)
- TXCore/SSH: SSHWire(string/mpint 编码)、ECDSAConv(DER→SSH 签名/P256 blob)、 HostKey(opensshFingerprint/evaluate/HostTriple/KnownHostsStore),+10 单测(含真实指纹向量、高位 DER) - TXTransport: SSHConfig.hostKeyVerifier 闭包 + SSHError.hostKeyMismatch; SSHSession 握手后 libssh2_session_hostkey 取 blob 交 verifier,拒绝则断开抛错 - app: 文件后端 KnownHostsStore(未签名 app 无 keychain 权限 SecItem -34018,host key 是 公钥非机密,沙盒文件对 TOFU 足够)+ TOFU verifier + firstUse 横幅 + mismatch alert(信任/取消) - 验证(192.168.9.199):首次信任 pin+接受连上;假 pin→不符→断开+告警,本次指纹与 ssh-keygen 逐字符一致 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import Foundation
|
||||
import Darwin
|
||||
import CSSH
|
||||
import TXCore
|
||||
|
||||
/// 基于 libssh2 的 SSH 传输。M0:系统 socket 直连(不过 tsnet),密码 / 私钥认证,
|
||||
/// 打开 PTY + shell,非阻塞事件循环把 PTY 输出投递给 `onBytes`。
|
||||
@@ -115,7 +116,24 @@ public final class SSHSession: Transport, @unchecked Sendable {
|
||||
libssh2_session_set_blocking(s, 1)
|
||||
let rc = libssh2_session_handshake(s, sock)
|
||||
guard rc == 0 else { throw SSHError.handshake(Int(rc)) }
|
||||
// TODO(M4/TXSecurity): 校验 host key 指纹(known_hosts 三元组)。M0 暂接受任意。
|
||||
try verifyHostKey(s)
|
||||
}
|
||||
|
||||
/// M4:TOFU host key 校验。取服务器 host key blob 交给 verifier;拒绝则断开并抛 hostKeyMismatch。
|
||||
private func verifyHostKey(_ s: OpaquePointer) throws {
|
||||
guard let verifier = config.hostKeyVerifier else { return } // 未配置校验器 → 历史行为
|
||||
var len = 0
|
||||
var type: Int32 = 0
|
||||
guard let raw = libssh2_session_hostkey(s, &len, &type), len > 0 else {
|
||||
throw SSHError.hostKeyMismatch(fingerprint: "(无法获取 host key)")
|
||||
}
|
||||
let blob = Data(bytes: raw, count: len)
|
||||
if verifier(blob, type) { return } // 接受(trusted / firstUse 已 pin)
|
||||
let fp = HostKey.opensshFingerprint(blob)
|
||||
"hostkey".withCString { r in "".withCString { l in
|
||||
_ = libssh2_session_disconnect_ex(s, 11, r, l) // BY_APPLICATION
|
||||
} }
|
||||
throw SSHError.hostKeyMismatch(fingerprint: fp)
|
||||
}
|
||||
|
||||
// MARK: - Auth
|
||||
|
||||
@@ -31,6 +31,10 @@ public struct SSHConfig: Sendable {
|
||||
public var terminalType: String
|
||||
public var initialCols: UInt16
|
||||
public var initialRows: UInt16
|
||||
/// M4 host key 校验:握手后以 (blob, keyType) 同步回调(在 ssh 串行队列)。返回 true=接受
|
||||
/// (trusted 或 firstUse 已 pin),false=拒绝(mismatch)→ SSHSession 断开并抛 hostKeyMismatch。
|
||||
/// nil 时不校验(历史行为)。校验/pin 的存储与 UI 由 app 层实现。
|
||||
public var hostKeyVerifier: (@Sendable (Data, Int32) -> Bool)?
|
||||
|
||||
public enum Authentication: Sendable {
|
||||
case password(String)
|
||||
@@ -44,7 +48,8 @@ public struct SSHConfig: Sendable {
|
||||
authentication: Authentication,
|
||||
terminalType: String = "xterm-256color",
|
||||
initialCols: UInt16 = 80,
|
||||
initialRows: UInt16 = 24
|
||||
initialRows: UInt16 = 24,
|
||||
hostKeyVerifier: (@Sendable (Data, Int32) -> Bool)? = nil
|
||||
) {
|
||||
self.host = host
|
||||
self.port = port
|
||||
@@ -53,6 +58,7 @@ public struct SSHConfig: Sendable {
|
||||
self.terminalType = terminalType
|
||||
self.initialCols = initialCols
|
||||
self.initialRows = initialRows
|
||||
self.hostKeyVerifier = hostKeyVerifier
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,4 +67,6 @@ public enum SSHError: Error, Sendable, Equatable {
|
||||
case handshake(Int)
|
||||
case authentication(Int)
|
||||
case channel(String)
|
||||
/// host key 与已 pin 记录不符(潜在 MITM)。携带本次呈现的 OpenSSH 指纹。
|
||||
case hostKeyMismatch(fingerprint: String)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user