Files
RouterOS/RouterOSAssistantTests/SSHHostKeyFingerprintTests.swift
KayandClaude Sonnet 5 13344c265e 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
2026-09-13 00:40:26 +02:00

36 lines
1.6 KiB
Swift

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