field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

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 }