commit 02a6aabad7f01ffe6cfa8b1976521d94bbabec39
parent d1c36a8617f5893351408aee3f7fa840761a4615
Author: triesap <tyson@radroots.org>
Date: Mon, 31 Aug 2026 18:43:09 +0000
ios: isolate qualification identity authority
- Add a closed typed network mode for loopback and public qualification.
- Permit automated presence and test secret policy only for isolated loopback.
- Keep public qualification on normal Apple presence without automatic unlock.
- Cover mode mutations, redaction, and package-boundary source guards.
Diffstat:
6 files changed, 97 insertions(+), 16 deletions(-)
diff --git a/README.md b/README.md
@@ -75,7 +75,10 @@ cargo extbuild run -- scripts/xcode.sh local-social-ui-test \
This harness is Debug-only, binds only explicit loopback ports, records bounded
protocol evidence under the extbuild results root, and cannot become a
-production endpoint fallback.
+production endpoint fallback. Its typed isolated-loopback mode is the only
+mode permitted to use automated user presence or test secret policy. Public
+and physical qualification retain normal Apple user presence, remain optional,
+and are not claimed by the deterministic simulator lane.
Passing `accessibility` as the final launcher argument runs Apple's
accessibility audit over every progressively disclosed Add composition at the
diff --git a/Radroots/App/RadrootsRemoteQualification.swift b/Radroots/App/RadrootsRemoteQualification.swift
@@ -2,6 +2,35 @@ import Foundation
import RadrootsKit
import RadrootsKitBindings
+enum RadrootsQualificationNetworkMode: Sendable, Equatable {
+ case isolatedLoopback
+ case publicEndpoint
+
+ init(profile: String?) throws {
+ switch profile {
+ case "simulator": self = .isolatedLoopback
+ case "public": self = .publicEndpoint
+ default:
+ throw RadrootsConfigurationError.invalid("qualification_network_profile")
+ }
+ }
+
+ var runtimeMode: String {
+ switch self {
+ case .isolatedLoopback: "simulator"
+ case .publicEndpoint: "production"
+ }
+ }
+
+ var permitsAutomatedUserPresence: Bool {
+ self == .isolatedLoopback
+ }
+
+ var permitsTestSecretPolicy: Bool {
+ self == .isolatedLoopback
+ }
+}
+
struct RadrootsRemoteQualificationEnvironment: Sendable, Equatable {
static let enabledKey = "RADROOTS_IOS_UI_TEST_REMOTE"
static let runIDKey = "RADROOTS_IOS_UI_TEST_RUN_ID"
@@ -14,7 +43,15 @@ struct RadrootsRemoteQualificationEnvironment: Sendable, Equatable {
let relayURLs: [String]
let blossomOrigins: [String]
let mediaFile: RadrootsFileReference?
- let runtimeMode: String
+ let networkMode: RadrootsQualificationNetworkMode
+
+ var runtimeMode: String {
+ networkMode.runtimeMode
+ }
+
+ var automatesIdentity: Bool {
+ networkMode.permitsAutomatedUserPresence && networkMode.permitsTestSecretPolicy
+ }
private static let mediaFixtureBase64 =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
@@ -75,17 +112,13 @@ struct RadrootsRemoteQualificationEnvironment: Sendable, Equatable {
let runID = try requiredRunID(environment[runIDKey])
let relays = separatedValues(environment[relayURLsKey])
let blossoms = separatedValues(environment[blossomOriginsKey])
- let runtimeMode: String
- switch environment[networkProfileKey] {
- case "public": runtimeMode = "production"
- case "simulator": runtimeMode = "simulator"
- default:
- throw RadrootsConfigurationError.invalid("qualification_network_profile")
- }
+ let networkMode = try RadrootsQualificationNetworkMode(
+ profile: environment[networkProfileKey]
+ )
guard !relays.isEmpty, blossoms.count == 1 else {
throw RadrootsConfigurationError.invalid("qualification_blossom_origin")
}
- if runtimeMode == "simulator" {
+ if networkMode == .isolatedLoopback {
guard
relays.allSatisfy({ isLoopbackEndpoint($0, schemes: ["ws"]) }),
blossoms.allSatisfy({ isLoopbackEndpoint($0, schemes: ["http"]) })
@@ -104,7 +137,7 @@ struct RadrootsRemoteQualificationEnvironment: Sendable, Equatable {
relayURLs: relays,
blossomOrigins: blossoms,
mediaFile: mediaFile,
- runtimeMode: runtimeMode
+ networkMode: networkMode
)
}
@@ -218,6 +251,12 @@ struct RadrootsRemoteQualificationEnvironment: Sendable, Equatable {
}
final class RadrootsRemoteQualificationUserPresence: RadrootsUserPresence, Sendable {
+ init(mode: RadrootsQualificationNetworkMode) throws {
+ guard mode.permitsAutomatedUserPresence, mode.permitsTestSecretPolicy else {
+ throw RadrootsConfigurationError.invalid("qualification_user_presence")
+ }
+ }
+
func currentStatus() async throws -> RadrootsUserPresenceStatus {
RadrootsUserPresenceStatus(
support: .deviceCredential,
diff --git a/Radroots/State/RadrootsIdentityStore.swift b/Radroots/State/RadrootsIdentityStore.swift
@@ -87,12 +87,14 @@ actor RadrootsIdentityStore {
let configuration: RadrootsIdentityCustodyConfiguration
let userPresence: any RadrootsUserPresence
#if DEBUG
- if qualification != nil {
+ if let qualification, qualification.automatesIdentity {
configuration = try RadrootsIdentityCustodyConfiguration(
namespace: namespace,
secretPolicy: .secureLocalSecret
)
- userPresence = RadrootsRemoteQualificationUserPresence()
+ userPresence = try RadrootsRemoteQualificationUserPresence(
+ mode: qualification.networkMode
+ )
} else {
configuration = try RadrootsIdentityCustodyConfiguration(namespace: namespace)
userPresence = RadrootsAppleUserPresence()
diff --git a/Radroots/State/RadrootsSessionStore.swift b/Radroots/State/RadrootsSessionStore.swift
@@ -112,7 +112,7 @@ actor RadrootsSessionStore {
runtimeClient: runtimeClient,
roots: roots,
protectedData: protectedData,
- automatesQualificationIdentity: qualification != nil
+ automatesQualificationIdentity: qualification?.automatesIdentity == true
)
}
diff --git a/RadrootsTests/RadrootsRemoteQualificationTests.swift b/RadrootsTests/RadrootsRemoteQualificationTests.swift
@@ -22,7 +22,11 @@ final class RadrootsRemoteQualificationTests: XCTestCase {
XCTAssertEqual(value.runID, "gbrqv1-12345678")
XCTAssertEqual(value.relayURLs, ["wss://write.example"])
XCTAssertEqual(value.blossomOrigins, ["https://media.example"])
+ XCTAssertEqual(value.networkMode, .publicEndpoint)
XCTAssertEqual(value.runtimeMode, "production")
+ XCTAssertFalse(value.networkMode.permitsAutomatedUserPresence)
+ XCTAssertFalse(value.networkMode.permitsTestSecretPolicy)
+ XCTAssertFalse(value.automatesIdentity)
XCTAssertEqual(
value.mediaFile,
RadrootsFileReference(scope: .data, relativePath: "qualification/input.png")
@@ -68,6 +72,8 @@ final class RadrootsRemoteQualificationTests: XCTestCase {
]) { _, new in new }
)
XCTAssertEqual(simulator?.runtimeMode, "simulator")
+ XCTAssertEqual(simulator?.networkMode, .isolatedLoopback)
+ XCTAssertEqual(simulator?.automatesIdentity, true)
XCTAssertThrowsError(
try RadrootsRemoteQualificationEnvironment.current(
environment: base.merging([
@@ -98,7 +104,7 @@ final class RadrootsRemoteQualificationTests: XCTestCase {
}
func testQualificationUserPresenceIsNoninteractiveAndDebugOnly() async throws {
- let presence = RadrootsRemoteQualificationUserPresence()
+ let presence = try RadrootsRemoteQualificationUserPresence(mode: .isolatedLoopback)
let request = try RadrootsUserPresenceRequest(reason: "Automated remote qualification")
let result = try await presence.verify(request)
let status = try await presence.currentStatus()
@@ -107,6 +113,25 @@ final class RadrootsRemoteQualificationTests: XCTestCase {
XCTAssertFalse(status.canEvaluateBiometrics)
}
+ func testPublicQualificationCannotSelectAutomatedPresence() throws {
+ let publicMode = RadrootsQualificationNetworkMode.publicEndpoint
+ XCTAssertFalse(publicMode.permitsAutomatedUserPresence)
+ XCTAssertFalse(publicMode.permitsTestSecretPolicy)
+ XCTAssertThrowsError(
+ try RadrootsRemoteQualificationUserPresence(mode: publicMode)
+ )
+ }
+
+ func testQualificationModeRejectsMutationsWithoutRenderingInput() {
+ let canary = "private-key-canary"
+ for profile in [nil, "", "SIMULATOR", "public ", "automatic", canary] {
+ XCTAssertThrowsError(try RadrootsQualificationNetworkMode(profile: profile)) { error in
+ XCTAssertFalse(String(describing: error).contains(canary))
+ XCTAssertFalse(error.localizedDescription.contains(canary))
+ }
+ }
+ }
+
func testQualificationUsesRunScopedRootsAndBackgroundSession() throws {
let base = try RadrootsAppleFileRoots(
appIdentifier: "org.radroots.field-ios",
diff --git a/scripts/verify-package-contract.sh b/scripts/verify-package-contract.sh
@@ -130,8 +130,20 @@ grep -Fq -- '-UIAccessibilityReduceMotionEnabled' \
"$repo_root/RadrootsUITests/RadrootsRemoteQualificationUITests.swift"
grep -Fq 'accessibility-extra-extra-extra-large' "$repo_root/scripts/xcode.sh"
grep -Fq '#if DEBUG' "$repo_root/Radroots/App/RadrootsRemoteQualification.swift"
-grep -Fq 'case "simulator": runtimeMode = "simulator"' \
+grep -Fq 'case "simulator": self = .isolatedLoopback' \
"$repo_root/Radroots/App/RadrootsRemoteQualification.swift"
+grep -Fq 'case "public": self = .publicEndpoint' \
+ "$repo_root/Radroots/App/RadrootsRemoteQualification.swift"
+grep -Fq 'if let qualification, qualification.automatesIdentity' \
+ "$repo_root/Radroots/State/RadrootsIdentityStore.swift"
+grep -Fq 'qualification?.automatesIdentity == true' \
+ "$repo_root/Radroots/State/RadrootsSessionStore.swift"
+if rg -n 'if qualification != nil|automatesQualificationIdentity: qualification != nil' \
+ "$repo_root/Radroots" --glob '*.swift'
+then
+ echo "error: qualification existence must not grant automated identity authority" >&2
+ exit 1
+fi
test -f "$repo_root/scripts/local-social-fixture.py"
test -f "$repo_root/.swiftformat"
test -f "$repo_root/.swiftlint.yml"