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}