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_idlives onSignedAttestation(the wire envelope), NOT insideAttestation(the structcleanlib-cosign-signeractually signs via JCScanonical_bytes()) — confirmed by readingcleanlib-cosign-signer/src/lib.rslines 91–140.key_idis 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/pubkeyscontrols 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):
PinnedKeyMap— a COMPILED-INkey_id -> PEMmap 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.- Unknown
key_idFAILS CLOSED —PinnedKeyMap::lookup_pemnever makes a network call; an unrecognizedkey_idis an immediate, permanentAttestationInvalid, never a fetch-and-hope. PubkeysEndpointLookupis DEMOTED to a convenience-only helper (see its doc comment) — it must never be wired as the defaultAttestationKeyLookupforverify_attestation/Client::verify_attestationagain. Its role now is exactlyPubkeysEndpointLookup::describe_unknown_key: producing HUMAN-READABLE advisory text (“unknown key_id, verify its fingerprint out of band”) — it must never feed a trust decision.- 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):
- Parse the wire envelope
{attestation: {...}, signature_b64, key_id}(the exact shapeVerdict::attestationcarries as a passthroughserde_json::Value— seetypes.rsCLEANLIB-496 doc comment). - Re-serialize the
attestationsub-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. - base64-decode
signature_b64, DER-decode as a P-256 ECDSA signature. - Resolve
key_idto a PEM via the caller-suppliedAttestationKeyLookup(the defaultPinnedKeyMap— compiled-in, fail-closed, no network), parse the PEM as aVerifyingKey. 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§
- Pinned
KeyMap - The DEFAULT
AttestationKeyLookupas of the gate-3 redesign (2026-09-13, BD-ratified per Jira CLEANLIB-379 comment 804236 — see the module-level “Design history” doc above). - Pubkeys
Endpoint Lookup - DEMOTED to convenience-only as of the gate-3 redesign (2026-09-13) — do
NOT wire this as the default
AttestationKeyLookupforverify_attestation/Client::verify_attestation.PinnedKeyMapis 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/pubkeyswould control what “verifies.”
Constants§
- DEFAULT_
PUBKEY_ CACHE_ TTL - Default TTL for a cached
key_id -> PEMentry (charter default: 1h). KMS key-version MATERIAL is immutable once created (onlystatechanges), 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§
- Attestation
KeyLookup - Resolves an attestation’s
key_idto a PEM-encoded P-256 public key.
Functions§
- verify_
attestation - Verify a
SignedAttestationenvelope ({attestation, signature_b64, key_id}— the exact shapeVerdict::attestationcarries) against a key resolved viakey_lookup.