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:
@@ -3,6 +3,34 @@ import Darwin
|
||||
import CSSH
|
||||
import TXCore
|
||||
|
||||
/// 承载 SSHSigner 供 libssh2 C 回调经 abstract 指针取用。
|
||||
final class SignerBox {
|
||||
let signer: SSHSigner
|
||||
init(_ s: SSHSigner) { self.signer = s }
|
||||
}
|
||||
|
||||
/// libssh2 publickey sign 回调(LIBSSH2_USERAUTH_PUBLICKEY_SIGN_FUNC):从 abstract 取签名器,
|
||||
/// 对 data 签名,把 SSH 格式签名体经 malloc 出参交回(libssh2 用 free 释放)。返回 0 成功。
|
||||
private func sshSignCallback(
|
||||
_ session: OpaquePointer?,
|
||||
_ sig: UnsafeMutablePointer<UnsafeMutablePointer<UInt8>?>?,
|
||||
_ sigLen: UnsafeMutablePointer<Int>?,
|
||||
_ data: UnsafePointer<UInt8>?,
|
||||
_ dataLen: Int,
|
||||
_ abstract: UnsafeMutablePointer<UnsafeMutableRawPointer?>?
|
||||
) -> Int32 {
|
||||
guard let boxPtr = abstract?.pointee else { return -1 }
|
||||
let box = Unmanaged<SignerBox>.fromOpaque(boxPtr).takeUnretainedValue()
|
||||
let input = (data != nil && dataLen > 0) ? Array(UnsafeBufferPointer(start: data, count: dataLen)) : []
|
||||
guard let sshSig = box.signer.signSSH(input), let out = sig else { return -1 }
|
||||
let n = sshSig.count
|
||||
guard let buf = malloc(n)?.assumingMemoryBound(to: UInt8.self) else { return -1 }
|
||||
sshSig.withUnsafeBytes { _ = memcpy(buf, $0.baseAddress, n) }
|
||||
out.pointee = buf
|
||||
sigLen?.pointee = n
|
||||
return 0
|
||||
}
|
||||
|
||||
/// 基于 libssh2 的 SSH 传输。M0:系统 socket 直连(不过 tsnet),密码 / 私钥认证,
|
||||
/// 打开 PTY + shell,非阻塞事件循环把 PTY 输出投递给 `onBytes`。
|
||||
///
|
||||
@@ -171,6 +199,19 @@ public final class SSHSession: Transport, @unchecked Sendable {
|
||||
}
|
||||
}
|
||||
}
|
||||
case .publicKeyCallback(let signer):
|
||||
// 私钥不出安全域:libssh2 经 sign_callback 拿签名(SE/软件密钥在 app 层)。
|
||||
let box = SignerBox(signer)
|
||||
let boxPtr = Unmanaged.passRetained(box).toOpaque()
|
||||
defer { Unmanaged<SignerBox>.fromOpaque(boxPtr).release() }
|
||||
var abstract: UnsafeMutableRawPointer? = boxPtr
|
||||
let blob = signer.publicKeyBlob
|
||||
rc = user.withCString { u in
|
||||
blob.withUnsafeBufferPointer { bp in
|
||||
libssh2_userauth_publickey(session, u, bp.baseAddress, bp.count,
|
||||
sshSignCallback, &abstract)
|
||||
}
|
||||
}
|
||||
}
|
||||
guard rc == 0 else { throw SSHError.authentication(Int(rc)) }
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ public struct SSHConfig: Sendable {
|
||||
public enum Authentication: Sendable {
|
||||
case password(String)
|
||||
case privateKey(pem: String, passphrase: String?)
|
||||
/// publickey 认证 + 外部签名器(Secure Enclave / 软件密钥)。私钥不出安全域,仅经回调签名。
|
||||
case publicKeyCallback(SSHSigner)
|
||||
}
|
||||
|
||||
public init(
|
||||
@@ -62,6 +64,15 @@ public struct SSHConfig: Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
/// SSH publickey 签名器:暴露 SSH 格式公钥 blob,并对挑战数据产出 SSH 格式签名。
|
||||
/// 实现方(app 层)用 Secure Enclave / 软件 P-256 密钥;私钥永不出安全域,只经 signSSH 使用。
|
||||
public protocol SSHSigner: AnyObject, Sendable {
|
||||
/// SSH 线格式公钥 blob(如 `string("ecdsa-sha2-nistp256")||string("nistp256")||string(Q)`)。
|
||||
var publicKeyBlob: [UInt8] { get }
|
||||
/// 对 libssh2 给的挑战数据签名,返回 SSH 线格式签名体(ecdsa 为 `mpint r||mpint s`,不含外层 string)。失败返回 nil。
|
||||
func signSSH(_ data: [UInt8]) -> [UInt8]?
|
||||
}
|
||||
|
||||
public enum SSHError: Error, Sendable, Equatable {
|
||||
case socket(String)
|
||||
case handshake(Int)
|
||||
|
||||
Reference in New Issue
Block a user