Skip to main content

confium_signatif/
testing.rs

1//! Specimen builders for the abstract test suite.
2//!
3//! Building one valid trusted artifact means generating keys,
4//! assembling a trust graph, signing an anchor bundle, and
5//! co-signing the artifact — work every test surface (the Rust
6//! suites, the browser binding, examples) used to duplicate. This
7//! module owns the specimen kit so suites compose from it instead.
8//!
9//! The module ships in the published crate on purpose: a separate
10//! test-support crate consumed as a dev-dependency would recreate the
11//! publish ordering deadlock (published crates must never
12//! dev-depend on a later-publishing workspace crate). It adds no
13//! dependencies beyond what `confium-signatif` already links.
14
15use ed25519_dalek::Signer;
16
17use crate::artifact::TrustedArtifact;
18use crate::bundle::{AnchorRoot, TrustAnchorBundle};
19use crate::graph::{AuthorityKind, AuthorityNode, DelegationEdge, TrustGraph};
20use crate::registry::{DimensionTag, Registry};
21use crate::scope::ScopeDimensions;
22
23/// A valid specimen: root-delegated end authority, signed anchor
24/// bundle, and a data-dimension co-signed artifact over it, plus the
25/// secret keys so tests can sign more, tamper, or re-issue.
26#[derive(Debug, Clone)]
27pub struct Fixture {
28    /// The co-signed trusted artifact.
29    pub artifact: TrustedArtifact,
30    /// The signed trust anchor bundle anchoring `root`.
31    pub bundle: TrustAnchorBundle,
32    /// The root → end delegation graph.
33    pub graph: TrustGraph,
34    /// The registry the artifact was signed under.
35    pub registry: Registry,
36    /// The root authority's signing key.
37    pub root_secret: ed25519_dalek::SigningKey,
38    /// The end authority's signing key (the artifact's co-signer).
39    pub end_secret: ed25519_dalek::SigningKey,
40}
41
42impl Fixture {
43    /// Build a fresh valid specimen with random keys.
44    pub fn valid() -> Fixture {
45        let mut seed = [0u8; 32];
46        rand_core::RngCore::fill_bytes(&mut rand_core::OsRng, &mut seed);
47        let root_secret = ed25519_dalek::SigningKey::from_bytes(&seed);
48        let mut seed = [0u8; 32];
49        rand_core::RngCore::fill_bytes(&mut rand_core::OsRng, &mut seed);
50        let end_secret = ed25519_dalek::SigningKey::from_bytes(&seed);
51
52        let registry = Registry::with_initial_values();
53        let root = AuthorityNode {
54            id: "root".into(),
55            kind: AuthorityKind::Root,
56            public_key: root_secret.verifying_key().as_bytes().to_vec(),
57            quorum: None,
58            scope: ScopeDimensions::unconstrained(),
59        };
60        let end = AuthorityNode {
61            id: "end".into(),
62            kind: AuthorityKind::EndCertificate,
63            public_key: end_secret.verifying_key().as_bytes().to_vec(),
64            quorum: None,
65            scope: ScopeDimensions::unconstrained(),
66        };
67        let mut graph = TrustGraph::new();
68        graph.add_node(root.clone());
69        graph.add_node(end.clone());
70        graph
71            .add_delegation(DelegationEdge {
72                parent: "root".into(),
73                child: "end".into(),
74                signature: root_secret
75                    .sign(&end.binding_bytes().unwrap())
76                    .to_bytes()
77                    .to_vec(),
78            })
79            .unwrap();
80
81        let bundle = Self::sign_bundle(
82            root.public_key.clone(),
83            &root_secret,
84            chrono::Utc::now() - chrono::Duration::hours(1),
85            chrono::Utc::now() + chrono::Duration::days(30),
86        );
87
88        let mut artifact = TrustedArtifact::new(
89            crate::artifact::ArtifactVersion { major: 1, minor: 0 },
90            "fixture-1",
91            serde_json::json!({"dose": 500}),
92            None,
93        )
94        .unwrap();
95        artifact
96            .sign(
97                DimensionTag::data(),
98                "Ed25519",
99                "end",
100                end_secret.verifying_key().as_bytes().to_vec(),
101                "root",
102                &|m| end_secret.sign(m).to_bytes().to_vec(),
103                &registry,
104            )
105            .unwrap();
106
107        Fixture {
108            artifact,
109            bundle,
110            graph,
111            registry,
112            root_secret,
113            end_secret,
114        }
115    }
116
117    fn sign_bundle(
118        root_key: Vec<u8>,
119        root_secret: &ed25519_dalek::SigningKey,
120        valid_from: chrono::DateTime<chrono::Utc>,
121        valid_until: chrono::DateTime<chrono::Utc>,
122    ) -> TrustAnchorBundle {
123        let mut bundle = TrustAnchorBundle {
124            bundle_version: "1".into(),
125            valid_from,
126            valid_until,
127            roots: vec![AnchorRoot {
128                name: "root".into(),
129                aggregate_key: root_key.clone(),
130                fingerprint: hex::encode(&root_key),
131                quorum: None,
132            }],
133            transparency_logs: vec![],
134            update_log: None,
135            bundle_signature: vec![],
136        };
137        let msg = bundle.signing_bytes().unwrap();
138        bundle.bundle_signature = root_secret.sign(&msg).to_bytes().to_vec();
139        bundle
140    }
141
142    /// The same artifact with a mutated payload: signature-validity
143    /// hard check must fail.
144    pub fn tampered_artifact(&self) -> TrustedArtifact {
145        let mut value = serde_json::to_value(&self.artifact).unwrap();
146        value["payload"]["dose"] = serde_json::json!(999);
147        serde_json::from_value(value).unwrap()
148    }
149
150    /// The same anchors in a bundle whose validity window has closed:
151    /// the bundle-validity hard check must fail.
152    pub fn expired_bundle(&self) -> TrustAnchorBundle {
153        Self::sign_bundle(
154            self.root_secret.verifying_key().as_bytes().to_vec(),
155            &self.root_secret,
156            chrono::Utc::now() - chrono::Duration::days(30),
157            chrono::Utc::now() - chrono::Duration::hours(1),
158        )
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use crate::verify::{VerifyOptions, verify_trusted_artifact};
166
167    #[test]
168    fn valid_specimen_ladders() {
169        let f = Fixture::valid();
170        let options = VerifyOptions {
171            transparency_included: true,
172            time_anchored: true,
173            time_attested_at: Some(chrono::Utc::now().to_rfc3339()),
174            accepted_labels: vec!["verified".into()],
175            ..VerifyOptions::default()
176        };
177        let verdict =
178            verify_trusted_artifact(&f.artifact, &f.bundle, &f.graph, &f.registry, &options)
179                .unwrap();
180        assert_eq!(verdict.label, "verified");
181        assert!(verdict.accept);
182    }
183
184    #[test]
185    fn tampered_specimen_hard_fails() {
186        let f = Fixture::valid();
187        let err = verify_trusted_artifact(
188            &f.tampered_artifact(),
189            &f.bundle,
190            &f.graph,
191            &f.registry,
192            &VerifyOptions::default(),
193        )
194        .unwrap_err();
195        assert!(format!("{err}").contains("signature_validity"), "{err}");
196    }
197
198    #[test]
199    fn expired_specimen_hard_fails() {
200        let f = Fixture::valid();
201        let err = verify_trusted_artifact(
202            &f.artifact,
203            &f.expired_bundle(),
204            &f.graph,
205            &f.registry,
206            &VerifyOptions::default(),
207        )
208        .unwrap_err();
209        assert!(format!("{err}").contains("bundle"), "{err}");
210    }
211}