Skip to main content

confium_signatif/
graph.rs

1//! The trust graph: delegation DAG and verification path-finding
2//! (SIGNATIF §7).
3//!
4//! Nodes are trust authorities (root, delegated, or end-certificate)
5//! carrying an aggregate public key, optional quorum parameters, and a
6//! multi-dimensional scope. Edges are delegation credentials — the
7//! parent's signature over the child's binding (identifier, key,
8//! quorum, scope). The graph generalizes a linear chain to a directed
9//! acyclic graph: cross-recognition and federated memberships produce
10//! multiple parents.
11//!
12//! [`TrustGraph::find_paths`] collects **every** path from an artifact
13//! signer to a root present in the trust anchor bundle, validating the
14//! delegation signature and the monotonic scope narrowing at each link.
15//! Multiple valid paths and multiple distinct roots feed the coverage
16//! report's cross-domain diversity scoring.
17
18use 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/// Threshold quorum parameters (T of N).
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29pub struct Quorum {
30    /// Threshold — signatures required.
31    pub t: u32,
32    /// Committee size.
33    pub n: u32,
34}
35
36impl Quorum {
37    /// Construct a quorum, validating T <= N and T >= 1.
38    ///
39    /// # Errors
40    ///
41    /// Returns [`SignatifError::Encoding`] when the parameters are
42    /// inconsistent.
43    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/// The kind of a trust authority node.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(rename_all = "snake_case")]
56pub enum AuthorityKind {
57    /// A root trust authority — verification terminus.
58    Root,
59    /// A delegated trust authority.
60    Delegated,
61    /// An end certificate authorizing one signing key.
62    EndCertificate,
63}
64
65/// A node in the trust graph.
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct AuthorityNode {
68    /// Stable identifier (typically a key fingerprint).
69    pub id: String,
70    /// Kind of authority.
71    pub kind: AuthorityKind,
72    /// Aggregate (or single) public key, SPKI-encoded.
73    pub public_key: Vec<u8>,
74    /// Quorum parameters when the authority is threshold.
75    pub quorum: Option<Quorum>,
76    /// The authority's authorization scope.
77    pub scope: ScopeDimensions,
78}
79
80impl AuthorityNode {
81    /// The canonical signing input for this node's binding: the JCS of
82    /// the node's identity material. Delegation signatures and anchor
83    /// bundle references are computed over these bytes.
84    ///
85    /// # Errors
86    ///
87    /// Propagates canonicalization errors.
88    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/// A delegation edge: the parent's credential over the child node.
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct DelegationEdge {
103    /// Parent authority identifier.
104    pub parent: String,
105    /// Child authority identifier.
106    pub child: String,
107    /// The parent's signature over the child's [`AuthorityNode::binding_bytes`].
108    pub signature: Vec<u8>,
109}
110
111/// One verified delegation link on a path.
112#[derive(Debug, Clone)]
113pub struct PathLink {
114    /// The parent authority.
115    pub parent: AuthorityNode,
116    /// The child authority.
117    pub child: AuthorityNode,
118    /// The delegation credential that was verified.
119    pub edge: DelegationEdge,
120}
121
122/// A complete verification path from an artifact signer to a root.
123#[derive(Debug, Clone)]
124pub struct VerificationPath {
125    /// Links in order from the signer's node up to (excluding) the root.
126    pub links: Vec<PathLink>,
127    /// The root authority terminating the path.
128    pub root: AuthorityNode,
129}
130
131impl VerificationPath {
132    /// The distinct authorities on this path, signer first.
133    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    /// The scope of the signer node (the most-narrow scope on the path).
146    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
154/// Verifies a delegation signature: `signature` over `message` under
155/// `public_key`. Implementations bind the concrete algorithm fleet
156/// (Ed25519, ECDSA-P256, ML-DSA, threshold aggregate verification).
157pub trait SignatureVerifier {
158    /// Verify `signature` over `message` under `public_key`.
159    fn verify(&self, public_key: &[u8], message: &[u8], signature: &[u8]) -> bool;
160}
161
162/// A no-op verifier that accepts everything — for graph topology tests
163/// only; never use in production code paths.
164#[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/// The trust graph: authorities and delegation edges forming a DAG.
174#[derive(Debug, Clone, Default, Serialize, Deserialize)]
175pub struct TrustGraph {
176    nodes: BTreeMap<String, AuthorityNode>,
177    edges: Vec<DelegationEdge>,
178}
179
180impl TrustGraph {
181    /// An empty graph.
182    pub fn new() -> Self {
183        Self::default()
184    }
185
186    /// Insert a node, replacing any node with the same identifier.
187    pub fn add_node(&mut self, node: AuthorityNode) {
188        self.nodes.insert(node.id.clone(), node);
189    }
190
191    /// Insert a delegation edge. Returns an error when either endpoint
192    /// is unknown or when the edge would create a cycle.
193    ///
194    /// # Errors
195    ///
196    /// [`SignatifError::Encoding`] for unknown endpoints or a cycle.
197    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    /// All nodes.
215    pub fn nodes(&self) -> impl Iterator<Item = &AuthorityNode> {
216        self.nodes.values()
217    }
218
219    /// All delegation edges.
220    pub fn edges(&self) -> &[DelegationEdge] {
221        &self.edges
222    }
223
224    /// Look up a node by identifier.
225    pub fn node(&self, id: &str) -> Option<&AuthorityNode> {
226        self.nodes.get(id)
227    }
228
229    /// Incoming delegation edges for a child node.
230    pub fn parents_of(&self, child: &str) -> Vec<&DelegationEdge> {
231        self.edges.iter().filter(|e| e.child == child).collect()
232    }
233
234    /// Cycle detection over the delegation graph.
235    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    /// Find **all** verification paths from `signer` to roots present in
264    /// `bundle`. Each link is validated: the parent's delegation
265    /// signature over the child's binding bytes, the monotonic scope
266    /// narrowing invariant, and — for the terminal link — that the root
267    /// matches an anchor in the bundle by aggregate key.
268    ///
269    /// # Errors
270    ///
271    /// Returns [`SignatifError::ScopeWidening`] on the first widening
272    /// link (a hard failure), and [`SignatifError::BadSignature`] when
273    /// a delegation credential does not verify.
274    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}