Skip to main content

confium_signatif/
bundle.rs

1//! Trust anchor bundles (SIGNATIF §7, Annex E).
2//!
3//! The bundle is the starting point of all verification paths: the set
4//! of root trust authorities (with aggregate keys and quorum
5//! parameters) and the recognized transparency logs. It is versioned,
6//! validity-bounded, signed by the root authority, deterministic for
7//! out-of-band distribution, and — per the framework — its updates are
8//! recorded in a transparency log.
9
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12
13use crate::error::{SignatifError, SignatifResult};
14use crate::graph::Quorum;
15use crate::jcs;
16
17/// One root trust authority in the bundle.
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct AnchorRoot {
20    /// Human-readable root name.
21    pub name: String,
22    /// The root's aggregate public key (SPKI or raw verifier bytes).
23    pub aggregate_key: Vec<u8>,
24    /// SHA-256 fingerprint of the aggregate key (hex).
25    pub fingerprint: String,
26    /// Quorum parameters of the root authority.
27    pub quorum: Option<Quorum>,
28}
29
30/// One recognized transparency log.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct AnchorLog {
33    /// Log name.
34    pub name: String,
35    /// The log operator's public key (verifies signed tree heads).
36    pub operator_key: Vec<u8>,
37    /// Log endpoint (primary or mirror).
38    pub endpoint: String,
39}
40
41/// A versioned, signed set of trust anchors.
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct TrustAnchorBundle {
44    /// Version identifier (e.g. "2026.08").
45    pub bundle_version: String,
46    /// Bundle validity start.
47    pub valid_from: DateTime<Utc>,
48    /// Bundle validity end.
49    pub valid_until: DateTime<Utc>,
50    /// Root trust authorities.
51    pub roots: Vec<AnchorRoot>,
52    /// Recognized transparency logs and mirrors.
53    pub transparency_logs: Vec<AnchorLog>,
54    /// The transparency-log reference recording this bundle update
55    /// (§7: bundle updates shall be recorded in a transparency log).
56    /// Signed as part of the bundle body.
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub update_log: Option<crate::discovery::LogRef>,
59    /// Threshold signature of the issuing root over the canonical
60    /// bundle body (all fields except this signature).
61    pub bundle_signature: Vec<u8>,
62}
63
64impl TrustAnchorBundle {
65    /// The canonical bytes covered by the bundle signature: JCS of the
66    /// bundle with `bundle_signature` cleared.
67    ///
68    /// # Errors
69    ///
70    /// Propagates canonicalization errors.
71    pub fn signing_bytes(&self) -> SignatifResult<Vec<u8>> {
72        let mut copy = self.clone();
73        copy.bundle_signature = Vec::new();
74        Ok(
75            jcs::canonicalize(&serde_json::to_value(&copy).expect("bundle serializes"))?
76                .into_bytes(),
77        )
78    }
79
80    /// Verify the bundle signature against the root whose key it was
81    /// produced with, and the validity period at `now`.
82    ///
83    /// # Errors
84    ///
85    /// [`SignatifError::BadSignature`] when no root key verifies the
86    /// signature; [`SignatifError::BundleValidity`] outside the
87    /// validity window.
88    pub fn verify(
89        &self,
90        now: DateTime<Utc>,
91        verifier: &dyn crate::graph::SignatureVerifier,
92    ) -> SignatifResult<()> {
93        if now < self.valid_from || now > self.valid_until {
94            return Err(SignatifError::BundleValidity);
95        }
96        let msg = self.signing_bytes()?;
97        if self.roots.is_empty() {
98            return Err(SignatifError::BadSignature {
99                context: "anchor bundle has no roots".into(),
100            });
101        }
102        if self
103            .roots
104            .iter()
105            .any(|r| verifier.verify(&r.aggregate_key, &msg, &self.bundle_signature))
106        {
107            return Ok(());
108        }
109        Err(SignatifError::BadSignature {
110            context: "anchor bundle signature".into(),
111        })
112    }
113
114    /// Whether `root_key` belongs to one of the bundle's roots — used
115    /// by path-finding to decide path termination.
116    pub fn matches_root(&self, root_key: &[u8]) -> bool {
117        self.roots.iter().any(|r| r.aggregate_key == root_key)
118    }
119
120    /// The root whose aggregate key equals `root_key`, if any.
121    pub fn root_by_key(&self, root_key: &[u8]) -> Option<&AnchorRoot> {
122        self.roots.iter().find(|r| r.aggregate_key == root_key)
123    }
124
125    /// Deterministic distribution bytes (JCS of the full bundle).
126    ///
127    /// # Errors
128    ///
129    /// Propagates canonicalization errors.
130    pub fn distribution_bytes(&self) -> SignatifResult<Vec<u8>> {
131        Ok(
132            jcs::canonicalize(&serde_json::to_value(self).expect("bundle serializes"))?
133                .into_bytes(),
134        )
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use crate::graph::AcceptAllVerifier;
142    use ed25519_dalek::Signer;
143
144    fn generate_key() -> ed25519_dalek::SigningKey {
145        use rand_core::RngCore;
146        let mut seed = [0u8; 32];
147        rand_core::OsRng.fill_bytes(&mut seed);
148        ed25519_dalek::SigningKey::from_bytes(&seed)
149    }
150
151    struct Ed25519Verifier;
152
153    impl crate::graph::SignatureVerifier for Ed25519Verifier {
154        fn verify(&self, pk: &[u8], msg: &[u8], sig: &[u8]) -> bool {
155            use ed25519_dalek::Signature;
156            use ed25519_dalek::Verifier;
157            let Ok(vk) = ed25519_dalek::VerifyingKey::from_bytes(pk.try_into().unwrap()) else {
158                return false;
159            };
160            let Ok(signature) = Signature::from_slice(sig) else {
161                return false;
162            };
163            vk.verify(msg, &signature).is_ok()
164        }
165    }
166
167    #[test]
168    fn bundle_signs_and_verifies() {
169        let sk = generate_key();
170        let pk = sk.verifying_key().as_bytes().to_vec();
171        let mut bundle = TrustAnchorBundle {
172            bundle_version: "2026.08".into(),
173            valid_from: Utc::now() - chrono::Duration::hours(1),
174            valid_until: Utc::now() + chrono::Duration::days(30),
175            roots: vec![AnchorRoot {
176                name: "root".into(),
177                aggregate_key: pk,
178                fingerprint: "00".into(),
179                quorum: None,
180            }],
181            transparency_logs: Vec::new(),
182            bundle_signature: Vec::new(),
183            update_log: None,
184        };
185        bundle.bundle_signature = sk
186            .sign(&bundle.signing_bytes().unwrap())
187            .to_bytes()
188            .to_vec();
189        assert!(bundle.verify(Utc::now(), &Ed25519Verifier).is_ok());
190        bundle.bundle_signature[3] ^= 1;
191        assert!(bundle.verify(Utc::now(), &Ed25519Verifier).is_err());
192    }
193
194    #[test]
195    fn update_log_reference_is_signed_content() {
196        let sk = generate_key();
197        let mut bundle = TrustAnchorBundle {
198            bundle_version: "2026.09".into(),
199            valid_from: Utc::now() - chrono::Duration::hours(1),
200            valid_until: Utc::now() + chrono::Duration::days(30),
201            roots: vec![AnchorRoot {
202                name: "root".into(),
203                aggregate_key: sk.verifying_key().as_bytes().to_vec(),
204                fingerprint: "00".into(),
205                quorum: None,
206            }],
207            transparency_logs: Vec::new(),
208            update_log: Some(crate::discovery::LogRef {
209                log: "nmi-log".into(),
210                sequence: 4242,
211            }),
212            bundle_signature: Vec::new(),
213        };
214        bundle.bundle_signature = sk
215            .sign(&bundle.signing_bytes().unwrap())
216            .to_bytes()
217            .to_vec();
218        assert!(bundle.verify(Utc::now(), &Ed25519Verifier).is_ok());
219        // Flipping the log reference breaks the signature: it is
220        // signed content.
221        bundle.update_log.as_mut().unwrap().sequence = 4243;
222        assert!(bundle.verify(Utc::now(), &Ed25519Verifier).is_err());
223    }
224
225    #[test]
226    fn expired_bundle_fails() {
227        let b = TrustAnchorBundle {
228            bundle_version: "1".into(),
229            valid_from: Utc::now() - chrono::Duration::days(30),
230            valid_until: Utc::now() - chrono::Duration::days(1),
231            roots: vec![],
232            transparency_logs: vec![],
233            bundle_signature: vec![],
234            update_log: None,
235        };
236        assert!(matches!(
237            b.verify(Utc::now(), &AcceptAllVerifier),
238            Err(SignatifError::BundleValidity)
239        ));
240    }
241}