lys-core 0.3.1

Domain-agnostic cryptographic trust primitives — certificate authority, Merkle transparency log, signed attestations, sealed envelopes, and Ed25519 key management.
Documentation
//! Signed attestations: the `lys/attestation/v2` tagged `COSE_Sign1`
//! artifact (RFC 9052) binding a payload's SHA-256 hash and a
//! unix-millisecond timestamp to an Ed25519 signer key.
//!
//! # Invariants
//!
//! - **The artifact is the only durable form.** [`Attestation`] implements
//!   no `serde`; the wire shape is exactly the tagged `COSE_Sign1` emitted by
//!   [`Attestation::to_cose_bytes`] — protected headers
//!   `{1: -8 (EdDSA), 3: "application/vnd.lys.attestation.v2+cbor",
//!   4: <raw 32-byte signer key>}`, an empty unprotected map, a claims
//!   payload `{1: <32-byte SHA-256 hash>, 2: <unix-ms timestamp>}`, and a
//!   64-byte Ed25519 signature over the RFC 9052 §4.4 `Sig_structure` with
//!   empty `external_aad`. Off-the-shelf COSE libraries verify it directly.
//! - **Canonical-encoding strictness.** Encoding is RFC 8949 §4.2 core
//!   deterministic, and [`Attestation::from_cose_bytes`] rejects any input
//!   that is not byte-identical to the canonical re-encoding of its parsed
//!   fields — even inputs whose signature is cryptographically valid
//!   (unprotected-header smuggling, indefinite lengths, oversized integer
//!   heads, reordering, tag stripping, trailing garbage).
//! - **Size window.** Canonical artifacts are always 191–199 bytes; the
//!   parser caps input at 1024 bytes before any CBOR work.
//! - **Non-oracle verification.** Every failure — parse, canonicality,
//!   header pins, payload mismatch, signature — collapses to the single
//!   [`TrustError::InvalidSignature`] value.
//! - **Domain separation, by two different mechanisms — ⛔ corrected.** This
//!   invariant used to read "byte-0 domain disjointness … every other signing
//!   context in this crate starts differently … so attestation signatures are
//!   structurally non-interchangeable with any other lys signature." **The
//!   stated mechanism is false**, and each new COSE format in this crate has
//!   falsified it again. The conclusion still holds; the argument for it does
//!   not, so it is replaced rather than patched:
//!
//!   - **Against non-COSE contexts**, byte-0 disjointness is genuine and is the
//!     mechanism: signed-note bodies are UTF-8, where `0x84` cannot appear at
//!     position 0, and X.509 TBS bytes begin `0x30`.
//!   - **Against the crate's other COSE artifacts it is no mechanism at all.**
//!     Every `COSE_Sign1` preimage in lys begins with the same twelve bytes,
//!     `0x84 0x6A "Signature1"` — the receipt, the consistency receipt and the
//!     anchor delegation included. What separates them is *inside* the signed
//!     bytes: a distinct protected content type, and protected buckets of
//!     different lengths and shapes (an attestation's is a 3-entry map of 80
//!     bytes; a receipt's a 4-entry map; a delegation's a 3-entry map of 86).
//!     Every one of those verifiers additionally pins **its own** content type
//!     rather than reading the wire's, so none will parse another's artifact.
//!
//!   The correction is recorded rather than silently applied because the
//!   original phrasing survived two new formats: `receipt/sign.rs` was written
//!   with the accurate argument and this module was not updated alongside it.
//!
//! # ⚠️ `signer_public_key` is a claim, not an authority
//!
//! [`verify_attestation`] and [`verify_attestation_bytes`] read the signer key
//! **out of the artifact** and verify against it. They take no expected-key
//! argument, so they establish that *somebody holding some key* signed this
//! payload — never that the signer is anyone the caller trusts. An attacker
//! signs an attestation over any payload with their own key, puts their own key
//! in `kid`, and the result is cryptographically perfect and vouches for
//! nothing. This is the same trap as a self-signed certificate.
//!
//! **Every caller must compare `attestation.signer_public_key` against a key it
//! independently trusts.** Nothing in the two functions named above does it for
//! you, and the signature check passing is not that comparison.
//!
//! ## ✅ 2026-08-09 — there is now an attributing verifier, and the paragraph that said there could not be was reasoning from a false premise
//!
//! [`verify_attestation_by_signer`] and [`verify_attestation_bytes_by_signer`]
//! take the expected key as an argument, compare it without an early exit, and
//! collapse an unexpected signer into the same [`TrustError::InvalidSignature`]
//! as a bad signature — so a prober cannot learn which key the verifier holds.
//!
//! ⛔ **What this section used to say, and why it was wrong.** It read: *"This
//! module cannot follow them: it is ungated, shipped and semver-bound, so the
//! fix is a `v3` signature rather than a change here, and until then the warning
//! is the mitigation."* Every fact in that sentence is true and the conclusion
//! does not follow. Being semver-bound forbids **changing** `verify_attestation`'s
//! signature; it does not forbid **adding** a function beside it, which is
//! additive and needs no new version of anything — not of the API and certainly
//! not of the wire format, which is untouched.
//!
//! ⭐ **The limitation read as unfixable because it had been framed as a change
//! to the existing function.** Having named the only fix and found it blocked,
//! the paragraph stopped looking — and "until then the warning is the
//! mitigation" then made a documented hole read as a decision someone had taken
//! deliberately. **A documented limitation whose stated reason is wrong is worse
//! than an undocumented one: it survives review, because the reader checks the
//! reason rather than the claim.**
//!
//! ## The residual weakness, which is real and is NOT closed
//!
//! `receipt` and `delegation` have **no** unattributed verify. This module still
//! does, it still ships, and it still has the shorter name — so the default
//! remains the one that authenticates nothing, and the safe call is the longer
//! one a hurried integrator will not reach for. That asymmetry cannot be fixed
//! additively.
//!
//! Removing or deprecating [`verify_attestation`] is a **breaking change to a
//! published crate** and therefore a release decision, not one this module can
//! take. It is recorded here as an open question for `0.3.0` rather than
//! resolved quietly in either direction.
//!
//! ## What this module PROVIDES, and what it merely PERMITS
//!
//! **A safety property supplied by the caller is not a property of this
//! library.** The distinction is invisible to anyone integrating by reading the
//! happy path — the call returns `Ok`, and nothing in the signature indicates
//! that half the guarantee was the caller's job — so it is drawn explicitly:
//!
//! | | |
//! |---|---|
//! | **Provided** | The signature is valid over the canonical artifact bytes, under strict Ed25519, for the key the artifact names. Canonical-encoding strictness, the header pins, the size window, and non-oracle failure. |
//! | **Permitted, never provided** | That the signer is anyone in particular. **Attribution is entirely the caller's**, and no argument to these functions can express it. |
//!
//! This was not written from the API but measured against a real integrator, by
//! someone who could read a repository this crate cannot see. One integrator's
//! node consumes this module and closes the gap **by construction** rather than by
//! discipline: at its single production verification site it looks up a registry
//! pin keyed on the claimed sender, passes it in, and resolves to one of three
//! verdicts — pin matches, pin differs (*forged*), or **no pin at all, which
//! yields `UnknownSigner` and can never become `Verified`.* The type makes the
//! comparison unskippable, so a missing pin cannot degrade into a pass.
//!
//! That shape is the worked example, offered because an instruction to "compare
//! the key" is weaker than a demonstration of what comparing it looks like when
//! it cannot be forgotten. **None of that construction ships here**, and it does
//! not travel to the next integrator by being true of this one — which is
//! exactly why the table above exists rather than a sentence saying callers
//! ought to be careful.
//!
//! *(Finding and framing: Waffles the Terrible, on that integrator's seat, 2026-08-08.
//! This crate's own review had bounded the gap as "mitigated in the callers I
//! found" — a search of this repository only, which could establish that the
//! hole existed but never that any consumer had closed it.)*
//!
//! Domain meaning (execution receipt, audit entry, dispatch attestation) is
//! applied by consumers; the trust crate only provides the sign/verify and
//! the artifact shape.
//!
//! [`TrustError::InvalidSignature`]: crate::error::TrustError::InvalidSignature

pub mod artifact;
mod encoding;
pub mod sign;

pub use artifact::Attestation;
pub use sign::{
    sign_attestation, verify_attestation, verify_attestation_by_signer, verify_attestation_bytes,
    verify_attestation_bytes_by_signer,
};