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:
98
apps/TerminalX/iOS/SigningKeyProvider.swift
Normal file
98
apps/TerminalX/iOS/SigningKeyProvider.swift
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user