Skip to main content

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}