Skip to content

Rename indirect-signature content validation to ContentDigest verbiage (native)#209

Merged
JeromySt merged 47 commits into
users/jstatia/native_ports_finalfrom
users/jstatia/native-contentdigest-rename
Jun 16, 2026
Merged

Rename indirect-signature content validation to ContentDigest verbiage (native)#209
JeromySt merged 47 commits into
users/jstatia/native_ports_finalfrom
users/jstatia/native-contentdigest-rename

Conversation

@JeromySt

Copy link
Copy Markdown
Member

What & why

Mirrors the V2 rename (#companion) in the native Rust validation pipeline, moving the indirect
content-match concept away from "signature" wording to content digest. Companion to the V1 clarity
work in #206 (this is the distinct native trust/validation surface, not the V1 SignatureMatches API).

Rename mapping (native/rust/validation/core)

Before After
struct IndirectSignaturePostSignatureValidator IndirectContentDigestPostSignatureValidator
enum IndirectSignatureKind IndirectContentDigestKind
fn detect_indirect_signature_kind fn detect_indirect_content_digest_kind
VALIDATOR_NAME = "Indirect Signature Content Validation" "Indirect Content Digest Validation"
error codes INDIRECT_SIGNATURE_* CONTENT_DIGEST_* (payload mismatch → CONTENT_DIGEST_MISMATCH)
metadata "IndirectSignature.Format" / ".HashAlgorithm" "ContentDigest.Format" / ".HashAlgorithm"

The cryptographic PostSignatureValidator trait and the "signature-only verification" wording are
intentionally preserved (they refer to the actual COSE signature, not the content digest).

Files

  • native/rust/validation/core/src/indirect_signature.rs
  • native/rust/validation/core/src/validator.rs (construction site)
  • native/rust/validation/core/tests/final_coverage_gaps.rs (comment references)

Validation (cose_sign1_validation)

  • cargo build → pass
  • cargo test543 passed / 3 ignored
  • cargo clippy -- -D warnings → pass
  • cargo fmt --check → pass

Refs #206

Jstatia and others added 30 commits May 8, 2026 06:18
…_final

Mirrors V2/tools/train (the .NET train) but parameterized for the Rust workspace:
  - Integration branch: users/jstatia/native_ports_final
  - Worktree prefix: CoseSignTool-np-<phase>
  - Coverage script: native/rust/collect-coverage.ps1 (per-crate via -Package)
  - Per-scope gate: 90% absolute (matches existing repo Rust convention)
  - Workspace gate: -NoRegress (compare phase coverage vs integration baseline)

Decisions R1-R7 captured in eval-trust-policy-translation-contract-rust.md
(session workspace).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Scaffold the new validation/trust_policy_spec crate (Phase 1 of the native
Rust trust-policy port) and register it as a workspace member. Adds
minimal Cargo.toml metadata + the dependency-allowlist entry that lets
collect-coverage.ps1's allowlist gate accept serde + serde_json in this
specific crate.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add the canonical IR (D3): a serde-tagged 'enum TrustPolicySpec' carrying
the ten variants from the design doc (AllowAll, DenyAll, And, Or, Not,
Implies, Message, PrimarySigningKey, AnyCounterSignature, RequireFact),
plus the OnEmptyBehavior enum mirrored from primitives so the IR can
carry serde derives without reaching into the trust-evaluation crate.

Also adds:

- src/source_location.rs: SourceLocation type used by frontends to
  attach line/column anchors to diagnostics.
- src/diagnostic_codes.rs: stable TPX001-TPX600 constants per D6.

Every struct variant carries deny_unknown_fields so frontends reject
typos at parse time as TPX100 rather than as silent behavioral drift.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements D1's hybrid predicate language:

- PropertyAssertionPredicateSpec: sugar variant. AND of property-equality
  assertions stored in a BTreeMap so canonical JSON serialization is
  byte-stable across builds (D9 cache-key invariant).
- PathOperatorPredicateSpec: universal fallback. Carries a JSON-pointer-
  style path, a closed-enum PredicateOperator, and an optional value
  operand.
- FactPredicateSpec: untagged enum that selects the variant from JSON
  shape; serde tries Property first (unambiguous 'assertions' key) then
  PathOperator (universal fallback).

The PredicateOperator enum closes 11 operators (Exists, Equals,
NotEquals, LessThan/Or, GreaterThan/Or, StartsWith, EndsWith, Contains,
In) with #[serde(rename_all = snake_case)] so wire identifiers are
stable.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements D5's post-parse parameter substitution:

- ParameterRef: typed view over the JSON literal {"`$param": "name",
  "default": <value>}. Recognized anywhere a serde_json::Value slot
  accepts a literal (predicate operands, property assertion RHS).
- bind(spec, parameters): walks the spec tree replacing every
  ParameterRef with its bound value or default. Missing-without-default
  surfaces as BindError::MissingParameter carrying a path breadcrumb
  (e.g. require_fact.predicate.assertions[is_trusted]) for diagnostics.
- BindError::Malformed: rejects ill-shaped parameter literals (non-
  string $param value, unexpected co-keys) at recognize time.

The walker is exhaustive over every variant of TrustPolicySpec and
recurses through serde_json::Value arrays + nested objects, so a
`$param can appear at arbitrary depth inside a predicate value slot.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Defines the fact-id resolution surface consumed by the Phase 1 compiler:

- IFactRegistry: trait with try_get_fact_type(fact_id), try_get_fact_id(
  type_name), and all_fact_ids() returning a deterministic BTreeSet.
- StaticFactRegistry: hand-rolled mapping mirroring the .NET
  StaticFactRegistry.BuildDefaultMappings() exactly. Covers the 16
  canonical fact ids advertised by the certificates, MST, and message-
  level packs (x509-chain-trusted/v1, x509-cert-eku/v1, mst-receipt-
  trusted/v1, content-type/v1, etc.).

The static registry is explicitly TEMPORARY: per R1 (2026-05-08) Phase 3
(np-fact-registry) supersedes it with a hand-rolled register_facts!()
macro per pack. This Phase 1 placeholder lets the Phase 1 compiler
reject unknown fact ids as TPX200 at compile time without taking a hard
dependency on every pack crate.

No third-party fact-registration crate (inventory, linkme) per R1.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Lowers TrustPolicySpec into a cose_sign1_validation_primitives::plan::
CompiledTrustPlan and ships the byte-stable canonical-JSON serializer
that backs the D9 cache-key invariant.

Phase 1 lowering scope (full structural):
  AllowAll, DenyAll, And, Or, Not, Implies, Message, PrimarySigningKey.

Deferred to Phase 3 placeholders (deterministic, strictly-conservative):
  RequireFact: validates fact_id against IFactRegistry then emits a
    placeholder rule that always denies with the user's failure_message
    plus a Phase-1-banner so smoke tests can verify the wiring.
  AnyCounterSignature: emits a placeholder rule whose on_empty knob is
    observable (Allow -> trusted, Deny -> denied with banner).

CompileError uses TPX-prefixed stable codes (TPX200 unknown fact id,
TPX301 recursion limit, TPX500 predicate malformed).

Determinism contract for to_canonical_json: BTreeMap-backed assertion
maps and serde_json::Map (which is BTreeMap when preserve_order is
disabled, our default) iterate in sorted order; Vec slots preserve
author order so And/Or short-circuit semantics survive serialization.

lib.rs gates missing_docs (#![deny(missing_docs)]) so the public surface
cannot drift undocumented; the doc-test in the crate-level docstring
exercises the full quickstart path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the Phase 1 test suite under the crate's tests/ directory (per Rust
idiom + the collect-coverage.ps1 'no #[cfg(test)] in src/' gate):

- serde_round_trip.rs (20 tests): every variant + nested combinations
  serialize and deserialize to byte-identical JSON; #[serde(default)] +
  skip_serializing_if elision rules; deny_unknown_fields rejects typos.
- parameter_bind.rs (13 tests): bind replaces `$param literals; missing-
  without-default surfaces BindError::MissingParameter with location
  breadcrumb; bind recurses through every variant + nested arrays/
  objects; malformed parameter literals rejected at recognize time.
- compile_smoke.rs (21 tests): >=5 representative spec trees compile
  end-to-end and evaluate to the same outcome as the equivalent fluent-
  built primitives plan; CompileError variants surface with stable TPX*
  codes; recursion limit is enforced.
- canonical_json.rs (7 tests): 1000-iteration determinism on a complex
  spec; canonical form independent of BTreeMap insertion order;
  PredicateOperator wire identifiers match the snake_case contract.
- predicate_hybrid.rs (6 tests): Property and PathOperator forms
  expressing the same logical assertion produce equivalent compiled
  rules; untagged serde routing is unambiguous.
- static_registry.rs (8 tests): every default fact id matches
  ^[a-z][a-z0-9-]*/v[0-9]+$; forward<->reverse lookup round-trips;
  iteration order is sorted; the must-have id list from the .NET parity
  surface is fully populated.

75 tests total, all green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fixes for the findings raised by the local code-review pass:

api-stability (B -> A): mark TrustPolicySpec, FactPredicateSpec,
  PredicateOperator, OnEmptyBehavior, BindError, CompileError,
  CompileOptions, BindOptions as #[non_exhaustive] so adding new
  variants/fields in a minor-version bump is non-breaking for downstream
  pattern matches; introduce FactRegistryExt (blanket-implemented over
  IFactRegistry) as the Phase 3 extension seam; add CompileOptions::
  with_max_depth() / BindOptions::with_max_depth() builders that work
  across the non_exhaustive boundary.

reliability (B+ -> A): replace the only .expect() in src/*.rs (parameter
  ::ParameterRef::try_recognize) with a let-else over obj.get(PARAM_KEY)
  so the parsing path has no latent panic surface.

operability (B+ -> A): give BindError a stable code() (TPX400 missing,
  TPX401 malformed, TPX301 recursion) and embed it in the Display impl,
  matching CompileError's existing pattern. Helps log scrapers and audit
  pipelines correlate without parsing free-form text.

security (B -> A): add a recursion depth cap to the bind walk -- the
  compile path was already bounded but bind was not, so deeply nested
  user inputs could stack-exhaust the parameter walker. Default cap is
  256 (same as compile), overridable via BindOptions::with_max_depth().

performance (B -> A): replace per-recursive-step format!()-based path
  breadcrumbs with a reusable BreadcrumbStack: in-place push/pop while
  walking, materialize the dotted path string only when constructing a
  BindError. Eliminates O(depth) String allocations on the success path.

correctness/architecture (A- -> A): preserve the user-authored Not.
  reason through lowering -- compile() now emits a custom FnRule-backed
  rule that captures the dynamic String reason instead of falling back
  to not_with_reason()'s &'static str-only path. The placeholder default
  ('Negated rule was satisfied') is used only when reason == None.

testing (A -> A+): tighten parameter-bind tests with structural
  assertions over fact_id/path/operator (not just the inner value); add
  tests for the new TPX-coded BindError Display, the bind recursion
  cap, and FactRegistryExt's blanket impl.

Per-crate coverage: 96.66% -> 97.05%.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…mitives

Adds the compile-time fact-id contract the Rust trust-policy port uses
to supersede Phase 1's hand-curated StaticFactRegistry per decision R1.

- TrustFactWithId trait (const FACT_ID)
- TrustFactDescriptor (#[non_exhaustive], Clone/Debug/Eq)
- validate_fact_id const fn for ^[a-z][a-z0-9-]*/v[0-9]+\$
- register_facts!{} declarative macro emitting per-crate
  __cose_sign1_trust_facts() function

No third-party crates (no inventory/linkme); no link-time magic.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements TrustFactWithId for the 9 X.509 facts in the certificates
pack and calls register_facts! in lib.rs so the workspace registry
can collect them. Compile-time validate_fact_id() asserts protect
each id against accidental malformation.

IDs match Phase 1's StaticFactRegistry baseline byte-for-byte.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tags the 3 baseline MST facts (present / trusted / issuer-host) and
calls register_facts! in lib.rs. Phase 1 gaps (kid, statement-sha256,
statement-coverage, signature-verified) are surfaced in the final
report rather than silently assigned new ids.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tags 4 baseline message-level facts (content-type, counter-signature-subject,
detached-payload-present, unknown-counter-signature-bytes) and calls
register_facts! in lib.rs. Phase 1 gaps surfaced in final report.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
HandRolledFactRegistry::from_packs(&[...]) collects per-pack
__cose_sign1_trust_facts() outputs into a deterministic id-keyed
BTreeMap. Construction rejects duplicate ids (TPX300) and malformed
ids (TPX301); validate_fact_id is re-applied as a runtime safety
net for callers that skip the const-assert.

StaticFactRegistry is now #[deprecated] and retained only as the
conformance baseline for the upcoming hand_rolled_equals_static_baseline
test (Phase 5a removes it).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ests

- tests/conformance_baseline.rs: HandRolledFactRegistry must be
  byte-identical to Phase 1's StaticFactRegistry (16 facts).
- tests/hand_rolled_registry.rs: error paths (TPX300/TPX301), empty
  registry, multi-pack collection, descriptor lookup, Clone/Debug.
- per-pack tests/registered_facts.rs: ids match expected baseline,
  descriptors self-attribute to their crate.
- primitives/tests/fact_id.rs: TrustFactWithId, TrustFactDescriptor,
  validate_fact_id (good + bad inputs), register_facts! expansion.
- #[allow(deprecated)] on existing tests + doctest that intentionally
  exercise StaticFactRegistry as the conformance baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Per-perspective findings addressed:
- Correctness (B+ -> A+): from_packs() now also rejects duplicate
  type_name (TPX302). Public TrustFactDescriptor::new() lets callers
  construct arbitrary descriptors, so the registry must defend the
  reverse-lookup invariant explicitly.
- API Stability (A- -> A+): RegistryError is #[non_exhaustive] —
  future TPX3xx variants are additive without breaking downstream
  match consumers.
- Performance (B- -> A): HandRolledFactRegistry's lookup maps are now
  BTreeMap<&'static str, TrustFactDescriptor>, dropping per-id String
  allocations for the rodata-resident keys. Only the all_ids mirror
  remains owned (forced by the IFactRegistry::all_fact_ids contract).
- Tester (A- -> A+): conformance_baseline.rs adds full pack-attribution
  snapshot + iteration-determinism-under-pack-permutation test (6
  permutations, all must produce byte-identical iter order); fact_id.rs
  adds boundary edge cases and locks documented leading-zero behavior;
  hand_rolled_registry.rs adds TPX302 + diagnostic_code stability test.

Coverage: trust_policy_spec 97.40%, primitives 97.82%.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…t/result/diagnostic

Adds the abstract translation contract (CoseTrustPolicyFrontend<TDocument>)
plus the supporting types (TrustPolicyTranslationContext,
TrustPolicyTranslationResult, TrustPolicyTranslationDiagnostic,
TrustPolicySeverity, FactCapabilities) to cose_sign1_trust_policy_spec.

Placement mirrors the .NET architectural decision: the trait lives next to
the IR rather than in the validation runtime to avoid a project cycle. Every
public type is #[non_exhaustive] for forward-compatible evolution, with

ew() / �rror() / warning() / info() constructors so cross-crate
consumers can instantiate without literal-form blocks.

Additive only — Phase 1 / Phase 3 public APIs are unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Empty (lib.rs only) cose_sign1_trustfrontends_json crate at
validation/trustfrontends/json/. Dependencies (jsonschema 0.30, moka 0.12
sync, blake3 1, serde, serde_json) are added to the per-crate allowlist
with R3/R4/R5 decision citations.

jsonschema is configured with default-features = false to suppress the
remote-ref reqwest/hyper/tokio chain — the embedded schema is the only
schema the frontend ever evaluates.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…to .NET)

