Files
gps-denied-onboard/_docs/02_document/contracts/c10_provisioning/manifest_verifier.md
T
Oleksandr Bezdieniezhnykh 880eabcb3f Decompose Step 6 snapshot: 140 task specs + contract docs
Closes out greenfield Step 6 (Decompose) for all 14 components
(C1-C13 + cross-cutting helpers/replay). Covers tasks AZ-266..AZ-446
plus the _dependencies_table.md and component contract documents.

State file updated to greenfield Step 7 (Implement), not_started.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 00:39:48 +03:00

8.8 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_manifest does not return outcome=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.verify record on every airborne verify call (outcome field 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"


@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, ...]
    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.

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 / revocationtrusted_public_keys is 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 VerifyFailReason value, new optional kwarg on verify_manifest, new field on VerificationResult — 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

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