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

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

View File

@@ -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)