TeraRemoteQualificationTests.swift (13128B)
1 import CryptoKit 2 import RadrootsKit 3 @testable import TeraApp 4 import TeraKitBindings 5 import XCTest 6 7 final class TeraRemoteQualificationTests: XCTestCase { 8 func testQualificationIsOptInAndCarriesOnlyPublicInputs() throws { 9 XCTAssertNil(try TeraRemoteQualificationEnvironment.current(environment: [:])) 10 11 let value = try XCTUnwrap( 12 TeraRemoteQualificationEnvironment.current( 13 environment: [ 14 TeraRemoteQualificationEnvironment.enabledKey: "1", 15 TeraRemoteQualificationEnvironment.runIDKey: "gbrqv1-12345678", 16 TeraRemoteQualificationEnvironment.relayURLsKey: "wss://write.example", 17 TeraRemoteQualificationEnvironment.blossomOriginsKey: "https://media.example", 18 TeraRemoteQualificationEnvironment.mediaRelativePathKey: "qualification/input.png", 19 TeraRemoteQualificationEnvironment.networkProfileKey: "public", 20 ] 21 ) 22 ) 23 XCTAssertEqual(value.runID, "gbrqv1-12345678") 24 XCTAssertEqual(value.relayURLs, ["wss://write.example"]) 25 XCTAssertEqual(value.blossomOrigins, ["https://media.example"]) 26 XCTAssertEqual(value.networkMode, .publicEndpoint) 27 XCTAssertEqual(value.runtimeMode, "production") 28 XCTAssertFalse(value.networkMode.permitsAutomatedUserPresence) 29 XCTAssertFalse(value.networkMode.permitsTestSecretPolicy) 30 XCTAssertFalse(value.automatesIdentity) 31 XCTAssertEqual( 32 value.mediaFile, 33 RadrootsFileReference(scope: .data, relativePath: "qualification/input.png") 34 ) 35 XCTAssertEqual( 36 value.keychainServicePrefix, 37 "org.radroots.ios.remote-qualification.gbrqv1-12345678" 38 ) 39 XCTAssertEqual( 40 value.identityMetadataKeyPrefix, 41 "org.radroots.ios.remote-qualification.gbrqv1-12345678.identity" 42 ) 43 } 44 45 func testQualificationRejectsAmbiguousOrUnsafeHarnessValues() { 46 let base = [ 47 TeraRemoteQualificationEnvironment.enabledKey: "1", 48 TeraRemoteQualificationEnvironment.runIDKey: "gbrqv1-12345678", 49 TeraRemoteQualificationEnvironment.blossomOriginsKey: "https://media.example", 50 TeraRemoteQualificationEnvironment.networkProfileKey: "public", 51 ] 52 XCTAssertThrowsError( 53 try TeraRemoteQualificationEnvironment.current( 54 environment: base.merging([ 55 TeraRemoteQualificationEnvironment.runIDKey: "../../bad", 56 ]) { _, new in new } 57 ) 58 ) 59 for endpoint in [ 60 "ws://localhost:21000", 61 "ws://[::1]:21000", 62 "wss://127.0.0.1:21000", 63 "ws://127.0.0.1", 64 "ws://127.0.0.1:0", 65 "ws://user@127.0.0.1:21000", 66 "ws://127.0.0.1:21000/path", 67 "ws://127.0.0.1:21000?query=1", 68 ] { 69 XCTAssertThrowsError( 70 try TeraQualificationEndpoint( 71 endpoint, 72 role: .relay, 73 mode: .isolatedLoopback 74 ) 75 ) 76 } 77 XCTAssertThrowsError( 78 try TeraQualificationEndpoint( 79 "https://127.0.0.1:21100", 80 role: .blossom, 81 mode: .publicEndpoint 82 ) 83 ) 84 XCTAssertThrowsError( 85 try TeraRemoteQualificationEnvironment.current( 86 environment: base.merging([ 87 TeraRemoteQualificationEnvironment.networkProfileKey: "simulator", 88 TeraRemoteQualificationEnvironment.relayURLsKey: 89 "ws://127.0.0.1:21000,ws://127.0.0.1:21000", 90 TeraRemoteQualificationEnvironment.blossomOriginsKey: 91 "http://127.0.0.1:21100", 92 ]) { _, new in new } 93 ) 94 ) 95 XCTAssertThrowsError( 96 try TeraRemoteQualificationEnvironment.current( 97 environment: base.merging([ 98 TeraRemoteQualificationEnvironment.networkProfileKey: "automatic", 99 ]) { _, new in new } 100 ) 101 ) 102 103 let simulator = try? TeraRemoteQualificationEnvironment.current( 104 environment: base.merging([ 105 TeraRemoteQualificationEnvironment.networkProfileKey: "simulator", 106 TeraRemoteQualificationEnvironment.relayURLsKey: "ws://127.0.0.1:21000", 107 TeraRemoteQualificationEnvironment.blossomOriginsKey: 108 "http://127.0.0.1:21100", 109 ]) { _, new in new } 110 ) 111 XCTAssertEqual(simulator?.runtimeMode, "simulator") 112 XCTAssertEqual(simulator?.networkMode, .isolatedLoopback) 113 XCTAssertEqual(simulator?.automatesIdentity, true) 114 XCTAssertThrowsError( 115 try TeraRemoteQualificationEnvironment.current( 116 environment: base.merging([ 117 TeraRemoteQualificationEnvironment.networkProfileKey: "simulator", 118 TeraRemoteQualificationEnvironment.relayURLsKey: 119 "wss://relay.example", 120 TeraRemoteQualificationEnvironment.blossomOriginsKey: 121 "https://media.example", 122 ]) { _, new in new } 123 ) 124 ) 125 XCTAssertThrowsError( 126 try TeraRemoteQualificationEnvironment.current( 127 environment: base.merging([ 128 TeraRemoteQualificationEnvironment.blossomOriginsKey: 129 "https://one.example,https://two.example", 130 ]) { _, new in new } 131 ) 132 ) 133 XCTAssertThrowsError( 134 try TeraRemoteQualificationEnvironment.current( 135 environment: base.merging([ 136 TeraRemoteQualificationEnvironment.mediaRelativePathKey: 137 "../../private-key", 138 ]) { _, new in new } 139 ) 140 ) 141 } 142 143 func testQualificationUserPresenceIsNoninteractiveAndDebugOnly() async throws { 144 let presence = try TeraRemoteQualificationUserPresence(mode: .isolatedLoopback) 145 let request = try RadrootsUserPresenceRequest(reason: "Automated remote qualification") 146 let result = try await presence.verify(request) 147 let status = try await presence.currentStatus() 148 XCTAssertTrue(result.verified) 149 XCTAssertEqual(result.policy, .deviceOwnerAuthentication) 150 XCTAssertFalse(status.canEvaluateBiometrics) 151 } 152 153 func testPublicQualificationCannotSelectAutomatedPresence() throws { 154 let publicMode = TeraQualificationNetworkMode.publicEndpoint 155 XCTAssertFalse(publicMode.permitsAutomatedUserPresence) 156 XCTAssertFalse(publicMode.permitsTestSecretPolicy) 157 XCTAssertThrowsError( 158 try TeraRemoteQualificationUserPresence(mode: publicMode) 159 ) 160 } 161 162 func testQualificationModeRejectsMutationsWithoutRenderingInput() { 163 let canary = "private-key-canary" 164 for profile in [nil, "", "SIMULATOR", "public ", "automatic", canary] { 165 XCTAssertThrowsError(try TeraQualificationNetworkMode(profile: profile)) { error in 166 XCTAssertFalse(String(describing: error).contains(canary)) 167 XCTAssertFalse(error.localizedDescription.contains(canary)) 168 } 169 } 170 } 171 172 func testQualificationUsesRunScopedRootsAndBackgroundSession() throws { 173 let base = try RadrootsAppleFileRoots( 174 appIdentifier: "org.radroots.field-ios", 175 dataRoot: URL(fileURLWithPath: "/tmp/data", isDirectory: true), 176 cacheRoot: URL(fileURLWithPath: "/tmp/cache", isDirectory: true), 177 temporaryRoot: URL(fileURLWithPath: "/tmp/temporary", isDirectory: true) 178 ) 179 let qualification = try XCTUnwrap( 180 TeraRemoteQualificationEnvironment.current( 181 environment: [ 182 TeraRemoteQualificationEnvironment.enabledKey: "1", 183 TeraRemoteQualificationEnvironment.runIDKey: "persona-p01-12345678", 184 TeraRemoteQualificationEnvironment.relayURLsKey: 185 "ws://127.0.0.1:21000", 186 TeraRemoteQualificationEnvironment.blossomOriginsKey: 187 "http://127.0.0.1:21100", 188 TeraRemoteQualificationEnvironment.networkProfileKey: "simulator", 189 ] 190 ) 191 ) 192 let isolated = try qualification.isolatedFileRoots(from: base) 193 XCTAssertEqual(isolated.dataRoot.path, "/tmp/data/persona-p01-12345678") 194 XCTAssertEqual(isolated.cacheRoot.path, "/tmp/cache/persona-p01-12345678") 195 XCTAssertEqual(isolated.temporaryRoot.path, "/tmp/temporary/persona-p01-12345678") 196 XCTAssertEqual( 197 qualification.backgroundTransferIdentifierSuffix, 198 "remote-qualification.persona-p01-12345678" 199 ) 200 } 201 202 func testQualificationMediaFixtureHasPinnedDigest() throws { 203 let digest = try SHA256.hash( 204 data: TeraRemoteQualificationEnvironment.mediaFixtureData() 205 ) 206 XCTAssertEqual( 207 digest.map { String(format: "%02x", $0) }.joined(), 208 "431ced6916a2a21a156e38701afe55bbd7f88969fbbfc56d7fe099d47f265460" 209 ) 210 } 211 212 func testAuthorizationEvidenceIsRedactedEphemeralAndRelaunchCleaned() throws { 213 let root = FileManager.default.temporaryDirectory.appendingPathComponent( 214 UUID().uuidString, 215 isDirectory: true 216 ) 217 defer { try? FileManager.default.removeItem(at: root) } 218 let temporary = root.appendingPathComponent("temporary", isDirectory: true) 219 let documents = root.appendingPathComponent("documents", isDirectory: true) 220 try FileManager.default.createDirectory( 221 at: temporary, 222 withIntermediateDirectories: true 223 ) 224 try FileManager.default.createDirectory( 225 at: documents, 226 withIntermediateDirectories: true 227 ) 228 let store = TeraRemoteQualificationEvidenceStore( 229 runID: "persona-p01-12345678", 230 temporaryRoot: temporary, 231 documentsRoot: documents 232 ) 233 try Data("signed-authorization-canary".utf8).write( 234 to: store.legacyAuthorizationFile 235 ) 236 237 try store.prepare() 238 XCTAssertFalse(FileManager.default.fileExists(atPath: store.legacyAuthorizationFile.path)) 239 try store.record(request: qualificationSigningRequest(), signatureHex: String(repeating: "b", count: 128)) 240 241 let data = try Data(contentsOf: store.evidenceFile) 242 XCTAssertLessThanOrEqual( 243 data.count, 244 TeraRemoteQualificationAuthorizationEvidence.maximumEncodedBytes 245 ) 246 let object = try XCTUnwrap(JSONSerialization.jsonObject(with: data) as? [String: Any]) 247 XCTAssertEqual( 248 Set(object.keys), 249 Set(["schema", "schema_version", "authorization_digest_sha256"]) 250 ) 251 XCTAssertEqual( 252 object["authorization_digest_sha256"] as? String, 253 "fc3a74758a976c6f8dc37a4168520627de9ae8bc6a8433af3064fda94ea3e21b" 254 ) 255 let rendered = try XCTUnwrap(String(bytes: data, encoding: .utf8)) 256 for forbidden in [ 257 "signed-authorization-canary", "operation-canary", "artifact-canary", 258 "request-canary", "content-canary", String(repeating: "a", count: 64), 259 String(repeating: "b", count: 128), 260 ] { 261 XCTAssertFalse(rendered.contains(forbidden)) 262 } 263 let attributes = try FileManager.default.attributesOfItem(atPath: store.evidenceFile.path) 264 XCTAssertEqual((attributes[.posixPermissions] as? NSNumber)?.intValue, 0o600) 265 266 try store.prepare() 267 XCTAssertFalse(FileManager.default.fileExists(atPath: store.evidenceFile.path)) 268 XCTAssertFalse(FileManager.default.fileExists(atPath: store.directory.path)) 269 } 270 271 func testAuthorizationEvidenceDigestIsDomainSeparatedAndFieldSensitive() { 272 let request = qualificationSigningRequest() 273 let baseline = TeraRemoteQualificationAuthorizationEvidence( 274 runID: "persona-p01-12345678", 275 request: request, 276 signatureHex: String(repeating: "b", count: 128) 277 ) 278 let changedRun = TeraRemoteQualificationAuthorizationEvidence( 279 runID: "persona-p02-12345678", 280 request: request, 281 signatureHex: String(repeating: "b", count: 128) 282 ) 283 let changedSignature = TeraRemoteQualificationAuthorizationEvidence( 284 runID: "persona-p01-12345678", 285 request: request, 286 signatureHex: String(repeating: "c", count: 128) 287 ) 288 XCTAssertNotEqual(baseline.authorizationDigestSHA256, changedRun.authorizationDigestSHA256) 289 XCTAssertNotEqual( 290 baseline.authorizationDigestSHA256, 291 changedSignature.authorizationDigestSHA256 292 ) 293 XCTAssertTrue( 294 TeraRemoteQualificationAuthorizationEvidence.digestDomain.hasSuffix("\0") 295 ) 296 } 297 298 func testAuthorizationEvidenceWriteFailureFailsSigningResult() async { 299 let signer = TeraGeneratedHostSigner( 300 signer: QualificationSignedSigner(), 301 clock: .fixed(unixSeconds: 1_800_000_000), 302 qualificationEvidenceRecorder: { _, _ in 303 throw CocoaError(.fileWriteNoPermission) 304 } 305 ) 306 let result = await signer.sign(request: qualificationSigningRequest()) 307 XCTAssertEqual(result.outcome, .failed) 308 XCTAssertNil(result.signatureHex) 309 XCTAssertEqual(result.completedAtUnixMs, 0) 310 } 311 312 private func qualificationSigningRequest() -> HostSigningRequest { 313 HostSigningRequest( 314 schemaVersion: 1, 315 operationKind: "blossom-upload", 316 operationId: "operation-canary", 317 artifactId: "artifact-canary", 318 signerRequestId: "request-canary", 319 publicKey: String(repeating: "a", count: 64), 320 purpose: .blossomUpload, 321 deadlineUnixMs: 1_800_000_300_000, 322 eventIdDigest: Data(repeating: 0xAA, count: 32), 323 expectedEventId: String(repeating: "a", count: 64), 324 createdAtUnixS: 1_800_000_000, 325 kind: 24242, 326 tags: [["t", "content-canary"]], 327 content: "content-canary" 328 ) 329 } 330 } 331 332 private struct QualificationSignedSigner: TeraRuntimeSigner { 333 func availability() async -> TeraRuntimeSignerAvailability { 334 .ready 335 } 336 337 func sign(_: TeraRuntimeSigningRequest) async -> TeraRuntimeSigningOutcome { 338 .signed(signatureHex: String(repeating: "b", count: 128)) 339 } 340 }