Files
Termind/TFMTests/DeferredKeyMaterialTests.swift

181 lines
11 KiB
Swift

// SPDX-FileCopyrightText: 2026 Kaede Akino
// SPDX-License-Identifier: GPL-3.0-only
import Foundation
import TermindApplication
import TermindDomain
import Testing
@testable import TermindRemoteInfrastructure
struct DeferredKeyMaterialTests {
@Test func nestedSSHFieldsAcceptDataSlicesWithNonzeroIndices() throws {
let container = Data([99, 99, 0, 0, 0, 7, 0, 0, 0, 3, 1, 2, 3])
let slice = container.dropFirst(2)
#expect(slice.startIndex != 0)
var offset = 0
let nested = try #require(SSHKeyMaterial.readSSHField(slice, offset: &offset))
#expect(offset == slice.count)
var nestedOffset = 0
#expect(SSHKeyMaterial.readSSHField(nested, offset: &nestedOffset) == Data([1, 2, 3]))
#expect(nestedOffset == nested.count)
}
@Test func invalidSSHFieldBoundsFailWithoutTrappingOrAdvancing() {
for bytes in [Data([0, 0, 0]), Data([255, 255, 255, 255]), Data([0, 0, 0, 2, 1])] {
for initialOffset in [-1, 0, Int.max] {
var offset = initialOffset
#expect(SSHKeyMaterial.readSSHField(bytes, offset: &offset) == nil)
#expect(offset == initialOffset)
}
}
}
@Test(arguments: [SSHKeyMaterial.Generation.ed25519, .rsa(bits: 2048), .ecdsa(bits: 256)])
func importRecognizesEncryptionWithoutUnlockingOrDeriving(_ generation: SSHKeyMaterial.Generation) throws {
let encrypted = try SSHKeyMaterial.generate(generation, comment: "test", passphrase: "secret")
let metadata = try SSHKeyMaterial.privateKeyMetadata(encrypted.privateKey)
#expect(metadata.isEncrypted && metadata.algorithm == .unknown)
let imported = try SSHKeyMaterial.imported(privateKey: encrypted.privateKey, publicKey: "")
#expect(imported.publicKey.isEmpty && imported.fingerprint.isEmpty && imported.algorithm == .unknown)
let supplied = try SSHKeyMaterial.imported(privateKey: encrypted.privateKey, publicKey: encrypted.publicKey)
#expect(supplied.fingerprint == encrypted.fingerprint && supplied.algorithm == encrypted.algorithm)
let plaintext = try SSHKeyMaterial.generate(generation, comment: "", passphrase: nil)
#expect(try !SSHKeyMaterial.privateKeyMetadata(plaintext.privateKey).isEncrypted)
#expect(try SSHKeyMaterial.imported(privateKey: plaintext.privateKey, publicKey: "").publicKey.isEmpty)
}
@Test(arguments: [
"", "not a key", "-----BEGIN ENCRYPTED PRIVATE KEY-----\nAAAA\n-----END ENCRYPTED PRIVATE KEY-----",
])
func invalidPrivateEnvelopesCannotBeSaved(_ input: String) {
#expect(throws: (any Error).self) { try SSHKeyMaterial.imported(privateKey: input, publicKey: "") }
}
@Test func analysisRetainsEncryptionMetadataWhenPublicMaterialIsInvalid() throws {
let encrypted = try SSHKeyMaterial.generate(.ed25519, comment: "", passphrase: "test")
let analysis = try SSHKeyMaterial.analyzeImport(privateKey: encrypted.privateKey, publicKey: "invalid")
#expect(analysis.metadata.isEncrypted)
#expect(throws: (any Error).self) { try analysis.material.get() }
#expect(throws: (any Error).self) {
try SSHKeyMaterial.imported(privateKey: encrypted.privateKey, publicKey: "invalid")
}
let corrected = try SSHKeyMaterial.analyzeImport(
privateKey: encrypted.privateKey, publicKey: encrypted.publicKey)
#expect(try corrected.material.get().fingerprint == encrypted.fingerprint)
}
@Test func analysisRejectsMismatchedPublicAlgorithm() throws {
let privateMaterial = try SSHKeyMaterial.generate(.ed25519, comment: "", passphrase: nil)
let publicMaterial = try SSHKeyMaterial.generate(.ecdsa(bits: 256), comment: "", passphrase: nil)
let analysis = try SSHKeyMaterial.analyzeImport(
privateKey: privateMaterial.privateKey, publicKey: publicMaterial.publicKey)
#expect(!analysis.metadata.isEncrypted)
#expect(throws: (any Error).self) { try analysis.material.get() }
}
@Test func suppliedPublicKeyMustHaveAValidWireBlob() throws {
let material = try SSHKeyMaterial.generate(.ed25519, comment: "", passphrase: nil)
for value in [
"ssh-ed25519 AAAA", material.publicKey + "\n" + material.publicKey,
material.publicKey.replacingOccurrences(of: "ssh-ed25519", with: "ssh-rsa"),
] {
#expect(throws: (any Error).self) { try SSHKeyMaterial.publicMaterial(value) }
}
}
@Test func opensshAndTraditionalPEMEncryptionIsDetectedWithoutAPassphrase() throws {
let directory = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: directory) }
for (format, type) in [("", "ed25519"), ("PEM", "rsa"), ("PEM", "ecdsa")] {
let path = directory.appending(path: UUID().uuidString).path
var args = ["-q", "-t", type, "-N", "fixture-passphrase", "-f", path]
if !format.isEmpty { args += ["-m", format] }
try run("/usr/bin/ssh-keygen", args)
let text = try String(contentsOfFile: path, encoding: .utf8)
#expect(try SSHKeyMaterial.privateKeyMetadata(text).isEncrypted)
#expect(try SSHKeyMaterial.imported(privateKey: text, publicKey: "").publicKey.isEmpty)
}
}
@Test(arguments: ["ed25519", "rsa", "ecdsa"])
func editingUnencryptedOpenSSHKeysReadsNestedMetadataWithoutDeriving(_ type: String) throws {
let directory = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: directory) }
let path = directory.appending(path: "key").path
try run("/usr/bin/ssh-keygen", ["-q", "-t", type, "-N", "", "-f", path])
let privateKey = try String(contentsOfFile: path, encoding: .utf8)
let publicKey = try String(contentsOfFile: path + ".pub", encoding: .utf8)
#expect(try !SSHKeyMaterial.privateKeyMetadata(privateKey).isEncrypted)
let material = try SSHKeyMaterial.imported(privateKey: privateKey, publicKey: publicKey)
#expect(material.algorithm != .unknown && !material.fingerprint.isEmpty)
#expect(try SSHKeyMaterial.imported(privateKey: privateKey, publicKey: "").publicKey.isEmpty)
}
@Test func defaultExportScriptQuotesArgumentsAndDoesNotDuplicateTheKey() throws {
let root = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: root) }
let directory = root.appending(path: "keys '$HOME; $(touch injected)")
let name = "authorized_keys';touch injected;#"
let material = try SSHKeyMaterial.generate(.ed25519, comment: "$(touch injected)", passphrase: nil)
let options = KeyExportOptions(
directory: directory.path, filename: name,
script: KeyExportOptions.defaultScript(keyName: "Work\n$(touch injected)"))
let command = try SSHKeyExportSession.command(publicKey: material.publicKey, options: options)
try run("/bin/sh", ["-c", command], directory: root)
try run("/bin/sh", ["-c", command], directory: root)
let file = directory.appending(path: name)
let contents = try String(contentsOf: file, encoding: .utf8)
#expect(contents.components(separatedBy: material.publicKey).count == 2)
#expect(contents.hasPrefix("# Work $(touch injected) Key by Termind\n"))
#expect(!FileManager.default.fileExists(atPath: root.appending(path: "injected").path))
#expect(try FileManager.default.attributesOfItem(atPath: directory.path)[.posixPermissions] as? Int == 0o700)
#expect(try FileManager.default.attributesOfItem(atPath: file.path)[.posixPermissions] as? Int == 0o600)
}
@Test func customScriptReceivesThreeLiteralArguments() throws {
let root = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: root) }
let material = try SSHKeyMaterial.generate(.ed25519, comment: "comment", passphrase: nil)
let options = KeyExportOptions(
directory: root.path, filename: "custom",
script: "printf '%s\\n%s\\n' \"$2\" \"$3\" > \"$1/$2\"")
try run("/bin/sh", ["-c", SSHKeyExportSession.command(publicKey: material.publicKey, options: options)])
#expect(
try String(contentsOf: root.appending(path: "custom"), encoding: .utf8) == "custom\n\(material.publicKey)\n"
)
}
@Test func defaultScriptPreservesAnExistingKeyWithoutATrailingNewline() throws {
let root = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: root) }
let existing = try SSHKeyMaterial.generate(.ed25519, comment: "existing", passphrase: nil)
let next = try SSHKeyMaterial.generate(.ed25519, comment: "next", passphrase: nil)
let file = root.appending(path: "authorized_keys")
try existing.publicKey.write(to: file, atomically: true, encoding: .utf8)
let options = KeyExportOptions(directory: root.path, script: KeyExportOptions.defaultScript(keyName: "Next"))
try run("/bin/sh", ["-c", SSHKeyExportSession.command(publicKey: next.publicKey, options: options)])
let lines = try String(contentsOf: file, encoding: .utf8).split(separator: "\n").map(String.init)
#expect(lines == [existing.publicKey, "# Next Key by Termind", next.publicKey])
}
private func run(_ executable: String, _ arguments: [String], directory: URL? = nil) throws {
let process = Process()
process.executableURL = URL(filePath: executable)
process.arguments = arguments
process.currentDirectoryURL = directory
let errors = Pipe()
process.standardError = errors
try process.run()
process.waitUntilExit()
let output = String(decoding: errors.fileHandleForReading.readDataToEndOfFile(), as: UTF8.self)
#expect(process.terminationStatus == 0, "\(output)")
guard process.terminationStatus == 0 else { throw CocoaError(.executableRuntimeMismatch) }
}
}