feat: M4b publickey 认证(SE 优先/软件回退,sign_callback)

- TXTransport: SSHSigner 协议 + Authentication.publicKeyCallback;SSHSession 用
  @convention(c) sshSignCallback(abstract 桥 SignerBox + malloc 出参)接 libssh2_userauth_publickey
- app SigningKeyProvider: SE 优先(kSecAttrTokenIDSecureEnclave),未签名/无 SE 回退软件 P-256
  (导出落文件持久化),同一 SSHSigner 路径;SecKeyCreateSignature(ecdsa)→ECDSAConv.derToSSHSignature
- 公钥 blob=ecdsa-sha2-nistp256||nistp256||X963;-txPubkeyAuth 日志打 authorized_keys 行
- 验证(192.168.9.199):不给密码,公钥加 authorized_keys 后免密登录成功(whoami=kid);
  模拟器 SE 生成 hw=true 可达,真机 SE 硬件签名+keychain 持久化待补

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
kid
2026-07-24 18:17:30 +08:00
parent 1a257fae7f
commit 5dbe937222
6 changed files with 168 additions and 3 deletions

View File

@@ -240,10 +240,14 @@ final class SSHTerminalModel: ObservableObject {
// MARK: -
/// M4b nil publickey(SE/)
private var pubkeySigner: SigningKeyProvider?
func connect(host: String, port: Int, username: String, password: String) {
let egress = tsnetAuthKey != nil ? "tsnet" : "direct"
let auth: SSHConfig.Authentication = pubkeySigner.map { .publicKeyCallback($0) } ?? .password(password)
config = SSHConfig(host: host, port: port, username: username,
authentication: .password(password),
authentication: auth,
hostKeyVerifier: makeHostKeyVerifier(egress: egress, host: host, port: port))
run(machine.reduce(.connectRequested))
}
@@ -324,6 +328,13 @@ final class SSHTerminalModel: ObservableObject {
autoCommand = nil // mosh txAutoCommand
}
let realPort = port == 0 ? 22 : port
// M4bpublickey SE/ authorized_keys
if d.bool(forKey: "txPubkeyAuth") {
pubkeySigner = SigningKeyProvider.loadOrCreate()
if let s = pubkeySigner {
NSLog("M4DBG authorized_keys hw=\(s.isHardwareBacked): \(s.authorizedKeysLine)")
}
}
// pin host key mismatch alert
if let b64 = d.string(forKey: "txHostKeyPinOverrideBase64"), let fake = Data(base64Encoded: b64) {
let egress = tsnetAuthKey != nil ? "tsnet" : "direct"

View File

@@ -0,0 +1,98 @@
import Foundation
import Security
import TXCore
import TXTransport
/// SSH publickey Secure Enclave
/// / SE 退 P-256** SSHSigner **
/// "SE /"
final class SigningKeyProvider: SSHSigner, @unchecked Sendable {
private let privateKey: SecKey
let publicKeyBlob: [UInt8]
let isHardwareBacked: Bool
/// authorized_keys key
var authorizedKeysLine: String {
"ecdsa-sha2-nistp256 " + Data(publicKeyBlob).base64EncodedString() + " terminalx@device"
}
private init(privateKey: SecKey, hardware: Bool) {
self.privateKey = privateKey
self.isHardwareBacked = hardware
let pub = SecKeyCopyPublicKey(privateKey)!
let x963 = SecKeyCopyExternalRepresentation(pub, nil)! as Data // 0x04||X||Y (65B)
self.publicKeyBlob = ECDSAConv.p256PublicKeyBlob(x963: [UInt8](x963))
}
// MARK: - SSHSigner
func signSSH(_ data: [UInt8]) -> [UInt8]? {
var err: Unmanaged<CFError>?
guard let der = SecKeyCreateSignature(
privateKey, .ecdsaSignatureMessageX962SHA256, Data(data) as CFData, &err) as Data? else {
NSLog("M4DBG SecKeyCreateSignature failed: \(String(describing: err?.takeRetainedValue()))")
return nil
}
return ECDSAConv.derToSSHSignature([UInt8](der)) // DER SEQUENCE{r,s} SSH mpint r||mpint s
}
// MARK: - /
private static var keyFileURL: URL {
let base = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true)[0]
return URL(fileURLWithPath: base).appendingPathComponent("ssh-ecdsa-p256.key")
}
static func loadOrCreate() -> SigningKeyProvider? {
// 1)
if let data = try? Data(contentsOf: keyFileURL),
let key = SecKeyCreateWithData(data as CFData, [
kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
kSecAttrKeySizeInBits: 256,
] as CFDictionary, nil) {
return SigningKeyProvider(privateKey: key, hardware: false)
}
// 2) Secure Enclave
if let se = createSecureEnclaveKey() {
return SigningKeyProvider(privateKey: se, hardware: true)
}
// 3) 退 P-256/ SE
guard let sw = createSoftwareKey() else { return nil }
if let ext = SecKeyCopyExternalRepresentation(sw, nil) as Data? {
try? ext.write(to: keyFileURL, options: .atomic)
}
return SigningKeyProvider(privateKey: sw, hardware: false)
}
private static func createSecureEnclaveKey() -> SecKey? {
guard let access = SecAccessControlCreateWithFlags(
nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, .privateKeyUsage, nil) else { return nil }
let attrs: [String: Any] = [
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrKeySizeInBits as String: 256,
kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
kSecPrivateKeyAttrs as String: [
kSecAttrIsPermanent as String: true, // keychain/(-34018)退+
kSecAttrApplicationTag as String: "ai.athom.terminalx.sshkey".data(using: .utf8)!,
kSecAttrAccessControl as String: access,
],
]
var err: Unmanaged<CFError>?
let k = SecKeyCreateRandomKey(attrs as CFDictionary, &err)
if k == nil { NSLog("M4DBG SE key unavailable (预期于模拟器): \(String(describing: err?.takeRetainedValue()))") }
return k
}
private static func createSoftwareKey() -> SecKey? {
let attrs: [String: Any] = [
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrKeySizeInBits as String: 256,
kSecPrivateKeyAttrs as String: [kSecAttrIsPermanent as String: false],
]
var err: Unmanaged<CFError>?
let k = SecKeyCreateRandomKey(attrs as CFDictionary, &err)
if k == nil { NSLog("M4DBG software key create failed: \(String(describing: err?.takeRetainedValue()))") }
return k
}
}