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}