confium_signatif/
bundle.rs1use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12
13use crate::error::{SignatifError, SignatifResult};
14use crate::graph::Quorum;
15use crate::jcs;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct AnchorRoot {
20 pub name: String,
22 pub aggregate_key: Vec<u8>,
24 pub fingerprint: String,
26 pub quorum: Option<Quorum>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct AnchorLog {
33 pub name: String,
35 pub operator_key: Vec<u8>,
37 pub endpoint: String,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct TrustAnchorBundle {
44 pub bundle_version: String,
46 pub valid_from: DateTime<Utc>,
48 pub valid_until: DateTime<Utc>,
50 pub roots: Vec<AnchorRoot>,
52 pub transparency_logs: Vec<AnchorLog>,
54 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub update_log: Option<crate::discovery::LogRef>,
59 pub bundle_signature: Vec<u8>,
62}
63
64impl TrustAnchorBundle {
65 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(©).expect("bundle serializes"))?
76 .into_bytes(),
77 )
78 }
79
80 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 pub fn matches_root(&self, root_key: &[u8]) -> bool {
117 self.roots.iter().any(|r| r.aggregate_key == root_key)
118 }
119
120 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 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 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}