145 lines
6.7 KiB
Swift
145 lines
6.7 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
import CSSH2
|
|
import CryptoKit
|
|
import Foundation
|
|
import OpenSSL
|
|
import TermindApplication
|
|
import TermindDomain
|
|
|
|
/// Uses the same pinned libssh2 OpenSSL parsers as SSH login, including encrypted
|
|
/// OpenSSH keys. Parser contexts are local to this operation, never the live session.
|
|
nonisolated enum SSHAgentKeySigner {
|
|
static func sign(
|
|
credential: SSHResolvedKeyCredential, passphrase: SSHSecret?,
|
|
algorithm: SSHKeyAlgorithm, data: Data, flags: UInt32, allowInteraction: Bool = true
|
|
) throws -> Data {
|
|
defer {
|
|
credential.clearSecrets()
|
|
passphrase?.clear()
|
|
}
|
|
guard flags & ~UInt32(6) == 0, algorithm == .rsa || flags == 0 else {
|
|
throw SSHAgentProtocol.Failure.invalid
|
|
}
|
|
let name: String
|
|
let payload: Data
|
|
switch credential {
|
|
case .securityKey: throw SSHInteractionRequired()
|
|
case .secureEnclave(let key):
|
|
guard algorithm == .ecdsa256 else { throw SSHAgentProtocol.Failure.invalid }
|
|
name = algorithm.rawValue
|
|
payload = try SecureEnclaveKey.sign(data, credential: key, allowInteraction: allowInteraction)
|
|
case .software(let secret, let savedPassphrase):
|
|
let passphrase = passphrase ?? savedPassphrase
|
|
defer { savedPassphrase?.clear() }
|
|
defer {
|
|
secret.clear()
|
|
passphrase?.clear()
|
|
}
|
|
_ = initialization
|
|
guard let session = libssh2_session_init_ex(nil, nil, nil, nil) else {
|
|
throw SSHAgentProtocol.Failure.unavailable
|
|
}
|
|
defer { _ = libssh2_session_free(session) }
|
|
var key: OpaquePointer?
|
|
let parser =
|
|
switch algorithm {
|
|
case .rsa: parseRSA
|
|
case .ed25519: parseEd25519
|
|
default: parseECDSA
|
|
}
|
|
let result = try secret.withUnsafeCStringAndConsume { bytes, count in
|
|
let parse: (UnsafePointer<CChar>?) -> Int32 = { password in
|
|
parser(&key, session, bytes, count, password)
|
|
}
|
|
if let passphrase { return try passphrase.withUnsafeCStringAndConsume { p, _ in parse(p) } }
|
|
return parse(nil)
|
|
}
|
|
defer { if let key { EVP_PKEY_free(key) } }
|
|
guard result == 0, let key else { throw SSHAgentProtocol.Failure.unavailable }
|
|
var signature: UnsafeMutablePointer<UInt8>?
|
|
var length = 0
|
|
defer { if let signature { libssh2_free(session, signature) } }
|
|
let status: Int32
|
|
switch algorithm {
|
|
case .ed25519:
|
|
name = algorithm.rawValue
|
|
status = data.withUnsafeBytes {
|
|
signEd25519(key, session, &signature, &length, $0.bindMemory(to: UInt8.self).baseAddress, $0.count)
|
|
}
|
|
case .rsa:
|
|
let hash: Data
|
|
if flags & 4 != 0 {
|
|
name = "rsa-sha2-512"
|
|
hash = Data(SHA512.hash(data: data))
|
|
} else if flags & 2 != 0 {
|
|
name = "rsa-sha2-256"
|
|
hash = Data(SHA256.hash(data: data))
|
|
} else {
|
|
name = "ssh-rsa"
|
|
hash = Data(Insecure.SHA1.hash(data: data))
|
|
}
|
|
status = hash.withUnsafeBytes {
|
|
let sign = flags == 0 ? signRSA1 : signRSA2
|
|
return sign(session, key, $0.bindMemory(to: UInt8.self).baseAddress, $0.count, &signature, &length)
|
|
}
|
|
default:
|
|
name = algorithm.rawValue
|
|
let hash: Data =
|
|
switch algorithm {
|
|
case .ecdsa384: Data(SHA384.hash(data: data))
|
|
case .ecdsa521: Data(SHA512.hash(data: data))
|
|
default: Data(SHA256.hash(data: data))
|
|
}
|
|
status = hash.withUnsafeBytes {
|
|
signECDSA(session, key, $0.bindMemory(to: UInt8.self).baseAddress, $0.count, &signature, &length)
|
|
}
|
|
}
|
|
guard status == 0, let signature, length > 0 else { throw SSHAgentProtocol.Failure.unavailable }
|
|
payload = Data(bytes: signature, count: length)
|
|
}
|
|
return SSHAgentProtocol.field(Data(name.utf8)) + SSHAgentProtocol.field(payload)
|
|
}
|
|
private static let initialization: Void = { precondition(libssh2_init(0) == 0) }()
|
|
}
|
|
|
|
// libssh2-spm 2.0.3 uses OpenSSL 3 EVP_PKEY contexts for all three parsers.
|
|
// These are already shipped symbols, like _libssh2_pub_priv_keyfilememory used
|
|
// by SSHKeyMaterial. Keep the independent signature tests when updating libssh2.
|
|
@_silgen_name("_libssh2_rsa_new_private_frommemory")
|
|
private nonisolated func parseRSA(
|
|
_ key: UnsafeMutablePointer<OpaquePointer?>, _ session: OpaquePointer, _ bytes: UnsafePointer<CChar>, _ count: Int,
|
|
_ passphrase: UnsafePointer<CChar>?
|
|
) -> Int32
|
|
@_silgen_name("_libssh2_ecdsa_new_private_frommemory")
|
|
private nonisolated func parseECDSA(
|
|
_ key: UnsafeMutablePointer<OpaquePointer?>, _ session: OpaquePointer, _ bytes: UnsafePointer<CChar>, _ count: Int,
|
|
_ passphrase: UnsafePointer<CChar>?
|
|
) -> Int32
|
|
@_silgen_name("_libssh2_ed25519_new_private_frommemory")
|
|
private nonisolated func parseEd25519(
|
|
_ key: UnsafeMutablePointer<OpaquePointer?>, _ session: OpaquePointer, _ bytes: UnsafePointer<CChar>, _ count: Int,
|
|
_ passphrase: UnsafePointer<CChar>?
|
|
) -> Int32
|
|
@_silgen_name("_libssh2_rsa_sha1_sign")
|
|
private nonisolated func signRSA1(
|
|
_ session: OpaquePointer, _ key: OpaquePointer, _ hash: UnsafePointer<UInt8>?, _ count: Int,
|
|
_ signature: UnsafeMutablePointer<UnsafeMutablePointer<UInt8>?>, _ length: UnsafeMutablePointer<Int>
|
|
) -> Int32
|
|
@_silgen_name("_libssh2_rsa_sha2_sign")
|
|
private nonisolated func signRSA2(
|
|
_ session: OpaquePointer, _ key: OpaquePointer, _ hash: UnsafePointer<UInt8>?, _ count: Int,
|
|
_ signature: UnsafeMutablePointer<UnsafeMutablePointer<UInt8>?>, _ length: UnsafeMutablePointer<Int>
|
|
) -> Int32
|
|
@_silgen_name("_libssh2_ecdsa_sign")
|
|
private nonisolated func signECDSA(
|
|
_ session: OpaquePointer, _ key: OpaquePointer, _ hash: UnsafePointer<UInt8>?, _ count: Int,
|
|
_ signature: UnsafeMutablePointer<UnsafeMutablePointer<UInt8>?>, _ length: UnsafeMutablePointer<Int>
|
|
) -> Int32
|
|
@_silgen_name("_libssh2_ed25519_sign")
|
|
private nonisolated func signEd25519(
|
|
_ key: OpaquePointer, _ session: OpaquePointer, _ signature: UnsafeMutablePointer<UnsafeMutablePointer<UInt8>?>,
|
|
_ length: UnsafeMutablePointer<Int>, _ bytes: UnsafePointer<UInt8>?, _ count: Int
|
|
) -> Int32
|