forked from kay/RouterOS
M7: SSH-Hostkey-TOFU (ersetzt .acceptAnything())
Größte offene Härtungslücke geschlossen: SSHTransport nutzte bisher .acceptAnything() für Host-Key-Validierung, akzeptierte also jeden Schlüssel ohne Prüfung -- ein Man-in-the-Middle im lokalen Netz wäre unbemerkt geblieben. Jetzt Trust-on-first-use wie beim REST-Zertifikat: - SSHHostKeyFingerprint: SHA256 über NIOSSHPublicKey.write(to:) (die SSH-Wire-Format-Bytes des Schlüssels) -- exakt die Bytes, die auch OpenSSH für seine SHA256:-Fingerabdrücke hasht. Per Unit-Test gegen einen echten ssh-keygen-erzeugten Testschlüssel kreuzgeprüft (SHA256:Hllxv6LLoHl2XTIXGGjUYJHbPFoH2F7iMrR74C5J95g), nicht geraten. - SSHHostKeyTrustStore: UserDefaults-Persistenz pro Host, Pendant zu CertificateTrustStore. - SSHTransport conformt jetzt selbst zu NIOSSHClientServerAuthentication- Delegate (wie RestTransport zu URLSessionDelegate) und übergibt sich selbst als .custom(self) Host-Key-Validator. - ConnectionService: neuer State .needsSSHHostKeyConfirmation, eigener Bestätigungs-Retry-Pfad (trustCurrentSSHHostKeyAndRetry), analog zum bestehenden Zertifikat-Flow. - ConnectView: zweiter Bestätigungsdialog mit Warnhinweis, dass ein geänderter Fingerabdruck bei zuvor schon verbundenen Routern auf ein manipuliertes Netzwerk hindeuten könnte. BackupService/FactoryResetService bekommen die TOFU-Prüfung automatisch mit (SSHTransport-Default-Parameter, gleicher UserDefaults-Speicher), ohne eigene Bestätigungs-UI -- in der Praxis unkritisch, da der Verbinden-Tab das Vertrauen immer zuerst herstellt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HReLXMbmPvtQ23p1iWiJNW
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import XCTest
|
||||
import NIOSSH
|
||||
@testable import RouterOSAssistant
|
||||
|
||||
final class SSHHostKeyFingerprintTests: XCTestCase {
|
||||
/// Cross-checked against `ssh-keygen -lf` on the same throwaway test key, which reported
|
||||
/// `SHA256:Hllxv6LLoHl2XTIXGGjUYJHbPFoH2F7iMrR74C5J95g` — confirms our hex fingerprint is
|
||||
/// SHA256 over the same bytes OpenSSH hashes (the key's SSH wire-format encoding).
|
||||
func testFingerprintMatchesOpenSSHsSHA256OverTheKeyBlob() throws {
|
||||
let openSSHLine = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIINj+h+IeiBNELAG6CcYbMxrdsSn8hnccQrk/XDwUa6U test"
|
||||
let key = try NIOSSHPublicKey(openSSHPublicKey: openSSHLine)
|
||||
|
||||
let hexFingerprint = SSHHostKeyFingerprint.sha256(of: key)
|
||||
let expectedBase64 = "Hllxv6LLoHl2XTIXGGjUYJHbPFoH2F7iMrR74C5J95g"
|
||||
|
||||
let digestBytes = hexFingerprint
|
||||
.split(separator: ":")
|
||||
.map { UInt8($0, radix: 16)! }
|
||||
let actualBase64 = Data(digestBytes).base64EncodedString()
|
||||
.replacingOccurrences(of: "=", with: "")
|
||||
|
||||
XCTAssertEqual(actualBase64, expectedBase64)
|
||||
}
|
||||
|
||||
func testDifferentKeysProduceDifferentFingerprints() throws {
|
||||
let keyA = try NIOSSHPublicKey(
|
||||
openSSHPublicKey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIINj+h+IeiBNELAG6CcYbMxrdsSn8hnccQrk/XDwUa6U test"
|
||||
)
|
||||
let keyB = try NIOSSHPublicKey(
|
||||
openSSHPublicKey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKFrnJfhSkd4VsrAhMBxc1MS6dm2LrDMMerNh3O4zl95 test2"
|
||||
)
|
||||
|
||||
XCTAssertNotEqual(SSHHostKeyFingerprint.sha256(of: keyA), SSHHostKeyFingerprint.sha256(of: keyB))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import XCTest
|
||||
@testable import RouterOSAssistant
|
||||
|
||||
final class SSHHostKeyTrustStoreTests: XCTestCase {
|
||||
override func tearDown() {
|
||||
UserDefaults.standard.removeObject(forKey: "RouterOSAssistant.TrustedSSHHostKeyFingerprints")
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testUntrustedByDefault() {
|
||||
let store = SSHHostKeyTrustStore()
|
||||
XCTAssertFalse(store.isTrusted(host: "192.0.2.1", fingerprint: "AA:BB"))
|
||||
}
|
||||
|
||||
func testTrustPersistsForSameHostAndFingerprint() {
|
||||
let store = SSHHostKeyTrustStore()
|
||||
store.trust(host: "192.0.2.1", fingerprint: "AA:BB")
|
||||
|
||||
XCTAssertTrue(store.isTrusted(host: "192.0.2.1", fingerprint: "AA:BB"))
|
||||
XCTAssertFalse(store.isTrusted(host: "192.0.2.1", fingerprint: "CC:DD"))
|
||||
XCTAssertFalse(store.isTrusted(host: "192.0.2.2", fingerprint: "AA:BB"))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user