- 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>
55 lines
2.3 KiB
Swift
55 lines
2.3 KiB
Swift
import Foundation
|
||
import TXCore
|
||
|
||
/// known_hosts pin 存储(文件后端)。
|
||
///
|
||
/// 为何用文件而非 Keychain:本 app 当前未签名(CODE_SIGNING_ALLOWED=NO),无 keychain-access-group
|
||
/// 权限 → SecItem 全返 errSecMissingEntitlement(-34018)。而 host key pin 是**公钥完整性数据、非机密**,
|
||
/// 存于 app 沙盒文件对 TOFU 足够(威胁模型是 MITM 换 key,沙盒文件完整性对非越狱设备与 Keychain 相当)。
|
||
/// 生产签名构建可换 Keychain 加固(另留 KnownHostsStore 协议便于替换)。
|
||
///
|
||
/// 格式:JSON `{ "egress|host|port": { "blob": "<base64>", "type": <int> } }`,
|
||
/// 落 Application Support/known-hosts.json。
|
||
final class KeychainKnownHostsStore: KnownHostsStore, @unchecked Sendable {
|
||
private let lock = NSLock()
|
||
private let url: URL
|
||
private var cache: [String: [String: Any]]
|
||
|
||
init() {
|
||
let base = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true)[0]
|
||
try? FileManager.default.createDirectory(atPath: base, withIntermediateDirectories: true)
|
||
url = URL(fileURLWithPath: base).appendingPathComponent("known-hosts.json")
|
||
if let data = try? Data(contentsOf: url),
|
||
let obj = try? JSONSerialization.jsonObject(with: data) as? [String: [String: Any]] {
|
||
cache = obj
|
||
} else {
|
||
cache = [:]
|
||
}
|
||
}
|
||
|
||
func lookup(_ triple: HostTriple) -> HostKeyRecord? {
|
||
lock.lock(); defer { lock.unlock() }
|
||
guard let entry = cache[triple.accountKey],
|
||
let b64 = entry["blob"] as? String, let blob = Data(base64Encoded: b64),
|
||
let type = entry["type"] as? Int else { return nil }
|
||
return HostKeyRecord(blob: blob, keyType: Int32(type))
|
||
}
|
||
|
||
func pin(_ triple: HostTriple, record: HostKeyRecord) {
|
||
lock.lock(); defer { lock.unlock() }
|
||
cache[triple.accountKey] = ["blob": record.blob.base64EncodedString(), "type": Int(record.keyType)]
|
||
persist()
|
||
}
|
||
|
||
func remove(_ triple: HostTriple) {
|
||
lock.lock(); defer { lock.unlock() }
|
||
cache[triple.accountKey] = nil
|
||
persist()
|
||
}
|
||
|
||
private func persist() {
|
||
guard let data = try? JSONSerialization.data(withJSONObject: cache) else { return }
|
||
try? data.write(to: url, options: .atomic)
|
||
}
|
||
}
|