Skip to content

Harden HCM matcher: artifact verification, tier ladder consistency, dedup - #12

Merged
oratis merged 3 commits into
mainfrom
opt/hardware-failclosed
Aug 1, 2026
Merged

Harden HCM matcher: artifact verification, tier ladder consistency, dedup#12
oratis merged 3 commits into
mainfrom
opt/hardware-failclosed

Conversation

@oratis

@oratis oratis commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Summary

Hardening pass on andromeda-hardware (HCM matcher + model). All changes are confined to crates/andromeda-hardware/**, schemas/**, and the two hardware dev docs (plus generated Cargo.lock).

1. Fail-closed artifact verification (security review #1)

evaluate_manifest/matcher previously trusted the declared artifacts[].sha256 and never used ArtifactPin.signing_key_id. Added an optional ArtifactVerifier hook:

  • New evaluate_manifest_with_verifier / evaluate_manifest_at_with_verifier take Option<&dyn ArtifactVerifier>. The existing evaluate_manifest/evaluate_manifest_at delegate with None, so the CLI and today's behavior are unchanged — but the trust boundary is now explicit in the API.
  • When a verifier is supplied, every pinned artifact must resolve and its computed SHA-256 must match the pin, else effective_tier is driven to Blocked; unresolvable-when-verifying is likewise fail-closed.
  • DirectoryArtifactVerifier resolves artifacts from a base directory, hashes with sha2, and wires signing_key_id into the contract via an optional trusted-key set (untrusted/unsigned = Blocked). Real signature cryptography is a documented TODO stub (checks key-id membership, not a detached signature).

2. SupportTier single source of truth (documentation review #2/#3)

  • Reordered the JSON schema tier enum to blocked, community, reference, supported, certified to match the model.rs declaration order.
  • Added a Rust test (schema_tier_enum_matches_model_declaration_order) that reads the schema file and asserts its enum order equals the SupportTier declaration order, so future drift fails CI.
  • Docs now state the ladder authoritatively (code is the source of truth) and note that the strategic-doc "OEM Reference Design" is a product-line label, unrelated to the SupportTier::Reference value.

3. Dedup hex-ID helpers (code-quality review #2)

strip_hex_prefix/id_matches were duplicated in matcher.rs and diagnosis.rs, and diagnosis::parse_hex used trim_start_matches("0x") (strips repeated prefixes). Extracted one shared helper in model.rs, made both callers use it with consistent single-strip semantics, and added a test for 0x0x10de / 0X10DE.

4. evaluate_device refactor (code-quality review #4)

Rewrote the "id matched but no bound driver" decision from an inspect-driven side effect (relying on any's lazy short-circuit) to an explicit two-step (collect id-matches, then check driver presence). Behavior identical.

5. deny_unknown_fields on HCM (code-quality review #1)

Added #[serde(deny_unknown_fields)] to HcmManifest, HardwareSelector, CapabilityRequirement (internally tagged — verified compatible), ArtifactPin, and CapabilityEvidence, so a mistyped manifest field is rejected instead of silently dropped. Added a test covering top-level, selector, requirement, and artifact structs (plus a valid-doc control).

6. serde(default) reconciliation (code-quality review #3)

Removed the redundant #[serde(default)] on DeviceInfo Option fields (absent Option already deserializes to None) and documented why DeviceInfo intentionally stays forward-compatible, unlike the untrusted manifest structs.

Validation

  • cargo fmt --all --check — clean
  • cargo clippy --workspace --all-targets --locked -- -D warnings — clean
  • cargo test --workspace --locked — 120 passing (hardware 32 → 43)
  • JSON schema parses and the repo example manifest validates against it (jsonschema Draft 2020-12)

Shape changes

  • New public API in andromeda-hardware: ArtifactVerifier, ArtifactVerdict, DirectoryArtifactVerifier, evaluate_manifest_with_verifier, evaluate_manifest_at_with_verifier. No existing signature changed; no CLI/JSON output shape change (the CLI still calls evaluate_manifest).
  • sha2 added as a dependency of the andromeda-hardware crate only.

🤖 Generated with Claude Code

oratis and others added 3 commits August 1, 2026 16:54
Reorder the JSON schema `tier` enum to
`blocked, community, reference, supported, certified` so it matches the
`SupportTier` declaration order in `model.rs` (JSON Schema enums are sets,
so this is a consistency fix, not a validation change).

State the ladder authoritatively in the hardware docs: `model.rs` is the
single source of truth, the schema mirrors it, and a CI test locks the two
together. Note that the strategic-doc "OEM Reference Design" product-line
label is unrelated to the `SupportTier::Reference` enum value (which is the
second-lowest, virtual-evidence-only tier).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move `strip_hex_prefix`/`id_matches` into `model.rs` as the single shared
implementation and have `diagnosis.rs` use it. `diagnosis::parse_hex`
previously used `trim_start_matches("0x")`, which strips repeated prefixes;
it now shares the single-strip semantics so `0x0x...` is treated
consistently everywhere. Add a unit test for the `0x0x10de` / `0X10DE`
edge cases.

Add `#[serde(deny_unknown_fields)]` to the untrusted HCM structs
(`HcmManifest`, `HardwareSelector`, `CapabilityRequirement`, `ArtifactPin`,
`CapabilityEvidence`) so a mistyped manifest field is rejected instead of
silently dropped (fail-closed). The attribute works on the internally
tagged `CapabilityRequirement` because the `type` tag is consumed by the
enum deserializer.

Reconcile the `#[serde(default)]` inconsistency on `DeviceInfo`: every
`Option` field already deserializes to `None` when absent, so drop the
redundant annotations and document why `DeviceInfo` intentionally stays
forward-compatible (no `deny_unknown_fields`) unlike the manifest structs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduce an optional `ArtifactVerifier` hook so the matcher can verify the
real bytes behind each pinned artifact instead of trusting the declared
`sha256`. New entry points `evaluate_manifest_with_verifier` /
`evaluate_manifest_at_with_verifier` take `Option<&dyn ArtifactVerifier>`;
the existing `evaluate_manifest`/`evaluate_manifest_at` delegate with
`None`, so today's callers (the CLI) are unchanged and the historical
"trust declared hashes" behavior is preserved but now explicit in the API.

When a verifier is supplied, every pinned artifact must resolve and its
computed SHA-256 must match the pin, or `effective_tier` is driven to
`Blocked`; unresolved-when-verifying is likewise fail-closed. `DirectoryArtifactVerifier`
resolves artifacts from a base directory, hashes them with `sha2`, and wires
`signing_key_id` into the contract via an optional trusted-key set
(untrusted/unsigned = Blocked). Actual signature cryptography is a
documented TODO stub that today checks key-id membership, not a real
detached signature.

Rewrite the `evaluate_device` "id matched but no bound driver" decision as
an explicit two-step (collect id-matches, then check driver presence)
instead of an `inspect`-driven side effect that relied on `any`'s lazy
short-circuit, so a future iterator refactor cannot silently break it.

Add tests: verified artifacts retain the declared tier; mismatched hash and
unresolvable-when-required both Block; the default (no-verifier) path still
trusts declared hashes; a schema drift guard locks the JSON schema `tier`
enum to the `SupportTier` declaration order; and unknown fields in the
untrusted manifest structs are rejected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant