Skip to main content

Module attestation_verify

Module attestation_verify 

Source
Expand description

Cosign migration gate 3 (Q6=a, CLEANLIB-379 overnight arc) — Rust reference implementation of SDK-side attestation signature verification.

Q14 (capability parity, not policy): this module exposes a capability — verify_attestation() — that an integrator calls when THEY decide a signature check matters. Nothing in cleanlib-client invokes this automatically on the normal verdict-fetch path; a caller that never calls it pays zero cost and sees identical behavior to before this module existed. Matches the CLI/MCP-tool posture (no forced default).

§Design history — why the trust root is a compiled-in map, not a fetch

The FIRST cut of this module (PR #536, merged acab7f44fd8d) shipped PubkeysEndpointLookup as the DEFAULT AttestationKeyLookup: it resolved an attestation’s key_id by fetching GET /v1/pubkeys from cleanlib-app — the SAME service whose signatures it was verifying. BD/PM-seat review (2026-09-13, ratified via Jira CLEANLIB-379 comment 804236) found this architecturally circular and directed a redesign:

  • key_id lives on SignedAttestation (the wire envelope), NOT inside Attestation (the struct cleanlib-cosign-signer actually signs via JCS canonical_bytes()) — confirmed by reading cleanlib-cosign-signer/src/lib.rs lines 91–140. key_id is UNSIGNED.
  • A verifier that uses that unsigned field to pick which key to trust, fetched from the same service producing the signature, gives no protection against a compromised/malicious service: whoever controls /v1/pubkeys controls what “verifies.” This made the business claim (“checked… without trusting us and without our service being up”) false under the PR #536 shape.

Ratified shape (this module, post-redesign):

  1. PinnedKeyMap — a COMPILED-IN key_id -> PEM map for every key ever signed with (today: staging + prod), shipped in the SDK binary, not fetched at runtime. This is the new DEFAULT trust root.
  2. Unknown key_id FAILS CLOSED — PinnedKeyMap::lookup_pem never makes a network call; an unrecognized key_id is an immediate, permanent AttestationInvalid, never a fetch-and-hope.
  3. PubkeysEndpointLookup is DEMOTED to a convenience-only helper (see its doc comment) — it must never be wired as the default AttestationKeyLookup for verify_attestation/Client::verify_attestation again. Its role now is exactly PubkeysEndpointLookup::describe_unknown_key: producing HUMAN-READABLE advisory text (“unknown key_id, verify its fingerprint out of band”) — it must never feed a trust decision.
  4. Publishing fingerprints somewhere independent of the service (so an operator has something to check describe_unknown_key’s advisory against) is a process/docs follow-up, tracked separately — not a code change in this module.

Verification recipe (unchanged by the redesign — only key RESOLUTION changed, not the verify math). Reproduces cleanlib-cosign-signer’s partner_guide_7_2_method_verifies_signer_signature test from the CONSUMER side, independently — this crate does NOT and must NOT depend on cleanlib-cosign-signer, which is publish = false / service-internal per PUBLISHING.md §1.2):

  1. Parse the wire envelope {attestation: {...}, signature_b64, key_id} (the exact shape Verdict::attestation carries as a passthrough serde_json::Value — see types.rs CLEANLIB-496 doc comment).
  2. Re-serialize the attestation sub-object using RFC-8785 JCS (serde_jcs) — this is what the signer actually signs (Attestation::canonical_bytes()), REGARDLESS of the wire’s incidental key order/whitespace. A natural parse + JCS-reserialize is byte-identical to the signer’s bytes; no hand-replicated field order.
  3. base64-decode signature_b64, DER-decode as a P-256 ECDSA signature.
  4. Resolve key_id to a PEM via the caller-supplied AttestationKeyLookup (the default PinnedKeyMap — compiled-in, fail-closed, no network), parse the PEM as a VerifyingKey.
  5. verifying_key.verify(canonical_bytes, &signature).

Errors route through the EXISTING CleanLibraryError taxonomy rather than a bespoke type: malformed envelopes and signature/key/canonical-form mismatches are AttestationInvalid (a variant CLEANLIB-657 already reserved for exactly this “signed attestation present but failed verification” case); a transient key-lookup failure (network/5xx while fetching /v1/pubkeys via the now convenience-only PubkeysEndpointLookup) is the existing Transport variant, so callers already branching on CleanLibraryError::is_retryable() get the right answer for free.

Structs§

PinnedKeyMap
The DEFAULT AttestationKeyLookup as of the gate-3 redesign (2026-09-13, BD-ratified per Jira CLEANLIB-379 comment 804236 — see the module-level “Design history” doc above).
PubkeysEndpointLookup
DEMOTED to convenience-only as of the gate-3 redesign (2026-09-13) — do NOT wire this as the default AttestationKeyLookup for verify_attestation/Client::verify_attestation. PinnedKeyMap is the default now. See the module-level “Design history” doc for why: fetching a verification key from the same service whose signature is being verified is architecturally circular — whoever controls /v1/pubkeys would control what “verifies.”

Constants§

DEFAULT_PUBKEY_CACHE_TTL
Default TTL for a cached key_id -> PEM entry (charter default: 1h). KMS key-version MATERIAL is immutable once created (only state changes), so this TTL exists to notice a NEW key-version being added to /v1/pubkeys (e.g. ahead of gate 5 cutover), not to detect rotation-in-place — there is no rotation-in-place for a given key_id.

Traits§

AttestationKeyLookup
Resolves an attestation’s key_id to a PEM-encoded P-256 public key.

Functions§

verify_attestation
Verify a SignedAttestation envelope ({attestation, signature_b64, key_id} — the exact shape Verdict::attestation carries) against a key resolved via key_lookup.