auths_core/ports/transparency_log.rs
1//! Transparency log port trait for pluggable log backends.
2//!
3//! Abstracts appending attestations to a transparency log and
4//! retrieving inclusion proofs. The SDK and CLI depend only on this
5//! trait — adapter selection happens at the composition root.
6
7use async_trait::async_trait;
8use auths_transparency::checkpoint::SignedCheckpoint;
9use auths_transparency::proof::{ConsistencyProof, InclusionProof};
10use auths_transparency::types::LogOrigin;
11use auths_verifier::Ed25519PublicKey;
12
13/// Result of submitting a leaf to a transparency log.
14///
15/// Args:
16/// * `leaf_index` — The zero-based index assigned to the leaf.
17/// * `inclusion_proof` — Merkle inclusion proof against the checkpoint.
18/// * `signed_checkpoint` — The log's signed checkpoint at submission time.
19///
20/// Usage:
21/// ```ignore
22/// let submission = log.submit(data, &pk, &sig).await?;
23/// assert!(submission.inclusion_proof.verify(&leaf_hash).is_ok());
24/// ```
25#[derive(Debug, Clone)]
26pub struct LogSubmission {
27 /// Zero-based leaf index in the log.
28 pub leaf_index: u64,
29 /// Merkle inclusion proof for the leaf against the checkpoint.
30 pub inclusion_proof: InclusionProof,
31 /// Signed checkpoint at the time of submission.
32 pub signed_checkpoint: SignedCheckpoint,
33}
34
35/// Static metadata about a transparency log backend.
36///
37/// Args:
38/// * `log_id` — Stable identifier for trust config lookup (e.g., `"sigstore-rekor"`).
39/// * `log_origin` — C2SP checkpoint origin string.
40/// * `log_public_key` — The log's public key for checkpoint verification.
41/// * `api_url` — Optional API endpoint URL.
42///
43/// Usage:
44/// ```ignore
45/// let meta = log.metadata();
46/// println!("Log: {} ({})", meta.log_id, meta.log_origin);
47/// ```
48#[derive(Debug, Clone)]
49pub struct LogMetadata {
50 /// Stable identifier used in trust config and bundle format.
51 pub log_id: String,
52 /// C2SP checkpoint origin string (byte-for-byte match required).
53 pub log_origin: LogOrigin,
54 /// The log's public key for checkpoint signature verification.
55 pub log_public_key: Ed25519PublicKey,
56 /// API endpoint URL, if applicable.
57 pub api_url: Option<String>,
58}
59
60/// Errors from transparency log operations.
61#[derive(Debug, thiserror::Error)]
62pub enum LogError {
63 /// The log rejected the submitted entry.
64 #[error("submission rejected: {reason}")]
65 SubmissionRejected {
66 /// Why the submission was rejected.
67 reason: String,
68 },
69
70 /// Network or connection error reaching the log.
71 #[error("network error: {0}")]
72 NetworkError(String),
73
74 /// Log returned HTTP 429; caller should wait and retry.
75 #[error("rate limited, retry after {retry_after_secs}s")]
76 RateLimited {
77 /// Seconds to wait before retrying.
78 retry_after_secs: u64,
79 },
80
81 /// Log returned an unparseable or unexpected response.
82 #[error("invalid response: {0}")]
83 InvalidResponse(String),
84
85 /// Requested entry not found in the log.
86 #[error("entry not found")]
87 EntryNotFound,
88
89 /// Consistency or inclusion proof verification failed.
90 #[error("consistency violation: {0}")]
91 ConsistencyViolation(String),
92
93 /// Log is temporarily or permanently unavailable.
94 #[error("log unavailable: {0}")]
95 Unavailable(String),
96}
97
98impl auths_crypto::AuthsErrorInfo for LogError {
99 fn error_code(&self) -> &'static str {
100 match self {
101 Self::SubmissionRejected { .. } => "AUTHS-E9001",
102 Self::NetworkError(_) => "AUTHS-E9002",
103 Self::RateLimited { .. } => "AUTHS-E9003",
104 Self::InvalidResponse(_) => "AUTHS-E9004",
105 Self::EntryNotFound => "AUTHS-E9005",
106 Self::ConsistencyViolation(_) => "AUTHS-E9006",
107 Self::Unavailable(_) => "AUTHS-E9007",
108 }
109 }
110
111 fn suggestion(&self) -> Option<&'static str> {
112 match self {
113 Self::SubmissionRejected { .. } => {
114 Some("Check the attestation format and payload size")
115 }
116 Self::NetworkError(_) => Some("Check your internet connection and the log's API URL"),
117 Self::RateLimited { .. } => Some("Wait and retry; the log is rate-limiting requests"),
118 Self::InvalidResponse(_) => {
119 Some("The log returned an unexpected response; check the log version")
120 }
121 Self::EntryNotFound => Some("The entry may not be sequenced yet; retry after a moment"),
122 Self::ConsistencyViolation(_) => {
123 Some("The log returned data that does not match what was submitted")
124 }
125 Self::Unavailable(_) => {
126 Some("The transparency log is unavailable; retry later or use --allow-unlogged")
127 }
128 }
129 }
130}
131
132/// Pluggable transparency log backend.
133///
134/// Abstracts appending attestations to a transparency log and retrieving
135/// Merkle proofs. Adapters translate backend-native formats (e.g., Rekor
136/// hashedrekord) to canonical `auths-transparency` types at the boundary.
137///
138/// This is the hexagonal seam for the log layer: `RekorClient` (in
139/// `auths-infra-rekor`) is the first adapter, talking to the public Sigstore
140/// Rekor log. A future operated-sequencer adapter (`SequencerTransparencyLog`)
141/// implements this **same** trait — `submit`/`get_checkpoint`/proofs over the
142/// org's own tile store — so the CLI and SDK consumers
143/// (`submit_attestation_to_log`, the compliance query) switch backends by
144/// swapping the `Arc<dyn TransparencyLog>` and never change. No new port type is
145/// needed; the sequencer is purely a second adapter behind this trait.
146///
147/// Usage:
148/// ```ignore
149/// let log: Arc<dyn TransparencyLog> = factory.create_log(&config)?;
150/// let submission = log.submit(&attestation_bytes, &pk, &sig).await?;
151/// ```
152#[async_trait]
153pub trait TransparencyLog: Send + Sync {
154 /// Submit a leaf to the log and receive an inclusion proof.
155 ///
156 /// The adapter wraps `leaf_data` in whatever envelope the backend
157 /// requires. `public_key` and `signature` are provided for backends
158 /// that verify entry signatures on submission.
159 ///
160 /// Args:
161 /// * `leaf_data` — Raw bytes to log (typically serialized attestation JSON).
162 /// * `public_key` — Signer's public key (Ed25519 DER or P-256 SEC1).
163 /// * `curve` — The curve of the public key.
164 /// * `signature` — Signature over `leaf_data`.
165 async fn submit(
166 &self,
167 leaf_data: &[u8],
168 public_key: &[u8],
169 curve: auths_crypto::CurveType,
170 signature: &[u8],
171 ) -> Result<LogSubmission, LogError>;
172
173 /// Fetch the log's current signed checkpoint.
174 async fn get_checkpoint(&self) -> Result<SignedCheckpoint, LogError>;
175
176 /// Fetch an inclusion proof for a leaf at `leaf_index` in a tree of `tree_size`.
177 ///
178 /// Args:
179 /// * `leaf_index` — Zero-based index of the leaf.
180 /// * `tree_size` — Tree size to prove inclusion against.
181 async fn get_inclusion_proof(
182 &self,
183 leaf_index: u64,
184 tree_size: u64,
185 ) -> Result<InclusionProof, LogError>;
186
187 /// Fetch a consistency proof between two tree sizes.
188 ///
189 /// Args:
190 /// * `old_size` — Earlier tree size.
191 /// * `new_size` — Later tree size.
192 async fn get_consistency_proof(
193 &self,
194 old_size: u64,
195 new_size: u64,
196 ) -> Result<ConsistencyProof, LogError>;
197
198 /// Return static metadata about this log backend.
199 fn metadata(&self) -> LogMetadata;
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn log_error_display() {
208 let err = LogError::SubmissionRejected {
209 reason: "payload too large".into(),
210 };
211 assert_eq!(err.to_string(), "submission rejected: payload too large");
212
213 let err = LogError::RateLimited {
214 retry_after_secs: 30,
215 };
216 assert_eq!(err.to_string(), "rate limited, retry after 30s");
217
218 let err = LogError::NetworkError("connection refused".into());
219 assert_eq!(err.to_string(), "network error: connection refused");
220
221 let err = LogError::Unavailable("service unavailable".into());
222 assert_eq!(err.to_string(), "log unavailable: service unavailable");
223 }
224
225 #[test]
226 fn log_error_codes() {
227 use auths_crypto::AuthsErrorInfo;
228
229 assert_eq!(
230 LogError::SubmissionRejected {
231 reason: String::new()
232 }
233 .error_code(),
234 "AUTHS-E9001"
235 );
236 assert_eq!(
237 LogError::NetworkError(String::new()).error_code(),
238 "AUTHS-E9002"
239 );
240 assert_eq!(
241 LogError::RateLimited {
242 retry_after_secs: 0
243 }
244 .error_code(),
245 "AUTHS-E9003"
246 );
247 assert_eq!(
248 LogError::InvalidResponse(String::new()).error_code(),
249 "AUTHS-E9004"
250 );
251 assert_eq!(LogError::EntryNotFound.error_code(), "AUTHS-E9005");
252 assert_eq!(
253 LogError::ConsistencyViolation(String::new()).error_code(),
254 "AUTHS-E9006"
255 );
256 assert_eq!(
257 LogError::Unavailable(String::new()).error_code(),
258 "AUTHS-E9007"
259 );
260 }
261
262 #[test]
263 fn log_error_suggestions_not_none() {
264 use auths_crypto::AuthsErrorInfo;
265
266 let variants: Vec<LogError> = vec![
267 LogError::SubmissionRejected {
268 reason: String::new(),
269 },
270 LogError::NetworkError(String::new()),
271 LogError::RateLimited {
272 retry_after_secs: 0,
273 },
274 LogError::InvalidResponse(String::new()),
275 LogError::EntryNotFound,
276 LogError::ConsistencyViolation(String::new()),
277 LogError::Unavailable(String::new()),
278 ];
279 for v in &variants {
280 assert!(
281 v.suggestion().is_some(),
282 "missing suggestion for {}",
283 v.error_code()
284 );
285 }
286 }
287
288 // Compile-time check: trait must be object-safe for Arc<dyn TransparencyLog>
289 fn _assert_object_safe(_: std::sync::Arc<dyn TransparencyLog>) {}
290}