Remove Host Key Changed
This commit is contained in:
1 parent
90fb1bfbcc
commit
762e6b1b57
21 files changed
+112
-139
No files matched your search
@@ -72,6 +72,8 @@ struct PersistenceCompatibilityTests {
|
||||
#expect(TermindMigrationPlan.schemas.contains { $0.versionIdentifier == TermindSchemaV26.versionIdentifier })
|
||||
#expect(TermindMigrationPlan.schemas.contains { $0.versionIdentifier == TermindSchemaV27.versionIdentifier })
|
||||
#expect(TermindMigrationPlan.schemas.contains { $0.versionIdentifier == TermindSchemaV28.versionIdentifier })
|
||||
#expect(TermindMigrationPlan.schemas.contains { $0.versionIdentifier == TermindSchemaV29.versionIdentifier })
|
||||
#expect(TermindMigrationPlan.schemas.contains { $0.versionIdentifier == TermindSchemaV30.versionIdentifier })
|
||||
#expect(TermindSchemaV22.models.contains { $0 == TermindSchemaV22HostModels.PersistentHost.self })
|
||||
#expect(!TermindSchemaV22.models.contains { $0 == TermindSchemaV24HostModels.PersistentHost.self })
|
||||
#expect(TermindSchemaV25.models.contains { $0 == TermindSchemaV24HostModels.PersistentHost.self })
|
||||
@@ -86,6 +88,34 @@ struct PersistenceCompatibilityTests {
|
||||
#expect(first.schema.entities.contains { $0.name == "PersistentSFTPPreferences" })
|
||||
}
|
||||
|
||||
@Test
|
||||
func version29StoreDropsMismatchStateWithoutLosingKnownHosts() throws {
|
||||
let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||
defer { try? FileManager.default.removeItem(at: directory) }
|
||||
let url = directory.appendingPathComponent("Termind.store")
|
||||
let oldSchema = Schema(versionedSchema: TermindSchemaV29.self)
|
||||
let oldConfiguration = ModelConfiguration(
|
||||
"Termind", schema: oldSchema, url: url, cloudKitDatabase: .none
|
||||
)
|
||||
let knownHost = KnownHostEntry(
|
||||
hostname: "server.invalid", algorithm: .ed25519,
|
||||
fingerprint: "SHA256:known-host"
|
||||
)
|
||||
var oldContainer: ModelContainer? = try ModelContainer(
|
||||
for: oldSchema, configurations: [oldConfiguration]
|
||||
)
|
||||
let oldRecord = TermindSchemaV29KnownHostModels.PersistentKnownHost(knownHost)
|
||||
oldRecord.isMismatched = true
|
||||
oldContainer?.mainContext.insert(oldRecord)
|
||||
try oldContainer?.mainContext.save()
|
||||
oldContainer = nil
|
||||
|
||||
let migrated = try TermindPersistence.makeContainer(url: url)
|
||||
let records = try migrated.mainContext.fetch(FetchDescriptor<PersistentKnownHost>())
|
||||
#expect(records.map(\.value) == [knownHost])
|
||||
}
|
||||
|
||||
@Test
|
||||
func version25StoreMigratesWithNoStartupSnippet() throws {
|
||||
let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||
|
||||
@@ -12242,22 +12242,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Host key changed" : {
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "主机密钥已更改"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "主機金鑰已更改"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Host key confirmation required" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
|
||||
@@ -131,12 +131,6 @@ public final class RemoteSessionConnectionController {
|
||||
return .accept
|
||||
}
|
||||
finishHostKeyAuthorization(.reject)
|
||||
if !challenge.expectedHostKeys.isEmpty {
|
||||
store.markKnownHostMismatched(
|
||||
hostname: challenge.observation.hostname,
|
||||
port: challenge.observation.port
|
||||
)
|
||||
}
|
||||
let authorizationID = UUID()
|
||||
hostKeyAuthorizationID = authorizationID
|
||||
waitForHostKey(
|
||||
|
||||
@@ -64,7 +64,6 @@ public protocol RemoteSessionHostStore: AnyObject, Sendable {
|
||||
func setConnectionStatus(_ status: ConnectionStatus, connectionID: UUID, hostID: Host.ID)
|
||||
func recordRemoteSystemInfo(_ systemInfo: RemoteSystemInfo?, for hostID: Host.ID)
|
||||
func trust(_ entry: KnownHostEntry)
|
||||
func markKnownHostMismatched(hostname: String, port: Int)
|
||||
@discardableResult func upsert(_ identity: Identity) -> Bool
|
||||
func upsert(_ identity: Identity, passwordUpdate: IdentityPasswordUpdate) throws
|
||||
func upsert(_ host: Host)
|
||||
|
||||
@@ -408,9 +408,6 @@ public final class HostLibraryModel: RemoteSessionHostStore {
|
||||
guard !knownHosts.isEmpty else {
|
||||
return String(localized: "Connect once to trust this host")
|
||||
}
|
||||
guard !knownHosts.contains(where: \.isMismatched) else {
|
||||
return String(localized: "Host key changed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1218,9 +1215,6 @@ public final class HostLibraryModel: RemoteSessionHostStore {
|
||||
let endpointMatches: (KnownHostEntry) -> Bool = {
|
||||
$0.matches(hostname: entry.hostname, port: entry.port)
|
||||
}
|
||||
for index in knownHosts.indices where endpointMatches(knownHosts[index]) {
|
||||
knownHosts[index].isMismatched = false
|
||||
}
|
||||
if let index = knownHosts.firstIndex(where: {
|
||||
endpointMatches($0) && $0.fingerprint == entry.fingerprint
|
||||
}) {
|
||||
@@ -1237,16 +1231,6 @@ public final class HostLibraryModel: RemoteSessionHostStore {
|
||||
persistCore()
|
||||
}
|
||||
|
||||
public func markKnownHostMismatched(hostname: String, port: Int) {
|
||||
var changed = false
|
||||
for index in knownHosts.indices
|
||||
where knownHosts[index].matches(hostname: hostname, port: port) {
|
||||
knownHosts[index].isMismatched = true
|
||||
changed = true
|
||||
}
|
||||
if changed { persistCore() }
|
||||
}
|
||||
|
||||
public func setExpanded(_ expanded: Bool, for groupID: HostGroup.ID) {
|
||||
guard group(groupID) != nil else { return }
|
||||
expansionState.setHostGroupExpanded(expanded, for: groupID)
|
||||
@@ -2130,12 +2114,10 @@ public final class HostLibraryModel: RemoteSessionHostStore {
|
||||
}
|
||||
let previous = knownHosts
|
||||
if let index = knownHosts.firstIndex(where: { $0.id == entry.id }) {
|
||||
// Editing the address or key explicitly creates a new trust decision.
|
||||
if knownHosts[index].identity != entry.identity || knownHosts[index].hostname != entry.hostname
|
||||
|| knownHosts[index].port != entry.port
|
||||
{
|
||||
entry.addedAt = Date()
|
||||
entry.isMismatched = false
|
||||
}
|
||||
knownHosts[index] = entry
|
||||
} else {
|
||||
|
||||
@@ -969,12 +969,6 @@ public final class ServerMetricsModel: MetricsSnapshotReading {
|
||||
) -> HostKeyAuthorization? {
|
||||
guard current(serverID, token), let server = servers.first(where: { $0.id == serverID }) else { return nil }
|
||||
finishHostKeyAuthorization(for: serverID, decision: .reject)
|
||||
if !challenge.expectedHostKeys.isEmpty {
|
||||
hosts.markKnownHostMismatched(
|
||||
hostname: challenge.observation.hostname,
|
||||
port: challenge.observation.port
|
||||
)
|
||||
}
|
||||
server.hostKey = challenge.observation
|
||||
server.hostKeyHostID = challenge.hostID
|
||||
server.hostKeyPrompt = SSHHostKeyPrompt(
|
||||
|
||||
@@ -13,17 +13,13 @@ public nonisolated struct KnownHostEntry: Identifiable, Codable, Hashable, Senda
|
||||
public var algorithm: SSHKeyAlgorithm = .ed25519
|
||||
public var fingerprint: String = ""
|
||||
public var addedAt: Date = Date(timeIntervalSince1970: 0)
|
||||
/// True once the key the server presented stopped matching the pinned one.
|
||||
/// Kept as data rather than a transient alert so the warning survives the
|
||||
/// dialog the user dismissed at 3am.
|
||||
public var isMismatched: Bool = false
|
||||
|
||||
public var identity: SSHHostKeyIdentity {
|
||||
SSHHostKeyIdentity(algorithm: algorithm, fingerprint: fingerprint)
|
||||
}
|
||||
public init(
|
||||
id: UUID = UUID(), hostname: String, port: Int = 22, algorithm: SSHKeyAlgorithm = .ed25519,
|
||||
fingerprint: String = "", addedAt: Date = Date(timeIntervalSince1970: 0), isMismatched: Bool = false
|
||||
fingerprint: String = "", addedAt: Date = Date(timeIntervalSince1970: 0)
|
||||
) {
|
||||
self.id = id
|
||||
self.hostname = hostname
|
||||
@@ -31,7 +27,6 @@ public nonisolated struct KnownHostEntry: Identifiable, Codable, Hashable, Senda
|
||||
self.algorithm = algorithm
|
||||
self.fingerprint = fingerprint
|
||||
self.addedAt = addedAt
|
||||
self.isMismatched = isMismatched
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -383,36 +383,26 @@ public enum TermindSchemaV7Models {
|
||||
}
|
||||
}
|
||||
|
||||
@Model
|
||||
public final class PersistentKnownHost {
|
||||
@Attribute(.unique) public var id: UUID = UUID()
|
||||
public var hostname = ""
|
||||
public var port = 22
|
||||
public var algorithm = SSHKeyAlgorithm.ed25519.rawValue
|
||||
public var fingerprint = ""
|
||||
public var addedAt = Date(timeIntervalSince1970: 0)
|
||||
public var isMismatched = false
|
||||
/// Frozen through V29. V30 removes the persisted mismatch flag from the live model.
|
||||
public enum TermindSchemaV29KnownHostModels {
|
||||
@Model
|
||||
public final class PersistentKnownHost {
|
||||
@Attribute(.unique) public var id: UUID = UUID()
|
||||
public var hostname = ""
|
||||
public var port = 22
|
||||
public var algorithm = SSHKeyAlgorithm.ed25519.rawValue
|
||||
public var fingerprint = ""
|
||||
public var addedAt = Date(timeIntervalSince1970: 0)
|
||||
public var isMismatched = false
|
||||
|
||||
public init(_ value: KnownHostEntry) {
|
||||
id = value.id
|
||||
apply(value)
|
||||
}
|
||||
|
||||
public func apply(_ value: KnownHostEntry) {
|
||||
hostname = value.hostname
|
||||
port = value.port
|
||||
algorithm = value.algorithm.rawValue
|
||||
fingerprint = value.fingerprint
|
||||
addedAt = value.addedAt
|
||||
isMismatched = value.isMismatched
|
||||
}
|
||||
|
||||
public var value: KnownHostEntry {
|
||||
KnownHostEntry(
|
||||
id: id, hostname: hostname, port: port,
|
||||
algorithm: SSHKeyAlgorithm(rawValue: algorithm) ?? .ed25519,
|
||||
fingerprint: fingerprint, addedAt: addedAt, isMismatched: isMismatched
|
||||
)
|
||||
public init(_ value: KnownHostEntry) {
|
||||
id = value.id
|
||||
hostname = value.hostname
|
||||
port = value.port
|
||||
algorithm = value.algorithm.rawValue
|
||||
fingerprint = value.fingerprint
|
||||
addedAt = value.addedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,7 +493,7 @@ public enum TermindSchemaV1: VersionedSchema {
|
||||
PersistentHostGroup.self,
|
||||
TermindSchemaV6Models.PersistentIdentity.self,
|
||||
TermindSchemaV6Models.PersistentSSHKey.self,
|
||||
PersistentKnownHost.self,
|
||||
TermindSchemaV29KnownHostModels.PersistentKnownHost.self,
|
||||
PersistentSnippetPackage.self,
|
||||
PersistentSnippet.self,
|
||||
TermindSchemaV19ForwardingModels.PersistentPortForwardingRule.self,
|
||||
@@ -782,6 +772,15 @@ public enum TermindSchemaV29: VersionedSchema {
|
||||
}
|
||||
}
|
||||
|
||||
public enum TermindSchemaV30: VersionedSchema {
|
||||
public static let versionIdentifier = Schema.Version(30, 0, 0)
|
||||
public static var models: [any PersistentModel.Type] {
|
||||
TermindSchemaV29.models.filter {
|
||||
$0 != TermindSchemaV29KnownHostModels.PersistentKnownHost.self
|
||||
} + [TermindSchemaV30KnownHostModels.PersistentKnownHost.self]
|
||||
}
|
||||
}
|
||||
|
||||
public enum TermindMigrationPlan: SchemaMigrationPlan {
|
||||
public static var schemas: [any VersionedSchema.Type] {
|
||||
[
|
||||
@@ -793,6 +792,7 @@ public enum TermindMigrationPlan: SchemaMigrationPlan {
|
||||
TermindSchemaV18.self, TermindSchemaV19.self, TermindSchemaV20.self, TermindSchemaV21.self,
|
||||
TermindSchemaV22.self, TermindSchemaV23.self, TermindSchemaV24.self, TermindSchemaV25.self,
|
||||
TermindSchemaV26.self, TermindSchemaV27.self, TermindSchemaV28.self, TermindSchemaV29.self,
|
||||
TermindSchemaV30.self,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -863,6 +863,7 @@ public enum TermindMigrationPlan: SchemaMigrationPlan {
|
||||
MigrationStage.lightweight(fromVersion: TermindSchemaV26.self, toVersion: TermindSchemaV27.self),
|
||||
MigrationStage.lightweight(fromVersion: TermindSchemaV27.self, toVersion: TermindSchemaV28.self),
|
||||
MigrationStage.lightweight(fromVersion: TermindSchemaV28.self, toVersion: TermindSchemaV29.self),
|
||||
MigrationStage.lightweight(fromVersion: TermindSchemaV29.self, toVersion: TermindSchemaV30.self),
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -870,3 +871,4 @@ public enum TermindMigrationPlan: SchemaMigrationPlan {
|
||||
public typealias PersistentHost = TermindSchemaV29HostModels.PersistentHost
|
||||
public typealias PersistentIdentity = TermindSchemaV25IdentityModels.PersistentIdentity
|
||||
public typealias PersistentSSHKey = TermindSchemaV7Models.PersistentSSHKey
|
||||
public typealias PersistentKnownHost = TermindSchemaV30KnownHostModels.PersistentKnownHost
|
||||
@@ -5,7 +5,7 @@ import SwiftData
|
||||
/// choose storage and own the container lifetime, never the model set.
|
||||
@MainActor
|
||||
public enum TermindPersistence {
|
||||
public static let schema = Schema(versionedSchema: TermindSchemaV29.self)
|
||||
public static let schema = Schema(versionedSchema: TermindSchemaV30.self)
|
||||
|
||||
public static func makeContainer(inMemory: Bool = false, url: URL? = nil) throws -> ModelContainer {
|
||||
precondition(!inMemory || url == nil, "An in-memory store cannot also have a file URL")
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import Foundation
|
||||
import SwiftData
|
||||
import TermindDomain
|
||||
|
||||
/// V30 removes the historical host-key mismatch marker while preserving Known Host trust records.
|
||||
public enum TermindSchemaV30KnownHostModels {
|
||||
@Model
|
||||
public final class PersistentKnownHost {
|
||||
@Attribute(.unique) public var id: UUID = UUID()
|
||||
public var hostname = ""
|
||||
public var port = 22
|
||||
public var algorithm = SSHKeyAlgorithm.ed25519.rawValue
|
||||
public var fingerprint = ""
|
||||
public var addedAt = Date(timeIntervalSince1970: 0)
|
||||
|
||||
public init(_ value: KnownHostEntry) {
|
||||
id = value.id
|
||||
apply(value)
|
||||
}
|
||||
|
||||
public func apply(_ value: KnownHostEntry) {
|
||||
hostname = value.hostname
|
||||
port = value.port
|
||||
algorithm = value.algorithm.rawValue
|
||||
fingerprint = value.fingerprint
|
||||
addedAt = value.addedAt
|
||||
}
|
||||
|
||||
public var value: KnownHostEntry {
|
||||
KnownHostEntry(
|
||||
id: id, hostname: hostname, port: port,
|
||||
algorithm: SSHKeyAlgorithm(rawValue: algorithm) ?? .ed25519,
|
||||
fingerprint: fingerprint, addedAt: addedAt
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -380,12 +380,6 @@ public final class AgentHostResolver: Sendable {
|
||||
?? (challenge.observation.port == 22
|
||||
? challenge.observation.hostname
|
||||
: "\(challenge.observation.hostname):\(challenge.observation.port)")
|
||||
if !challenge.expectedHostKeys.isEmpty {
|
||||
store.markKnownHostMismatched(
|
||||
hostname: challenge.observation.hostname,
|
||||
port: challenge.observation.port
|
||||
)
|
||||
}
|
||||
let response = await interaction.confirmHostKey(
|
||||
AgentSSHHostKeyRequest(
|
||||
invocationID: invocationID, hostID: challenge.hostID,
|
||||
|
||||
@@ -77,7 +77,7 @@ sibling working tree is not the app dependency. Follow `AGENTS.md` for package r
|
||||
- SSH import previews are window-owned indexed public metadata. Host selection never selects credentials; imported
|
||||
identities require selected keys. The actor retains scoped descriptors/digests, not private-key text/passphrases;
|
||||
reread selected keys at unlock/commit and fail closed if changed. Picking/drop share validation; config parsing
|
||||
executes nothing. Create credentials before one Vault save, clean up failure, never overwrite records/clear mismatch.
|
||||
executes nothing. Create credentials before one Vault save, clean up failure, and never overwrite records.
|
||||
- macOS appearance/language are local; System appearance clears `NSApplication.appearance`. Login launch reads
|
||||
`SMAppService`, not mirrored preferences; quiet launch hides the app without suppressing its window. Mobile uses
|
||||
Apple's per-app language settings without an injected locale override.
|
||||
|
||||
@@ -572,10 +572,6 @@ struct MobileVaultView: View {
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
HStack(spacing: 6) {
|
||||
if app.library.knownHosts(for: host).contains(where: \.isMismatched) {
|
||||
MobileCardChip(
|
||||
title: "Host key changed", symbol: "exclamationmark.shield", tint: .orange)
|
||||
}
|
||||
ForEach(MobileCardLayout.visibleTags(host.tags), id: \.self) { tag in
|
||||
MobileCardChip(text: tag)
|
||||
}
|
||||
|
||||
@@ -36,8 +36,7 @@ struct MobileCardRenderingTests {
|
||||
if index == 3 {
|
||||
_ = app.knownHosts.save(
|
||||
KnownHostEntry(
|
||||
hostname: host.host, fingerprint: "SHA256:0123456789abcdefghijklmnopqrstuvwxyzABCDE",
|
||||
isMismatched: true))
|
||||
hostname: host.host, fingerprint: "SHA256:0123456789abcdefghijklmnopqrstuvwxyzABCDE"))
|
||||
}
|
||||
}
|
||||
let editableKnownHost = KnownHostEntry(
|
||||
|
||||
@@ -174,13 +174,10 @@ extension HostLibraryModel {
|
||||
fingerprint: "SHA256:2mF8sK1pQ7xR4vB9nL3yT6wC0dH5jA2uZgE7iOxNbVr",
|
||||
addedAt: now.addingTimeInterval(-604_800)
|
||||
),
|
||||
// One deliberately broken pin, so the warning state is visible at P0
|
||||
// instead of first appearing the day it matters.
|
||||
KnownHostEntry(
|
||||
hostname: "staging.internal", algorithm: .rsa,
|
||||
fingerprint: "SHA256:5vT2nB8xQ1mL7kR4pW9yC3sD6fH0jG5uZaE8iXoNbVr",
|
||||
addedAt: now.addingTimeInterval(-15_552_000),
|
||||
isMismatched: true
|
||||
addedAt: now.addingTimeInterval(-15_552_000)
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -123,9 +123,6 @@ extension TransferLibraryModel: RemoteSessionHostStore {
|
||||
library.recordRemoteSystemInfo(systemInfo, for: hostID)
|
||||
}
|
||||
func trust(_ entry: KnownHostEntry) { library.trust(entry) }
|
||||
func markKnownHostMismatched(hostname: String, port: Int) {
|
||||
library.markKnownHostMismatched(hostname: hostname, port: port)
|
||||
}
|
||||
@discardableResult func upsert(_ identity: Identity) -> Bool { library.upsert(identity) }
|
||||
func upsert(_ identity: Identity, passwordUpdate: IdentityPasswordUpdate) throws {
|
||||
try library.upsert(identity, passwordUpdate: passwordUpdate)
|
||||
|
||||
@@ -421,13 +421,6 @@ private struct HostCard: View {
|
||||
|
||||
private var status: ConnectionStatus { app.hostLibrary.status(of: host.id) }
|
||||
|
||||
/// A pinned key that stopped matching. Shown here, on the host, because that
|
||||
/// is what the fingerprint belongs to — there is no separate Known Hosts
|
||||
/// list to go and check.
|
||||
private var keyMismatched: Bool {
|
||||
app.hostLibrary.knownHosts(for: host).contains(where: \.isMismatched)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
card.clearAllHostHistoryConfirmation(hostIDs: $clearHistoryHostIDs)
|
||||
}
|
||||
@@ -486,13 +479,6 @@ private struct HostCard: View {
|
||||
|
||||
private var footer: some View {
|
||||
HStack(spacing: Metrics.Space.xs) {
|
||||
if keyMismatched {
|
||||
CardChip(
|
||||
text: String(localized: "Host key changed"),
|
||||
symbol: "exclamationmark.triangle.fill",
|
||||
tint: .orange
|
||||
)
|
||||
}
|
||||
ForEach(host.tags.prefix(2), id: \.self) { tag in
|
||||
CardChip(text: tag)
|
||||
}
|
||||
|
||||
@@ -864,7 +864,6 @@ struct AgentHostAuthenticationTests {
|
||||
#expect(decision == .accept)
|
||||
#expect(attempt.configuration.expectedHostKeys == [old.identity])
|
||||
#expect(Set(store.knownHosts(for: host).map(\.identity)) == [old.identity, observation.identity])
|
||||
#expect(store.knownHosts(for: host).allSatisfy { !$0.isMismatched })
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -278,12 +278,11 @@ struct ConnectionJourneyTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
func trustingTheSameFingerprintIsIdempotentAndClearsMismatch() {
|
||||
func trustingTheSameFingerprintIsIdempotent() {
|
||||
let host = Host(name: "Server", host: "server.example")
|
||||
let original = KnownHostEntry(
|
||||
hostname: host.host, algorithm: .ed25519,
|
||||
fingerprint: "SHA256:key", addedAt: Date(timeIntervalSince1970: 1),
|
||||
isMismatched: true
|
||||
fingerprint: "SHA256:key", addedAt: Date(timeIntervalSince1970: 1)
|
||||
)
|
||||
let store = HostLibraryModel(hosts: [host], knownHosts: [original])
|
||||
|
||||
@@ -298,28 +297,19 @@ struct ConnectionJourneyTests {
|
||||
#expect(entries.count == 1)
|
||||
#expect(entries[0].id == original.id)
|
||||
#expect(entries[0].addedAt == Date(timeIntervalSince1970: 2))
|
||||
#expect(!entries[0].isMismatched)
|
||||
}
|
||||
|
||||
@Test
|
||||
func mismatchAndForgetOperationsApplyToTheHostKeySet() {
|
||||
func trustAndForgetOperationsApplyToTheHostKeySet() {
|
||||
let host = Host(name: "Server", host: "server.example")
|
||||
let first = KnownHostEntry(hostname: host.host, fingerprint: "SHA256:first")
|
||||
let second = KnownHostEntry(hostname: host.host, fingerprint: "SHA256:second")
|
||||
let store = HostLibraryModel(hosts: [host], knownHosts: [first, second])
|
||||
|
||||
store.markKnownHostMismatched(hostname: host.host, port: host.port)
|
||||
for entry in store.knownHosts(for: host) {
|
||||
#expect(entry.isMismatched)
|
||||
}
|
||||
|
||||
store.trust(
|
||||
KnownHostEntry(hostname: host.host, fingerprint: "SHA256:third")
|
||||
)
|
||||
#expect(store.knownHosts(for: host).count == 3)
|
||||
for entry in store.knownHosts(for: host) {
|
||||
#expect(!entry.isMismatched)
|
||||
}
|
||||
|
||||
store.forget(knownHost: first.id)
|
||||
#expect(store.knownHosts(for: host).count == 2)
|
||||
|
||||
@@ -56,8 +56,6 @@ struct SSHImportTests {
|
||||
let store = HostLibraryModel(knownHosts: [decoded])
|
||||
let host = Host(name: "IPv6", host: "2001:db8::1", port: 2222)
|
||||
#expect(try store.expectedHostKeys(for: host) == [entry.identity])
|
||||
store.markKnownHostMismatched(hostname: host.host, port: host.port)
|
||||
#expect(store.knownHosts[0].isMismatched)
|
||||
store.forgetKnownHosts(for: host)
|
||||
#expect(store.knownHosts.isEmpty)
|
||||
}
|
||||
@@ -427,8 +425,8 @@ struct SSHImportTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
func importRetainsMismatchWarningsAndRequiresConflictReview() async {
|
||||
let original = KnownHostEntry(hostname: "host", fingerprint: "SHA256:old", isMismatched: true)
|
||||
func importRequiresKnownHostConflictReview() async {
|
||||
let original = KnownHostEntry(hostname: "host", fingerprint: "SHA256:old")
|
||||
let store = HostLibraryModel(knownHosts: [original])
|
||||
let model = SSHImportModel(library: store, reader: SSHImportReader())
|
||||
model.items = [
|
||||
@@ -441,7 +439,7 @@ struct SSHImportTests {
|
||||
#expect(model.canImport)
|
||||
await model.commit()
|
||||
#expect(model.completed)
|
||||
#expect(store.knownHosts.first?.isMismatched == true)
|
||||
#expect(store.knownHosts.first?.fingerprint == original.fingerprint)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -115,10 +115,10 @@ struct VaultSyncTests {
|
||||
#expect(Set(received.knownHosts) == Set([plain, hashed]))
|
||||
|
||||
var edited = plain
|
||||
edited.isMismatched = true
|
||||
edited.algorithm = .ecdsa256
|
||||
#expect(library.saveKnownHost(edited))
|
||||
try transfer(plain.id)
|
||||
#expect(try destinationPersistence.load().knownHosts.first { $0.id == plain.id }?.isMismatched == true)
|
||||
#expect(try destinationPersistence.load().knownHosts.first { $0.id == plain.id }?.algorithm == .ecdsa256)
|
||||
|
||||
#expect(library.deleteKnownHosts([plain.id]))
|
||||
try transfer(plain.id)
|
||||
|
||||
Reference in new issue
Block a user