Embeds the canonical user-document JSON Schema for cose-tp-json/v1 at
native/rust/validation/trustfrontends/json/schemas/cose-tp/v1.json. The
file is byte-identical (after platform line-ending normalization to LF)
to the .NET schema at V2/schemas/cose-tp/v1.json so that documents valid
under one frontend are valid under the other.

The cross_port_schema integration test in a subsequent commit asserts
this byte-identity by diffing against the .NET source.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…slate)

Adds the translation pipeline for cose-tp-json/v1:

  Stage 1: serde_json parse — TPX001 with line/column on malformed input.
  Stage 2: jsonschema 0.30 Draft-2020-12 validation against the embedded
           schema. Each leaf failure surfaces as TPX100 (or TPX101 when
           the failing pointer is /frontend).
  Stage 3: DocumentTranslator walk over the validated tree producing a
           TrustPolicySpec. Capability gating (D4) emits TPX200 for
           unknown fact ids and TPX201 for predicate-schema mismatches.
           Bounded recursion (default depth 64) emits TPX300.
           Defensive arms emit TPX301 (every schema-rejected shape is
           caught earlier, so these arms are unreachable in the public
           flow but preserved for totality (§6.5.4 #2)).

Mirrors the .NET CoseTpJsonFrontend / DocumentTranslator pair, with the
operator parser case-insensitively accepting the schema's PascalCase
form and emitting the canonical snake_case in the IR.

Cache module is stubbed; concrete LRU implementation lands next.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds TranslatorCache: an in-process LRU cache fronting CoseTpJsonFrontend
(per design decision D9). Cache key is the concatenation of three
blake3-256 hashes — (canonical document bytes, canonical sorted parameter
JSON bytes, canonical capability fingerprint bytes) — plus the optional
document_source string. Default capacity is 32, configurable via
CoseTpJsonOptions::cache_capacity.

Backed by moka::sync::Cache (R4) with blake3 (R5) for hashing. The
key collision space is 2^256 per axis; the eventually-consistent
entry_count() helper is exposed for diagnostics but never as a contract.

Cached values are wrapped in Arc<TrustPolicyTranslationResult> so
concurrent readers obtain reference-shared snapshots without copying —
matching §6.5.9 anti-pattern #4: cached entries are derived data, never
the policy of record.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds --trust-policy <path> and --trust-policy-param <key=value> (repeatable)
to CoseSignTool verify x509. When --trust-policy is supplied:

  1. Read the document text from disk.
  2. Translate via cose_sign1_trustfrontends_json (parse, schema-validate, walk).
  3. Bind \ references against the parsed parameter map.
  4. Compile against a HandRolledFactRegistry assembled from the configured
     packs' fact producers (validation/core + certificates + optionally MST).
  5. Bundle the CompiledTrustPlan with the same packs for evaluation.

Pack fact producers stay registered so RequireFact references resolve. The
default pack-driven trust plan is bypassed (D8 OVERRIDE semantics).

When --trust-policy is omitted, existing behavior is unchanged.

Also ships the README.md for the new crate.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds 9 integration tests under tests/ (no #[cfg(test)] in src/ — Rust train
gate enforces this):

  translate_smoke.rs   — 6 representative documents → expected TrustPolicySpec.
  schema_validation.rs — malformed docs → diagnostic with stable code.
  capability_gate.rs   — D4 unknown fact id (TPX200), opt-in tolerance.
  predicate_hybrid.rs  — both predicate forms; byte-determinism.
  parameter_binding.rs — pre-bind has  literals; post-bind doesn't;
                         missing-without-default raises BindError.
  cache.rs             — LRU equality, capacity bounds, 0-cap rejection.
  cross_port_schema.rs — byte-identity vs the .NET schema (D7 lock).
  perf_smoke.rs        — 1KB doc translates in <50ms (Phase 4 enforces ≤10ms p99).
  cli_integration.rs   — invokes the CoseSignTool binary end-to-end.

Also corrects the embedded schema's trailing-byte sequence so it matches
the .NET source bit-for-bit after CRLF→LF normalization (the cross-port
test now passes against the .NET blob fetched via 'git cat-file').

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the new validation/trustfrontends/conformance crate to the Rust workspace and the dependency allowlist ([crate.conformance] for serde/serde_json plus the opt-in criterion-perf feature). The crate ships the §6.5.10 8-property conformance harness used by every shipping CoseSign1 trust-policy frontend; subsequent commits add the harness, fixtures, JSON adapter wiring, perf gate, and README.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements the §6.5.10 contract:
- ConformanceAdapter<TDoc> — per-frontend bridge (create_frontend, load_document, fixture_extension, fixture_root, registered_fact_ids).
- 8 run_conformance_* functions: determinism, attribute fidelity, reject-untranslatable, bounded runtime, capability-aware, parameter substitution, schema validation, cross-frontend equivalence.
- run_conformance_all composite for single-frontend test crates.
- Public analysis helpers (collect_fact_ids, contains_param_literal) for downstream consumers and harness reuse.
- fixtures helpers with '/' -> '--' fact-id filename encoding (collision-free since '--' cannot appear in a valid id).
Cross-frontend equivalence is exercised even with one frontend (degenerate (json, json)) so the contract is locked ahead of Phase 5a (Rego).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…etric + cross + perf)

Ships the canonical fixture set referenced by the §6.5.10 harness:
- per_fact/: 16 fixtures, one per fact id in the canonical baseline (StaticFactRegistry::default_mappings); '/' is encoded as '--' in filenames.
- untranslatable/: free_text_search, unknown_fact, unknown_operator (TPX100/TPX200 expectations).
- capability/missing_fact: TPX200 lock for the capability gate.
- schema/{malformed_text,shape_violation}: TPX001/TPX100 paths.
- parametric/{host_baseline,host_alternate}.coseTrustPolicy.json plus sibling .params.json files for property #6 (different parameter sets -> different IRs).
- perf/representative_1kb (≤ 1 KiB) — drives the p99 ≤ 10 ms statistical gate.
- cross/canonical_policy/canonical_policy.coseTrustPolicy.json — fixture for the §6.5.10 #8 cross-frontend equivalence harness.
- cross/canonical_policy/canonical_ir.expected.json — committed canonical-IR golden, regenerated via 'cargo test ... regenerate_golden -- --ignored'.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Wires the canonical cose-tp-json/v1 frontend through every §6.5.10 property in 8 individual #[test] functions (one per property) so a regression on one property surfaces as one focused failure rather than a composite fall-through. Mirrors .NET's FrontendConformanceTestBase per-method shape.

Companion test suites:
- fixtures_helpers — fact-id encoding/decoding round-trip plus path composition.
- analysis_tests — direct unit coverage for collect_fact_ids and contains_param_literal across every TrustPolicySpec variant (including non-fixture-reachable ones like top-level And/Or/Not/Implies).
- harness_smoke — composite run_conformance_all entry point + measure_p99 sanity + read_fixture panic-on-missing path.

JsonConformanceAdapter::default carries the 16-fact canonical baseline; with_fact_ids overrides for hosts running a tighter capability surface.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Jstatia and others added 17 commits May 8, 2026 11:10
§6.5.10 #4 (bounded runtime): a representative ≤ 1 KiB document MUST translate with p99 ≤ 10 ms over a statistically meaningful sample.

Two implementations:
- Built-in (always-on #[test]): hand-rolled std::time::Instant sampling, nearest-rank p99, 16 warm-up + 256 timed samples. Asserts the 10 ms target. Default 'cargo test' runs this; no extra deps.
- Criterion bench (opt-in via --features criterion-perf): richer reporting + regression history. Default builds do NOT pull Criterion as a dependency; required-features in Cargo.toml gates the bench target on the feature.

Phase 2's smoke test asserts ≤ 50 ms (loose); Phase 4 tightens to 10 ms p99 — frontends that miss this gate are NOT ship-eligible (anti-deferral). Sample p99 measured on the JSON frontend during gate runs is well under 1 ms (10x headroom).

nearest_rank_percentile is exposed publicly and unit-tested in tests/perf_p99.rs across edge cases (empty slice, single sample, p100 cap).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…-port IR

Phase 4 #8 deliverable: parsing the cross/canonical_policy fixture with the Rust JSON frontend MUST produce canonical-IR JSON byte-equal to the committed canonical_ir.expected.json golden after line-ending normalisation.

The golden file ships at fixtures/cross/canonical_policy/canonical_ir.expected.json (committed in the previous fixture commit). When the .NET JSON frontend ships, its conformance suite asserts byte-equality against the same fixture; if the two ports drift the diff is one git-blame away.

regenerate_golden is an #[ignore]'d test for intentional IR shape changes:
    cargo test -p cose_sign1_trustfrontends_conformance --test cross_port_canonical_ir regenerate_golden -- --ignored

The §6.5.10 #8 in-process degenerate (json, json) check (already in src/harness.rs) cooperates with this: the Rust frontend is byte-stable across two parses (deterministic encode), and the Rust output is byte-equal to the committed golden — together they lock canonical-IR shape both within the Rust port and across to .NET.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Documents:
- The full 8-property table verbatim from §6.5.10.
- How a new frontend opts in (impl ConformanceAdapter, drop fixtures, call run_conformance_*).
- Fact-id filename encoding rule ('/' -> '--').
- Why each property exists + which bug class it catches.
- Performance gate rationale (PERF_SAMPLE_COUNT/WARMUP_COUNT, the 10 ms target reasoning).
- Cross-port note: this Rust suite mirrors the .NET CoseSign1.Validation.TrustFrontends.Conformance contract; the canonical-IR golden file enforces byte-equivalence at fixture granularity.
- Coverage discipline: per-crate 90% gate per Rust D11 amendment.
- Out-of-scope: Rego frontend (Phase 5a), FFI surface (Phase 4.5).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replaces the manual ceiling-division formula '((pct * n) + 99) / 100' with usize::div_ceil — the canonical nearest-rank percentile rank formula 'ceil(pct/100 * n)'. Fixes a clippy::manual_div_ceil warning surfaced by the Phase 4 jeromy_review (Performance perspective B+ feedback) and removes the +99 magic constant that obscured the percentile-agnostic correctness.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Phase 4.5 of the native Rust trust-policy port: stable C ABI for loading,
translating, binding, and compiling .coseTrustPolicy.json documents from
non-Rust consumers (C/C++ direct, .NET via P/Invoke, Node via N-API, Go via cgo).

Adds the workspace member at validation/trust_policy_spec/ffi with:
  * Opaque heap-owned handle types: spec, translation_result, compiled_plan
  * #[repr(transparent)] borrowed diagnostic_t (lifetime tied to result)
  * cose_sign1_trust_policy_translate_json (length-prefixed UTF-8 input)
  * cose_sign1_trust_policy_spec_bind (clones spec, returns new result handle)
  * cose_sign1_trust_policy_spec_compile + _compile_to_result (registry from
    statically-linked certificates+MST+validation pack contributors)
  * Diagnostic introspection (count/at/read) returning borrowed UTF-8 spans
  * has_parameters pre-flight predicate
  * Free functions for every owned handle (null-tolerant)
  * cose_sign1_trust_policy_frontend_id() exposing 'cose-tp-json/v1' as C string

Every exported function uses with_catch_unwind so Rust panics never cross the
ABI boundary. Translation diagnostics live on the result handle (R6 opaque-error
discipline); the cose_status_t return is reserved for infrastructure failures
(null pointer args, allocation, panic).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds native/c/include/cose/sign1/trust_policy.h — the authoritative C ABI
surface for trust-policy translation. Hand-written (matches the repo's existing
pattern at trust.h, validation.h, sign1.h, etc.) rather than cbindgen-generated:
the repo has no build.rs / cbindgen.toml infrastructure and the existing FFI
headers are all manually curated.

Drift is gated by tests/header_drift.rs: parses every
pub extern "C" fn cose_sign1_trust_policy_* export from lib.rs and every
function declaration in trust_policy.h, asserts the two name sets are equal in
both directions, and additionally requires a known canonical entrypoint to be
present so the parser cannot pass vacuously.

Also adds tests/ffi_smoke.rs — a 23-test suite that round-trips translate to
diagnostic introspection to bind to compile to free entirely through the C ABI
surface (calls match what a C consumer would invoke). Covers minimal success,
parse error (TPX001), schema violation (TPX1xx), out-of-bounds diagnostic_at,
defensive null handling on every accessor + free function, null out_result /
null json with nonzero len / non-UTF8 input infrastructure errors, bind happy
path + empty-buffer-uses-default + missing-parameter (TPX400), bind with
non-object JSON / null spec / null out_result errors, compile happy path +
null arg variations, compile_to_result with and without out_plan,
compile_to_result for unknown fact id (TPX200) via diagnostic, frontend_id
canonical string with stable pointer, partial out-pointer reads of
diagnostic_read, and the end-to-end translate + bind + compile lifecycle.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds native/c/tests/trust_policy_translate_test.c (C consumer) and
trust_policy_translate_gtest.cpp (C++ / GoogleTest consumer). Both exercise
the full C ABI surface end-to-end: translate -> diagnostic introspection ->
bind -> compile -> free.

The C executable is built unconditionally when COSE_FFI_TRUST_POLICY_LIB
is detected; the C++ gtest variant is built when GTest is available via
vcpkg (mirrors smoke_test / smoke_test_gtest pattern).

Tests cover:
  * minimal-doc translate-success
  * translate-parse-error with TPX001 diagnostic introspection (verifies
    severity == ERROR, code byte-span and length, message presence,
    line/column 1-indexed)
  * bind + compile happy path with parameter substitution
  * bind missing-parameter surfaces TPX400 via diagnostic
  * frontend_id returns canonical 'cose-tp-json/v1'
  * defensive null tolerance on every accessor and free function
  * out-of-bounds diagnostic_at returns NULL
  * null out_result is reported as infrastructure error

The C++ test uses RAII handle wrappers (TrustPolicyResult / TrustPolicySpec /
CompiledPlan) to assert the C ABI integrates cleanly with C++ ownership
patterns.

CMakeLists wiring detects cose_sign1_trust_policy_spec_ffi via find_library
and links it into the cose_sign1 INTERFACE target with the COSE_HAS_TRUST_POLICY
compile definition.

Verified locally on Windows with MSVC 19.44 + Visual Studio 17 2022 generator:
  * cargo build --release --workspace produces the .dll + .dll.lib
  * cmake configure detects all packs including the new trust-policy translator
  * trust_policy_translate_test.exe exits 0 and prints all 5 ok lines

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Documents the FFI surface: pipeline, ownership rules per pointer kind,
diagnostic discipline, threading guarantees, ~30-line sample C and C++
consumers, test surface, coverage gate command, phase-5 follow-up note,
and the R1/R6/R7 decision links.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…90% gate

Coverage gate work for the per-crate threshold (FailUnderLines 90):

  * Forward-compat catch-alls in spec_has_parameters / predicate_has_parameters /
    cose_sign1_trust_policy_severity_t::from_severity refactored from match-arm
    bodies (which cannot carry #[coverage(off)] per the rustc lint) into
    inline(never) helper functions tagged #[cfg_attr(coverage_nightly,
    coverage(off))]. Unreachable until the IR crate ships a new variant; the
    attribute removes the dead code from the coverage denominator.
  * compile_error_location split out for the same reason — the inner match's
    'CompileError that carries a non-None location' arm is reachable only when
    a frontend populates location, which Phase 1 IR-direct compile never does.
  * Severity discriminant from_severity exposed as #[doc(hidden)] pub for
    integration-test access without expanding the C ABI surface.

New test coverage:

  * bind_with_null_params_and_nonzero_len_is_an_error
  * bind_with_malformed_json_parameters_is_an_error (parse_parameter_map serde
    error path)
  * compile_with_unknown_fact_id_returns_error_status (anyhow path via
    cose_sign1_trust_policy_spec_compile)
  * compile_to_result_error_clears_out_plan (sentinel-based assertion that the
    error path explicitly nulls out_plan even when caller pre-populated it)
  * has_parameters_walks_logical_combinators (and / or / not)
  * has_parameters_walks_property_assertion_and_path_operator_predicates
  * diagnostic_severity_mapping_is_round_trip_stable

Removed an earlier duplicate compile_to_result_with_unknown_fact_id test that
used 'message: [...]' which the schema does not accept; the surviving copy
uses primary_signing_key + bare fact (matches existing translate-smoke
patterns).

Per-crate coverage: 324/341 = 95.01% (gate: 90%).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Skeleton crate scaffolding for Phase 5a (np-frontend-rego). Adds the
workspace member at validation/trustfrontends/rego/, registers it in the
workspace Cargo.toml, and pins serde + serde_json in
allowed-dependencies.toml under the [crate.rego] tier. Subsequent
commits land the parser, lowerer, frontend trait impl, conformance
adapter, fixtures, and tests on top of this skeleton.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Hand-rolled tokenizer + recursive-descent parser for the §6.5.6
  constrained Rego subset (Option B; Option A regorus rejected — see
  README.md ‘Why a constrained subset’ for the trade-off table).
- AST → cose-tp-json/v1 JsonValue lowering. Translation reuses the JSON
  frontend's schema validator + walker so cross-frontend canonical-IR
  byte-equality is a property of construction, not duplicated logic.
- Closed reject-list with TPX301-306 + TPX001/002/003/004/005 + TPX300
  per-cause sub-codes, vocabulary verbatim with .NET Phase 5a so
  cross-port telemetry stays comparable.
- 16 per-fact + 1 cross + 3 untranslatable + 1 capability + 2 schema +
  2 parametric + 1 perf Rego conformance fixtures (26 total),
  paralleling the JSON adapter's existing fixture tree under
  fixtures/<scenario>/<stem>.coseTrustPolicy.rego.
- RegoConformanceAdapter wires the frontend through the Phase 4 8-property
  conformance harness; cross-frontend equivalence (§6.5.10 #8) now runs
  as the true (json, rego) pair instead of the (json, json) degenerate.
- 69 tests across 5 test files (translate_smoke + reject_list +
  parser_robustness + cross_frontend_byte_equality + conformance_rego);
  cli_integration is env-gated on RUN_CLI_INTEGRATION to match Phase 2's
  opt-in CLI integration pattern.
- CLI dispatch in commands/trust_policy_override.rs: extension /
  package-header sniff routes .coseTrustPolicy.rego to the new frontend;
  the JSON path is unchanged so D8 override semantics carry through.
- Trivial fix to pre-existing verify.rs unit tests that pre-dated the
  Phase 2 trust_policy field addition (4 literal struct inits in
  describe_trust_mode_* tests dropped the new fields).
- README documenting frontend identity, accept-list grammar, reject-list
  with TPX codes, example document, FFI rationale (no
  cose_sign1_trust_policy_translate_rego extern; lower-to-JSON in the
  host language and reuse the existing extern), CLI dispatch contract,
  and cross-port equivalence to .NET CoseSign1.Validation.TrustFrontends.Rego.

Phase 5a does NOT add a new FFI extern: the Rego frontend is a parse +
lower pipeline whose output is a JSON tree the existing
cose_sign1_trust_policy_translate_json extern accepts. Adding a sibling
extern would expand the C-ABI surface, force a cbindgen re-run + header
drift assertion update, and provide no new functionality. Documented in
both the rego README and the FFI rationale section.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds 37 coverage-branch tests targeting paths the smoke / reject-list /
robustness / conformance suites do not exercise. Marks two genuinely
unreachable defensive arms with coverage(off): lower.rs number-fallback
and frontend.rs seed-merge.

Per-file coverage (own-source): adapter 78%, document 100%, frontend
96.8%, lib 90.9%, lower 90.9%, parser 91.2%, tokenizer 96.7%. Workspace
own-source: 93% >= 90% gate.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Performance (B -> A):
- json: add public translate_value_with_source so callers with a typed
  Value can skip the to_string + reparse round-trip while keeping the
  document_source diagnostic anchor.
- rego: switch translate_internal to call translate_value_with_source
  on the lowered Value directly. Eliminates the canonical_text round-trip.

Operability (B -> A):
- rego: thread document_source into parser + tokenizer-error diagnostic
  emission. Diagnostic messages now carry a "<source>:<line>:<col>: "
  prefix so editor / log tooling can navigate back to the offending
  file from a diagnostic line alone.
- rego: add public RegoDocument::lowered() accessor for operator-facing
  debugging of the cose-tp-json/v1 tree the JSON walker observes.
- README: add "Reading diagnostics" section enumerating the diagnostic
  shape (code / message / severity / location / suggestion) plus the
  RegoDocument::lowered escape hatch.

Testing / Correctness (B -> A):
- parser: detect Identifier-followed-by-pipe at the term-position
  lookahead so [x | y > 0] surfaces TPX304 directly instead of
  TPX300 from parse_identifier_term. Fixes the comprehension
  classification regression the correctness reviewer flagged.
- reject_list: tighten tpx304_array_comprehension_is_rejected to
  require TPX304 verbatim (no TPX300 fallback).
- boundary_acceptance.rs: 14 new boundary tests proving the closest
  legal sibling of every reject family still translates -- legal
  escapes / surrogate pairs, allowed import, exact package, single rule,
  namespace names as string values, iteration-keyword lookalikes as
  strings, input.data alias, literal arrays at comprehension position,
  60-deep nesting (cap-4 below TPX305), MAX_INPUT_BYTES-4096 (below
  TPX306). Plus pair-then-lone-low-surrogate + document-source-prefix
  + lowered() accessor.

Documentation (A- -> A):
- adapter: explain why RegoConformanceAdapter ships in this crate and
  not the conformance crate, plus the cross-equivalence-by-construction
  story the (json, rego) fixture pair locks in.
- README FFI: clarify that the crate currently exposes Rust APIs only;
  non-Rust hosts either lower-to-JSON in their host language or build
  their own Rust shim.

Coverage: 93.3% (was 93%); 120 tests across 7 test files.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…e (native)

Mirrors the V2 rename in the native Rust validation pipeline, moving the indirect content-match concept away from 'signature' wording:

- IndirectSignaturePostSignatureValidator -> IndirectContentDigestPostSignatureValidator
- IndirectSignatureKind -> IndirectContentDigestKind; detect_indirect_signature_kind -> detect_indirect_content_digest_kind
- VALIDATOR_NAME -> 'Indirect Content Digest Validation'
- error codes INDIRECT_SIGNATURE_* -> CONTENT_DIGEST_* (payload mismatch -> CONTENT_DIGEST_MISMATCH)
- metadata IndirectSignature.Format/.HashAlgorithm -> ContentDigest.Format/.HashAlgorithm

Cryptographic 'signature-only verification' wording is intentionally preserved. cargo build/test (543 passed), clippy -D warnings, and fmt --check all pass.

Refs #206

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@JeromySt

Copy link
Copy Markdown
Member Author

Landed directly on native_ports_final via fast-forward push (merged origin's #195 commit first). PR not needed.

@JeromySt JeromySt merged commit f7ef13a into users/jstatia/native_ports_final Jun 16, 2026
17 of 19 checks passed
@JeromySt JeromySt deleted the users/jstatia/native-contentdigest-rename branch June 16, 2026 22:16
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.

2 participants