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