From 6589d28a783d6bbf1fd615831639056c900dab69 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Thu, 9 Jul 2026 22:02:54 -0700 Subject: [PATCH 1/3] Support signed remote admin in licensed mode --- Localizable.xcstrings | 24 ++- Meshtastic.xcodeproj/project.pbxproj | 4 + .../Accessory Manager/AccessoryManager.swift | 30 ++- Meshtastic/Views/Settings/Settings.swift | 29 ++- .../RemoteAdminTransportPolicyTests.swift | 198 ++++++++++++++++++ 5 files changed, 279 insertions(+), 6 deletions(-) create mode 100644 MeshtasticTests/RemoteAdminTransportPolicyTests.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 26787b3e0..9fd2b06f1 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -69928,6 +69928,17 @@ } } }, + "Remote Signed Admin: %@" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Remote Signed Admin: %@" + } + } + } + }, "Remove" : { "localizations" : { "da" : { @@ -70531,6 +70542,17 @@ } } }, + "Request Signed Admin: %@" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Request Signed Admin: %@" + } + } + } + }, "Requested" : { "comment" : "A label indicating that a trace route was requested.", "isCommentAutoGenerated" : true @@ -103259,4 +103281,4 @@ } }, "version" : "1.1" -} \ No newline at end of file +} diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 8e8d6c3cb..2a475ffec 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -331,6 +331,7 @@ DD00003ASUPPORTLVL0000001 /* SupportLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD00003ASUPPORTLVL0000000 /* SupportLevel.swift */; }; DD00003C000SNAP00000001 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = DD00003B000SNAP00000000 /* SnapshotTesting */; }; DD0000CHSETST0000000001 /* ChannelSetSaveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0000CHSETST0000000000 /* ChannelSetSaveTests.swift */; }; + DD0000RADMINTST00000001 /* RemoteAdminTransportPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0000RADMINTST00000000 /* RemoteAdminTransportPolicyTests.swift */; }; DD0000MQTTFWDFLTTST00001 /* MqttForwardFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0000MQTTFWDFLTTST00000 /* MqttForwardFilterTests.swift */; }; DD0000DEVLNKTST000000001 /* DeviceLinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0000DEVLNKTST000000000 /* DeviceLinkTests.swift */; }; DD1827FB0000000000000002 /* RegionCodesFirmwareLocaleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1827FA0000000000000002 /* RegionCodesFirmwareLocaleTests.swift */; }; @@ -943,6 +944,7 @@ DD000031FWEDTENUM0000000 /* FirmwareEditionEnum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirmwareEditionEnum.swift; sourceTree = ""; }; DD00003ASUPPORTLVL0000000 /* SupportLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportLevel.swift; sourceTree = ""; }; DD0000CHSETST0000000000 /* ChannelSetSaveTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelSetSaveTests.swift; sourceTree = ""; }; + DD0000RADMINTST00000000 /* RemoteAdminTransportPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteAdminTransportPolicyTests.swift; sourceTree = ""; }; DD0000MQTTFWDFLTTST00000 /* MqttForwardFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MqttForwardFilterTests.swift; sourceTree = ""; }; DD0000DEVLNKTST000000000 /* DeviceLinkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceLinkTests.swift; sourceTree = ""; }; DD1827FA0000000000000002 /* RegionCodesFirmwareLocaleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegionCodesFirmwareLocaleTests.swift; sourceTree = ""; }; @@ -1692,6 +1694,7 @@ DD0000DEVLNKTST000000000 /* DeviceLinkTests.swift */, BEEF00102F00000100000003 /* MeshtasticChannelURLTests.swift */, DD0000CHSETST0000000000 /* ChannelSetSaveTests.swift */, + DD0000RADMINTST00000000 /* RemoteAdminTransportPolicyTests.swift */, DD0000MQTTFWDFLTTST00000 /* MqttForwardFilterTests.swift */, DD1827FA0000000000000002 /* RegionCodesFirmwareLocaleTests.swift */, DD000031STATMSGDISP00000 /* StatusMessageDisplayTests.swift */, @@ -2776,6 +2779,7 @@ DD7E5750000000000000008B /* LockdownPassphraseStoreTests.swift in Sources */, DD0000DEVLNKTST000000001 /* DeviceLinkTests.swift in Sources */, DD0000CHSETST0000000001 /* ChannelSetSaveTests.swift in Sources */, + DD0000RADMINTST00000001 /* RemoteAdminTransportPolicyTests.swift in Sources */, DD0000MQTTFWDFLTTST00001 /* MqttForwardFilterTests.swift in Sources */, DD1827FB0000000000000002 /* RegionCodesFirmwareLocaleTests.swift in Sources */, DD000031STATMSGDISP00001 /* StatusMessageDisplayTests.swift in Sources */, diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift index ee20fc05d..a46696466 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift @@ -518,12 +518,40 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate { await active.connection.isConnected else { throw AccessoryError.connectionFailed("Not connected to any device") } - try await active.connection.send(data) + try await active.connection.send(applyingLicensedRemoteAdminPolicy(to: data, connectedDeviceNum: active.device.num)) if let debugDescription { Logger.transport.info("📻 \(debugDescription, privacy: .public)") } } + /// Licensed-mode remote administration is authenticated by the firmware's verified packet + /// signature and must remain plaintext on air. Apply that invariant at the final send boundary + /// so both the shared Admin helper and older direct-send call sites receive the same treatment. + private func applyingLicensedRemoteAdminPolicy(to data: ToRadio, connectedDeviceNum: Int64?) -> ToRadio { + guard let connectedDeviceNum else { return data } + guard case let .packet(packet) = data.payloadVariant, + case let .decoded(decoded) = packet.payloadVariant, + decoded.portnum == .adminApp, + packet.to != UInt32(truncatingIfNeeded: connectedDeviceNum) + else { return data } + + let ownerNum = connectedDeviceNum + var descriptor = FetchDescriptor(predicate: #Predicate { $0.num == ownerNum }) + descriptor.fetchLimit = 1 + guard let connectedOwner = try? context.fetch(descriptor).first, + connectedOwner.isLicensed + else { return data } + + var plaintextPacket = packet + plaintextPacket.channel = 0 + plaintextPacket.pkiEncrypted = false + plaintextPacket.publicKey = Data() + + var result = data + result.packet = plaintextPacket + return result + } + func didReceive(_ event: ConnectionEvent) async { let shouldIgnoreTransientEvent = isClosingConnection || userRequestedConnectionCancellation || activeConnection == nil diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 9f8c9c52d..2f1faed03 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -11,6 +11,16 @@ import OSLog import TipKit import MeshtasticProtobufs +enum RemoteAdminWording { + static func activeFormat(localOwnerIsLicensed: Bool) -> String { + localOwnerIsLicensed ? "Remote Signed Admin: %@" : "Remote PKI Admin: %@" + } + + static func requestFormat(localOwnerIsLicensed: Bool) -> String { + localOwnerIsLicensed ? "Request Signed Admin: %@" : "Request PKI Admin: %@" + } +} + struct Settings: View { @Environment(\.modelContext) private var context @Environment(\.colorScheme) private var colorScheme @@ -28,6 +38,11 @@ struct Settings: View { NodeInfoEntity.adminPickerOrder(nodes) } + private var localOwnerIsLicensed: Bool { + guard let activeDeviceNum = accessoryManager.activeDeviceNum else { return false } + return nodes.first(where: { $0.num == activeDeviceNum })?.user?.isLicensed == true + } + @State private var selectedNode: Int = 0 @State private var preferredNodeNum: Int = 0 @@ -573,11 +588,14 @@ struct Settings: View { accessoryManager.activeConnection?.device.transportType.icon ?? Image("questionmark.circle") } .tag(Int(node.num)) - } else if node.canRemoteAdmin && UserDefaults.enableAdministration && node.sessionPasskey != nil { /// Nodes using the new PKI system + } else if node.canRemoteAdmin && UserDefaults.enableAdministration && node.sessionPasskey != nil { Label { - Text("Remote PKI Admin: \(node.user?.longName ?? "Unknown".localized)") + Text(String.localizedStringWithFormat( + RemoteAdminWording.activeFormat(localOwnerIsLicensed: localOwnerIsLicensed).localized, + node.user?.longName ?? "Unknown".localized + )) } icon: { - Image(systemName: "av.remote") + Image(systemName: localOwnerIsLicensed ? "checkmark.shield" : "av.remote") } .font(.caption2) .tag(Int(node.num)) @@ -590,7 +608,10 @@ struct Settings: View { .tag(Int(node.num)) } else if UserDefaults.enableAdministration && node.user?.pkiEncrypted ?? false { Label { - Text("Request PKI Admin: \(node.user?.longName?.addingVariationSelectors ?? "Unknown".localized)") + Text(String.localizedStringWithFormat( + RemoteAdminWording.requestFormat(localOwnerIsLicensed: localOwnerIsLicensed).localized, + node.user?.longName?.addingVariationSelectors ?? "Unknown".localized + )) } icon: { Image(systemName: "rectangle.and.hand.point.up.left") } diff --git a/MeshtasticTests/RemoteAdminTransportPolicyTests.swift b/MeshtasticTests/RemoteAdminTransportPolicyTests.swift new file mode 100644 index 000000000..c7ff39a06 --- /dev/null +++ b/MeshtasticTests/RemoteAdminTransportPolicyTests.swift @@ -0,0 +1,198 @@ +// +// RemoteAdminTransportPolicyTests.swift +// MeshtasticTests +// + +import Foundation +import MeshtasticProtobufs +import SwiftData +import Testing +@testable import Meshtastic + +actor RecordingRemoteAdminConnection: Connection { + let type: TransportType = .ble + var isConnected = true + private(set) var sentPackets: [ToRadio] = [] + + func send(_ data: ToRadio) async throws { + sentPackets.append(data) + } + + func connect() async throws -> AsyncStream { + AsyncStream { $0.finish() } + } + + func disconnect(withError: Error?, shouldReconnect: Bool) async throws {} + func drainPendingPackets() async throws {} + func startDrainPendingPackets() throws {} + func appDidEnterBackground() {} + func appDidBecomeActive() {} +} + +@MainActor +@Suite("Licensed remote Admin transport", .serialized) +struct RemoteAdminTransportPolicyTests { + + private enum TestError: Error { + case missingMeshPacket + } + + private func makeManager( + connectedDeviceNum: Int64, + connection: RecordingRemoteAdminConnection + ) -> AccessoryManager { + let manager = AccessoryManager(transports: []) + manager.context = sharedModelContainer.mainContext + manager.activeDeviceNum = connectedDeviceNum + manager.activeConnection = ( + device: Device( + id: UUID(), + name: "Test radio", + transportType: .ble, + identifier: "remote-admin-\(connectedDeviceNum)", + connectionState: .connected, + num: connectedDeviceNum + ), + connection: connection + ) + return manager + } + + private func user(num: Int64, isLicensed: Bool) throws -> UserEntity { + let context = sharedModelContainer.mainContext + let requestedNum = num + var descriptor = FetchDescriptor(predicate: #Predicate { $0.num == requestedNum }) + descriptor.fetchLimit = 1 + let result = try context.fetch(descriptor).first ?? UserEntity() + if result.modelContext == nil { + result.num = num + context.insert(result) + } + result.isLicensed = isLicensed + try context.save() + return result + } + + private func removeUser(num: Int64) throws { + let context = sharedModelContainer.mainContext + let requestedNum = num + let descriptor = FetchDescriptor(predicate: #Predicate { $0.num == requestedNum }) + for existing in try context.fetch(descriptor) { + context.delete(existing) + } + try context.save() + } + + private func adminPacket(from: Int64, to: Int64) -> ToRadio { + var decoded = DataMessage() + decoded.portnum = .adminApp + + var packet = MeshPacket() + packet.from = UInt32(truncatingIfNeeded: from) + packet.to = UInt32(truncatingIfNeeded: to) + packet.channel = 7 + packet.pkiEncrypted = true + packet.publicKey = Data([0x01, 0x02, 0x03]) + packet.decoded = decoded + + var toRadio = ToRadio() + toRadio.packet = packet + return toRadio + } + + private func lastMeshPacket(from connection: RecordingRemoteAdminConnection) async throws -> MeshPacket { + guard let toRadio = await connection.sentPackets.last, + case let .packet(packet) = toRadio.payloadVariant + else { throw TestError.missingMeshPacket } + return packet + } + + @Test("Licensed remote Admin clears PKI fields at the final send boundary") + func licensedRemoteAdminIsPlaintext() async throws { + let ownerNum: Int64 = 4_010_000_001 + let remoteNum: Int64 = 4_010_000_002 + _ = try user(num: ownerNum, isLicensed: true) + let connection = RecordingRemoteAdminConnection() + let manager = makeManager(connectedDeviceNum: ownerNum, connection: connection) + + try await manager.send(adminPacket(from: ownerNum, to: remoteNum)) + + let packet = try await lastMeshPacket(from: connection) + #expect(packet.channel == 0) + #expect(packet.pkiEncrypted == false) + #expect(packet.publicKey.isEmpty) + } + + @Test("The shared Admin helper also records a plaintext licensed remote request") + func sharedAdminHelperIsPlaintext() async throws { + let ownerNum: Int64 = 4_010_000_003 + let remoteNum: Int64 = 4_010_000_004 + let owner = try user(num: ownerNum, isLicensed: true) + let remote = try user(num: remoteNum, isLicensed: false) + let connection = RecordingRemoteAdminConnection() + let manager = makeManager(connectedDeviceNum: ownerNum, connection: connection) + + try await manager.requestDeviceConfig(fromUser: owner, toUser: remote) + + let packet = try await lastMeshPacket(from: connection) + #expect(packet.decoded.portnum == .adminApp) + #expect(packet.channel == 0) + #expect(packet.pkiEncrypted == false) + #expect(packet.publicKey.isEmpty) + } + + @Test("Normal-mode remote Admin preserves existing packet fields") + func normalRemoteAdminIsUnchanged() async throws { + let ownerNum: Int64 = 4_010_000_005 + let remoteNum: Int64 = 4_010_000_006 + _ = try user(num: ownerNum, isLicensed: false) + let connection = RecordingRemoteAdminConnection() + let manager = makeManager(connectedDeviceNum: ownerNum, connection: connection) + + try await manager.send(adminPacket(from: ownerNum, to: remoteNum)) + + let packet = try await lastMeshPacket(from: connection) + #expect(packet.channel == 7) + #expect(packet.pkiEncrypted) + #expect(packet.publicKey == Data([0x01, 0x02, 0x03])) + } + + @Test("Licensed self Admin preserves existing packet fields") + func licensedSelfAdminIsUnchanged() async throws { + let ownerNum: Int64 = 4_010_000_007 + _ = try user(num: ownerNum, isLicensed: true) + let connection = RecordingRemoteAdminConnection() + let manager = makeManager(connectedDeviceNum: ownerNum, connection: connection) + + try await manager.send(adminPacket(from: ownerNum, to: ownerNum)) + + let packet = try await lastMeshPacket(from: connection) + #expect(packet.channel == 7) + #expect(packet.pkiEncrypted) + #expect(packet.publicKey == Data([0x01, 0x02, 0x03])) + } + + @Test("Unknown connected-owner mode does not assume licensed transport") + func unknownOwnerModeIsUnchanged() async throws { + let ownerNum: Int64 = 4_010_000_008 + let remoteNum: Int64 = 4_010_000_009 + try removeUser(num: ownerNum) + let connection = RecordingRemoteAdminConnection() + let manager = makeManager(connectedDeviceNum: ownerNum, connection: connection) + + try await manager.send(adminPacket(from: ownerNum, to: remoteNum)) + + let packet = try await lastMeshPacket(from: connection) + #expect(packet.channel == 7) + #expect(packet.pkiEncrypted) + #expect(packet.publicKey == Data([0x01, 0x02, 0x03])) + } + + @Test("Remote Admin wording distinguishes signed and PKI modes") + func conditionalWording() { + #expect(RemoteAdminWording.activeFormat(localOwnerIsLicensed: true) == "Remote Signed Admin: %@") + #expect(RemoteAdminWording.requestFormat(localOwnerIsLicensed: true) == "Request Signed Admin: %@") + #expect(RemoteAdminWording.activeFormat(localOwnerIsLicensed: false) == "Remote PKI Admin: %@") + #expect(RemoteAdminWording.requestFormat(localOwnerIsLicensed: false) == "Request PKI Admin: %@") + } +} From 3159748421b475c8763faa022b2b5a0ad17a28e1 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Thu, 9 Jul 2026 22:07:53 -0700 Subject: [PATCH 2/3] Keep broadcast admin outside licensed remote policy --- .../Accessory Manager/AccessoryManager.swift | 1 + .../RemoteAdminTransportPolicyTests.swift | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift index a46696466..0e61874b4 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift @@ -532,6 +532,7 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate { guard case let .packet(packet) = data.payloadVariant, case let .decoded(decoded) = packet.payloadVariant, decoded.portnum == .adminApp, + packet.to != Constants.maximumNodeNum, packet.to != UInt32(truncatingIfNeeded: connectedDeviceNum) else { return data } diff --git a/MeshtasticTests/RemoteAdminTransportPolicyTests.swift b/MeshtasticTests/RemoteAdminTransportPolicyTests.swift index c7ff39a06..d1d107014 100644 --- a/MeshtasticTests/RemoteAdminTransportPolicyTests.swift +++ b/MeshtasticTests/RemoteAdminTransportPolicyTests.swift @@ -172,6 +172,22 @@ struct RemoteAdminTransportPolicyTests { #expect(packet.publicKey == Data([0x01, 0x02, 0x03])) } + @Test("Licensed broadcast Admin is not converted to remote Admin transport") + func licensedBroadcastAdminIsUnchanged() async throws { + let ownerNum: Int64 = 4_010_000_010 + _ = try user(num: ownerNum, isLicensed: true) + let connection = RecordingRemoteAdminConnection() + let manager = makeManager(connectedDeviceNum: ownerNum, connection: connection) + + try await manager.send(adminPacket(from: ownerNum, to: Int64(Constants.maximumNodeNum))) + + let packet = try await lastMeshPacket(from: connection) + #expect(packet.to == Constants.maximumNodeNum) + #expect(packet.channel == 7) + #expect(packet.pkiEncrypted) + #expect(packet.publicKey == Data([0x01, 0x02, 0x03])) + } + @Test("Unknown connected-owner mode does not assume licensed transport") func unknownOwnerModeIsUnchanged() async throws { let ownerNum: Int64 = 4_010_000_008 From ea84f7f0ce58f83f8e977261d16db8b854a22c4f Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Thu, 9 Jul 2026 22:37:35 -0700 Subject: [PATCH 3/3] Address signed admin review findings --- Meshtastic/Views/Settings/Settings.swift | 17 +++++++++++++---- .../RemoteAdminTransportPolicyTests.swift | 17 +++++++++++------ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 2f1faed03..e50edc019 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -12,8 +12,16 @@ import TipKit import MeshtasticProtobufs enum RemoteAdminWording { - static func activeFormat(localOwnerIsLicensed: Bool) -> String { - localOwnerIsLicensed ? "Remote Signed Admin: %@" : "Remote PKI Admin: %@" + struct ActivePresentation: Equatable { + let format: String + let systemImage: String + let representsVerifiedSignature: Bool + } + + static func activePresentation(localOwnerIsLicensed: Bool) -> ActivePresentation { + localOwnerIsLicensed + ? ActivePresentation(format: "Remote Signed Admin: %@", systemImage: "checkmark.shield", representsVerifiedSignature: true) + : ActivePresentation(format: "Remote PKI Admin: %@", systemImage: "av.remote", representsVerifiedSignature: false) } static func requestFormat(localOwnerIsLicensed: Bool) -> String { @@ -589,13 +597,14 @@ struct Settings: View { } .tag(Int(node.num)) } else if node.canRemoteAdmin && UserDefaults.enableAdministration && node.sessionPasskey != nil { + let presentation = RemoteAdminWording.activePresentation(localOwnerIsLicensed: localOwnerIsLicensed) Label { Text(String.localizedStringWithFormat( - RemoteAdminWording.activeFormat(localOwnerIsLicensed: localOwnerIsLicensed).localized, + presentation.format.localized, node.user?.longName ?? "Unknown".localized )) } icon: { - Image(systemName: localOwnerIsLicensed ? "checkmark.shield" : "av.remote") + Image(systemName: presentation.systemImage) } .font(.caption2) .tag(Int(node.num)) diff --git a/MeshtasticTests/RemoteAdminTransportPolicyTests.swift b/MeshtasticTests/RemoteAdminTransportPolicyTests.swift index d1d107014..96dc3a7f6 100644 --- a/MeshtasticTests/RemoteAdminTransportPolicyTests.swift +++ b/MeshtasticTests/RemoteAdminTransportPolicyTests.swift @@ -1,7 +1,4 @@ -// -// RemoteAdminTransportPolicyTests.swift -// MeshtasticTests -// +// MARK: RemoteAdminTransportPolicyTests import Foundation import MeshtasticProtobufs @@ -146,6 +143,7 @@ struct RemoteAdminTransportPolicyTests { let ownerNum: Int64 = 4_010_000_005 let remoteNum: Int64 = 4_010_000_006 _ = try user(num: ownerNum, isLicensed: false) + _ = try user(num: remoteNum, isLicensed: true) let connection = RecordingRemoteAdminConnection() let manager = makeManager(connectedDeviceNum: ownerNum, connection: connection) @@ -206,9 +204,16 @@ struct RemoteAdminTransportPolicyTests { @Test("Remote Admin wording distinguishes signed and PKI modes") func conditionalWording() { - #expect(RemoteAdminWording.activeFormat(localOwnerIsLicensed: true) == "Remote Signed Admin: %@") + let signed = RemoteAdminWording.activePresentation(localOwnerIsLicensed: true) + #expect(signed.format == "Remote Signed Admin: %@") + #expect(signed.systemImage == "checkmark.shield") + #expect(signed.representsVerifiedSignature) #expect(RemoteAdminWording.requestFormat(localOwnerIsLicensed: true) == "Request Signed Admin: %@") - #expect(RemoteAdminWording.activeFormat(localOwnerIsLicensed: false) == "Remote PKI Admin: %@") + + let pki = RemoteAdminWording.activePresentation(localOwnerIsLicensed: false) + #expect(pki.format == "Remote PKI Admin: %@") + #expect(pki.systemImage == "av.remote") + #expect(pki.representsVerifiedSignature == false) #expect(RemoteAdminWording.requestFormat(localOwnerIsLicensed: false) == "Request PKI Admin: %@") } }