1use std::collections::BTreeMap;
19
20use serde::{Deserialize, Serialize};
21
22use crate::bundle::TrustAnchorBundle;
23use crate::error::{SignatifError, SignatifResult};
24use crate::jcs;
25use crate::scope::ScopeDimensions;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29pub struct Quorum {
30 pub t: u32,
32 pub n: u32,
34}
35
36impl Quorum {
37 pub fn new(t: u32, n: u32) -> SignatifResult<Self> {
44 if t == 0 || t > n {
45 return Err(SignatifError::Encoding(format!(
46 "invalid quorum {t} of {n}: require 1 <= T <= N"
47 )));
48 }
49 Ok(Self { t, n })
50 }
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(rename_all = "snake_case")]
56pub enum AuthorityKind {
57 Root,
59 Delegated,
61 EndCertificate,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct AuthorityNode {
68 pub id: String,
70 pub kind: AuthorityKind,
72 pub public_key: Vec<u8>,
74 pub quorum: Option<Quorum>,
76 pub scope: ScopeDimensions,
78}
79
80impl AuthorityNode {
81 pub fn binding_bytes(&self) -> SignatifResult<Vec<u8>> {
89 let v = serde_json::json!({
90 "id": self.id,
91 "kind": self.kind,
92 "public_key": hex::encode(&self.public_key),
93 "quorum": self.quorum,
94 "scope": self.scope,
95 });
96 Ok(jcs::canonicalize(&v)?.into_bytes())
97 }
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct DelegationEdge {
103 pub parent: String,
105 pub child: String,
107 pub signature: Vec<u8>,
109}
110
111#[derive(Debug, Clone)]
113pub struct PathLink {
114 pub parent: AuthorityNode,
116 pub child: AuthorityNode,
118 pub edge: DelegationEdge,
120}
121
122#[derive(Debug, Clone)]
124pub struct VerificationPath {
125 pub links: Vec<PathLink>,
127 pub root: AuthorityNode,
129}
130
131impl VerificationPath {
132 pub fn authorities(&self) -> Vec<&AuthorityNode> {
134 let mut out = Vec::new();
135 if let Some(first) = self.links.first() {
136 out.push(&first.child);
137 }
138 for link in &self.links {
139 out.push(&link.parent);
140 }
141 out.push(&self.root);
142 out
143 }
144
145 pub fn signer_scope(&self) -> &ScopeDimensions {
147 self.links
148 .first()
149 .map(|l| &l.child.scope)
150 .unwrap_or(&self.root.scope)
151 }
152}
153
154pub trait SignatureVerifier {
158 fn verify(&self, public_key: &[u8], message: &[u8], signature: &[u8]) -> bool;
160}
161
162#[derive(Debug, Clone, Copy, Default)]
165pub struct AcceptAllVerifier;
166
167impl SignatureVerifier for AcceptAllVerifier {
168 fn verify(&self, _pk: &[u8], _msg: &[u8], _sig: &[u8]) -> bool {
169 true
170 }
171}
172
173#[derive(Debug, Clone, Default, Serialize, Deserialize)]
175pub struct TrustGraph {
176 nodes: BTreeMap<String, AuthorityNode>,
177 edges: Vec<DelegationEdge>,
178}
179
180impl TrustGraph {
181 pub fn new() -> Self {
183 Self::default()
184 }
185
186 pub fn add_node(&mut self, node: AuthorityNode) {
188 self.nodes.insert(node.id.clone(), node);
189 }
190
191 pub fn add_delegation(&mut self, edge: DelegationEdge) -> SignatifResult<()> {
198 if !self.nodes.contains_key(&edge.parent) || !self.nodes.contains_key(&edge.child) {
199 return Err(SignatifError::Encoding(format!(
200 "delegation references unknown node(s) {} -> {}",
201 edge.parent, edge.child
202 )));
203 }
204 self.edges.push(edge);
205 if !self.is_acyclic() {
206 self.edges.pop();
207 return Err(SignatifError::Encoding(
208 "delegation would create a cycle in the trust graph".into(),
209 ));
210 }
211 Ok(())
212 }
213
214 pub fn nodes(&self) -> impl Iterator<Item = &AuthorityNode> {
216 self.nodes.values()
217 }
218
219 pub fn edges(&self) -> &[DelegationEdge] {
221 &self.edges
222 }
223
224 pub fn node(&self, id: &str) -> Option<&AuthorityNode> {
226 self.nodes.get(id)
227 }
228
229 pub fn parents_of(&self, child: &str) -> Vec<&DelegationEdge> {
231 self.edges.iter().filter(|e| e.child == child).collect()
232 }
233
234 pub fn is_acyclic(&self) -> bool {
236 #[derive(Clone, Copy, PartialEq)]
237 enum Mark {
238 #[allow(dead_code)]
239 White,
240 Grey,
241 Black,
242 }
243 fn visit(graph: &TrustGraph, id: &str, marks: &mut BTreeMap<String, Mark>) -> bool {
244 match marks.get(id).copied() {
245 Some(Mark::Grey) => false,
246 Some(Mark::Black) => true,
247 _ => {
248 marks.insert(id.to_string(), Mark::Grey);
249 for edge in graph.parents_of(id) {
250 if !visit(graph, &edge.parent, marks) {
251 return false;
252 }
253 }
254 marks.insert(id.to_string(), Mark::Black);
255 true
256 }
257 }
258 }
259 let mut marks: BTreeMap<String, Mark> = BTreeMap::new();
260 self.nodes.keys().all(|id| visit(self, id, &mut marks))
261 }
262
263 pub fn find_paths(
275 &self,
276 signer: &str,
277 bundle: &TrustAnchorBundle,
278 verifier: &dyn SignatureVerifier,
279 ) -> SignatifResult<Vec<VerificationPath>> {
280 let signer_node = self.nodes.get(signer).ok_or(SignatifError::NoPath)?;
281 let mut paths = Vec::new();
282 let mut prefix: Vec<PathLink> = Vec::new();
283 self.walk(signer_node, bundle, verifier, &mut prefix, &mut paths)?;
284 Ok(paths)
285 }
286
287 fn walk(
288 &self,
289 node: &AuthorityNode,
290 bundle: &TrustAnchorBundle,
291 verifier: &dyn SignatureVerifier,
292 prefix: &mut Vec<PathLink>,
293 out: &mut Vec<VerificationPath>,
294 ) -> SignatifResult<()> {
295 if node.kind == AuthorityKind::Root {
296 if bundle.matches_root(&node.public_key) {
297 out.push(VerificationPath {
298 links: prefix.clone(),
299 root: node.clone(),
300 });
301 }
302 return Ok(());
303 }
304 for edge in self.parents_of(&node.id) {
305 let parent = match self.nodes.get(&edge.parent) {
306 Some(p) => p,
307 None => continue,
308 };
309 if !verifier.verify(&parent.public_key, &node.binding_bytes()?, &edge.signature) {
310 return Err(SignatifError::BadSignature {
311 context: format!("delegation {} -> {}", edge.parent, node.id),
312 });
313 }
314 if let Some(dim) = node.scope.first_widened_dimension(&parent.scope) {
315 return Err(SignatifError::ScopeWidening {
316 parent: parent.id.clone(),
317 child: node.id.clone(),
318 dimension: dim,
319 });
320 }
321 prefix.push(PathLink {
322 parent: parent.clone(),
323 child: node.clone(),
324 edge: edge.clone(),
325 });
326 self.walk(parent, bundle, verifier, prefix, out)?;
327 prefix.pop();
328 }
329 Ok(())
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336 use crate::bundle::AnchorRoot;
337 use crate::scope::ScopeValue;
338 use chrono::{Duration, Utc};
339 use sha2::Digest as _;
340
341 fn generate_key() -> ed25519_dalek::SigningKey {
342 use rand_core::RngCore;
343 let mut seed = [0u8; 32];
344 rand_core::OsRng.fill_bytes(&mut seed);
345 ed25519_dalek::SigningKey::from_bytes(&seed)
346 }
347
348 fn ed25519_keypair() -> (ed25519_dalek::SigningKey, Vec<u8>) {
349 let sk = generate_key();
350 let pk = sk.verifying_key().as_bytes().to_vec();
351 (sk, pk)
352 }
353
354 struct Ed25519Verifier;
355
356 impl SignatureVerifier for Ed25519Verifier {
357 fn verify(&self, pk: &[u8], msg: &[u8], sig: &[u8]) -> bool {
358 use ed25519_dalek::Signature;
359 use ed25519_dalek::Verifier;
360 let Ok(vk) = ed25519_dalek::VerifyingKey::from_bytes(pk.try_into().unwrap()) else {
361 return false;
362 };
363 let Ok(signature) = Signature::from_slice(sig) else {
364 return false;
365 };
366 vk.verify(msg, &signature).is_ok()
367 }
368 }
369
370 fn root(id: &str, scope: ScopeDimensions) -> (AuthorityNode, ed25519_dalek::SigningKey) {
371 let (sk, pk) = ed25519_keypair();
372 (
373 AuthorityNode {
374 id: id.into(),
375 kind: AuthorityKind::Root,
376 public_key: pk,
377 quorum: None,
378 scope,
379 },
380 sk,
381 )
382 }
383
384 fn child(
385 id: &str,
386 kind: AuthorityKind,
387 scope: ScopeDimensions,
388 key: &ed25519_dalek::SigningKey,
389 ) -> AuthorityNode {
390 AuthorityNode {
391 id: id.into(),
392 kind,
393 public_key: key.verifying_key().as_bytes().to_vec(),
394 quorum: Some(Quorum::new(2, 3).unwrap()),
395 scope,
396 }
397 }
398
399 fn sign(sk: &ed25519_dalek::SigningKey, node: &AuthorityNode) -> Vec<u8> {
400 use ed25519_dalek::Signer;
401 sk.sign(&node.binding_bytes().unwrap()).to_bytes().to_vec()
402 }
403
404 fn bundle_for(root_node: &AuthorityNode) -> TrustAnchorBundle {
405 TrustAnchorBundle {
406 bundle_version: "2026.08".into(),
407 valid_from: Utc::now() - Duration::hours(1),
408 valid_until: Utc::now() + Duration::days(365),
409 roots: vec![AnchorRoot {
410 name: root_node.id.clone(),
411 aggregate_key: root_node.public_key.clone(),
412 fingerprint: hex::encode(sha2::Sha256::digest(&root_node.public_key)),
413 quorum: root_node.quorum,
414 }],
415 transparency_logs: Vec::new(),
416 bundle_signature: Vec::new(),
417 update_log: None,
418 }
419 }
420
421 #[test]
422 fn finds_single_path_and_validates_links() {
423 let mut parent_scope = ScopeDimensions::unconstrained();
424 parent_scope.set("domain", ScopeValue::Wildcard);
425 let (root_node, root_sk) = root("root-1", parent_scope);
426
427 let mut child_scope = ScopeDimensions::unconstrained();
428 child_scope.set("domain", ScopeValue::Single("pharma".into()));
429 let (end_sk, _) = ed25519_keypair();
430 let end = child("end-1", AuthorityKind::EndCertificate, child_scope, &end_sk);
431
432 let mut graph = TrustGraph::new();
433 graph.add_node(root_node.clone());
434 graph.add_node(end.clone());
435 graph
436 .add_delegation(DelegationEdge {
437 parent: "root-1".into(),
438 child: "end-1".into(),
439 signature: sign(&root_sk, &end),
440 })
441 .unwrap();
442
443 let bundle = bundle_for(&root_node);
444 let paths = graph
445 .find_paths("end-1", &bundle, &Ed25519Verifier)
446 .unwrap();
447 assert_eq!(paths.len(), 1);
448 assert_eq!(paths[0].root.id, "root-1");
449 assert_eq!(paths[0].links.len(), 1);
450 }
451
452 #[test]
453 fn tampered_delegation_signature_is_hard_failure() {
454 let (root_node, root_sk) = root("root-1", ScopeDimensions::unconstrained());
455 let (end_sk, _) = ed25519_keypair();
456 let end = child(
457 "end-1",
458 AuthorityKind::EndCertificate,
459 ScopeDimensions::unconstrained(),
460 &end_sk,
461 );
462 let mut bad = sign(&root_sk, &end);
463 bad[0] ^= 1;
464 let mut graph = TrustGraph::new();
465 graph.add_node(root_node.clone());
466 graph.add_node(end);
467 graph
468 .add_delegation(DelegationEdge {
469 parent: "root-1".into(),
470 child: "end-1".into(),
471 signature: bad,
472 })
473 .unwrap();
474 let err = graph
475 .find_paths("end-1", &bundle_for(&root_node), &Ed25519Verifier)
476 .unwrap_err();
477 assert!(matches!(err, SignatifError::BadSignature { .. }));
478 }
479
480 #[test]
481 fn scope_widening_is_hard_failure() {
482 let mut narrow = ScopeDimensions::unconstrained();
483 narrow.set("domain", ScopeValue::Single("pharma".into()));
484 let mut wide = ScopeDimensions::unconstrained();
485 wide.set(
486 "domain",
487 ScopeValue::Set(["pharma", "food"].iter().map(|s| s.to_string()).collect()),
488 );
489 let (root_node, root_sk) = root("root-1", narrow);
490 let (end_sk, _) = ed25519_keypair();
491 let end = child("end-1", AuthorityKind::EndCertificate, wide, &end_sk);
492 let mut graph = TrustGraph::new();
493 graph.add_node(root_node.clone());
494 let edge_sig = sign(&root_sk, &end);
495 graph.add_node(end);
496 graph
497 .add_delegation(DelegationEdge {
498 parent: "root-1".into(),
499 child: "end-1".into(),
500 signature: edge_sig,
501 })
502 .unwrap();
503 let err = graph
504 .find_paths("end-1", &bundle_for(&root_node), &Ed25519Verifier)
505 .unwrap_err();
506 match err {
507 SignatifError::ScopeWidening { dimension, .. } => assert_eq!(dimension, "domain"),
508 other => panic!("expected widening, got {other:?}"),
509 }
510 }
511
512 #[test]
513 fn multiple_roots_yield_multiple_paths() {
514 let (r1, s1) = root("root-1", ScopeDimensions::unconstrained());
515 let (r2, s2) = root("root-2", ScopeDimensions::unconstrained());
516 let (end_sk, _) = ed25519_keypair();
517 let end = child(
518 "end-1",
519 AuthorityKind::EndCertificate,
520 ScopeDimensions::unconstrained(),
521 &end_sk,
522 );
523 let mut graph = TrustGraph::new();
524 graph.add_node(r1.clone());
525 graph.add_node(r2.clone());
526 graph.add_node(end.clone());
527 for (parent, sk) in [("root-1", &s1), ("root-2", &s2)] {
528 graph
529 .add_delegation(DelegationEdge {
530 parent: parent.into(),
531 child: "end-1".into(),
532 signature: sign(sk, &end),
533 })
534 .unwrap();
535 }
536 let mut bundle = bundle_for(&r1);
537 bundle.roots.push(AnchorRoot {
538 name: "root-2".into(),
539 aggregate_key: r2.public_key.clone(),
540 fingerprint: hex::encode(sha2::Sha256::digest(&r2.public_key)),
541 quorum: None,
542 });
543 let paths = graph
544 .find_paths("end-1", &bundle, &Ed25519Verifier)
545 .unwrap();
546 assert_eq!(paths.len(), 2);
547 }
548
549 #[test]
550 fn cycles_are_rejected_at_insertion() {
551 let (r, _) = root("root-1", ScopeDimensions::unconstrained());
552 let (a_sk, _) = ed25519_keypair();
553 let a = child(
554 "a",
555 AuthorityKind::Delegated,
556 ScopeDimensions::unconstrained(),
557 &a_sk,
558 );
559 let mut graph = TrustGraph::new();
560 graph.add_node(r);
561 graph.add_node(a.clone());
562 graph
563 .add_delegation(DelegationEdge {
564 parent: "root-1".into(),
565 child: "a".into(),
566 signature: vec![0],
567 })
568 .unwrap();
569 assert!(
570 graph
571 .add_delegation(DelegationEdge {
572 parent: "a".into(),
573 child: "root-1".into(),
574 signature: vec![0],
575 })
576 .is_err()
577 );
578 assert!(graph.is_acyclic());
579 }
580}