Skip to main content

auths_transparency/
lib.rs

1// crate-level allow during curve-agnostic refactor.
2#![allow(clippy::disallowed_methods)]
3#![warn(missing_docs)]
4//! Append-only transparency log for Auths.
5//!
6//! Implements C2SP tlog-tiles Merkle tree types, proof verification,
7//! signed note format, and tile storage abstractions.
8//!
9//! ## Feature Flags
10//!
11//! - `native` (default) — enables `TileStore` trait and async tile I/O
12//! - Without features — WASM-safe core: types, Merkle math, proofs, notes
13
14/// Offline verification bundles.
15pub mod bundle;
16/// Log checkpoint types.
17pub mod checkpoint;
18/// Transparency log entry types.
19pub mod entry;
20/// Error types for transparency operations.
21pub mod error;
22/// RFC 6962 Merkle tree operations.
23pub mod merkle;
24/// C2SP signed note format.
25pub mod note;
26/// Inclusion and consistency proof types.
27pub mod proof;
28/// Tile storage trait (behind `native` feature).
29pub mod store;
30/// C2SP tlog-tiles path encoding.
31pub mod tile;
32/// Core newtypes: `MerkleHash`, `LogOrigin`.
33pub mod types;
34/// Offline bundle verification (requires `native` feature for Ed25519).
35#[cfg(feature = "native")]
36pub mod verify;
37/// Witness protocol for split-view protection (requires `native` feature).
38#[cfg(feature = "native")]
39pub mod witness;
40/// Typed, fail-closed witness-diversity policy loader.
41pub mod witness_policy;
42/// The log write path: append leaves, sign checkpoints, mint inclusion
43/// proofs (requires `native` feature).
44#[cfg(feature = "native")]
45pub mod writer;
46
47// Re-export core types
48pub use bundle::{
49    BundleVerificationReport, CheckpointStatus, DelegationChainLink, DelegationStatus,
50    InclusionStatus, NamespaceStatus, OfflineBundle, SignatureStatus, WitnessStatus,
51};
52pub use checkpoint::{Checkpoint, SignedCheckpoint, WitnessCosignature};
53pub use entry::{AccessTier, Entry, EntryBody, EntryContent, EntryType};
54pub use error::TransparencyError;
55pub use merkle::{
56    compute_root, hash_children, hash_leaf, prove_inclusion, verify_consistency, verify_inclusion,
57};
58pub use note::{
59    C2spSignature, NoteSignature, build_signature_line, compute_ecdsa_key_id, compute_key_id,
60    parse_signed_note, parse_signed_note_c2sp, serialize_signed_note,
61};
62pub use proof::{ConsistencyProof, InclusionProof};
63pub use tile::{TILE_HEIGHT, TILE_WIDTH, leaf_tile, tile_count, tile_path};
64pub use types::{LogOrigin, MerkleHash};
65
66#[cfg(feature = "native")]
67mod fs_store;
68#[cfg(feature = "native")]
69pub use fs_store::FsTileStore;
70
71#[cfg(feature = "s3")]
72/// S3-compatible tile store (Tigris, AWS S3, MinIO).
73pub mod s3_store;
74#[cfg(feature = "s3")]
75pub use s3_store::S3TileStore;
76
77#[cfg(feature = "native")]
78pub use store::TileStore;
79#[cfg(feature = "native")]
80pub use verify::{verify_bundle, verify_checkpoint_signature, verify_witness_cosignatures};
81#[cfg(feature = "native")]
82pub use writer::{AppendedLeaf, LogSigningKey, LogWriter};
83
84#[cfg(feature = "native")]
85pub use witness::{
86    ALG_COSIGNATURE_V1, CosignRequest, CosignResponse, DEFAULT_WITNESS_TIMEOUT, WitnessClient,
87    WitnessResult, build_cosignature_line, collect_witness_cosignatures, compute_witness_key_id,
88    cosignature_signed_message, extract_cosignatures, parse_cosignature, serialize_cosignature,
89};
90
91use auths_verifier::CanonicalDid;
92
93pub use auths_keri::witness::independence::{
94    EquivocationDetection, HonestyCeiling, IndependencePolicy, OperatorAttributes,
95    WitnessOperatorInfo,
96};
97
98pub use witness_policy::{
99    WitnessPolicy, WitnessPolicyEntry, WitnessPolicyError, ceiling_for_policy_load,
100};
101
102/// Trust root for verifying transparency log checkpoints.
103///
104/// Contains the log's signing public key and an optional witness list.
105/// For Epic 1 (fn-72), this is hardcoded in the verifier binary.
106/// TUF-based distribution comes in fn-76.
107///
108/// Args:
109/// * `log_public_key` — The Ed25519 public key of the log operator.
110/// * `log_origin` — The log origin string for checkpoint verification.
111/// * `witnesses` — List of trusted witness public keys and names.
112///
113/// Usage:
114/// ```ignore
115/// let trust_root = TrustRoot {
116///     log_public_key: Ed25519PublicKey::from_bytes(key_bytes),
117///     log_origin: LogOrigin::new("auths.dev/log")?,
118///     witnesses: vec![],
119/// };
120/// ```
121#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
122pub struct TrustRoot {
123    /// The log operator's Ed25519 public key. Used only when
124    /// `signature_algorithm == Ed25519`. Under the `EcdsaP256` path,
125    /// this field MUST NOT be the trust anchor — see
126    /// [`Self::ecdsa_log_public_key_der`] for that.
127    pub log_public_key: auths_verifier::Ed25519PublicKey,
128    /// The log origin string (e.g., "auths.dev/log").
129    pub log_origin: LogOrigin,
130    /// Trusted witness keys. Empty for Epic 1.
131    pub witnesses: Vec<TrustRootWitness>,
132    /// Signature algorithm for checkpoint verification. Defaults to Ed25519.
133    /// Rekor production shard uses EcdsaP256.
134    #[serde(default)]
135    pub signature_algorithm: auths_verifier::SignatureAlgorithm,
136    /// DER-encoded SubjectPublicKeyInfo for the log's ECDSA-P256
137    /// public key. REQUIRED when `signature_algorithm == EcdsaP256`;
138    /// the verifier compares the bundle-carried ECDSA pubkey against
139    /// this pinned value byte-for-byte before using it to verify the
140    /// checkpoint signature. Without this field, an attacker can
141    /// submit a bundle with a self-chosen ECDSA key that verifies
142    /// its own forged signature — a classic "trust the key I sent
143    /// you" anti-pattern.
144    #[serde(default)]
145    pub ecdsa_log_public_key_der: Option<Vec<u8>>,
146    /// Minimum-diversity thresholds the *actual cosigning quorum* must clear,
147    /// layered on top of the `n/2+1` count. Defaults to **unconstrained** (no
148    /// diversity required) so a legacy trust root that pins no policy keeps its
149    /// existing behavior; a commons trust root built from a pinned
150    /// `witness_policy.json` carries real thresholds and enforces them.
151    #[serde(default = "IndependencePolicy::unconstrained")]
152    pub independence_policy: IndependencePolicy,
153}
154
155/// A trusted witness in the [`TrustRoot`].
156///
157/// Args:
158/// * `witness_did` — The witness's device DID.
159/// * `name` — Human-readable witness name.
160/// * `public_key` — Witness Ed25519 public key.
161///
162/// Usage:
163/// ```ignore
164/// let witness = TrustRootWitness {
165///     witness_did: CanonicalDid::parse("did:key:z6Mk...")?,
166///     name: "witness-1".into(),
167///     public_key: Ed25519PublicKey::from_bytes(key_bytes),
168/// };
169/// ```
170#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
171pub struct TrustRootWitness {
172    /// The witness's device DID.
173    pub witness_did: CanonicalDid,
174    /// Human-readable witness name.
175    pub name: String,
176    /// Witness Ed25519 public key.
177    pub public_key: auths_verifier::Ed25519PublicKey,
178    /// Operator-independence attributes (shared [`WitnessOperatorInfo`]). Absent ⇒
179    /// this witness cannot contribute to proving cosigning-quorum independence.
180    #[serde(default)]
181    pub operator_info: Option<WitnessOperatorInfo>,
182}
183
184impl TrustRootWitness {
185    /// Build the [`OperatorAttributes`] for this witness, keyed by its public key.
186    ///
187    /// Returns `None` when the witness has no `operator_info` — the caller must
188    /// treat that as "independence cannot be proven", not as a distinct operator.
189    ///
190    /// Args:
191    /// * `self`: The trusted witness.
192    ///
193    /// Usage:
194    /// ```ignore
195    /// let attrs = witness.operator_attributes();
196    /// ```
197    pub fn operator_attributes(&self) -> Option<OperatorAttributes> {
198        self.operator_info
199            .as_ref()
200            .map(|info| info.to_attributes(hex::encode(self.public_key.as_bytes())))
201    }
202}
203
204/// Multi-log trust configuration.
205///
206/// Indexes trust material by log ID. Each log entry is a [`TrustRoot`].
207/// The `default_log` selects which log is used when none is specified.
208///
209/// Usage:
210/// ```ignore
211/// let config = TrustConfig::default_config();
212/// let root = config.get_log("sigstore-rekor").unwrap();
213/// ```
214#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
215pub struct TrustConfig {
216    /// ID of the default log. Must reference a key in `logs`.
217    #[serde(default, skip_serializing_if = "Option::is_none")]
218    pub default_log: Option<String>,
219    /// Map of log ID to trust material.
220    pub logs: std::collections::HashMap<String, TrustRoot>,
221}
222
223impl TrustConfig {
224    /// Look up trust material for a specific log by ID.
225    pub fn get_log(&self, log_id: &str) -> Option<&TrustRoot> {
226        self.logs.get(log_id)
227    }
228
229    /// Get the default log's trust material.
230    pub fn default_log(&self) -> Option<(&str, &TrustRoot)> {
231        let id = self.default_log.as_deref()?;
232        self.logs.get(id).map(|root| (id, root))
233    }
234
235    /// Validate the config at load time.
236    ///
237    /// Checks:
238    /// - If `default_log` is set, it must reference a key in `logs`.
239    /// - Every `TrustRoot` whose `signature_algorithm` is
240    ///   `EcdsaP256` MUST carry a non-empty
241    ///   [`TrustRoot::ecdsa_log_public_key_der`]. Without it the
242    ///   verifier would trust whatever ECDSA key the bundle carried
243    ///   (→ trivially forgeable checkpoint). Without this check
244    ///   operators can ship configs that look healthy and silently
245    ///   accept any ECDSA signature.
246    pub fn validate(&self) -> std::result::Result<(), TransparencyError> {
247        if let Some(ref id) = self.default_log
248            && !self.logs.contains_key(id)
249        {
250            return Err(TransparencyError::InvalidNote(format!(
251                "default_log '{}' not found in logs. Available: {:?}",
252                id,
253                self.logs.keys().collect::<Vec<_>>()
254            )));
255        }
256
257        for (log_id, root) in &self.logs {
258            if matches!(
259                root.signature_algorithm,
260                auths_verifier::SignatureAlgorithm::EcdsaP256
261            ) {
262                let has_key = root
263                    .ecdsa_log_public_key_der
264                    .as_ref()
265                    .is_some_and(|b| !b.is_empty());
266                if !has_key {
267                    return Err(TransparencyError::InvalidNote(format!(
268                        "log '{}' declares signature_algorithm=EcdsaP256 but \
269                         ecdsa_log_public_key_der is missing or empty — refuse to \
270                         trust a bundle-carried ECDSA key",
271                        log_id
272                    )));
273                }
274            }
275        }
276        Ok(())
277    }
278
279    /// Compiled-in default: Rekor production shard.
280    ///
281    /// Origin pinned from `GET https://rekor.sigstore.dev/api/v1/log`
282    /// on 2026-04-09. Public key from sigstore trusted_root.json.
283    pub fn default_config() -> Self {
284        use base64::Engine;
285        use base64::engine::general_purpose::STANDARD;
286        use std::collections::HashMap;
287
288        // Rekor production shard ECDSA P-256 public key (DER SPKI).
289        // Source: https://github.com/sigstore/root-signing/blob/main/targets/trusted_root.json
290        // Decoded from the base64 below at build / init time so the
291        // trust root carries the actual pinned bytes, not a zero
292        // placeholder.
293        const REKOR_PROD_PUBKEY_B64: &str = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==";
294        // The decode target is a fixed compile-time base64 literal.
295        // A failure here would be a source-code typo, not a runtime
296        // condition — using `unwrap_or_default` keeps clippy's
297        // `expect_used` gate satisfied. `TrustConfig::validate()`'s
298        // "ECDSA config must carry a non-empty pinned key" check
299        // is the tripwire: an accidentally-empty decode fails at
300        // config load time, not silently.
301        let rekor_ecdsa_pk_der = STANDARD.decode(REKOR_PROD_PUBKEY_B64).unwrap_or_default();
302
303        // Ed25519 field kept as zero-bytes because the production
304        // shard does not use Ed25519. The dispatch is on
305        // `signature_algorithm`, so this placeholder is never
306        // consulted.
307        let rekor_root = TrustRoot {
308            log_public_key: auths_verifier::Ed25519PublicKey::from_bytes([0u8; 32]),
309            // Origin pinned from: GET https://rekor.sigstore.dev/api/v1/log → signedTreeHead, first line
310            log_origin: LogOrigin::new_unchecked("rekor.sigstore.dev - 1193050959916656506"),
311            witnesses: vec![],
312            signature_algorithm: auths_verifier::SignatureAlgorithm::EcdsaP256,
313            ecdsa_log_public_key_der: Some(rekor_ecdsa_pk_der),
314            independence_policy: IndependencePolicy::unconstrained(),
315        };
316
317        let mut logs = HashMap::new();
318        logs.insert("sigstore-rekor".to_string(), rekor_root);
319
320        Self {
321            default_log: Some("sigstore-rekor".to_string()),
322            logs,
323        }
324    }
325}