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:
kid
2026-07-24 18:06:39 +08:00
parent 7abe61a71f
commit 1a257fae7f
9 changed files with 321 additions and 5 deletions

View File

@@ -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)
}
/// M4TOFU 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

View File

@@ -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 pinfalse=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)
}