field_ios

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

local-social-fixture.py (100862B)


      1 #!/usr/bin/env python3
      2 """Bounded loopback Nostr and Blossom fixture for iOS simulator tests."""
      3 
      4 from __future__ import annotations
      5 
      6 import argparse
      7 import base64
      8 import hashlib
      9 import http.server
     10 import importlib.metadata
     11 import ipaddress
     12 import json
     13 import os
     14 import re
     15 import signal
     16 import socket
     17 import socketserver
     18 import stat
     19 import subprocess
     20 import struct
     21 import sys
     22 import tempfile
     23 import threading
     24 import time
     25 import unicodedata
     26 from pathlib import Path
     27 from typing import Any
     28 
     29 from jsonschema import Draft202012Validator
     30 from jsonschema.exceptions import SchemaError, ValidationError
     31 
     32 MAX_HTTP_BODY = 16 * 1024 * 1024
     33 MAX_WEBSOCKET_MESSAGE = 2 * 1024 * 1024
     34 MAX_EVENTS = 256
     35 MAX_BLOBS = 16
     36 MAX_JSON_BYTES = 64 * 1024
     37 BUD11_EVENT_KIND = 24_242
     38 BUD11_CONTENT_MAX_BYTES = 4_096
     39 BUD11_AUTHORIZATION_MAX_BYTES = 16 * 1024
     40 BUD11_AUTHORIZATION_ENCODED_MAX_BYTES = 21_846
     41 BUD11_MAX_LIFETIME_SECONDS = 300
     42 BUD11_MAX_CREATED_AGE_SECONDS = 300
     43 BUD11_SERVER_DOMAIN = "127.0.0.1"
     44 BUD11_MUTATION_SCHEMA = "tera.ios.local-social.bud11-mutations.v1"
     45 BUD11_MUTATIONS = (
     46     ("canonical", "none", "http", True),
     47     ("wrong-scheme", "authorization_scheme", "http", False),
     48     ("padded-base64", "padded_base64", "http", False),
     49     ("wrong-kind", "wrong_kind", "http", False),
     50     ("empty-content", "empty_content", "http", False),
     51     ("oversized-content", "oversized_content", "http", False),
     52     ("leading-content-space", "leading_content_space", "http", False),
     53     ("control-content", "control_content", "http", False),
     54     ("missing-action", "missing_action", "http", False),
     55     ("wrong-action", "wrong_action", "http", False),
     56     ("duplicate-action", "duplicate_action", "http", False),
     57     ("wrong-hash", "wrong_hash", "http", False),
     58     ("duplicate-hash", "duplicate_hash", "http", False),
     59     ("missing-server", "missing_server", "http", False),
     60     ("wrong-server", "wrong_server", "http", False),
     61     ("uppercase-server", "uppercase_server", "http", False),
     62     ("duplicate-server", "duplicate_server", "http", False),
     63     ("missing-expiration", "missing_expiration", "http", False),
     64     ("noncanonical-expiration", "noncanonical_expiration", "http", False),
     65     ("expired", "expired", "http", False),
     66     ("created-at-not-past", "created_at_not_past", "http", False),
     67     ("lifetime-too-long", "lifetime_too_long", "http", False),
     68     ("unknown-tag", "unknown_tag", "http", False),
     69     ("event-id-mutation", "event_id", "http", False),
     70     ("signature-mutation", "signature", "http", False),
     71     ("relay-publication", "none", "relay", False),
     72 )
     73 PERSONA_ALIASES = ("P01", "P02", "P03", "P04", "P05")
     74 FLOW_KINDS = {
     75     "Update": 1,
     76     "PhotoUpdate": 1,
     77     "Ask": 1,
     78     "Event": 31923,
     79     "FoodAvailability": 30402,
     80 }
     81 PHOTO_PERSONAS = frozenset(("P01", "P03", "P04"))
     82 PERSONA_CONTROL_SCHEMA = "tera.ios.local-social.persona-control.v1"
     83 PERSONA_ATTEMPT_SCHEMA = "tera.ios.local-social.persona-attempt-evidence.v1"
     84 PERSONA_RESULT_V2_SCHEMA = "tera.ios.local-social.persona-results.v2"
     85 PERSONA_TEST_TARGET = "TeraUITests"
     86 PERSONA_TEST_IDENTIFIER = "TeraUITests/testLocalSocialDeterministicPersonas"
     87 PERSONA_TEST_ACTION = "test"
     88 PERSONA_TEST_CONFIGURATION = "Debug"
     89 PERSONA_XCRESULT_NODE_IDENTIFIER = (
     90     "TeraRemoteQualificationUITests/testLocalSocialDeterministicPersonas()"
     91 )
     92 PERSONA_XCRESULT_NODE_URL = (
     93     "test://com.apple.xcode/Tera/TeraUITests/"
     94     "TeraRemoteQualificationUITests/testLocalSocialDeterministicPersonas"
     95 )
     96 MAX_XCRESULT_JSON_BYTES = 1024 * 1024
     97 MAX_PERSONA_ATTACHMENT_BYTES = 64 * 1024
     98 MAX_PERSONA_ATTACHMENTS_BYTES = 15 * MAX_PERSONA_ATTACHMENT_BYTES
     99 MAX_RESULT_BUNDLE_ENTRIES = 65_536
    100 MAX_RESULT_BUNDLE_FILE_BYTES = 1024 * 1024 * 1024
    101 MAX_RESULT_BUNDLE_BYTES = 8 * 1024 * 1024 * 1024
    102 MAX_RESULT_BUNDLE_RELATIVE_PATH_BYTES = 1024
    103 RESULT_BUNDLE_DIGEST_DOMAIN = b"radroots.ios.persona_result_bundle.v1\0"
    104 VERIFIER_PYTHON = (3, 14, 7)
    105 VERIFIER_JSONSCHEMA = "4.26.0"
    106 PERSONA_ATTACHMENT_NAMES = tuple(
    107     f"radroots-local-social-P{persona:02d}-A{attempt:02d}.json"
    108     for persona in range(1, 6)
    109     for attempt in range(1, 4)
    110 )
    111 PERSONA_XCRESULT_ATTACHMENT_NAME = re.compile(
    112     r"^radroots-local-social-(P0[1-5]-A0[1-3])_0_"
    113     r"[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}\.json$"
    114 )
    115 FORBIDDEN_EVIDENCE_KEYS = frozenset(
    116     ("private_key", "secret", "seed", "signed_event", "event_content", "raw_event")
    117 )
    118 
    119 
    120 def strict_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
    121     value: dict[str, Any] = {}
    122     for key, item in pairs:
    123         if key in value:
    124             raise ValueError("duplicate JSON member")
    125         value[key] = item
    126     return value
    127 
    128 
    129 def read_json(path: Path, maximum: int = MAX_JSON_BYTES) -> tuple[bytes, Any]:
    130     with path.open("rb") as stream:
    131         raw = stream.read(maximum + 1)
    132     if not raw or len(raw) > maximum:
    133         raise ValueError("JSON input is empty or exceeds its byte bound")
    134     value = json.loads(raw, object_pairs_hook=strict_object)
    135     return raw, value
    136 
    137 
    138 def read_json_bounded(path: Path, maximum: int) -> tuple[bytes, Any]:
    139     return read_json(path, maximum)
    140 
    141 
    142 def exact_keys(value: Any, keys: set[str], name: str) -> dict[str, Any]:
    143     if not isinstance(value, dict) or set(value) != keys:
    144         raise ValueError(f"{name} has an invalid field inventory")
    145     return value
    146 
    147 
    148 def validate_persona_suite(value: Any) -> dict[str, Any]:
    149     root = exact_keys(
    150         value,
    151         {"schema", "schema_version", "locale", "media_fixture_sha256", "personas"},
    152         "persona suite",
    153     )
    154     if (
    155         root["schema"] != "tera.ios.local-social.personas.v1"
    156         or root["schema_version"] != 1
    157         or root["locale"] != "en_US"
    158         or not lowercase_hex(root["media_fixture_sha256"], 64)
    159         or not isinstance(root["personas"], list)
    160         or len(root["personas"]) != 5
    161     ):
    162         raise ValueError("persona suite header is invalid")
    163     expected = (
    164         ("P01", "age_18_27", "experienced_direct", ("Update", "PhotoUpdate", "Ask")),
    165         (
    166             "P02",
    167             "age_18_27",
    168             "novice_progressive_disclosure",
    169             ("Event", "FoodAvailability", "Update"),
    170         ),
    171         (
    172             "P03",
    173             "age_18_27",
    174             "nontechnical_validation_recovery",
    175             ("PhotoUpdate", "Ask", "Event"),
    176         ),
    177         (
    178             "P04",
    179             "adult_other",
    180             "novice_accessibility_keyboard",
    181             ("FoodAvailability", "Update", "PhotoUpdate"),
    182         ),
    183         (
    184             "P05",
    185             "adult_other",
    186             "general_transport_retry_relaunch",
    187             ("Ask", "Event", "FoodAvailability"),
    188         ),
    189     )
    190     attempts: list[dict[str, Any]] = []
    191     for persona_index, (persona_value, expected_value) in enumerate(
    192         zip(root["personas"], expected, strict=True), 1
    193     ):
    194         persona = exact_keys(
    195             persona_value,
    196             {"alias", "synthetic_age_band", "interaction_profile", "attempts"},
    197             "persona",
    198         )
    199         alias, age_band, profile, flows = expected_value
    200         if (
    201             persona["alias"] != alias
    202             or persona["synthetic_age_band"] != age_band
    203             or persona["interaction_profile"] != profile
    204             or not isinstance(persona["attempts"], list)
    205             or len(persona["attempts"]) != 3
    206         ):
    207             raise ValueError("persona matrix is not exact")
    208         for attempt_index, (attempt_value, flow) in enumerate(
    209             zip(persona["attempts"], flows, strict=True), 1
    210         ):
    211             attempt = exact_keys(
    212                 attempt_value,
    213                 {"id", "order", "flow", "marker", "expected_failure"},
    214                 "attempt",
    215             )
    216             expected_id = f"{alias}-A{attempt_index:02d}"
    217             expected_order = (persona_index - 1) * 3 + attempt_index
    218             if (
    219                 attempt["id"] != expected_id
    220                 or attempt["order"] != expected_order
    221                 or attempt["flow"] != flow
    222                 or not isinstance(attempt["marker"], str)
    223                 or not 1 <= len(attempt["marker"].encode("ascii")) <= 24
    224                 or attempt["marker"]
    225                 != f"rr-{alias.lower()}-a{attempt_index:02d}-{flow_marker(flow)}"
    226                 or attempt["expected_failure"]
    227                 not in {"none", "validation_recovery", "transport_retry_relaunch"}
    228             ):
    229                 raise ValueError("persona attempt is not exact")
    230             attempts.append(attempt)
    231     if (
    232         [item["expected_failure"] for item in attempts].count("validation_recovery")
    233         != 1
    234     ):
    235         raise ValueError("persona suite must contain one validation-recovery vector")
    236     if (
    237         [item["expected_failure"] for item in attempts].count(
    238             "transport_retry_relaunch"
    239         )
    240         != 1
    241     ):
    242         raise ValueError("persona suite must contain one transport-retry vector")
    243     if any(
    244         sum(item["flow"] == flow for item in attempts) != 3 for flow in FLOW_KINDS
    245     ):
    246         raise ValueError("each Add flow must have exactly three attempts")
    247     return root
    248 
    249 
    250 def validate_bud11_mutation_corpus(value: Any) -> dict[str, Any]:
    251     root = exact_keys(value, {"schema", "schema_version", "vectors"}, "BUD-11 corpus")
    252     if (
    253         root["schema"] != BUD11_MUTATION_SCHEMA
    254         or root["schema_version"] != 1
    255         or not isinstance(root["vectors"], list)
    256     ):
    257         raise ValueError("BUD-11 corpus header is invalid")
    258     observed = []
    259     for value in root["vectors"]:
    260         vector = exact_keys(
    261             value,
    262             {"id", "mutation", "surface", "expected_accepted"},
    263             "BUD-11 vector",
    264         )
    265         if (
    266             not isinstance(vector["id"], str)
    267             or not isinstance(vector["mutation"], str)
    268             or vector["surface"] not in {"http", "relay"}
    269             or type(vector["expected_accepted"]) is not bool
    270         ):
    271             raise ValueError("BUD-11 vector is invalid")
    272         observed.append(
    273             (
    274                 vector["id"],
    275                 vector["mutation"],
    276                 vector["surface"],
    277                 vector["expected_accepted"],
    278             )
    279         )
    280     if tuple(observed) != BUD11_MUTATIONS:
    281         raise ValueError("BUD-11 corpus inventory is invalid")
    282     return root
    283 
    284 
    285 def load_bud11_mutation_corpus(path: Path) -> tuple[bytes, dict[str, Any]]:
    286     raw, value = read_json(path)
    287     corpus = validate_bud11_mutation_corpus(value)
    288     canonical = (
    289         "{\n"
    290         f'  "schema": {json.dumps(value.get("schema"))},\n'
    291         f'  "schema_version": {json.dumps(value.get("schema_version"))},\n'
    292         '  "vectors": [\n'
    293         + ",\n".join(
    294             "    " + json.dumps(vector, ensure_ascii=False)
    295             for vector in value.get("vectors", [])
    296         )
    297         + "\n  ]\n}\n"
    298     ).encode("utf-8")
    299     if raw != canonical:
    300         raise ValueError("BUD-11 corpus is noncanonical")
    301     return raw, corpus
    302 
    303 
    304 def flow_marker(flow: str) -> str:
    305     return {
    306         "Update": "update",
    307         "PhotoUpdate": "photo",
    308         "Ask": "ask",
    309         "Event": "event",
    310         "FoodAvailability": "food",
    311     }[flow]
    312 
    313 
    314 def load_persona_suite(path: Path) -> tuple[bytes, dict[str, Any]]:
    315     raw, value = read_json(path)
    316     return raw, validate_persona_suite(value)
    317 
    318 
    319 def validate_schema_file(path: Path, expected_id: str) -> bytes:
    320     raw, _ = load_schema_file(path, expected_id)
    321     return raw
    322 
    323 
    324 def load_schema_file(path: Path, expected_id: str) -> tuple[bytes, dict[str, Any]]:
    325     raw, value = read_json(path)
    326     if not isinstance(value, dict):
    327         raise ValueError("schema boundary is invalid")
    328     root = exact_keys(value, set(value), "schema")
    329     if (
    330         root.get("$schema") != "https://json-schema.org/draft/2020-12/schema"
    331         or root.get("$id") != expected_id
    332         or root.get("type") != "object"
    333         or root.get("additionalProperties") is not False
    334     ):
    335         raise ValueError("schema boundary is invalid")
    336     if schema_contains_external_reference(root):
    337         raise ValueError("schema contains an external reference")
    338     try:
    339         Draft202012Validator.check_schema(root)
    340     except SchemaError as error:
    341         raise ValueError("schema fails Draft 2020-12 meta-validation") from error
    342     return raw, root
    343 
    344 
    345 def schema_contains_external_reference(value: Any) -> bool:
    346     if isinstance(value, dict):
    347         for key, item in value.items():
    348             if key in {"$ref", "$dynamicRef"} and (
    349                 not isinstance(item, str) or not item.startswith("#/")
    350             ):
    351                 return True
    352             if schema_contains_external_reference(item):
    353                 return True
    354     elif isinstance(value, list):
    355         return any(schema_contains_external_reference(item) for item in value)
    356     return False
    357 
    358 
    359 def validate_schema_instance(
    360     schema: dict[str, Any], value: Any, name: str
    361 ) -> None:
    362     try:
    363         Draft202012Validator(schema).validate(value)
    364     except ValidationError as error:
    365         raise ValueError(f"{name} disagrees with its JSON schema") from error
    366 
    367 
    368 def verify_toolchain_identity() -> None:
    369     if sys.version_info[:3] != VERIFIER_PYTHON:
    370         raise RuntimeError("persona verifier Python identity is unavailable")
    371     try:
    372         schema_version = importlib.metadata.version("jsonschema")
    373     except importlib.metadata.PackageNotFoundError as error:
    374         raise RuntimeError(
    375             "persona verifier schema dependency is unavailable"
    376         ) from error
    377     if schema_version != VERIFIER_JSONSCHEMA:
    378         raise RuntimeError("persona verifier schema dependency is unavailable")
    379 
    380 
    381 def persona_attempts(suite: dict[str, Any]) -> dict[str, dict[str, Any]]:
    382     return {
    383         attempt["id"]: {**attempt, "persona": persona["alias"]}
    384         for persona in suite["personas"]
    385         for attempt in persona["attempts"]
    386     }
    387 
    388 
    389 def read_control(path: Path) -> dict[str, Any] | None:
    390     if not path.is_file():
    391         return None
    392     try:
    393         _, value = read_json(path, 1024)
    394         control = exact_keys(
    395             value,
    396             {"schema", "active_persona", "blossom_enabled"},
    397             "persona control",
    398         )
    399     except (OSError, UnicodeError, ValueError, json.JSONDecodeError):
    400         return None
    401     if (
    402         control["schema"] != PERSONA_CONTROL_SCHEMA
    403         or control["active_persona"] not in PERSONA_ALIASES
    404         or type(control["blossom_enabled"]) is not bool
    405     ):
    406         return None
    407     return control
    408 
    409 
    410 class FixtureState:
    411     def __init__(
    412         self,
    413         evidence: Path,
    414         control: Path,
    415         blossom_port: int,
    416         suite: dict[str, Any] | None = None,
    417     ) -> None:
    418         self._evidence = evidence
    419         self.control = control
    420         self.blossom_port = blossom_port
    421         self._suite = suite
    422         self._attempts = persona_attempts(suite) if suite is not None else {}
    423         self._lock = threading.Lock()
    424         self._events: dict[str, dict[str, Any]] = {}
    425         self._blobs: dict[str, tuple[bytes, str]] = {}
    426         self._upload_attempts = 0
    427         self._accepted_uploads = 0
    428         self._retrievals = 0
    429         self._subscriptions = 0
    430         self._subscriptions_by_persona = {alias: 0 for alias in PERSONA_ALIASES}
    431         self._accepted_attempts: dict[str, dict[str, Any]] = {}
    432         self._identity_by_persona: dict[str, str] = {}
    433         self._persona_by_identity: dict[str, str] = {}
    434         self._accepted_uploads_by_persona = {alias: 0 for alias in PERSONA_ALIASES}
    435         self._uploaded_digests_by_persona = {
    436             alias: set() for alias in PERSONA_ALIASES
    437         }
    438         self._retrievals_by_persona = {alias: set() for alias in PERSONA_ALIASES}
    439         self._unknown_attempts = 0
    440         self._duplicate_attempts = 0
    441         self._expected_failure_rejections = 0
    442         self._transport_rejected_attempts: set[str] = set()
    443         self._events_accepted_during_expected_failures = 0
    444         self._accepted_connections = 0
    445         self._rejected_connections = 0
    446         self._non_loopback_attempts = 0
    447         self._production_network_contacts = 0
    448         self._unintended_publications = 0
    449         self._write_evidence()
    450 
    451     def observe_connection(self, host: str) -> bool:
    452         try:
    453             permitted = ipaddress.ip_address(host).is_loopback
    454         except ValueError:
    455             permitted = False
    456         with self._lock:
    457             if permitted:
    458                 self._accepted_connections += 1
    459             else:
    460                 self._rejected_connections += 1
    461                 self._non_loopback_attempts += 1
    462                 self._production_network_contacts += 1
    463             self._write_evidence_locked()
    464         return permitted
    465 
    466     def publish(self, event: dict[str, Any]) -> bool | None:
    467         if not valid_nostr_event(event) or not verify_nostr_signature(event):
    468             return False
    469         if event["kind"] == BUD11_EVENT_KIND:
    470             if self._suite is not None:
    471                 with self._lock:
    472                     self._unintended_publications += 1
    473                     self._write_evidence_locked()
    474             return False
    475         if self._suite is not None:
    476             return self._publish_persona_event(event)
    477         event_id = event["id"]
    478         with self._lock:
    479             if event_id not in self._events and len(self._events) >= MAX_EVENTS:
    480                 return False
    481             self._events[event_id] = event
    482             self._write_evidence_locked()
    483         return True
    484 
    485     def _publish_persona_event(self, event: dict[str, Any]) -> bool | None:
    486         attempt = classify_attempt(event, self._attempts)
    487         control = read_control(self.control)
    488         with self._lock:
    489             if attempt is None or control is None:
    490                 self._unknown_attempts += 1
    491                 self._unintended_publications += 1
    492                 self._write_evidence_locked()
    493                 return False
    494             attempt_id = attempt["id"]
    495             persona = attempt["persona"]
    496             if control["active_persona"] != persona:
    497                 self._unknown_attempts += 1
    498                 self._unintended_publications += 1
    499                 self._write_evidence_locked()
    500                 return False
    501             if event["kind"] != FLOW_KINDS[attempt["flow"]]:
    502                 self._unintended_publications += 1
    503                 self._write_evidence_locked()
    504                 return False
    505             public_key = event["pubkey"]
    506             existing_identity = self._identity_by_persona.get(persona)
    507             existing_persona = self._persona_by_identity.get(public_key)
    508             if (
    509                 (existing_identity is not None and existing_identity != public_key)
    510                 or (existing_persona is not None and existing_persona != persona)
    511             ):
    512                 self._unintended_publications += 1
    513                 self._write_evidence_locked()
    514                 return False
    515             self._identity_by_persona[persona] = public_key
    516             self._persona_by_identity[public_key] = persona
    517             if (
    518                 attempt["expected_failure"] == "transport_retry_relaunch"
    519                 and attempt_id not in self._transport_rejected_attempts
    520             ):
    521                 self._transport_rejected_attempts.add(attempt_id)
    522                 self._expected_failure_rejections += 1
    523                 self._write_evidence_locked()
    524                 return None
    525             if attempt_id in self._accepted_attempts:
    526                 self._duplicate_attempts += 1
    527                 self._write_evidence_locked()
    528                 return False
    529             if len(self._events) >= MAX_EVENTS:
    530                 return False
    531             self._events[event["id"]] = event
    532             self._accepted_attempts[attempt_id] = {
    533                 "id": attempt_id,
    534                 "flow": attempt["flow"],
    535                 "event_kind": event["kind"],
    536                 "accepted": True,
    537                 "expected_failure_rejections": int(
    538                     attempt_id in self._transport_rejected_attempts
    539                 ),
    540             }
    541             self._write_evidence_locked()
    542         return True
    543 
    544     def query(self, filters: list[dict[str, Any]]) -> list[dict[str, Any]]:
    545         control = read_control(self.control)
    546         with self._lock:
    547             self._subscriptions += 1
    548             if control is not None:
    549                 self._subscriptions_by_persona[control["active_persona"]] += 1
    550             events = list(self._events.values())
    551             self._write_evidence_locked()
    552         selected = [
    553             event for event in events if any(matches(event, item) for item in filters)
    554         ]
    555         selected.sort(key=lambda item: (item.get("created_at", 0), item.get("id", "")))
    556         limits = [
    557             item["limit"]
    558             for item in filters
    559             if isinstance(item, dict) and type(item.get("limit")) is int
    560         ]
    561         count = min(max(max(limits, default=len(selected)), 0), MAX_EVENTS)
    562         return selected[-count:] if count else []
    563 
    564     def upload(
    565         self,
    566         body: bytes,
    567         media_type: str,
    568         expected_hash: str,
    569         authorization: str,
    570     ) -> tuple[bool, dict[str, Any]]:
    571         digest = hashlib.sha256(body).hexdigest()
    572         control = read_control(self.control)
    573         with self._lock:
    574             self._upload_attempts += 1
    575             persona = control["active_persona"] if control is not None else None
    576             if self._suite is None:
    577                 allowed = self.control.is_file() and valid_blossom_authorization(
    578                     authorization, expected_hash, BUD11_SERVER_DOMAIN
    579                 )
    580             else:
    581                 allowed = (
    582                     control is not None
    583                     and control["blossom_enabled"]
    584                     and persona in PHOTO_PERSONAS
    585                     and valid_blossom_authorization(
    586                         authorization, expected_hash, BUD11_SERVER_DOMAIN
    587                     )
    588                 )
    589             capacity = digest in self._blobs or len(self._blobs) < MAX_BLOBS
    590             if allowed and capacity and digest == expected_hash:
    591                 self._blobs[digest] = (body, media_type)
    592                 self._accepted_uploads += 1
    593                 if persona is not None:
    594                     self._accepted_uploads_by_persona[persona] += 1
    595                     self._uploaded_digests_by_persona[persona].add(digest)
    596             self._write_evidence_locked()
    597         descriptor = {
    598             "url": f"http://127.0.0.1:{self.blossom_port}/{digest}.png",
    599             "sha256": digest,
    600             "size": len(body),
    601             "type": media_type,
    602             "uploaded": int(time.time()),
    603         }
    604         return allowed and capacity and digest == expected_hash, descriptor
    605 
    606     def retrieve(self, digest: str) -> tuple[bytes, str] | None:
    607         control = read_control(self.control)
    608         with self._lock:
    609             value = self._blobs.get(digest)
    610             if value is not None:
    611                 if self._suite is None:
    612                     self._retrievals += 1
    613                 elif control is not None:
    614                     persona = control["active_persona"]
    615                     if digest in self._uploaded_digests_by_persona[persona]:
    616                         before = len(self._retrievals_by_persona[persona])
    617                         self._retrievals_by_persona[persona].add(digest)
    618                         self._retrievals += (
    619                             len(self._retrievals_by_persona[persona]) - before
    620                         )
    621                 self._write_evidence_locked()
    622             return value
    623 
    624     def _write_evidence(self) -> None:
    625         with self._lock:
    626             self._write_evidence_locked()
    627 
    628     def _write_evidence_locked(self) -> None:
    629         if self._suite is None:
    630             payload = {
    631                 "schema": "tera-ios-local-social-fixture-evidence-v1",
    632                 "schema_version": 1,
    633                 "accepted_events": len(self._events),
    634                 "event_kinds": sorted(
    635                     event["kind"]
    636                     for event in self._events.values()
    637                     if isinstance(event.get("kind"), int)
    638                 ),
    639                 "subscriptions": self._subscriptions,
    640                 "upload_attempts": self._upload_attempts,
    641                 "accepted_uploads": self._accepted_uploads,
    642                 "retrievals": self._retrievals,
    643                 "accepted_connections": self._accepted_connections,
    644                 "rejected_connections": self._rejected_connections,
    645                 "non_loopback_attempts": self._non_loopback_attempts,
    646                 "production_network_contacts": self._production_network_contacts,
    647             }
    648         else:
    649             payload = self._persona_evidence()
    650         temporary = self._evidence.with_suffix(".tmp")
    651         temporary.write_text(
    652             json.dumps(payload, sort_keys=True) + "\n", encoding="utf-8"
    653         )
    654         os.replace(temporary, self._evidence)
    655 
    656     def _persona_evidence(self) -> dict[str, Any]:
    657         personas = []
    658         for persona in self._suite["personas"]:
    659             alias = persona["alias"]
    660             public_key = self._identity_by_persona.get(alias)
    661             personas.append(
    662                 {
    663                     "alias": alias,
    664                     "identity_sha256": identity_digest(public_key),
    665                     "subscriptions": self._subscriptions_by_persona[alias],
    666                     "accepted_uploads": self._accepted_uploads_by_persona[alias],
    667                     "retrievals": len(self._retrievals_by_persona[alias]),
    668                     "attempts": [
    669                         self._accepted_attempts[attempt["id"]]
    670                         for attempt in persona["attempts"]
    671                         if attempt["id"] in self._accepted_attempts
    672                     ],
    673                 }
    674             )
    675         kind_counts = {
    676             str(kind): sum(event["kind"] == kind for event in self._events.values())
    677             for kind in (1, 31923, 30402)
    678         }
    679         flow_counts = {
    680             flow: sum(item["flow"] == flow for item in self._accepted_attempts.values())
    681             for flow in FLOW_KINDS
    682         }
    683         return {
    684             "schema": "tera.ios.local-social.persona-evidence.v1",
    685             "schema_version": 1,
    686             "personas": personas,
    687             "flow_counts": flow_counts,
    688             "accepted_events": len(self._events),
    689             "event_kind_counts": kind_counts,
    690             "upload_attempts": self._upload_attempts,
    691             "accepted_uploads": self._accepted_uploads,
    692             "retrievals": self._retrievals,
    693             "distinct_identities": len(self._persona_by_identity),
    694             "unknown_attempts": self._unknown_attempts,
    695             "duplicate_attempts": self._duplicate_attempts,
    696             "expected_failure_rejections": self._expected_failure_rejections,
    697             "events_accepted_during_expected_failures": (
    698                 self._events_accepted_during_expected_failures
    699             ),
    700             "accepted_connections": self._accepted_connections,
    701             "rejected_connections": self._rejected_connections,
    702             "non_loopback_attempts": self._non_loopback_attempts,
    703             "production_network_contacts": self._production_network_contacts,
    704             "unintended_publications": self._unintended_publications,
    705             "final_candidate_data_loss": 0,
    706         }
    707 
    708 
    709 def matches(event: dict[str, Any], item: dict[str, Any]) -> bool:
    710     if not isinstance(item, dict):
    711         return False
    712     if isinstance(item.get("ids"), list) and event.get("id") not in item["ids"]:
    713         return False
    714     if (
    715         isinstance(item.get("authors"), list)
    716         and event.get("pubkey") not in item["authors"]
    717     ):
    718         return False
    719     if isinstance(item.get("kinds"), list) and event.get("kind") not in item["kinds"]:
    720         return False
    721     created_at = event.get("created_at")
    722     if not isinstance(created_at, int):
    723         return False
    724     if isinstance(item.get("since"), int) and created_at < item["since"]:
    725         return False
    726     if isinstance(item.get("until"), int) and created_at > item["until"]:
    727         return False
    728     tags = event.get("tags")
    729     if not isinstance(tags, list):
    730         return False
    731     for key, expected in item.items():
    732         if not key.startswith("#") or not isinstance(expected, list):
    733             continue
    734         name = key[1:]
    735         if not any(
    736             isinstance(tag, list)
    737             and len(tag) >= 2
    738             and tag[0] == name
    739             and tag[1] in expected
    740             for tag in tags
    741         ):
    742             return False
    743     return True
    744 
    745 
    746 def valid_nostr_event(event: dict[str, Any]) -> bool:
    747     if not isinstance(event, dict) or set(event) != {
    748         "id",
    749         "pubkey",
    750         "created_at",
    751         "kind",
    752         "tags",
    753         "content",
    754         "sig",
    755     }:
    756         return False
    757     if not lowercase_hex(event["id"], 64) or not lowercase_hex(event["pubkey"], 64):
    758         return False
    759     if not lowercase_hex(event["sig"], 128):
    760         return False
    761     if type(event["created_at"]) is not int or event["created_at"] < 0:
    762         return False
    763     if type(event["kind"]) is not int or not 0 <= event["kind"] <= 0xFFFF_FFFF:
    764         return False
    765     if not isinstance(event["content"], str) or not isinstance(event["tags"], list):
    766         return False
    767     if not all(
    768         isinstance(tag, list) and all(isinstance(value, str) for value in tag)
    769         for tag in event["tags"]
    770     ):
    771         return False
    772     preimage = json.dumps(
    773         [
    774             0,
    775             event["pubkey"],
    776             event["created_at"],
    777             event["kind"],
    778             event["tags"],
    779             event["content"],
    780         ],
    781         ensure_ascii=False,
    782         separators=(",", ":"),
    783     ).encode("utf-8")
    784     return hashlib.sha256(preimage).hexdigest() == event["id"]
    785 
    786 
    787 def lowercase_hex(value: Any, length: int) -> bool:
    788     return (
    789         isinstance(value, str)
    790         and len(value) == length
    791         and all(character in "0123456789abcdef" for character in value)
    792     )
    793 
    794 
    795 def classify_attempt(
    796     event: dict[str, Any], attempts: dict[str, dict[str, Any]]
    797 ) -> dict[str, Any] | None:
    798     values = [event.get("content")]
    799     for tag in event.get("tags", []):
    800         values.extend(tag)
    801     matches = [
    802         attempt
    803         for attempt in attempts.values()
    804         if (
    805             photo_attempt_matches(event, attempt)
    806             if attempt.get("flow") == "PhotoUpdate"
    807             else attempt["marker"] in values
    808         )
    809     ]
    810     return matches[0] if len(matches) == 1 else None
    811 
    812 
    813 def photo_attempt_matches(event: dict[str, Any], attempt: dict[str, Any]) -> bool:
    814     if attempt.get("flow") != "PhotoUpdate":
    815         return False
    816     content = event.get("content")
    817     if not isinstance(content, str):
    818         return False
    819     lines = content.split("\n")
    820     if len(lines) != 2 or lines[0] != attempt.get("marker"):
    821         return False
    822     url = lines[1]
    823     match = re.fullmatch(
    824         r"http://127\.0\.0\.1:([1-9][0-9]{0,4})/([0-9a-f]{64})\.png", url
    825     )
    826     if match is None or int(match.group(1)) > 65535:
    827         return False
    828     digest = match.group(2)
    829     imeta = [
    830         tag
    831         for tag in event.get("tags", [])
    832         if isinstance(tag, list) and tag[:1] == ["imeta"]
    833     ]
    834     return len(imeta) == 1 and all(
    835         field in imeta[0]
    836         for field in (f"url {url}", f"x {digest}", "m image/png")
    837     )
    838 
    839 
    840 def identity_digest(public_key: str | None) -> str:
    841     if public_key is None:
    842         return "0" * 64
    843     return hashlib.sha256(
    844         b"radroots.ios.local-social.persona-identity.v1\0"
    845         + bytes.fromhex(public_key)
    846     ).hexdigest()
    847 
    848 
    849 SECP256K1_FIELD = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
    850 SECP256K1_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
    851 SECP256K1_GENERATOR = (
    852     0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798,
    853     0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8,
    854 )
    855 
    856 
    857 def point_add(
    858     left: tuple[int, int] | None, right: tuple[int, int] | None
    859 ) -> tuple[int, int] | None:
    860     if left is None:
    861         return right
    862     if right is None:
    863         return left
    864     if left[0] == right[0] and left[1] != right[1]:
    865         return None
    866     if left == right:
    867         if left[1] == 0:
    868             return None
    869         slope = (3 * left[0] * left[0]) * pow(2 * left[1], -1, SECP256K1_FIELD)
    870     else:
    871         slope = (right[1] - left[1]) * pow(
    872             right[0] - left[0], -1, SECP256K1_FIELD
    873         )
    874     slope %= SECP256K1_FIELD
    875     x = (slope * slope - left[0] - right[0]) % SECP256K1_FIELD
    876     y = (slope * (left[0] - x) - left[1]) % SECP256K1_FIELD
    877     return x, y
    878 
    879 
    880 def point_multiply(value: int, point: tuple[int, int]) -> tuple[int, int] | None:
    881     result = None
    882     current: tuple[int, int] | None = point
    883     while value:
    884         if value & 1:
    885             result = point_add(result, current)
    886         current = point_add(current, current)
    887         value >>= 1
    888     return result
    889 
    890 
    891 def tagged_hash(tag: str, payload: bytes) -> bytes:
    892     tag_hash = hashlib.sha256(tag.encode("ascii")).digest()
    893     return hashlib.sha256(tag_hash + tag_hash + payload).digest()
    894 
    895 
    896 def verify_bip340(public_key: bytes, message: bytes, signature: bytes) -> bool:
    897     if len(public_key) != 32 or len(message) != 32 or len(signature) != 64:
    898         return False
    899     x = int.from_bytes(public_key, "big")
    900     r = int.from_bytes(signature[:32], "big")
    901     s = int.from_bytes(signature[32:], "big")
    902     if x >= SECP256K1_FIELD or r >= SECP256K1_FIELD or s >= SECP256K1_ORDER:
    903         return False
    904     y_squared = (pow(x, 3, SECP256K1_FIELD) + 7) % SECP256K1_FIELD
    905     y = pow(y_squared, (SECP256K1_FIELD + 1) // 4, SECP256K1_FIELD)
    906     if pow(y, 2, SECP256K1_FIELD) != y_squared:
    907         return False
    908     if y & 1:
    909         y = SECP256K1_FIELD - y
    910     challenge = int.from_bytes(
    911         tagged_hash("BIP0340/challenge", signature[:32] + public_key + message),
    912         "big",
    913     ) % SECP256K1_ORDER
    914     negative = (x, (-y) % SECP256K1_FIELD)
    915     candidate = point_add(
    916         point_multiply(s, SECP256K1_GENERATOR),
    917         point_multiply(challenge, negative),
    918     )
    919     return candidate is not None and candidate[1] % 2 == 0 and candidate[0] == r
    920 
    921 
    922 def verify_nostr_signature(event: dict[str, Any]) -> bool:
    923     try:
    924         return verify_bip340(
    925             bytes.fromhex(event["pubkey"]),
    926             bytes.fromhex(event["id"]),
    927             bytes.fromhex(event["sig"]),
    928         )
    929     except (KeyError, TypeError, ValueError):
    930         return False
    931 
    932 
    933 def valid_bud11_server_domain(value: Any) -> bool:
    934     if (
    935         not isinstance(value, str)
    936         or not value
    937         or len(value) > 253
    938         or not value.isascii()
    939         or value.lower() != value
    940     ):
    941         return False
    942     try:
    943         address = ipaddress.ip_address(value)
    944     except ValueError:
    945         labels = value.split(".")
    946         return not all(label.isdigit() for label in labels) and all(
    947             1 <= len(label) <= 63
    948             and re.fullmatch(r"[a-z0-9](?:[a-z0-9-]*[a-z0-9])?", label)
    949             is not None
    950             for label in labels
    951         )
    952     return isinstance(address, ipaddress.IPv4Address) and str(address) == value
    953 
    954 
    955 def valid_bud11_content(value: Any) -> bool:
    956     return (
    957         isinstance(value, str)
    958         and value
    959         and len(value.encode("utf-8")) <= BUD11_CONTENT_MAX_BYTES
    960         and value.strip() == value
    961         and not any(
    962             unicodedata.category(character) == "Cc"
    963             and character not in "\t\n\r"
    964             for character in value
    965         )
    966     )
    967 
    968 
    969 def canonical_unsigned_decimal(value: Any) -> int | None:
    970     if (
    971         not isinstance(value, str)
    972         or not value
    973         or len(value) > 20
    974         or any(character not in "0123456789" for character in value)
    975         or (len(value) > 1 and value.startswith("0"))
    976     ):
    977         return None
    978     parsed = int(value)
    979     return parsed if parsed <= 0xFFFF_FFFF_FFFF_FFFF else None
    980 
    981 
    982 def valid_blossom_authorization(
    983     authorization: str,
    984     expected_hash: str,
    985     expected_server: str,
    986     now_unix_s: int | None = None,
    987 ) -> bool:
    988     if not lowercase_hex(expected_hash, 64) or not valid_bud11_server_domain(
    989         expected_server
    990     ):
    991         return False
    992     if not authorization.startswith("Nostr "):
    993         return False
    994     payload = authorization.removeprefix("Nostr ")
    995     if (
    996         not payload
    997         or len(payload) > BUD11_AUTHORIZATION_ENCODED_MAX_BYTES
    998         or any(character.isspace() for character in payload)
    999         or "=" in payload
   1000         or re.fullmatch(r"[A-Za-z0-9_-]+", payload) is None
   1001     ):
   1002         return False
   1003     try:
   1004         raw = base64.urlsafe_b64decode(payload + "=" * (-len(payload) % 4))
   1005         if len(raw) > BUD11_AUTHORIZATION_MAX_BYTES:
   1006             return False
   1007         if base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii") != payload:
   1008             return False
   1009         event = json.loads(raw, object_pairs_hook=strict_object)
   1010     except (ValueError, json.JSONDecodeError):
   1011         return False
   1012     if not valid_nostr_event(event) or not verify_nostr_signature(event):
   1013         return False
   1014     if event["kind"] != BUD11_EVENT_KIND or not valid_bud11_content(event["content"]):
   1015         return False
   1016     if len(event["tags"]) != 4 or event["tags"][0] != ["t", "upload"]:
   1017         return False
   1018     expiration_tag = event["tags"][1]
   1019     if (
   1020         len(expiration_tag) != 2
   1021         or expiration_tag[0] != "expiration"
   1022         or event["tags"][2] != ["x", expected_hash]
   1023         or event["tags"][3] != ["server", expected_server]
   1024     ):
   1025         return False
   1026     expiration = canonical_unsigned_decimal(expiration_tag[1])
   1027     now = int(time.time()) if now_unix_s is None else now_unix_s
   1028     return (
   1029         type(now) is int
   1030         and now >= 0
   1031         and expiration is not None
   1032         and event["created_at"] < now < expiration
   1033         and now - event["created_at"] <= BUD11_MAX_CREATED_AGE_SECONDS
   1034         and 0 < expiration - event["created_at"] <= BUD11_MAX_LIFETIME_SECONDS
   1035     )
   1036 
   1037 
   1038 class ObservableLoopbackServerMixin:
   1039     _fixture_state: FixtureState
   1040 
   1041     def verify_request(
   1042         self, request: socket.socket, client_address: tuple[str, int]
   1043     ) -> bool:
   1044         del request
   1045         return self._fixture_state.observe_connection(client_address[0])
   1046 
   1047 
   1048 class ReusableThreadingServer(
   1049     ObservableLoopbackServerMixin, socketserver.ThreadingTCPServer
   1050 ):
   1051     allow_reuse_address = True
   1052     daemon_threads = True
   1053 
   1054     def __init__(
   1055         self,
   1056         server_address: tuple[str, int],
   1057         handler: type[socketserver.BaseRequestHandler],
   1058         state: FixtureState,
   1059     ) -> None:
   1060         self._fixture_state = state
   1061         super().__init__(server_address, handler)
   1062 
   1063 
   1064 class ObservableLoopbackHTTPServer(
   1065     ObservableLoopbackServerMixin, http.server.ThreadingHTTPServer
   1066 ):
   1067     def __init__(
   1068         self,
   1069         server_address: tuple[str, int],
   1070         handler: type[http.server.BaseHTTPRequestHandler],
   1071         state: FixtureState,
   1072     ) -> None:
   1073         self._fixture_state = state
   1074         super().__init__(server_address, handler)
   1075 
   1076 
   1077 class LoopbackConnectionFactory:
   1078     @staticmethod
   1079     def relay(port: int, state: FixtureState) -> ReusableThreadingServer:
   1080         return ReusableThreadingServer(("127.0.0.1", port), RelayHandler, state)
   1081 
   1082     @staticmethod
   1083     def blossom(port: int, state: FixtureState) -> ObservableLoopbackHTTPServer:
   1084         return ObservableLoopbackHTTPServer(
   1085             ("127.0.0.1", port), BlossomHandler, state
   1086         )
   1087 
   1088 
   1089 class RelayHandler(socketserver.BaseRequestHandler):
   1090     state: FixtureState
   1091 
   1092     def handle(self) -> None:
   1093         self.request.settimeout(15)
   1094         headers = read_until(self.request, b"\r\n\r\n", 16 * 1024)
   1095         lines = headers.decode("ascii").split("\r\n")
   1096         if not lines or lines[0] != "GET / HTTP/1.1":
   1097             return
   1098         fields = {}
   1099         for line in lines[1:]:
   1100             if ":" in line:
   1101                 key, value = line.split(":", 1)
   1102                 fields[key.lower()] = value.strip()
   1103         key = fields.get("sec-websocket-key")
   1104         if not key or fields.get("upgrade", "").lower() != "websocket":
   1105             return
   1106         accept = base64.b64encode(
   1107             hashlib.sha1(
   1108                 (key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode()
   1109             ).digest()
   1110         ).decode()
   1111         self.request.sendall(
   1112             (
   1113                 "HTTP/1.1 101 Switching Protocols\r\n"
   1114                 "Upgrade: websocket\r\n"
   1115                 "Connection: Upgrade\r\n"
   1116                 f"Sec-WebSocket-Accept: {accept}\r\n\r\n"
   1117             ).encode("ascii")
   1118         )
   1119         while True:
   1120             frame = read_frame(self.request)
   1121             if frame is None:
   1122                 return
   1123             opcode, payload = frame
   1124             if opcode == 0x8:
   1125                 return
   1126             if opcode == 0x9:
   1127                 send_frame(self.request, 0xA, payload)
   1128                 continue
   1129             if opcode != 0x1:
   1130                 continue
   1131             try:
   1132                 message = json.loads(payload)
   1133             except (UnicodeDecodeError, json.JSONDecodeError):
   1134                 continue
   1135             if not isinstance(message, list) or not message:
   1136                 continue
   1137             if (
   1138                 message[0] == "EVENT"
   1139                 and len(message) == 2
   1140                 and isinstance(message[1], dict)
   1141             ):
   1142                 event_id = message[1].get("id", "")
   1143                 accepted = self.state.publish(message[1])
   1144                 if accepted is None:
   1145                     return
   1146                 send_json(
   1147                     self.request,
   1148                     ["OK", event_id, accepted, "" if accepted else "invalid"],
   1149                 )
   1150             elif (
   1151                 message[0] == "REQ"
   1152                 and len(message) >= 3
   1153                 and isinstance(message[1], str)
   1154             ):
   1155                 subscription = message[1]
   1156                 filters = [value for value in message[2:] if isinstance(value, dict)]
   1157                 for event in self.state.query(filters):
   1158                     send_json(self.request, ["EVENT", subscription, event])
   1159                 send_json(self.request, ["EOSE", subscription])
   1160 
   1161 
   1162 def read_until(stream: socket.socket, marker: bytes, maximum: int) -> bytes:
   1163     value = bytearray()
   1164     while marker not in value:
   1165         chunk = stream.recv(4096)
   1166         if not chunk:
   1167             raise ConnectionError("socket closed")
   1168         value.extend(chunk)
   1169         if len(value) > maximum:
   1170             raise ValueError("request too large")
   1171     return bytes(value)
   1172 
   1173 
   1174 def read_exact(stream: socket.socket, length: int) -> bytes:
   1175     value = bytearray()
   1176     while len(value) < length:
   1177         chunk = stream.recv(length - len(value))
   1178         if not chunk:
   1179             raise ConnectionError("socket closed")
   1180         value.extend(chunk)
   1181     return bytes(value)
   1182 
   1183 
   1184 def read_frame(stream: socket.socket) -> tuple[int, bytes] | None:
   1185     try:
   1186         first, second = read_exact(stream, 2)
   1187     except (ConnectionError, OSError, TimeoutError):
   1188         return None
   1189     if first & 0x80 == 0 or second & 0x80 == 0:
   1190         return None
   1191     length = second & 0x7F
   1192     if length == 126:
   1193         length = struct.unpack("!H", read_exact(stream, 2))[0]
   1194     elif length == 127:
   1195         length = struct.unpack("!Q", read_exact(stream, 8))[0]
   1196     if length > MAX_WEBSOCKET_MESSAGE:
   1197         return None
   1198     mask = read_exact(stream, 4)
   1199     payload = bytearray(read_exact(stream, length))
   1200     for index in range(length):
   1201         payload[index] ^= mask[index % 4]
   1202     return first & 0x0F, bytes(payload)
   1203 
   1204 
   1205 def send_frame(stream: socket.socket, opcode: int, payload: bytes) -> None:
   1206     length = len(payload)
   1207     header = bytearray([0x80 | opcode])
   1208     if length < 126:
   1209         header.append(length)
   1210     elif length <= 0xFFFF:
   1211         header.append(126)
   1212         header.extend(struct.pack("!H", length))
   1213     else:
   1214         header.append(127)
   1215         header.extend(struct.pack("!Q", length))
   1216     stream.sendall(header + payload)
   1217 
   1218 
   1219 def send_json(stream: socket.socket, value: Any) -> None:
   1220     send_frame(stream, 0x1, json.dumps(value, separators=(",", ":")).encode())
   1221 
   1222 
   1223 class BlossomHandler(http.server.BaseHTTPRequestHandler):
   1224     state: FixtureState
   1225     protocol_version = "HTTP/1.1"
   1226 
   1227     def do_PUT(self) -> None:  # noqa: N802
   1228         if self.path != "/upload":
   1229             self.send_error(404)
   1230             return
   1231         try:
   1232             length = int(self.headers.get("Content-Length", "-1"))
   1233         except ValueError:
   1234             length = -1
   1235         authorization = self.headers.get("Authorization", "")
   1236         expected_hash = self.headers.get("X-SHA-256", "")
   1237         media_type = self.headers.get("Content-Type", "")
   1238         if (
   1239             length < 1
   1240             or length > MAX_HTTP_BODY
   1241             or not lowercase_hex(expected_hash, 64)
   1242             or not authorization.startswith("Nostr ")
   1243             or media_type != "image/png"
   1244         ):
   1245             self.send_error(400)
   1246             return
   1247         body = self.rfile.read(length)
   1248         accepted, descriptor = self.state.upload(
   1249             body, media_type, expected_hash, authorization
   1250         )
   1251         if not accepted:
   1252             self.respond(503, b'{"error":"fixture_retry_required"}', "application/json")
   1253             return
   1254         self.respond(
   1255             200,
   1256             json.dumps(descriptor, separators=(",", ":")).encode(),
   1257             "application/json",
   1258         )
   1259 
   1260     def do_GET(self) -> None:  # noqa: N802
   1261         component = self.path.removeprefix("/")
   1262         if component.endswith(".png") and lowercase_hex(component[:-4], 64):
   1263             value = self.state.retrieve(component[:-4])
   1264             if value is not None:
   1265                 self.respond(200, value[0], value[1])
   1266                 return
   1267         self.respond(404, b'{"error":"not_found"}', "application/json")
   1268 
   1269     def respond(self, status: int, body: bytes, content_type: str) -> None:
   1270         self.send_response(status)
   1271         self.send_header("Content-Type", content_type)
   1272         self.send_header("Content-Length", str(len(body)))
   1273         self.send_header("Connection", "close")
   1274         self.end_headers()
   1275         self.wfile.write(body)
   1276 
   1277     def log_message(self, format: str, *args: Any) -> None:
   1278         return
   1279 
   1280 
   1281 def serve(arguments: argparse.Namespace) -> int:
   1282     evidence = Path(arguments.evidence).resolve()
   1283     ready = Path(arguments.ready).resolve()
   1284     control = Path(arguments.control).resolve()
   1285     for path in (evidence, ready, control):
   1286         path.parent.mkdir(parents=True, exist_ok=True)
   1287     control.unlink(missing_ok=True)
   1288     ready.unlink(missing_ok=True)
   1289     suite = None
   1290     if arguments.persona_fixture is not None:
   1291         _, suite = load_persona_suite(Path(arguments.persona_fixture).resolve())
   1292     state = FixtureState(evidence, control, arguments.blossom_port, suite)
   1293     RelayHandler.state = state
   1294     BlossomHandler.state = state
   1295     relay = LoopbackConnectionFactory.relay(arguments.relay_port, state)
   1296     blossom = LoopbackConnectionFactory.blossom(arguments.blossom_port, state)
   1297     threads = [
   1298         threading.Thread(target=relay.serve_forever, daemon=True),
   1299         threading.Thread(target=blossom.serve_forever, daemon=True),
   1300     ]
   1301     for thread in threads:
   1302         thread.start()
   1303     ready.write_text(
   1304         json.dumps(
   1305             {
   1306                 "schema": "tera-ios-local-social-fixture-ready-v1",
   1307                 "relay": f"ws://127.0.0.1:{arguments.relay_port}",
   1308                 "blossom": f"http://127.0.0.1:{arguments.blossom_port}",
   1309             },
   1310             sort_keys=True,
   1311         )
   1312         + "\n",
   1313         encoding="utf-8",
   1314     )
   1315     stopped = threading.Event()
   1316 
   1317     def stop(_signum: int, _frame: Any) -> None:
   1318         stopped.set()
   1319 
   1320     signal.signal(signal.SIGTERM, stop)
   1321     signal.signal(signal.SIGINT, stop)
   1322     stopped.wait()
   1323     relay.shutdown()
   1324     blossom.shutdown()
   1325     relay.server_close()
   1326     blossom.server_close()
   1327     for thread in threads:
   1328         thread.join(timeout=5)
   1329     return 0
   1330 
   1331 
   1332 def verify(arguments: argparse.Namespace) -> int:
   1333     _, payload = read_json(Path(arguments.evidence).resolve())
   1334     if (
   1335         payload.get("schema") != "tera-ios-local-social-fixture-evidence-v1"
   1336         or payload.get("accepted_events", 0) < 5
   1337         or payload.get("subscriptions", 0) < 1
   1338         or payload.get("upload_attempts", 0) < 1
   1339         or payload.get("accepted_uploads", 0) < 1
   1340         or payload.get("retrievals", 0) < 1
   1341         or payload.get("non_loopback_attempts") != 0
   1342         or payload.get("production_network_contacts") != 0
   1343     ):
   1344         raise SystemExit("local-social fixture evidence is incomplete")
   1345     print("local-social fixture evidence verified")
   1346     return 0
   1347 
   1348 
   1349 def verify_accessibility(arguments: argparse.Namespace) -> int:
   1350     _, payload = read_json(Path(arguments.evidence).resolve())
   1351     if (
   1352         payload.get("schema") != "tera-ios-local-social-fixture-evidence-v1"
   1353         or payload.get("accepted_events") != 0
   1354         or payload.get("upload_attempts") != 0
   1355         or payload.get("accepted_uploads") != 0
   1356         or payload.get("retrievals") != 0
   1357         or payload.get("subscriptions", 0) < 1
   1358     ):
   1359         raise SystemExit("local-social accessibility fixture evidence is invalid")
   1360     print("local-social accessibility fixture evidence verified")
   1361     return 0
   1362 
   1363 
   1364 def verify_persona_fixture(arguments: argparse.Namespace) -> int:
   1365     raw, suite = load_persona_suite(Path(arguments.fixture).resolve())
   1366     fixture_schema, fixture_schema_value = load_schema_file(
   1367         Path(arguments.fixture_schema).resolve(),
   1368         "https://radroots.org/schemas/ios/local-social-personas.v1.schema.json",
   1369     )
   1370     result_schema = validate_schema_file(
   1371         Path(arguments.result_schema).resolve(),
   1372         "https://radroots.org/schemas/ios/local-social-persona-results.v1.schema.json",
   1373     )
   1374     attempt_schema = validate_schema_file(
   1375         Path(arguments.attempt_schema).resolve(),
   1376         "https://radroots.org/schemas/ios/local-social-persona-attempt-evidence.v1.schema.json",
   1377     )
   1378     result_v2_schema = validate_schema_file(
   1379         Path(arguments.result_v2_schema).resolve(),
   1380         "https://radroots.org/schemas/ios/local-social-persona-results.v2.schema.json",
   1381     )
   1382     validate_schema_instance(fixture_schema_value, suite, "persona fixture")
   1383     print(
   1384         "local-social persona fixtures verified: "
   1385         f"fixture={hashlib.sha256(raw).hexdigest()} "
   1386         f"fixture_schema={hashlib.sha256(fixture_schema).hexdigest()} "
   1387         f"result_schema={hashlib.sha256(result_schema).hexdigest()} "
   1388         f"attempt_schema={hashlib.sha256(attempt_schema).hexdigest()} "
   1389         f"result_v2_schema={hashlib.sha256(result_v2_schema).hexdigest()}"
   1390     )
   1391     return 0
   1392 
   1393 
   1394 def verify_bud11_corpus(arguments: argparse.Namespace) -> int:
   1395     raw, corpus = load_bud11_mutation_corpus(Path(arguments.corpus).resolve())
   1396     schema, schema_value = load_schema_file(
   1397         Path(arguments.schema).resolve(),
   1398         "https://radroots.org/schemas/ios/bud11-upload-authorization-mutations.v1.schema.json",
   1399     )
   1400     validate_schema_instance(schema_value, corpus, "BUD-11 corpus")
   1401     print(
   1402         "BUD-11 mutation corpus verified: "
   1403         f"corpus={hashlib.sha256(raw).hexdigest()} "
   1404         f"schema={hashlib.sha256(schema).hexdigest()}"
   1405     )
   1406     return 0
   1407 
   1408 
   1409 def bounded_directory_inventory(
   1410     path: Path,
   1411     maximum_entries: int = MAX_RESULT_BUNDLE_ENTRIES,
   1412     maximum_relative_path_bytes: int = MAX_RESULT_BUNDLE_RELATIVE_PATH_BYTES,
   1413 ) -> list[tuple[int, bytes, Path]]:
   1414     if path.is_symlink() or not path.is_dir():
   1415         raise ValueError("result bundle is not a regular directory")
   1416     pending = [(path, Path())]
   1417     inventory: list[tuple[int, bytes, Path]] = []
   1418     while pending:
   1419         directory, relative_directory = pending.pop()
   1420         with os.scandir(directory) as entries:
   1421             for entry in entries:
   1422                 relative_path = relative_directory / entry.name
   1423                 try:
   1424                     relative = relative_path.as_posix().encode("utf-8")
   1425                 except UnicodeError as error:
   1426                     raise ValueError("result bundle path is not UTF-8") from error
   1427                 if not relative or len(relative) > maximum_relative_path_bytes:
   1428                     raise ValueError("result bundle path exceeds its byte bound")
   1429                 if len(inventory) >= maximum_entries:
   1430                     raise ValueError("result bundle exceeds its entry bound")
   1431                 if entry.is_symlink():
   1432                     raise ValueError("result bundle contains a symbolic link")
   1433                 item = Path(entry.path)
   1434                 if entry.is_dir(follow_symlinks=False):
   1435                     inventory.append((0, relative, item))
   1436                     pending.append((item, relative_path))
   1437                 elif entry.is_file(follow_symlinks=False):
   1438                     inventory.append((1, relative, item))
   1439                 else:
   1440                     raise ValueError("result bundle contains an unsupported entry")
   1441     return sorted(inventory, key=lambda item: item[1])
   1442 
   1443 
   1444 def directory_digest(
   1445     path: Path,
   1446     maximum_entries: int = MAX_RESULT_BUNDLE_ENTRIES,
   1447     maximum_file_bytes: int = MAX_RESULT_BUNDLE_FILE_BYTES,
   1448     maximum_total_bytes: int = MAX_RESULT_BUNDLE_BYTES,
   1449 ) -> str:
   1450     inventory = bounded_directory_inventory(path, maximum_entries)
   1451     digest = hashlib.sha256()
   1452     digest.update(RESULT_BUNDLE_DIGEST_DOMAIN)
   1453     digest.update(len(inventory).to_bytes(4, "big"))
   1454     total_bytes = 0
   1455     for kind, relative, item in inventory:
   1456         digest.update(bytes((kind,)))
   1457         digest.update(len(relative).to_bytes(8, "big"))
   1458         digest.update(relative)
   1459         if kind == 0:
   1460             continue
   1461         descriptor = os.open(
   1462             item,
   1463             os.O_RDONLY | os.O_CLOEXEC | getattr(os, "O_NOFOLLOW", 0),
   1464         )
   1465         try:
   1466             before = os.fstat(descriptor)
   1467             if not stat.S_ISREG(before.st_mode) or before.st_size < 0:
   1468                 raise ValueError("result bundle contains an unsupported entry")
   1469             size = before.st_size
   1470             if size > maximum_file_bytes or total_bytes > maximum_total_bytes - size:
   1471                 raise ValueError("result bundle exceeds its byte bound")
   1472             digest.update(size.to_bytes(8, "big"))
   1473             remaining = size
   1474             with os.fdopen(descriptor, "rb", closefd=False) as stream:
   1475                 while remaining:
   1476                     chunk = stream.read(min(64 * 1024, remaining))
   1477                     if not chunk:
   1478                         raise ValueError("result bundle file changed while hashing")
   1479                     digest.update(chunk)
   1480                     remaining -= len(chunk)
   1481                 if stream.read(1):
   1482                     raise ValueError("result bundle file changed while hashing")
   1483             after = os.fstat(descriptor)
   1484             if (
   1485                 before.st_dev != after.st_dev
   1486                 or before.st_ino != after.st_ino
   1487                 or before.st_size != after.st_size
   1488                 or before.st_mode != after.st_mode
   1489                 or before.st_mtime_ns != after.st_mtime_ns
   1490                 or before.st_ctime_ns != after.st_ctime_ns
   1491             ):
   1492                 raise ValueError("result bundle file changed while hashing")
   1493             total_bytes += size
   1494         finally:
   1495             os.close(descriptor)
   1496     return digest.hexdigest()
   1497 
   1498 
   1499 def simulator_metadata(udid: str, result_bundle: Path) -> dict[str, str]:
   1500     devices = run_json_command_bounded(
   1501         ["xcrun", "simctl", "list", "devices", "--json"],
   1502         MAX_XCRESULT_JSON_BYTES,
   1503     )
   1504     matches = [
   1505         (runtime, device)
   1506         for runtime, values in devices.get("devices", {}).items()
   1507         for device in values
   1508         if device.get("udid") == udid
   1509     ]
   1510     if len(matches) != 1:
   1511         raise ValueError("simulator identity is unavailable")
   1512     runtime, simulator = matches[0]
   1513     runtime_version = runtime.rsplit("iOS-", 1)[-1].replace("-", ".")
   1514     summary = run_json_command_bounded(
   1515         [
   1516             "xcrun",
   1517             "xcresulttool",
   1518             "get",
   1519             "test-results",
   1520             "summary",
   1521             "--path",
   1522             str(result_bundle),
   1523         ],
   1524         MAX_XCRESULT_JSON_BYTES,
   1525     )
   1526     result_devices = [
   1527         row.get("device")
   1528         for row in summary.get("devicesAndConfigurations", [])
   1529         if isinstance(row, dict)
   1530         and isinstance(row.get("device"), dict)
   1531         and isinstance(row["device"].get("deviceId"), str)
   1532         and row["device"]["deviceId"].upper() == udid.upper()
   1533     ]
   1534     if len(result_devices) != 1:
   1535         raise ValueError("result bundle simulator identity is unavailable")
   1536     result_device = result_devices[0]
   1537     version = result_device.get("osVersion")
   1538     architecture = result_device.get("architecture")
   1539     if (
   1540         summary.get("result") != "Passed"
   1541         or summary.get("failedTests") != 0
   1542         or summary.get("passedTests") != 1
   1543         or summary.get("skippedTests") != 0
   1544         or simulator.get("isAvailable") is not True
   1545         or result_device.get("platform") != "iOS Simulator"
   1546         or version != runtime_version
   1547         or not isinstance(version, str)
   1548         or re.fullmatch(r"[1-9][0-9]*(\.[0-9]+){1,2}", version) is None
   1549         or int(version.split(".", 1)[0]) < 18
   1550         or architecture != "arm64"
   1551     ):
   1552         raise ValueError("simulator does not satisfy the development platform contract")
   1553     canonical_version = version if version.count(".") == 2 else f"{version}.0"
   1554     return {
   1555         "udid": udid.upper(),
   1556         "os": f"iOS {canonical_version}",
   1557         "architecture": architecture,
   1558     }
   1559 
   1560 
   1561 def validate_persona_evidence(value: Any, suite: dict[str, Any]) -> dict[str, Any]:
   1562     keys = {
   1563         "schema", "schema_version", "personas", "flow_counts", "accepted_events",
   1564         "event_kind_counts", "upload_attempts", "accepted_uploads", "retrievals",
   1565         "distinct_identities", "unknown_attempts", "duplicate_attempts",
   1566         "expected_failure_rejections", "events_accepted_during_expected_failures",
   1567         "accepted_connections", "rejected_connections", "non_loopback_attempts",
   1568         "production_network_contacts",
   1569         "unintended_publications",
   1570         "final_candidate_data_loss",
   1571     }
   1572     evidence = exact_keys(value, keys, "persona evidence")
   1573     if (
   1574         evidence["schema"] != "tera.ios.local-social.persona-evidence.v1"
   1575         or evidence["schema_version"] != 1
   1576     ):
   1577         raise ValueError("persona evidence header is invalid")
   1578     expected_scalars = {
   1579         "accepted_events": 15,
   1580         "accepted_uploads": 3,
   1581         "retrievals": 3,
   1582         "distinct_identities": 5,
   1583         "unknown_attempts": 0,
   1584         "duplicate_attempts": 0,
   1585         "expected_failure_rejections": 1,
   1586         "events_accepted_during_expected_failures": 0,
   1587         "non_loopback_attempts": 0,
   1588         "production_network_contacts": 0,
   1589         "unintended_publications": 0,
   1590         "final_candidate_data_loss": 0,
   1591     }
   1592     if any(evidence.get(key) != expected for key, expected in expected_scalars.items()):
   1593         raise ValueError("persona evidence totals are invalid")
   1594     if (
   1595         type(evidence["accepted_connections"]) is not int
   1596         or not 1 <= evidence["accepted_connections"] <= 61_440
   1597         or type(evidence["rejected_connections"]) is not int
   1598         or not 0 <= evidence["rejected_connections"] <= 61_440
   1599     ):
   1600         raise ValueError("persona connection evidence is invalid")
   1601     if evidence["flow_counts"] != {flow: 3 for flow in FLOW_KINDS}:
   1602         raise ValueError("persona flow counts are invalid")
   1603     if evidence["event_kind_counts"] != {"1": 9, "31923": 3, "30402": 3}:
   1604         raise ValueError("persona event kind counts are invalid")
   1605     if not isinstance(evidence["personas"], list) or len(evidence["personas"]) != 5:
   1606         raise ValueError("persona evidence inventory is invalid")
   1607     identity_digests = set()
   1608     for persona, expected in zip(evidence["personas"], suite["personas"], strict=True):
   1609         exact_keys(
   1610             persona,
   1611             {
   1612                 "alias",
   1613                 "identity_sha256",
   1614                 "subscriptions",
   1615                 "accepted_uploads",
   1616                 "retrievals",
   1617                 "attempts",
   1618             },
   1619             "persona evidence row",
   1620         )
   1621         if (
   1622             persona["alias"] != expected["alias"]
   1623             or not lowercase_hex(persona["identity_sha256"], 64)
   1624             or persona["identity_sha256"] == "0" * 64
   1625             or type(persona["subscriptions"]) is not int
   1626             or not 1 <= persona["subscriptions"] <= 4096
   1627             or persona["accepted_uploads"] != int(persona["alias"] in PHOTO_PERSONAS)
   1628             or persona["retrievals"] != int(persona["alias"] in PHOTO_PERSONAS)
   1629             or not isinstance(persona["attempts"], list)
   1630             or len(persona["attempts"]) != 3
   1631         ):
   1632             raise ValueError("persona evidence row is invalid")
   1633         identity_digests.add(persona["identity_sha256"])
   1634         for attempt, expected_attempt in zip(
   1635             persona["attempts"], expected["attempts"], strict=True
   1636         ):
   1637             exact_keys(
   1638                 attempt,
   1639                 {"id", "flow", "event_kind", "accepted", "expected_failure_rejections"},
   1640                 "attempt evidence row",
   1641             )
   1642             if (
   1643                 attempt["id"] != expected_attempt["id"]
   1644                 or attempt["flow"] != expected_attempt["flow"]
   1645                 or attempt["event_kind"] != FLOW_KINDS[expected_attempt["flow"]]
   1646                 or attempt["accepted"] is not True
   1647                 or attempt["expected_failure_rejections"]
   1648                 != int(
   1649                     expected_attempt["expected_failure"]
   1650                     == "transport_retry_relaunch"
   1651                 )
   1652             ):
   1653                 raise ValueError("attempt evidence row is invalid")
   1654     if len(identity_digests) != 5:
   1655         raise ValueError("persona identities are not distinct")
   1656     return evidence
   1657 
   1658 
   1659 def valid_ios_runtime(value: Any) -> bool:
   1660     if not isinstance(value, str) or not re.fullmatch(
   1661         r"iOS ([1-9][0-9]*)(\.[0-9]+){1,2}", value
   1662     ):
   1663         return False
   1664     return int(value.split()[1].split(".", 1)[0]) >= 18
   1665 
   1666 
   1667 def canonical_evidence_bytes(value: Any) -> bytes:
   1668     return json.dumps(
   1669         value, ensure_ascii=True, separators=(",", ":"), sort_keys=True
   1670     ).encode("utf-8")
   1671 
   1672 
   1673 def evidence_contains_forbidden_key(value: Any) -> bool:
   1674     if isinstance(value, dict):
   1675         return any(
   1676             key.lower() in FORBIDDEN_EVIDENCE_KEYS
   1677             or evidence_contains_forbidden_key(item)
   1678             for key, item in value.items()
   1679         )
   1680     if isinstance(value, list):
   1681         return any(evidence_contains_forbidden_key(item) for item in value)
   1682     return False
   1683 
   1684 
   1685 def persona_invocation() -> dict[str, str]:
   1686     return {
   1687         "target": PERSONA_TEST_TARGET,
   1688         "identifier": PERSONA_TEST_IDENTIFIER,
   1689         "action": PERSONA_TEST_ACTION,
   1690         "configuration": PERSONA_TEST_CONFIGURATION,
   1691     }
   1692 
   1693 
   1694 def validate_persona_attempt_evidence(
   1695     value: Any,
   1696     suite: dict[str, Any],
   1697     *,
   1698     require_measured_network: bool,
   1699 ) -> dict[str, Any]:
   1700     keys = {
   1701         "schema",
   1702         "schema_version",
   1703         "test_invocation",
   1704         "source",
   1705         "app_build_sha256",
   1706         "simulator",
   1707         "run_id",
   1708         "persona_run_id",
   1709         "persona_alias",
   1710         "attempt_id",
   1711         "attempt_order",
   1712         "flow",
   1713         "expected_failure",
   1714         "public_identity_sha256",
   1715         "endpoint_policy_sha256",
   1716         "ui_observation",
   1717         "network_observation",
   1718         "accessibility",
   1719         "artifact_digests",
   1720     }
   1721     attempt = exact_keys(value, keys, "persona attempt evidence")
   1722     if evidence_contains_forbidden_key(attempt):
   1723         raise ValueError("persona attempt evidence contains a forbidden field")
   1724     if (
   1725         attempt["schema"] != PERSONA_ATTEMPT_SCHEMA
   1726         or attempt["schema_version"] != 1
   1727         or attempt["test_invocation"] != persona_invocation()
   1728         or not lowercase_hex(attempt["app_build_sha256"], 64)
   1729         or not lowercase_hex(attempt["public_identity_sha256"], 64)
   1730         or attempt["public_identity_sha256"] == "0" * 64
   1731         or not lowercase_hex(attempt["endpoint_policy_sha256"], 64)
   1732         or not re.fullmatch(
   1733             r"[a-z0-9][a-z0-9-]{6,62}[a-z0-9]", attempt["run_id"]
   1734         )
   1735     ):
   1736         raise ValueError("persona attempt evidence header is invalid")
   1737     source = exact_keys(attempt["source"], {"commit", "tree"}, "attempt source")
   1738     if not lowercase_hex(source["commit"], 40) or not lowercase_hex(
   1739         source["tree"], 40
   1740     ):
   1741         raise ValueError("persona attempt source identity is invalid")
   1742     simulator = exact_keys(
   1743         attempt["simulator"], {"udid", "os", "architecture"}, "attempt simulator"
   1744     )
   1745     if (
   1746         not isinstance(simulator["udid"], str)
   1747         or re.fullmatch(r"[A-F0-9-]{36}", simulator["udid"]) is None
   1748         or not valid_ios_runtime(simulator["os"])
   1749         or simulator["architecture"] != "arm64"
   1750     ):
   1751         raise ValueError("persona attempt simulator is invalid")
   1752     expected_attempts = {
   1753         candidate["id"]: (persona, candidate)
   1754         for persona in suite["personas"]
   1755         for candidate in persona["attempts"]
   1756     }
   1757     expected = expected_attempts.get(attempt["attempt_id"])
   1758     if expected is None:
   1759         raise ValueError("persona attempt identity is unknown")
   1760     expected_persona, expected_attempt = expected
   1761     alias = expected_persona["alias"]
   1762     if (
   1763         attempt["persona_alias"] != alias
   1764         or attempt["attempt_order"] != expected_attempt["order"]
   1765         or attempt["flow"] != expected_attempt["flow"]
   1766         or attempt["expected_failure"] != expected_attempt["expected_failure"]
   1767         or re.fullmatch(
   1768             rf"persona-{alias.lower()}-[0-9a-f]{{24}}", attempt["persona_run_id"]
   1769         )
   1770         is None
   1771     ):
   1772         raise ValueError("persona attempt binding is invalid")
   1773     ui = exact_keys(
   1774         attempt["ui_observation"],
   1775         {
   1776             "validation_attempted",
   1777             "validation_rejected",
   1778             "retry_attempts",
   1779             "relaunches",
   1780             "retention_verified",
   1781             "today_projection_verified",
   1782         },
   1783         "attempt UI observation",
   1784     )
   1785     validation = expected_attempt["expected_failure"] == "validation_recovery"
   1786     retry = expected_attempt["expected_failure"] == "transport_retry_relaunch"
   1787     if (
   1788         ui["validation_attempted"] is not validation
   1789         or ui["validation_rejected"] is not validation
   1790         or ui["retry_attempts"] != int(retry)
   1791         or ui["relaunches"] != int(retry)
   1792         or ui["retention_verified"] is not True
   1793         or ui["today_projection_verified"] is not True
   1794     ):
   1795         raise ValueError("persona attempt UI observation is invalid")
   1796     network = attempt["network_observation"]
   1797     if network == {"state": "pending_step_258"}:
   1798         if require_measured_network:
   1799             raise ValueError("persona attempt network evidence is not measured")
   1800     else:
   1801         network = exact_keys(
   1802             network,
   1803             {
   1804                 "state",
   1805                 "accepted_connections",
   1806                 "rejected_connections",
   1807                 "non_loopback_attempts",
   1808                 "subscriptions",
   1809                 "accepted_events",
   1810                 "accepted_uploads",
   1811                 "retrievals",
   1812                 "unintended_publications",
   1813                 "events_accepted_during_expected_failure",
   1814                 "final_candidate_data_loss",
   1815             },
   1816             "attempt network observation",
   1817         )
   1818         if network["state"] != "measured" or any(
   1819             type(network[key]) is not int or not 0 <= network[key] <= 4096
   1820             for key in network
   1821             if key != "state"
   1822         ):
   1823             raise ValueError("persona attempt network observation is invalid")
   1824         expected_media = int(attempt["flow"] == "PhotoUpdate")
   1825         if (
   1826             network["accepted_events"] != 1
   1827             or network["accepted_uploads"] != expected_media
   1828             or network["retrievals"] != expected_media
   1829             or network["non_loopback_attempts"] != 0
   1830             or network["unintended_publications"] != 0
   1831             or network["events_accepted_during_expected_failure"] != 0
   1832             or network["final_candidate_data_loss"] != 0
   1833         ):
   1834             raise ValueError("persona attempt measured result is invalid")
   1835     accessibility = exact_keys(
   1836         attempt["accessibility"],
   1837         {
   1838             "locale",
   1839             "content_size",
   1840             "reduce_motion",
   1841             "progressive_disclosure",
   1842             "labels_values_traits",
   1843             "keyboard_focus",
   1844             "visible_actions",
   1845             "voiceover_user_observed",
   1846         },
   1847         "attempt accessibility observation",
   1848     )
   1849     if (
   1850         accessibility["locale"] != "en_US"
   1851         or accessibility["content_size"]
   1852         != "accessibility-extra-extra-extra-large"
   1853         or accessibility["reduce_motion"] is not True
   1854         or any(
   1855             type(accessibility[key]) is not bool
   1856             for key in (
   1857                 "progressive_disclosure",
   1858                 "labels_values_traits",
   1859                 "keyboard_focus",
   1860                 "visible_actions",
   1861                 "voiceover_user_observed",
   1862             )
   1863         )
   1864         or accessibility["visible_actions"] is not True
   1865         or accessibility["voiceover_user_observed"] is not False
   1866         or accessibility["progressive_disclosure"]
   1867         is not (expected_persona["interaction_profile"] == "novice_progressive_disclosure")
   1868         or accessibility["keyboard_focus"]
   1869         is not (expected_persona["interaction_profile"] == "novice_accessibility_keyboard")
   1870     ):
   1871         raise ValueError("persona attempt accessibility evidence is invalid")
   1872     artifacts = attempt["artifact_digests"]
   1873     if not isinstance(artifacts, list) or len(artifacts) > 4:
   1874         raise ValueError("persona attempt artifact inventory is invalid")
   1875     roles: set[str] = set()
   1876     for artifact in artifacts:
   1877         artifact = exact_keys(artifact, {"role", "sha256"}, "attempt artifact")
   1878         if (
   1879             artifact["role"]
   1880             not in {"media_input", "uploaded_blob", "retrieved_blob", "ui_snapshot"}
   1881             or artifact["role"] in roles
   1882             or not lowercase_hex(artifact["sha256"], 64)
   1883         ):
   1884             raise ValueError("persona attempt artifact is invalid")
   1885         roles.add(artifact["role"])
   1886     return attempt
   1887 
   1888 
   1889 def exact_persona_test_node(value: Any) -> dict[str, Any]:
   1890     if not isinstance(value, dict) or set(value) != {
   1891         "devices",
   1892         "testNodes",
   1893         "testPlanConfigurations",
   1894     }:
   1895         raise ValueError("xcresult test inventory is invalid")
   1896     matches: list[dict[str, Any]] = []
   1897 
   1898     def visit(node: Any) -> None:
   1899         if not isinstance(node, dict):
   1900             raise ValueError("xcresult test node is invalid")
   1901         if node.get("nodeType") == "Test Case" and (
   1902             node.get("nodeIdentifier") == PERSONA_XCRESULT_NODE_IDENTIFIER
   1903             or node.get("nodeIdentifierURL") == PERSONA_XCRESULT_NODE_URL
   1904         ):
   1905             matches.append(node)
   1906         children = node.get("children", [])
   1907         if not isinstance(children, list):
   1908             raise ValueError("xcresult child inventory is invalid")
   1909         for child in children:
   1910             visit(child)
   1911 
   1912     nodes = value["testNodes"]
   1913     if not isinstance(nodes, list):
   1914         raise ValueError("xcresult test inventory is invalid")
   1915     for node in nodes:
   1916         visit(node)
   1917     if len(matches) != 1:
   1918         raise ValueError("xcresult exact persona test is unavailable")
   1919     match = matches[0]
   1920     if (
   1921         match.get("nodeIdentifier") != PERSONA_XCRESULT_NODE_IDENTIFIER
   1922         or match.get("nodeIdentifierURL") != PERSONA_XCRESULT_NODE_URL
   1923         or match.get("name") != "testLocalSocialDeterministicPersonas()"
   1924         or match.get("nodeType") != "Test Case"
   1925         or match.get("result") != "Passed"
   1926     ):
   1927         raise ValueError("xcresult exact persona test did not pass")
   1928     return match
   1929 
   1930 
   1931 def run_json_command_bounded(command: list[str], maximum: int) -> Any:
   1932     process = subprocess.Popen(command, stdout=subprocess.PIPE)
   1933     assert process.stdout is not None
   1934     raw = process.stdout.read(maximum + 1)
   1935     process.stdout.close()
   1936     if len(raw) > maximum:
   1937         process.kill()
   1938         process.wait()
   1939         raise ValueError("command JSON output exceeds its byte bound")
   1940     return_code = process.wait()
   1941     if return_code != 0:
   1942         raise subprocess.CalledProcessError(return_code, command)
   1943     if not raw:
   1944         raise ValueError("command JSON output exceeds its byte bound")
   1945     return json.loads(raw, object_pairs_hook=strict_object)
   1946 
   1947 
   1948 def exported_attachment_inventory(path: Path) -> set[str]:
   1949     inventory = bounded_directory_inventory(
   1950         path,
   1951         len(PERSONA_ATTACHMENT_NAMES) + 1,
   1952         255,
   1953     )
   1954     if any(kind != 1 or b"/" in relative for kind, relative, _ in inventory):
   1955         raise ValueError("xcresult attachment export inventory is invalid")
   1956     return {relative.decode("utf-8") for _, relative, _ in inventory}
   1957 
   1958 
   1959 def xcresult_attachment_attempt_id(name: object) -> str | None:
   1960     if not isinstance(name, str):
   1961         return None
   1962     matched = PERSONA_XCRESULT_ATTACHMENT_NAME.fullmatch(name)
   1963     return matched.group(1) if matched is not None else None
   1964 
   1965 
   1966 def load_exported_persona_attachments(
   1967     export_directory: Path,
   1968     suite: dict[str, Any],
   1969     *,
   1970     require_measured_network: bool,
   1971 ) -> list[tuple[bytes, dict[str, Any]]]:
   1972     inventory = exported_attachment_inventory(export_directory)
   1973     manifest_raw, manifest_value = read_json_bounded(
   1974         export_directory / "manifest.json", MAX_XCRESULT_JSON_BYTES
   1975     )
   1976     del manifest_raw
   1977     if not isinstance(manifest_value, list) or len(manifest_value) != 1:
   1978         raise ValueError("xcresult attachment manifest is invalid")
   1979     group = exact_keys(
   1980         manifest_value[0],
   1981         {"testIdentifier", "testIdentifierURL", "attachments"},
   1982         "xcresult attachment group",
   1983     )
   1984     if (
   1985         group["testIdentifier"] != PERSONA_XCRESULT_NODE_IDENTIFIER
   1986         or group["testIdentifierURL"] != PERSONA_XCRESULT_NODE_URL
   1987         or not isinstance(group["attachments"], list)
   1988         or len(group["attachments"]) != 15
   1989     ):
   1990         raise ValueError("xcresult attachment test binding is invalid")
   1991     attachments_by_name: dict[str, tuple[bytes, dict[str, Any]]] = {}
   1992     exported_names: set[str] = set()
   1993     total_bytes = 0
   1994     allowed_manifest_keys = {
   1995         "exportedFileName",
   1996         "suggestedHumanReadableName",
   1997         "isAssociatedWithFailure",
   1998         "configurationName",
   1999         "deviceName",
   2000         "deviceId",
   2001         "timestamp",
   2002         "repetitionNumber",
   2003         "arguments",
   2004     }
   2005     for row_value in group["attachments"]:
   2006         if not isinstance(row_value, dict) or not {
   2007             "exportedFileName",
   2008             "suggestedHumanReadableName",
   2009             "isAssociatedWithFailure",
   2010             "configurationName",
   2011             "deviceName",
   2012             "deviceId",
   2013         }.issubset(row_value) or not set(row_value).issubset(allowed_manifest_keys):
   2014             raise ValueError("xcresult attachment row is invalid")
   2015         exported = row_value["exportedFileName"]
   2016         name = row_value["suggestedHumanReadableName"]
   2017         attempt_id = xcresult_attachment_attempt_id(name)
   2018         if attempt_id is None:
   2019             raise ValueError("xcresult attempt attachment identity is invalid")
   2020         canonical_name = f"radroots-local-social-{attempt_id}.json"
   2021         if (
   2022             canonical_name not in PERSONA_ATTACHMENT_NAMES
   2023             or canonical_name in attachments_by_name
   2024             or not isinstance(exported, str)
   2025             or exported in exported_names
   2026             or not 1 <= len(exported.encode("utf-8")) <= 255
   2027             or Path(exported).name != exported
   2028             or row_value["isAssociatedWithFailure"] is not False
   2029             or row_value["configurationName"] != "Test Scheme Action"
   2030             or not isinstance(row_value["deviceName"], str)
   2031             or not row_value["deviceName"]
   2032             or not isinstance(row_value["deviceId"], str)
   2033         ):
   2034             raise ValueError("xcresult attempt attachment identity is invalid")
   2035         exported_names.add(exported)
   2036         path = export_directory / exported
   2037         if path.is_symlink() or not path.is_file():
   2038             raise ValueError("xcresult attempt attachment is not a regular file")
   2039         raw, value = read_json_bounded(path, MAX_PERSONA_ATTACHMENT_BYTES)
   2040         if raw != canonical_evidence_bytes(value):
   2041             raise ValueError("persona attempt attachment is noncanonical")
   2042         attempt = validate_persona_attempt_evidence(
   2043             value, suite, require_measured_network=require_measured_network
   2044         )
   2045         if attempt_id != attempt["attempt_id"]:
   2046             raise ValueError("xcresult attachment name does not bind its attempt")
   2047         total_bytes += len(raw)
   2048         if total_bytes > MAX_PERSONA_ATTACHMENTS_BYTES:
   2049             raise ValueError("xcresult attempt attachments exceed their aggregate bound")
   2050         attachments_by_name[canonical_name] = (raw, attempt)
   2051     if tuple(sorted(attachments_by_name)) != tuple(sorted(PERSONA_ATTACHMENT_NAMES)):
   2052         raise ValueError("xcresult persona attachment inventory is incomplete")
   2053     if inventory != {"manifest.json", *exported_names}:
   2054         raise ValueError("xcresult attachment export inventory is invalid")
   2055     return [attachments_by_name[name] for name in PERSONA_ATTACHMENT_NAMES]
   2056 
   2057 
   2058 def extract_persona_attempt_attachments(
   2059     result_bundle: Path,
   2060     suite: dict[str, Any],
   2061     *,
   2062     require_measured_network: bool,
   2063 ) -> list[tuple[bytes, dict[str, Any]]]:
   2064     tests = run_json_command_bounded(
   2065         [
   2066             "xcrun",
   2067             "xcresulttool",
   2068             "get",
   2069             "test-results",
   2070             "tests",
   2071             "--path",
   2072             str(result_bundle),
   2073         ],
   2074         MAX_XCRESULT_JSON_BYTES,
   2075     )
   2076     exact_persona_test_node(tests)
   2077     with tempfile.TemporaryDirectory() as directory:
   2078         export_directory = Path(directory)
   2079         subprocess.run(
   2080             [
   2081                 "xcrun",
   2082                 "xcresulttool",
   2083                 "export",
   2084                 "attachments",
   2085                 "--test-id",
   2086                 PERSONA_XCRESULT_NODE_URL,
   2087                 "--path",
   2088                 str(result_bundle),
   2089                 "--output-path",
   2090                 str(export_directory),
   2091             ],
   2092             check=True,
   2093         )
   2094         return load_exported_persona_attachments(
   2095             export_directory,
   2096             suite,
   2097             require_measured_network=require_measured_network,
   2098         )
   2099 
   2100 
   2101 def reconstruct_persona_result_v2(
   2102     suite: dict[str, Any],
   2103     attachments: list[tuple[bytes, dict[str, Any]]],
   2104     *,
   2105     fixture_sha256: str,
   2106     fixture_schema_sha256: str,
   2107     attempt_schema_sha256: str,
   2108     result_schema_sha256: str,
   2109     result_bundle_sha256: str,
   2110     forward_repairs: list[str],
   2111 ) -> dict[str, Any]:
   2112     if len(attachments) != 15:
   2113         raise ValueError("persona result requires exactly 15 attempt attachments")
   2114     attempts = [
   2115         validate_persona_attempt_evidence(value, suite, require_measured_network=True)
   2116         for _, value in attachments
   2117     ]
   2118     expected_ids = [
   2119         candidate["id"]
   2120         for persona in suite["personas"]
   2121         for candidate in persona["attempts"]
   2122     ]
   2123     if [attempt["attempt_id"] for attempt in attempts] != expected_ids:
   2124         raise ValueError("persona result attempt inventory is not exact")
   2125     first = attempts[0]
   2126     shared_fields = (
   2127         "test_invocation",
   2128         "source",
   2129         "app_build_sha256",
   2130         "simulator",
   2131         "run_id",
   2132         "endpoint_policy_sha256",
   2133     )
   2134     if any(
   2135         attempt[field] != first[field]
   2136         for attempt in attempts[1:]
   2137         for field in shared_fields
   2138     ):
   2139         raise ValueError("persona attempt evidence spans multiple run identities")
   2140     for digest in (
   2141         fixture_sha256,
   2142         fixture_schema_sha256,
   2143         attempt_schema_sha256,
   2144         result_schema_sha256,
   2145         result_bundle_sha256,
   2146     ):
   2147         if not lowercase_hex(digest, 64):
   2148             raise ValueError("persona result digest identity is invalid")
   2149     if (
   2150         not isinstance(forward_repairs, list)
   2151         or len(forward_repairs) > 16
   2152         or len(set(forward_repairs)) != len(forward_repairs)
   2153         or any(not lowercase_hex(revision, 40) for revision in forward_repairs)
   2154     ):
   2155         raise ValueError("persona result forward-repair inventory is invalid")
   2156     identity_by_persona: dict[str, str] = {}
   2157     persona_rows = []
   2158     for persona in suite["personas"]:
   2159         alias = persona["alias"]
   2160         rows = [attempt for attempt in attempts if attempt["persona_alias"] == alias]
   2161         identities = {row["public_identity_sha256"] for row in rows}
   2162         if len(rows) != 3 or len(identities) != 1:
   2163             raise ValueError("persona result identity reuse is invalid")
   2164         identity = identities.pop()
   2165         identity_by_persona[alias] = identity
   2166         subscriptions = sum(
   2167             row["network_observation"]["subscriptions"] for row in rows
   2168         )
   2169         if subscriptions < 1:
   2170             raise ValueError("persona result is missing a subscription")
   2171         persona_rows.append(
   2172             {
   2173                 "alias": alias,
   2174                 "public_identity_sha256": identity,
   2175                 "subscriptions": subscriptions,
   2176                 "attempt_ids": [row["attempt_id"] for row in rows],
   2177             }
   2178         )
   2179     if len(set(identity_by_persona.values())) != 5:
   2180         raise ValueError("persona result identities are not distinct")
   2181     network_rows = [attempt["network_observation"] for attempt in attempts]
   2182     accepted_events = sum(row["accepted_events"] for row in network_rows)
   2183     flow_counts = {
   2184         flow: sum(attempt["flow"] == flow for attempt in attempts)
   2185         for flow in FLOW_KINDS
   2186     }
   2187     event_kind_counts = {
   2188         str(kind): sum(
   2189             FLOW_KINDS[attempt["flow"]] == kind
   2190             and attempt["network_observation"]["accepted_events"] == 1
   2191             for attempt in attempts
   2192         )
   2193         for kind in (1, 31923, 30402)
   2194     }
   2195     result = {
   2196         "schema": PERSONA_RESULT_V2_SCHEMA,
   2197         "schema_version": 2,
   2198         "run_id": first["run_id"],
   2199         "test_invocation": first["test_invocation"],
   2200         "source": first["source"],
   2201         "app_build_sha256": first["app_build_sha256"],
   2202         "endpoint_policy_sha256": first["endpoint_policy_sha256"],
   2203         "fixture_sha256": fixture_sha256,
   2204         "fixture_schema_sha256": fixture_schema_sha256,
   2205         "attempt_schema_sha256": attempt_schema_sha256,
   2206         "result_schema_sha256": result_schema_sha256,
   2207         "simulator": first["simulator"],
   2208         "result_bundle_sha256": result_bundle_sha256,
   2209         "attachments": [
   2210             {
   2211                 "attempt_id": attempt["attempt_id"],
   2212                 "sha256": hashlib.sha256(raw).hexdigest(),
   2213             }
   2214             for raw, attempt in attachments
   2215         ],
   2216         "personas": persona_rows,
   2217         "flow_counts": flow_counts,
   2218         "accepted_events": accepted_events,
   2219         "event_kind_counts": event_kind_counts,
   2220         "accepted_uploads": sum(row["accepted_uploads"] for row in network_rows),
   2221         "retrievals": sum(row["retrievals"] for row in network_rows),
   2222         "distinct_identities": len(set(identity_by_persona.values())),
   2223         "unknown_attempts": len(
   2224             set(expected_ids) - {row["attempt_id"] for row in attempts}
   2225         ),
   2226         "duplicate_attempts": len(attempts) - len({row["attempt_id"] for row in attempts}),
   2227         "expected_failure_rejections": sum(
   2228             attempt["ui_observation"]["validation_rejected"]
   2229             or attempt["ui_observation"]["retry_attempts"] > 0
   2230             for attempt in attempts
   2231         ),
   2232         "events_accepted_during_expected_failures": sum(
   2233             row["events_accepted_during_expected_failure"] for row in network_rows
   2234         ),
   2235         "accepted_connections": sum(row["accepted_connections"] for row in network_rows),
   2236         "rejected_connections": sum(row["rejected_connections"] for row in network_rows),
   2237         "non_loopback_attempts": sum(row["non_loopback_attempts"] for row in network_rows),
   2238         "unintended_publications": sum(
   2239             row["unintended_publications"] for row in network_rows
   2240         ),
   2241         "final_candidate_data_loss": sum(
   2242             row["final_candidate_data_loss"] for row in network_rows
   2243         ),
   2244         "accessibility": {
   2245             "locale": "en_US",
   2246             "content_size": "accessibility-extra-extra-extra-large",
   2247             "reduce_motion": True,
   2248             "semantic_attempts": sum(
   2249                 attempt["accessibility"]["labels_values_traits"] for attempt in attempts
   2250             ),
   2251             "keyboard_focus_attempts": sum(
   2252                 attempt["accessibility"]["keyboard_focus"] for attempt in attempts
   2253             ),
   2254             "voiceover_user_observed": False,
   2255         },
   2256         "forward_repairs": forward_repairs,
   2257         "complete_matrix_rerun": True,
   2258     }
   2259     if (
   2260         result["flow_counts"] != {flow: 3 for flow in FLOW_KINDS}
   2261         or result["accepted_events"] != 15
   2262         or result["event_kind_counts"] != {"1": 9, "31923": 3, "30402": 3}
   2263         or result["accepted_uploads"] != 3
   2264         or result["retrievals"] != 3
   2265         or result["distinct_identities"] != 5
   2266         or result["unknown_attempts"] != 0
   2267         or result["duplicate_attempts"] != 0
   2268         or result["expected_failure_rejections"] != 2
   2269         or result["events_accepted_during_expected_failures"] != 0
   2270         or result["accepted_connections"] < 1
   2271         or result["non_loopback_attempts"] != 0
   2272         or result["unintended_publications"] != 0
   2273         or result["final_candidate_data_loss"] != 0
   2274         or result["accessibility"]["semantic_attempts"] < 3
   2275         or result["accessibility"]["keyboard_focus_attempts"] != 3
   2276     ):
   2277         raise ValueError("persona result reconstructed outcome is invalid")
   2278     return result
   2279 
   2280 
   2281 def validate_persona_result(
   2282     value: Any,
   2283     suite: dict[str, Any],
   2284     fixture_sha256: str,
   2285     fixture_schema_sha256: str,
   2286     result_schema_sha256: str,
   2287 ) -> dict[str, Any]:
   2288     keys = {
   2289         "schema",
   2290         "schema_version",
   2291         "run_id",
   2292         "source_commit",
   2293         "source_tree",
   2294         "fixture_sha256",
   2295         "fixture_schema_sha256",
   2296         "result_schema_sha256",
   2297         "simulator",
   2298         "result_bundle_sha256",
   2299         "evidence_sha256",
   2300         "personas",
   2301         "flow_counts",
   2302         "accepted_events",
   2303         "event_kind_counts",
   2304         "accepted_uploads",
   2305         "retrievals",
   2306         "distinct_identities",
   2307         "unknown_attempts",
   2308         "duplicate_attempts",
   2309         "expected_failure_rejections",
   2310         "events_accepted_during_expected_failures",
   2311         "production_network_contacts",
   2312         "unintended_publications",
   2313         "final_candidate_data_loss",
   2314         "accessibility",
   2315         "forward_repairs",
   2316         "complete_matrix_rerun",
   2317     }
   2318     result = exact_keys(value, keys, "persona result")
   2319     if (
   2320         result["schema"] != "tera.ios.local-social.persona-results.v1"
   2321         or result["schema_version"] != 1
   2322         or not re.fullmatch(r"[a-z0-9][a-z0-9-]{6,62}[a-z0-9]", result["run_id"])
   2323         or not lowercase_hex(result["source_commit"], 40)
   2324         or not lowercase_hex(result["source_tree"], 40)
   2325     ):
   2326         raise ValueError("persona result header is invalid")
   2327     expected_digests = {
   2328         "fixture_sha256": fixture_sha256,
   2329         "fixture_schema_sha256": fixture_schema_sha256,
   2330         "result_schema_sha256": result_schema_sha256,
   2331     }
   2332     if any(result[key] != digest for key, digest in expected_digests.items()):
   2333         raise ValueError("persona result contract digest is invalid")
   2334     if not lowercase_hex(result["result_bundle_sha256"], 64) or not lowercase_hex(
   2335         result["evidence_sha256"], 64
   2336     ):
   2337         raise ValueError("persona result evidence digest is invalid")
   2338     simulator = exact_keys(
   2339         result["simulator"], {"udid", "os", "architecture"}, "simulator"
   2340     )
   2341     if (
   2342         not isinstance(simulator["udid"], str)
   2343         or re.fullmatch(r"[A-F0-9-]{36}", simulator["udid"]) is None
   2344         or not valid_ios_runtime(simulator["os"])
   2345         or simulator["architecture"] != "arm64"
   2346     ):
   2347         raise ValueError("persona result simulator is invalid")
   2348     accessibility = exact_keys(
   2349         result["accessibility"],
   2350         {
   2351             "locale",
   2352             "content_size",
   2353             "reduce_motion",
   2354             "semantic_audit",
   2355             "voiceover_user_observed",
   2356         },
   2357         "accessibility result",
   2358     )
   2359     if accessibility != {
   2360         "locale": "en_US",
   2361         "content_size": "accessibility-extra-extra-extra-large",
   2362         "reduce_motion": True,
   2363         "semantic_audit": "passed",
   2364         "voiceover_user_observed": False,
   2365     }:
   2366         raise ValueError("persona accessibility result is invalid")
   2367     repairs = result["forward_repairs"]
   2368     if (
   2369         not isinstance(repairs, list)
   2370         or len(repairs) > 16
   2371         or len(set(repairs)) != len(repairs)
   2372         or any(not lowercase_hex(commit, 40) for commit in repairs)
   2373         or result["complete_matrix_rerun"] is not True
   2374     ):
   2375         raise ValueError("persona rerun evidence is invalid")
   2376     expected_scalars = {
   2377         "accepted_events": 15,
   2378         "accepted_uploads": 3,
   2379         "retrievals": 3,
   2380         "distinct_identities": 5,
   2381         "unknown_attempts": 0,
   2382         "duplicate_attempts": 0,
   2383         "expected_failure_rejections": 1,
   2384         "events_accepted_during_expected_failures": 0,
   2385         "production_network_contacts": 0,
   2386         "unintended_publications": 0,
   2387         "final_candidate_data_loss": 0,
   2388     }
   2389     if any(result.get(key) != expected for key, expected in expected_scalars.items()):
   2390         raise ValueError("persona result totals are invalid")
   2391     if result["flow_counts"] != {flow: 3 for flow in FLOW_KINDS} or result[
   2392         "event_kind_counts"
   2393     ] != {"1": 9, "31923": 3, "30402": 3}:
   2394         raise ValueError("persona result event inventory is invalid")
   2395     if not isinstance(result["personas"], list) or len(result["personas"]) != 5:
   2396         raise ValueError("persona result inventory is invalid")
   2397     identities = set()
   2398     for persona, expected in zip(result["personas"], suite["personas"], strict=True):
   2399         exact_keys(
   2400             persona,
   2401             {"alias", "identity_sha256", "subscriptions", "attempts"},
   2402             "persona result row",
   2403         )
   2404         if (
   2405             persona["alias"] != expected["alias"]
   2406             or not lowercase_hex(persona["identity_sha256"], 64)
   2407             or persona["identity_sha256"] == "0" * 64
   2408             or type(persona["subscriptions"]) is not int
   2409             or not 1 <= persona["subscriptions"] <= 4096
   2410             or not isinstance(persona["attempts"], list)
   2411             or len(persona["attempts"]) != 3
   2412         ):
   2413             raise ValueError("persona result row is invalid")
   2414         identities.add(persona["identity_sha256"])
   2415         for attempt, expected_attempt in zip(
   2416             persona["attempts"], expected["attempts"], strict=True
   2417         ):
   2418             exact_keys(
   2419                 attempt,
   2420                 {
   2421                     "id",
   2422                     "flow",
   2423                     "event_kind",
   2424                     "accepted",
   2425                     "expected_failure_rejections",
   2426                 },
   2427                 "attempt result row",
   2428             )
   2429             if (
   2430                 attempt["id"] != expected_attempt["id"]
   2431                 or attempt["flow"] != expected_attempt["flow"]
   2432                 or attempt["event_kind"] != FLOW_KINDS[expected_attempt["flow"]]
   2433                 or attempt["accepted"] is not True
   2434                 or attempt["expected_failure_rejections"]
   2435                 != int(
   2436                     expected_attempt["expected_failure"]
   2437                     == "transport_retry_relaunch"
   2438                 )
   2439             ):
   2440                 raise ValueError("attempt result row is invalid")
   2441     if len(identities) != 5:
   2442         raise ValueError("persona result identities are not distinct")
   2443     return result
   2444 
   2445 
   2446 def verify_persona(arguments: argparse.Namespace) -> int:
   2447     fixture_raw, suite = load_persona_suite(Path(arguments.fixture).resolve())
   2448     fixture_schema_raw, fixture_schema = load_schema_file(
   2449         Path(arguments.fixture_schema).resolve(),
   2450         "https://radroots.org/schemas/ios/local-social-personas.v1.schema.json",
   2451     )
   2452     attempt_schema_raw, attempt_schema = load_schema_file(
   2453         Path(arguments.attempt_schema).resolve(),
   2454         "https://radroots.org/schemas/ios/local-social-persona-attempt-evidence.v1.schema.json",
   2455     )
   2456     result_schema_raw, result_schema = load_schema_file(
   2457         Path(arguments.result_v2_schema).resolve(),
   2458         "https://radroots.org/schemas/ios/local-social-persona-results.v2.schema.json",
   2459     )
   2460     validate_schema_instance(fixture_schema, suite, "persona fixture")
   2461     evidence_path = Path(arguments.evidence).resolve()
   2462     _, evidence_value = read_json(evidence_path)
   2463     evidence = validate_persona_evidence(evidence_value, suite)
   2464     result_bundle = Path(arguments.result_bundle).resolve()
   2465     if not result_bundle.is_dir():
   2466         raise ValueError("XCUITest result bundle is unavailable")
   2467     if not lowercase_hex(arguments.source_commit, 40) or not lowercase_hex(
   2468         arguments.source_tree, 40
   2469     ):
   2470         raise ValueError("source identity is invalid")
   2471     attachments = extract_persona_attempt_attachments(
   2472         result_bundle, suite, require_measured_network=True
   2473     )
   2474     for _, attempt in attachments:
   2475         validate_schema_instance(attempt_schema, attempt, "persona attempt evidence")
   2476     simulator = simulator_metadata(arguments.simulator_id, result_bundle)
   2477     result = reconstruct_persona_result_v2(
   2478         suite,
   2479         attachments,
   2480         fixture_sha256=hashlib.sha256(fixture_raw).hexdigest(),
   2481         fixture_schema_sha256=hashlib.sha256(fixture_schema_raw).hexdigest(),
   2482         attempt_schema_sha256=hashlib.sha256(attempt_schema_raw).hexdigest(),
   2483         result_schema_sha256=hashlib.sha256(result_schema_raw).hexdigest(),
   2484         result_bundle_sha256=directory_digest(result_bundle),
   2485         forward_repairs=arguments.forward_repair_commit,
   2486     )
   2487     validate_schema_instance(result_schema, result, "persona v2 result")
   2488     if (
   2489         result["run_id"] != arguments.run_id
   2490         or result["source"]
   2491         != {"commit": arguments.source_commit, "tree": arguments.source_tree}
   2492         or result["simulator"] != simulator
   2493         or result["accepted_events"] != evidence["accepted_events"]
   2494         or result["accepted_uploads"] != evidence["accepted_uploads"]
   2495         or result["retrievals"] != evidence["retrievals"]
   2496         or result["non_loopback_attempts"] != evidence["non_loopback_attempts"]
   2497         or result["unintended_publications"] != evidence["unintended_publications"]
   2498         or result["final_candidate_data_loss"] != evidence["final_candidate_data_loss"]
   2499         or result["accepted_connections"] > evidence["accepted_connections"]
   2500         or result["rejected_connections"] > evidence["rejected_connections"]
   2501     ):
   2502         raise ValueError("persona aggregate does not match fixture evidence")
   2503     output = Path(arguments.output).resolve()
   2504     output.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8")
   2505     raw, reloaded = read_json(output)
   2506     if raw != (json.dumps(reloaded, indent=2) + "\n").encode("utf-8") or reloaded != result:
   2507         raise ValueError("persona v2 result is noncanonical")
   2508     print(
   2509         "local-social measured persona result verified: "
   2510         f"{hashlib.sha256(raw).hexdigest()}"
   2511     )
   2512     return 0
   2513 
   2514 
   2515 def verify_persona_result_file(
   2516     path: Path,
   2517     suite: dict[str, Any],
   2518     fixture_sha256: str,
   2519     fixture_schema_sha256: str,
   2520     result_schema_sha256: str,
   2521 ) -> dict[str, Any]:
   2522     raw, value = read_json(path)
   2523     canonical = (json.dumps(value, indent=2) + "\n").encode("utf-8")
   2524     if raw != canonical:
   2525         raise ValueError("persona result is noncanonical")
   2526     return validate_persona_result(
   2527         value,
   2528         suite,
   2529         fixture_sha256,
   2530         fixture_schema_sha256,
   2531         result_schema_sha256,
   2532     )
   2533 
   2534 
   2535 def verify_persona_result(arguments: argparse.Namespace) -> int:
   2536     fixture_raw, suite = load_persona_suite(Path(arguments.fixture).resolve())
   2537     fixture_schema_raw, fixture_schema = load_schema_file(
   2538         Path(arguments.fixture_schema).resolve(),
   2539         "https://radroots.org/schemas/ios/local-social-personas.v1.schema.json",
   2540     )
   2541     result_schema_raw, result_schema = load_schema_file(
   2542         Path(arguments.result_schema).resolve(),
   2543         "https://radroots.org/schemas/ios/local-social-persona-results.v1.schema.json",
   2544     )
   2545     validate_schema_instance(fixture_schema, suite, "persona fixture")
   2546     result = verify_persona_result_file(
   2547         Path(arguments.result).resolve(),
   2548         suite,
   2549         hashlib.sha256(fixture_raw).hexdigest(),
   2550         hashlib.sha256(fixture_schema_raw).hexdigest(),
   2551         hashlib.sha256(result_schema_raw).hexdigest(),
   2552     )
   2553     validate_schema_instance(result_schema, result, "persona v1 result")
   2554     print("local-social persona result contract verified")
   2555     return 0
   2556 
   2557 
   2558 def parser() -> argparse.ArgumentParser:
   2559     root = argparse.ArgumentParser()
   2560     commands = root.add_subparsers(dest="command", required=True)
   2561     serve_command = commands.add_parser("serve")
   2562     serve_command.add_argument("--relay-port", type=int, required=True)
   2563     serve_command.add_argument("--blossom-port", type=int, required=True)
   2564     serve_command.add_argument("--evidence", required=True)
   2565     serve_command.add_argument("--ready", required=True)
   2566     serve_command.add_argument("--control", required=True)
   2567     serve_command.add_argument("--persona-fixture")
   2568     verify_command = commands.add_parser("verify")
   2569     verify_command.add_argument("--evidence", required=True)
   2570     accessibility_command = commands.add_parser("verify-accessibility")
   2571     accessibility_command.add_argument("--evidence", required=True)
   2572     fixture_command = commands.add_parser("verify-persona-fixture")
   2573     fixture_command.add_argument("--fixture", required=True)
   2574     fixture_command.add_argument("--fixture-schema", required=True)
   2575     fixture_command.add_argument("--result-schema", required=True)
   2576     fixture_command.add_argument("--attempt-schema", required=True)
   2577     fixture_command.add_argument("--result-v2-schema", required=True)
   2578     bud11_command = commands.add_parser("verify-bud11-corpus")
   2579     bud11_command.add_argument("--corpus", required=True)
   2580     bud11_command.add_argument("--schema", required=True)
   2581     persona_command = commands.add_parser("verify-persona")
   2582     persona_command.add_argument("--fixture", required=True)
   2583     persona_command.add_argument("--fixture-schema", required=True)
   2584     persona_command.add_argument("--result-schema", required=True)
   2585     persona_command.add_argument("--attempt-schema", required=True)
   2586     persona_command.add_argument("--result-v2-schema", required=True)
   2587     persona_command.add_argument("--evidence", required=True)
   2588     persona_command.add_argument("--result-bundle", required=True)
   2589     persona_command.add_argument("--output", required=True)
   2590     persona_command.add_argument("--source-commit", required=True)
   2591     persona_command.add_argument("--source-tree", required=True)
   2592     persona_command.add_argument("--run-id", required=True)
   2593     persona_command.add_argument("--simulator-id", required=True)
   2594     persona_command.add_argument("--forward-repair-commit", action="append", default=[])
   2595     result_command = commands.add_parser("verify-persona-result")
   2596     result_command.add_argument("--fixture", required=True)
   2597     result_command.add_argument("--fixture-schema", required=True)
   2598     result_command.add_argument("--result-schema", required=True)
   2599     result_command.add_argument("--result", required=True)
   2600     return root
   2601 
   2602 
   2603 def main() -> int:
   2604     verify_toolchain_identity()
   2605     arguments = parser().parse_args()
   2606     if arguments.command == "serve":
   2607         return serve(arguments)
   2608     if arguments.command == "verify":
   2609         return verify(arguments)
   2610     if arguments.command == "verify-accessibility":
   2611         return verify_accessibility(arguments)
   2612     if arguments.command == "verify-persona-fixture":
   2613         return verify_persona_fixture(arguments)
   2614     if arguments.command == "verify-bud11-corpus":
   2615         return verify_bud11_corpus(arguments)
   2616     if arguments.command == "verify-persona-result":
   2617         return verify_persona_result(arguments)
   2618     return verify_persona(arguments)
   2619 
   2620 
   2621 if __name__ == "__main__":
   2622     raise SystemExit(main())