mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-21 21:31:13 +00:00
e0be591b06
Architecture, contracts, and task amendments for the flight-route-driven preflight + cold-start origin feature (ADR-010). No source code touched in this commit; the implementation commits for AZ-489 / AZ-490 / AZ-419 land separately. * architecture.md: ADR-010, new Principle #14, amended Principle #11, external systems gain flights service + Mission Planner UI, data model gains Flight / Waypoint / TakeoffOrigin. * system-flows.md: F1 gains phase 0 (Flight resolve), F2 gains cold-start ladder, F7 gains mid-flight bounded-delta GPS gate. * glossary.md: Flight, Flights API, Mid-flight bounded-delta GPS gate, Mission Planner UI, Takeoff origin, Waypoint. * C10: description + cache_provisioner + manifest_verifier bumped to v1.1 carrying takeoff_origin + flight_id in the manifest hash. * C12: description updated + new flights_api_client.md contract v1.0. * C5: description + state_estimator_protocol bumped to v1.1 with set_takeoff_origin + 3-clause spoof-promotion gate. * AZ-323/324/325/326/328/419 amended in place. AZ-490 spec created (C5 set_takeoff_origin entrypoint). * Dependencies table: 142 tasks / 478 pts / 15 forward edges (2 new tasks, 2 backward deps, 2 forward deps from AZ-419). * Leftovers cleared: 2026-05-11 Jira transition entries for AZ-355 and AZ-386 are deleted (Jira reconnected; both already transitioned in their respective implementation commits). Co-authored-by: Cursor <cursoragent@cursor.com>
11 KiB
11 KiB
Contract: ManifestVerifier (C10)
Type: Python Protocol (@runtime_checkable) — local in-process API.
Producer task: AZ-324_c10_manifest_verifier
Consumers:
- C5 State Estimator / takeoff-arming gate (F2 phase) — refuses to arm if
verify_manifestdoes not returnoutcome=pass. (E-C5 / AZ-249.) - C12 Operator Tooling — runs verify before flight handoff to surface drift between F1 build time and F2 takeoff (E-C12 / AZ-253).
- C13 FDR — emits a
manifest.verifyrecord on every airborne verify call (outcomefield gates downstream).
Purpose
ManifestVerifier is the read-only validator for the C10-produced cache Manifest. It is the takeoff trust anchor for AC-NEW-1 ("no engine deserialization at takeoff before manifest verify") and D-C10-3 ("SHA-256 content-hash gate over every shipped artifact"). At F2 takeoff, every artifact listed in the Manifest is re-hashed and compared to its recorded digest; any mismatch fails the verdict and prevents arming. The Ed25519 signature over the Manifest is verified against a pinned operator public key before any artifact is touched — defence-in-depth against a spliced Manifest pointing at attacker-chosen content hashes.
Public Surface
from pathlib import Path
from typing import Protocol, runtime_checkable
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
@runtime_checkable
class ManifestVerifier(Protocol):
"""Read-only verifier for a C10-produced Manifest.json.
Fail-closed: any deviation in signature, schema, or per-artifact hash
yields `VerificationResult(outcome=fail, ...)`. Never raises on a verify
failure — operators / takeoff arming code branch on `outcome`.
Raises only on resource errors (Manifest.json missing, key file
unreadable) — those are environment problems, not verify outcomes.
"""
def verify_manifest(
self,
*,
manifest_path: Path,
trusted_public_keys: tuple[Ed25519PublicKey, ...],
) -> VerificationResult: ...
DTOs
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
class VerifyOutcome(Enum):
PASS = "pass"
FAIL = "fail"
class VerifyFailReason(Enum):
MANIFEST_NOT_FOUND = "manifest_not_found"
SIGNATURE_NOT_FOUND = "signature_not_found"
SIGNATURE_INVALID = "signature_invalid"
UNTRUSTED_PUBLIC_KEY = "untrusted_public_key"
SCHEMA_VIOLATION = "schema_violation"
ARTIFACT_MISSING = "artifact_missing"
ARTIFACT_HASH_MISMATCH = "artifact_hash_mismatch"
TILES_COVERAGE_MISMATCH = "tiles_coverage_mismatch"
MANIFEST_SELF_HASH_MISMATCH = "manifest_self_hash_mismatch"
TAKEOFF_ORIGIN_INVALID = "takeoff_origin_invalid" # ADR-010: schema check on the LatLonAlt block
TAKEOFF_ORIGIN_OUT_OF_BBOX = "takeoff_origin_out_of_bbox" # ADR-010: origin must lie inside the cache bbox
@dataclass(frozen=True)
class ArtifactCheck:
relative_path: str
expected_sha256: str
actual_sha256: str | None # None if file missing
matched: bool
@dataclass(frozen=True)
class VerificationResult:
outcome: VerifyOutcome
fail_reasons: tuple[VerifyFailReason, ...]
fail_details: tuple[str, ...] # human-readable diagnostic per reason
signing_public_key_fingerprint: str | None # populated when signature parses, even if untrusted
per_artifact_checks: tuple[ArtifactCheck, ...]
takeoff_origin: LatLonAlt | None # ADR-010 + AZ-490: passed through from Manifest body; None when Manifest carries no origin
flight_id: UUID | None # ADR-010: provenance of which Flight produced the build
elapsed_ms: int
Invariants
| ID | Invariant | Why |
|---|---|---|
| MV-INV-1 | The verifier is fail-closed: any deviation produces outcome=FAIL with at least one VerifyFailReason; never returns PASS with non-empty fail_reasons. |
AC-NEW-1 / D-C10-3 — takeoff cannot arm on a partial verify. |
| MV-INV-2 | Signature verification happens BEFORE per-artifact hashing. If the signature is invalid or untrusted, no file content is read beyond the Manifest itself. | Defence-in-depth: a malicious Manifest must not trick the verifier into hashing attacker-chosen file paths. |
| MV-INV-3 | The Manifest's own Manifest.json.sha256 sidecar (written by AZ-323) must match sha256(Manifest.json); mismatch is MANIFEST_SELF_HASH_MISMATCH. |
The sidecar is the entry point of the chain of trust — drift here means tampering or atomic-write failure. |
| MV-INV-4 | Per-artifact paths are interpreted relative to manifest_path.parent; absolute paths in the Manifest are rejected as SCHEMA_VIOLATION. |
Prevents a malicious Manifest from pointing outside cache_root. |
| MV-INV-5 | tiles_coverage mismatch is reported separately from ARTIFACT_HASH_MISMATCH because tiles are hashed in aggregate (per AZ-323). The verifier re-derives the aggregate hash from a TileMetadataStore query if available, OR (in airborne F2 mode) treats the recorded tiles_coverage_sha256 as authoritative and only verifies the Manifest signature + non-tile artifacts. |
Airborne C5 may not load 100k per-tile rows just to arm; the trust chain is signature → manifest_hash → tiles_coverage_sha256. C12 / operator mode does the full re-derivation. |
| MV-INV-6 | The verifier never writes to disk, never opens network sockets, never calls C13. Telemetry is the caller's responsibility. | Read-only contract — composable in airborne C5 + operator C12 contexts without side-effect surprise. |
| MV-INV-7 | elapsed_ms is recorded for every call (pass or fail) so operators and C5 can observe drift in verify cost on slow disks. |
NFR for C10-PT-01's takeoff load budget. |
| MV-INV-8 | When the Manifest body carries a takeoff_origin, the verifier checks: (a) the LatLonAlt block is well-formed (-90 ≤ lat ≤ 90, -180 ≤ lon ≤ 180, alt finite) → TAKEOFF_ORIGIN_INVALID otherwise, (b) the lat/lon falls inside the Manifest's bbox → TAKEOFF_ORIGIN_OUT_OF_BBOX otherwise. When the Manifest body carries no takeoff_origin, the field is absent from VerificationResult (None) and no origin check runs. |
ADR-010: garbage / out-of-bbox origin must not silently propagate to C5.set_takeoff_origin. |
| MV-INV-9 | takeoff_origin is surfaced on VerificationResult even on FAIL outcomes when the Manifest body parsed (so caller can inspect what was attempted), but the takeoff-arming gate only consumes it on PASS. |
Diagnostics — operators can see "your origin was X and that's why we rejected it". |
Non-Goals
- Signature production — owned by AZ-323's
ManifestSigner. The verifier never signs. - Cache repair — the verifier reports failures; rebuild is owned by AZ-325 (the orchestrator).
- Trusted-key distribution / revocation —
trusted_public_keysis supplied by the caller; this contract does not define a key registry. - Coverage check (orphan files in cache_root) — owned by AZ-325 (
ManifestCoverageError); the verifier checks "every Manifest entry exists and matches", not "every cache_root file is in the Manifest". - Rollback to prior-good Manifest — out of scope; caller decides next action on
FAIL.
Versioning
- v1.0.0 — initial Protocol surface (this document).
- Breaking changes — adding a required argument, removing a
VerifyFailReason, changing semantics of an existing one — bump major. - Additive changes — new
VerifyFailReasonvalue, new optional kwarg onverify_manifest, new field onVerificationResult— bump minor. Consumers MUST handle unknown reasons gracefully (default to FAIL). - Patch — clarifications, doc edits, bug-fix tests.
| Version | Date | Notes | Author |
|---|---|---|---|
| 1.0.0 | 2026-05-10 | Initial contract — produced by AZ-324 (E-C10 decomposition) | autodev |
| 1.1.0 | 2026-05-11 | Additive: VerificationResult.takeoff_origin + flight_id; new VerifyFailReason.TAKEOFF_ORIGIN_INVALID + TAKEOFF_ORIGIN_OUT_OF_BBOX; MV-INV-8 + MV-INV-9. ADR-010 + AZ-490. |
autodev |
Test Cases (consumer side)
| ID | Scenario | Expected Outcome |
|---|---|---|
| MV-TC-1 | Valid Manifest + trusted key + all artifacts present + hashes match | outcome=PASS, empty fail_reasons, per_artifact_checks all matched=True |
| MV-TC-2 | Manifest.json missing | outcome=FAIL, fail_reasons=(MANIFEST_NOT_FOUND,); no further work |
| MV-TC-3 | Manifest.json.sig missing | outcome=FAIL, fail_reasons=(SIGNATURE_NOT_FOUND,); signature_public_key_fingerprint=None |
| MV-TC-4 | Signature does not verify | outcome=FAIL, fail_reasons=(SIGNATURE_INVALID,); no per-artifact checks performed |
| MV-TC-5 | Signature verifies but key is not in trusted_public_keys |
outcome=FAIL, fail_reasons=(UNTRUSTED_PUBLIC_KEY,); fingerprint populated |
| MV-TC-6 | Schema violation (missing required key, absolute path, wrong types) | outcome=FAIL, fail_reasons=(SCHEMA_VIOLATION,) with detail naming the field |
| MV-TC-7 | One engine missing on disk | outcome=FAIL, fail_reasons=(ARTIFACT_MISSING,); per_artifact_checks shows that engine with actual_sha256=None, matched=False |
| MV-TC-8 | One engine present but bytes drifted | outcome=FAIL, fail_reasons=(ARTIFACT_HASH_MISMATCH,); offending check has matched=False |
| MV-TC-9 | Multiple failures (missing + drifted + signature OK) | fail_reasons contains BOTH ARTIFACT_MISSING and ARTIFACT_HASH_MISMATCH; per-artifact checks complete (don't short-circuit on first failure) |
| MV-TC-10 | Manifest.json.sha256 sidecar mismatch |
outcome=FAIL, fail_reasons=(MANIFEST_SELF_HASH_MISMATCH,); signature path NOT consulted |
| MV-TC-11 | Tampered Manifest body but matching sidecar | outcome=FAIL, fail_reasons=(SIGNATURE_INVALID,) (the signature cannot match if body changed even by 1 byte) |
| MV-TC-12 | Conformance: isinstance(ManifestVerifier, my_impl) |
True |
| MV-TC-13 | Tier-2 Tile-coverage check (operator mode with TileMetadataStore) | If recomputed tiles_coverage_sha256 differs → TILES_COVERAGE_MISMATCH; if matches → that part passes |
| MV-TC-14 | Empty trusted_public_keys |
outcome=FAIL, fail_reasons=(UNTRUSTED_PUBLIC_KEY,) (every key is untrusted by definition) |
| MV-TC-15 | Pristine Manifest verified inside 100 ms on Tier-2 (excludes per-tile re-walk) | elapsed_ms ≤ 100 for the signature + non-tile artifact path |
| MV-TC-16 | Manifest body carries no takeoff_origin |
outcome=PASS; VerificationResult.takeoff_origin is None |
| MV-TC-17 | Manifest body carries a well-formed takeoff_origin inside bbox |
outcome=PASS; VerificationResult.takeoff_origin populated as LatLonAlt |
| MV-TC-18 | Manifest body carries a malformed takeoff_origin (lat = 200) |
outcome=FAIL, fail_reasons contains TAKEOFF_ORIGIN_INVALID; takeoff_origin field is populated for diagnostics |
| MV-TC-19 | Manifest body carries takeoff_origin outside the recorded bbox |
outcome=FAIL, fail_reasons contains TAKEOFF_ORIGIN_OUT_OF_BBOX |