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)