Skip to main content

confium_signatif/
discovery.rs

1//! Chain discovery strategies (SIGNATIF §7, §16).
2//!
3//! A signature wrapper shall carry or reference the full delegation
4//! chain to a root anchor under one of three strategies:
5//!
6//! - [`ChainDelivery::Embedded`] — the full chain inline, fully
7//!   offline-capable;
8//! - [`ChainDelivery::LogReference`] — transparency-log sequence
9//!   pointers, resolved on first encounter (then cacheable);
10//! - [`ChainDelivery::Hybrid`] — the immediate chain inline plus log
11//!   references for freshness.
12
13use std::collections::HashMap;
14
15use serde::{Deserialize, Serialize};
16
17use crate::error::{SignatifError, SignatifResult};
18
19/// A pointer to a delegation credential stored in a transparency log.
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21pub struct LogRef {
22    /// Name of the log holding the entry.
23    pub log: String,
24    /// Sequence number of the certificate entry in that log.
25    pub sequence: u64,
26}
27
28/// How an artifact carries its delegation chain.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30#[serde(tag = "strategy", rename_all = "snake_case")]
31pub enum ChainDelivery {
32    /// The full chain of DER certificates inline (offline-capable).
33    Embedded {
34        /// DER certificates from signer-adjacent to root-adjacent.
35        chain: Vec<Vec<u8>>,
36    },
37    /// Transparency-log sequence pointers for every chain credential.
38    LogReference {
39        /// One pointer per delegation credential on the chain.
40        refs: Vec<LogRef>,
41    },
42    /// Immediate chain inline, log references for freshness checks.
43    Hybrid {
44        /// The immediate (signer-adjacent) credentials inline.
45        immediate: Vec<Vec<u8>>,
46        /// Pointers for the remaining chain and freshness verification.
47        refs: Vec<LogRef>,
48    },
49}
50
51impl ChainDelivery {
52    /// Whether the strategy alone can reconstruct the full chain with
53    /// no network access.
54    pub fn is_offline_capable(&self) -> bool {
55        matches!(self, ChainDelivery::Embedded { .. })
56    }
57}
58
59/// Resolves log references into certificate bytes. Production
60/// implementations bind to a transparency log client (the confium
61/// log-server `/v1/certificates` API); tests bind to in-memory fakes.
62pub trait LogResolver {
63    /// Fetch the certificate bytes for a log reference.
64    ///
65    /// # Errors
66    ///
67    /// Implementations return [`SignatifError::Encoding`] for misses.
68    fn resolve(&mut self, r: &LogRef) -> SignatifResult<Vec<u8>>;
69}
70
71/// A caching resolver decorator: first-encounter fetches go to the
72/// inner resolver, subsequent ones hit the cache — the §16 caching
73/// requirement for connected delivery.
74#[derive(Debug, Default)]
75pub struct CachingResolver<R> {
76    inner: R,
77    cache: HashMap<(String, u64), Vec<u8>>,
78}
79
80impl<R: LogResolver> LogResolver for CachingResolver<R> {
81    fn resolve(&mut self, r: &LogRef) -> SignatifResult<Vec<u8>> {
82        CachingResolver::resolve(self, r)
83    }
84}
85
86impl<R: LogResolver> CachingResolver<R> {
87    /// Wrap an inner resolver with a cache.
88    pub fn new(inner: R) -> Self {
89        Self {
90            inner,
91            cache: HashMap::new(),
92        }
93    }
94
95    /// Resolve with caching.
96    ///
97    /// # Errors
98    ///
99    /// Propagates inner resolver errors.
100    pub fn resolve(&mut self, r: &LogRef) -> SignatifResult<Vec<u8>> {
101        if let Some(hit) = self.cache.get(&(r.log.clone(), r.sequence)) {
102            return Ok(hit.clone());
103        }
104        let fetched = self.inner.resolve(r)?;
105        self.cache
106            .insert((r.log.clone(), r.sequence), fetched.clone());
107        Ok(fetched)
108    }
109
110    /// Number of cached entries (observable for tests and metrics).
111    pub fn cache_len(&self) -> usize {
112        self.cache.len()
113    }
114}
115
116/// Reconstruct the full certificate chain from a delivery strategy,
117/// using `resolver` only for non-embedded strategies.
118///
119/// # Errors
120///
121/// Returns [`SignatifError::Encoding`] when a reference cannot be
122/// resolved.
123pub fn reconstruct_chain(
124    delivery: &ChainDelivery,
125    resolver: Option<&mut dyn LogResolver>,
126) -> SignatifResult<Vec<Vec<u8>>> {
127    match delivery {
128        ChainDelivery::Embedded { chain } => Ok(chain.clone()),
129        ChainDelivery::LogReference { refs } => {
130            let resolver = resolver.ok_or_else(|| {
131                SignatifError::Encoding("log-reference delivery requires a resolver".into())
132            })?;
133            refs.iter().map(|r| resolver.resolve(r)).collect()
134        }
135        ChainDelivery::Hybrid { immediate, refs } => {
136            let mut chain = immediate.clone();
137            let resolver = resolver.ok_or_else(|| {
138                SignatifError::Encoding("hybrid delivery requires a resolver".into())
139            })?;
140            for r in refs {
141                chain.push(resolver.resolve(r)?);
142            }
143            Ok(chain)
144        }
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    struct FakeLog {
153        entries: HashMap<(String, u64), Vec<u8>>,
154        fetches: std::cell::Cell<u32>,
155    }
156
157    impl LogResolver for &FakeLog {
158        fn resolve(&mut self, r: &LogRef) -> SignatifResult<Vec<u8>> {
159            self.fetches.set(self.fetches.get() + 1);
160            self.entries
161                .get(&(r.log.clone(), r.sequence))
162                .cloned()
163                .ok_or_else(|| SignatifError::Encoding("miss".into()))
164        }
165    }
166
167    #[test]
168    fn embedded_is_offline() {
169        let d = ChainDelivery::Embedded {
170            chain: vec![vec![1]],
171        };
172        assert!(d.is_offline_capable());
173        assert_eq!(reconstruct_chain(&d, None).unwrap(), vec![vec![1]]);
174    }
175
176    #[test]
177    fn log_reference_resolves_and_caches() {
178        let log = FakeLog {
179            entries: [("pharma-log".to_string(), 7u64)]
180                .iter()
181                .map(|(l, s)| ((l.clone(), *s), vec![9, 9]))
182                .collect(),
183            fetches: std::cell::Cell::new(0),
184        };
185        let d = ChainDelivery::LogReference {
186            refs: vec![LogRef {
187                log: "pharma-log".into(),
188                sequence: 7,
189            }],
190        };
191        let mut cache = CachingResolver::new(&log);
192        let chain = reconstruct_chain(&d, Some(&mut cache)).unwrap();
193        assert_eq!(chain, vec![vec![9, 9]]);
194        assert_eq!(cache.cache_len(), 1);
195        // Second resolution is served from cache: one inner fetch total.
196        reconstruct_chain(&d, Some(&mut cache)).unwrap();
197        assert_eq!(log.fetches.get(), 1);
198    }
199
200    #[test]
201    fn hybrid_combines_inline_and_resolved() {
202        let log = FakeLog {
203            entries: [("log".to_string(), 1u64)]
204                .iter()
205                .map(|(l, s)| ((l.clone(), *s), vec![2]))
206                .collect(),
207            fetches: std::cell::Cell::new(0),
208        };
209        let d = ChainDelivery::Hybrid {
210            immediate: vec![vec![1]],
211            refs: vec![LogRef {
212                log: "log".into(),
213                sequence: 1,
214            }],
215        };
216        assert_eq!(
217            reconstruct_chain(&d, Some(&mut &log)).unwrap(),
218            vec![vec![1], vec![2]]
219        );
220    }
221}