iPhone Scanner-Fleet Test Plan¶
Use this plan when capturing deterministic physical iPhone evidence for the
QR Trust scanner experience. The plan follows the scanner-fleet evidence packet
in docs/public/network-contracts/examples/scanner-fleet-evidence-reference.json.
The goal is not to prove that the backend verifier works in isolation. The goal is to prove that the deployed scanner app turns issuer legitimacy, destination binding, runtime safety, and scanner-visible decision state into a clear green/orange/red user experience.
Preconditions¶
Run the native scanner smoke gate:
make smoke-ios
This verifies the iOS scanner contract and simulator build. It does not replace the physical-device evidence capture below.
Start the local stack with HTTPS and admin mode. If the repo-owned Postgres or Redis ports are already occupied, use the shared-infra target:
make up-https-admin-shared-infra FRONTEND_PUBLISH_PORT=5174
If this repo owns its own local ports, this is also valid:
make up-https-admin
Before opening Xcode, generate and validate the local iOS provider profile:
make ios-provider-config
make check-ios-provider-config
make iphone-evidence-preflight
The provider profile is a local lab build setting. End users do not paste verifier endpoints, API keys, or admin tokens into the iOS app.
If the Mac's Wi-Fi IP changes and Bonjour is blocked on the network, regenerate the local certificate and provider config before rebuilding the app:
scripts/create_local_https_certs.sh <new-mac-lan-ip>
make ios-provider-config
make check-ios-provider-config
docker compose restart api frontend
Then run the app on a physical iPhone from:
ios/VerifierLabApp/VerifierLabApp.xcodeproj
The Mac and iPhone must be on the same Wi-Fi. The iPhone must trust the local
mkcert root CA when the provider profile points at local HTTPS.
For verifier-profile fixtures, run the provider with the desired state, then refresh the provider profile from the iPhone Settings tab:
VERIFIER_PROVIDER_PROFILE_STATE=stale docker compose up -d --build api
VERIFIER_PROVIDER_PROFILE_STATE=revoked docker compose up -d --build api
This preserves the paper boundary because the state comes from the scanner-side provider profile, not from the QR payload. It also matches the production expectation: stale or revoked provider state must be refreshed or removed through app state, not by deleting and reinstalling the app.
Capture Packet¶
Create the ignored local handoff packet:
make iphone-evidence-packet
This writes:
local/iphone-evidence-packet/README.mdlocal/iphone-evidence-packet/required-artifacts.tsvlocal/iphone-evidence-packet/accessibility-templates/local/iphone-evidence-packet/incoming/
Use the packet README as the operator checklist while holding the phone. During capture, check progress with:
make iphone-evidence-status
Strict evidence checks are expected to fail until every referenced screenshot, history image, and accessibility text trace has been captured. The checks also reject non-PNG image files, placeholder accessibility text, and accessibility traces whose fixture, decision color, or decision state does not match the scanner-fleet packet.
App And Browser Setup¶
Use the browser lab on the laptop to generate QR artifacts:
https://<mac-lan-ip-or-bonjour-name>:5174/
Use the iPhone app only to scan those QR codes and display the user-facing result. Do not use the old developer iOS harness for this evidence packet.
Important capture rules:
- Do not click the browser lab's
Check scanner decisionaction before the phone scan. That can consume one-time QR state before the phone captures it. - For one-time replay evidence, scan the same generated QR twice without regenerating it.
- For reusable-public evidence, repeated scans of the same QR should remain green until issuer, destination, runtime, or profile state changes.
- For orange and red outcomes, capture the result screen and history entry before opening any destination.
- Red outcomes must not be opened.
- Orange outcomes may expose an open-with-caution path, but evidence should show that the scanner did not silently open the destination.
Fixture Matrix¶
Each fixture requires three artifacts:
- result screenshot
- matching History tab screenshot
- accessibility text trace for the result screen
Use the exact filenames below. The importer copies them into
docs/public/evidence/iphone/ according to the scanner-fleet packet.
| Fixture | Browser lab setup | Expected iPhone result | Required artifact basenames |
|---|---|---|---|
green_reusable_public |
Scenario valid, Usage policy reusable_public, Nonce timestamped |
Green verified_issuer; repeated scans stay valid |
accepted-reusable-public.png, history-accepted-reusable-public.png, accessibility-accepted-reusable-public.txt |
green_one_time_first_pass |
Scenario valid, Usage policy one_time, Nonce fixed; first scan only |
Green accepted |
accepted-one-time-first-pass.png, history-accepted-one-time-first-pass.png, accessibility-accepted-one-time-first-pass.txt |
red_one_time_replay |
Reuse the exact QR from green_one_time_first_pass; scan it a second time |
Red one_time_replay |
replay-guard.png, history-replay-guard.png, accessibility-replay-guard.txt |
red_expired_qr |
Scenario expired, Usage policy reusable_public, Nonce fixed |
Red expired |
expired.png, history-expired.png, accessibility-expired.txt |
red_destination_mismatch |
Scenario payload-mismatch, Usage policy reusable_public, Nonce timestamped |
Red destination_policy_mismatch |
payload-mismatch.png, history-payload-mismatch.png, accessibility-payload-mismatch.txt |
red_resolver_final_target_mismatch |
Scenario redirect-final-mismatch, Usage policy reusable_public, Nonce timestamped |
Red resolver_final_target_mismatch |
resolver-final-target-mismatch.png, history-resolver-final-target-mismatch.png, accessibility-resolver-final-target-mismatch.txt |
orange_plain_url_unrecognized |
Scan a normal public URL QR that was not generated by QR Trust, for example a Wikipedia URL | Orange plain_url_unrecognized |
plain-url-unrecognized.png, history-plain-url-unrecognized.png, accessibility-plain-url-unrecognized.txt |
orange_verifier_unavailable_visible_destination |
Generate Scenario valid, Usage reusable_public, then make the provider unreachable before the phone scan |
Orange verifier_unavailable_visible_destination; visible destination, no positive trust claim |
verifier-unavailable-visible-destination.png, history-verifier-unavailable-visible-destination.png, accessibility-verifier-unavailable-visible-destination.txt |
orange_stale_verifier_profile |
Run the provider with VERIFIER_PROVIDER_PROFILE_STATE=stale, open iOS Settings, tap Refresh provider profile, then scan a QR Trust URL-bearing fixture. Do not substitute the browser lab stale-cache scenario unless the artifact is explicitly marked as a cache-staleness implementation gap. |
Orange profile_stale |
profile-stale.png, history-profile-stale.png, accessibility-profile-stale.txt |
red_revoked_verifier_profile |
Run the provider with VERIFIER_PROVIDER_PROFILE_STATE=revoked, open iOS Settings, tap Refresh provider profile, then scan a QR Trust URL-bearing fixture. Do not substitute the browser lab revoked scenario; that is issuer-certificate revocation, not verifier-profile revocation. |
Red profile_revoked |
profile-revoked.png, history-profile-revoked.png, accessibility-profile-revoked.txt |
If a fixture cannot be produced from the current app or lab controls, record it as an implementation gap. Do not substitute a different screenshot under the required filename.
Accessibility Trace¶
The accessibility file is plain text. It should include the user-facing labels that a reviewer or VoiceOver pass would encounter on the result screen.
Minimal format:
fixture_id: red_destination_mismatch
decision_state: destination_policy_mismatch
decision_color: red
screen: Scan result
status: Red status
title: Do not open
message: The destination changed or is outside the issuer-approved policy.
destination: evi...ple.example
actions:
- View decision path
- Scan another QR
notes: Red outcome does not expose a silent open path.
Do not include API keys, admin tokens, signing keys, or raw secrets.
The capture packet generates one starter accessibility template per fixture.
Copy a template into the incoming folder only after replacing every REPLACE
placeholder with observed app labels.
Import And Validate¶
Copy the exported files into:
local/iphone-evidence-packet/incoming/
Then import and validate:
make import-iphone-evidence IPHONE_EVIDENCE_SOURCE_DIR=local/iphone-evidence-packet/incoming
git add docs/public/evidence/iphone
make iphone-evidence-status
make check-iphone-evidence
make release-audit
make release-audit-strict
Set IPHONE_EVIDENCE_OVERWRITE=true only when intentionally replacing an
existing captured artifact:
IPHONE_EVIDENCE_OVERWRITE=true make import-iphone-evidence IPHONE_EVIDENCE_SOURCE_DIR=local/iphone-evidence-packet/incoming
Acceptance Criteria¶
The evidence package is complete when:
- every fixture in the scanner-fleet evidence packet has result, history, and accessibility artifacts
- green reusable-public scans remain reusable
- one-time replay blocks only after the first accepted one-time scan
- plain non-QR-Trust URLs are orange, not red, unless another risk reason applies
- verifier-unavailable scans do not become green
- red decisions do not open
- orange and red decisions show user friction and reason codes
make check-iphone-evidencepassesmake release-audit-strictpasses
This evidence proves the scanner UX consumed managed trust state correctly at scan time. It does not prove that the root program, delegated authority, issuer enrollment, or destination policy is production-governed.