Skip to main content

confium_signatif/
registry.rs

1//! The five scheme-maintained registries (SIGNATIF Annex C).
2//!
3//! A scheme adopting the framework owns registries for the extensible
4//! value spaces: trust dimensions, algorithms, ceremony types, format
5//! profiles, and scope dimensions. Each entry carries a lifecycle
6//! status consumed by the verification pipeline (`deprecated`
7//! downgrades, `retired` rejects). Registries are deterministic,
8//! serializable documents so a scheme can publish them.
9
10use serde::{Deserialize, Serialize};
11
12use crate::error::{SignatifError, SignatifResult};
13
14/// Lifecycle status of a registry entry (algorithm agility §20).
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum Status {
18    /// Recognized and usable.
19    Active,
20    /// Announced for removal: artifacts using it are downgraded.
21    Deprecated,
22    /// Removed: artifacts using it are rejected.
23    Retired,
24}
25
26/// A registry entry.
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct Entry {
29    /// Entry identifier (e.g. `Ed25519`, `data`, `/conf/format-cose`).
30    pub name: String,
31    /// Human-readable description.
32    pub description: String,
33    /// Lifecycle status.
34    pub status: Status,
35    /// Reference to the specification or standard.
36    pub reference: String,
37}
38
39/// The trust dimension tags used in co-signature blocks.
40#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
41pub struct DimensionTag(String);
42
43impl DimensionTag {
44    /// The `data` dimension — the primary content attestation.
45    pub const DATA: &'static str = "data";
46    /// The `person` dimension — a human witnessed or authorized.
47    pub const PERSON: &'static str = "person";
48    /// The `time` dimension — existence at a stated time.
49    pub const TIME: &'static str = "time";
50    /// The `location` dimension — occurrence at coordinates.
51    pub const LOCATION: &'static str = "location";
52    /// The `environment` dimension — ambient conditions in bounds.
53    pub const ENVIRONMENT: &'static str = "environment";
54    /// The `authorization` dimension — action permitted by policy.
55    pub const AUTHORIZATION: &'static str = "authorization";
56    /// The `identity` dimension — device or person is genuine.
57    pub const IDENTITY: &'static str = "identity";
58    /// The `oracle` dimension — external data had a stated value.
59    pub const ORACLE: &'static str = "oracle";
60
61    /// The `data` dimension — the primary content attestation.
62    pub fn data() -> Self {
63        Self(Self::DATA.into())
64    }
65    /// The `person` dimension — a human witnessed or authorized.
66    pub fn person() -> Self {
67        Self(Self::PERSON.into())
68    }
69    /// The `time` dimension — existence at a stated time.
70    pub fn time() -> Self {
71        Self(Self::TIME.into())
72    }
73    /// The `location` dimension — occurrence at coordinates.
74    pub fn location() -> Self {
75        Self(Self::LOCATION.into())
76    }
77    /// The `environment` dimension — ambient conditions in bounds.
78    pub fn environment() -> Self {
79        Self(Self::ENVIRONMENT.into())
80    }
81    /// The `authorization` dimension — action permitted by policy.
82    pub fn authorization() -> Self {
83        Self(Self::AUTHORIZATION.into())
84    }
85    /// The `identity` dimension — device or person is genuine.
86    pub fn identity() -> Self {
87        Self(Self::IDENTITY.into())
88    }
89    /// The `oracle` dimension — external data had a stated value.
90    pub fn oracle() -> Self {
91        Self(Self::ORACLE.into())
92    }
93
94    /// Build a custom dimension tag (scheme-registered extensions).
95    pub fn custom(name: &str) -> Self {
96        Self(name.into())
97    }
98
99    /// The tag's string value.
100    pub fn as_str(&self) -> &str {
101        &self.0
102    }
103}
104
105/// A named registry with entries.
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct ValueRegistry {
108    /// Registry name (for error messages and publication).
109    pub registry_name: String,
110    /// The entries.
111    pub entries: Vec<Entry>,
112}
113
114impl ValueRegistry {
115    /// An empty registry.
116    pub fn new(name: &str) -> Self {
117        Self {
118            registry_name: name.to_string(),
119            entries: Vec::new(),
120        }
121    }
122
123    /// Register an entry.
124    pub fn register(&mut self, entry: Entry) {
125        self.entries.push(entry);
126    }
127
128    /// Look up an entry by name.
129    pub fn get(&self, name: &str) -> Option<&Entry> {
130        self.entries.iter().find(|e| e.name == name)
131    }
132
133    /// Whether the entry exists with the given status usable by new
134    /// attestations (active or deprecated).
135    pub fn contains(&self, name: &str) -> bool {
136        self.get(name)
137            .map(|e| e.status != Status::Retired)
138            .unwrap_or(false)
139    }
140
141    /// Returns the entry when it can be used for *new* attestations;
142    /// retired entries cannot sign new artifacts.
143    pub fn usable(&self, name: &str) -> Option<&Entry> {
144        self.get(name).filter(|e| e.status != Status::Retired)
145    }
146
147    /// The status of an entry, if registered.
148    pub fn status(&self, name: &str) -> Option<Status> {
149        self.get(name).map(|e| e.status)
150    }
151
152    /// Transition an entry's status (the deprecation process §20).
153    ///
154    /// # Errors
155    ///
156    /// Returns a registry error when the entry is unknown.
157    pub fn set_status(&mut self, name: &str, status: Status) -> SignatifResult<()> {
158        let entry = self
159            .entries
160            .iter_mut()
161            .find(|e| e.name == name)
162            .ok_or_else(|| SignatifError::Registry {
163                registry: self.registry_name.clone(),
164                entry: name.to_string(),
165            })?;
166        entry.status = status;
167        Ok(())
168    }
169}
170
171/// The five scheme-maintained registries in one bundle.
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct Registry {
174    /// Trust dimension tag registry.
175    pub dimensions: ValueRegistry,
176    /// Algorithm identifier registry (classical, post-quantum,
177    /// composite) with status.
178    pub algorithms: ValueRegistry,
179    /// Ceremony type registry.
180    pub ceremony_types: ValueRegistry,
181    /// Format profile registry.
182    pub format_profiles: ValueRegistry,
183    /// Scope dimension registry.
184    pub scope_dimensions: ValueRegistry,
185}
186
187impl Registry {
188    /// The five registries populated with the framework's initial
189    /// values (Annex C: initial content is the scheme's decision at
190    /// establishment; these are Confium's defaults).
191    pub fn with_initial_values() -> Self {
192        let mut dimensions = ValueRegistry::new("trust-dimension");
193        for (name, desc, anchor) in [
194            (
195                DimensionTag::DATA,
196                "The primary content (measured value, record)",
197                "Transparency log",
198            ),
199            (
200                DimensionTag::PERSON,
201                "A human witnessed or authorized the act",
202                "Hardware token",
203            ),
204            (
205                DimensionTag::TIME,
206                "The artifact existed at a stated time",
207                "External timestamp source",
208            ),
209            (
210                DimensionTag::LOCATION,
211                "The event occurred at stated coordinates",
212                "Location authority signal",
213            ),
214            (
215                DimensionTag::ENVIRONMENT,
216                "Ambient conditions were within stated bounds",
217                "Calibrated sensor",
218            ),
219            (
220                DimensionTag::AUTHORIZATION,
221                "The action was permitted under a policy",
222                "Regulatory framework",
223            ),
224            (
225                DimensionTag::IDENTITY,
226                "The device or person is genuine",
227                "Identity authority",
228            ),
229            (
230                DimensionTag::ORACLE,
231                "External data had a stated value at a time",
232                "Multi-source agreement",
233            ),
234        ] {
235            dimensions.register(Entry {
236                name: name.to_string(),
237                description: desc.into(),
238                status: Status::Active,
239                reference: anchor.into(),
240            });
241        }
242
243        let mut algorithms = ValueRegistry::new("algorithm");
244        for (name, description, reference) in [
245            (
246                "Ed25519",
247                "Edwards-curve digital signature (classical)",
248                "RFC 8032",
249            ),
250            (
251                "ECDSA-P256",
252                "ECDSA over NIST P-256 (classical)",
253                "FIPS 186-4",
254            ),
255            (
256                "ML-DSA-44",
257                "Module-lattice signature (post-quantum)",
258                "FIPS 204",
259            ),
260            (
261                "ML-DSA-65",
262                "Module-lattice signature (post-quantum)",
263                "FIPS 204",
264            ),
265            (
266                "ML-DSA-87",
267                "Module-lattice signature (post-quantum)",
268                "FIPS 204",
269            ),
270            (
271                "SLH-DSA-128s",
272                "Stateless hash-based signature (post-quantum)",
273                "FIPS 205",
274            ),
275            (
276                "Composite-Ed25519-MLDSA65",
277                "AND-composition Ed25519 + ML-DSA-65 (composite)",
278                "SIGNATIF §9.4",
279            ),
280            (
281                "Threshold-CMP20-P256",
282                "CMP20 threshold ECDSA P-256 aggregate",
283                "Confium confium-tc-cmp20",
284            ),
285            (
286                "Threshold-FROST-Ed25519",
287                "FROST threshold Ed25519 aggregate",
288                "Confium confium-tc-frost-ed25519",
289            ),
290        ] {
291            algorithms.register(Entry {
292                name: name.into(),
293                description: description.into(),
294                status: Status::Active,
295                reference: reference.into(),
296            });
297        }
298
299        let mut ceremony_types = ValueRegistry::new("ceremony-type");
300        for name in ["dkg", "reshare", "sign", "revoke", "rotation"] {
301            ceremony_types.register(Entry {
302                name: name.into(),
303                description: format!("Threshold {name} ceremony"),
304                status: Status::Active,
305                reference: "SIGNATIF §17".into(),
306            });
307        }
308
309        let mut format_profiles = ValueRegistry::new("format-profile");
310        for (name, reference) in [
311            ("/conf/format-cose", "RFC 8152 COSE Sig_Structure"),
312            ("/conf/format-jws", "RFC 7515 JWS detached content"),
313            ("/conf/format-xmldsig", "W3C XML Signature + Exclusive C14N"),
314        ] {
315            format_profiles.register(Entry {
316                name: name.into(),
317                description: "Signature envelope format profile".into(),
318                status: Status::Active,
319                reference: reference.into(),
320            });
321        }
322
323        let mut scope_dimensions = ValueRegistry::new("scope-dimension");
324        for name in ["domain", "subdomain", "class", "instance", "identity"] {
325            scope_dimensions.register(Entry {
326                name: name.into(),
327                description: format!("Scope dimension `{name}`"),
328                status: Status::Active,
329                reference: "SIGNATIF §11".into(),
330            });
331        }
332
333        Self {
334            dimensions,
335            algorithms,
336            ceremony_types,
337            format_profiles,
338            scope_dimensions,
339        }
340    }
341
342    /// Load a registry from its published bytes (the Annex C
343    /// publication convention: schemes publish `registry.json` at a
344    /// stable URL; every verification surface loads the same bytes).
345    /// Round-trips with [`Registry::publication_bytes`].
346    ///
347    /// # Errors
348    ///
349    /// Returns an encoding error on malformed JSON.
350    pub fn from_published_bytes(bytes: &[u8]) -> SignatifResult<Self> {
351        serde_json::from_slice(bytes)
352            .map_err(|e| SignatifError::Encoding(format!("published registry: {e}")))
353    }
354
355    /// Deterministic publication bytes for a registry (JCS).
356    ///
357    /// # Errors
358    ///
359    /// Propagates canonicalization errors.
360    pub fn publication_bytes(&self) -> SignatifResult<Vec<u8>> {
361        let v = serde_json::to_value(self).expect("registry serializes");
362        Ok(crate::jcs::canonicalize(&v)?.into_bytes())
363    }
364
365    /// Declare a scheme-registered extension dimension.
366    pub fn register_dimension(&mut self, tag: &str, description: &str) {
367        self.dimensions.register(Entry {
368            name: tag.into(),
369            description: description.into(),
370            status: Status::Active,
371            reference: "scheme-registered".into(),
372        });
373    }
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379
380    #[test]
381    fn initial_values_populate_all_five() {
382        let r = Registry::with_initial_values();
383        assert!(r.dimensions.entries.len() >= 8);
384        assert!(r.algorithms.get("Ed25519").is_some());
385        assert!(r.algorithms.get("ML-DSA-65").is_some());
386        assert!(r.ceremony_types.get("dkg").is_some());
387        assert!(r.format_profiles.get("/conf/format-cose").is_some());
388        assert!(r.scope_dimensions.get("domain").is_some());
389    }
390
391    #[test]
392    fn deprecation_lifecycle() {
393        let mut r = Registry::with_initial_values();
394        r.algorithms
395            .set_status("ECDSA-P256", Status::Deprecated)
396            .unwrap();
397        assert_eq!(r.algorithms.status("ECDSA-P256"), Some(Status::Deprecated));
398        assert!(r.algorithms.usable("ECDSA-P256").is_some());
399        r.algorithms
400            .set_status("ECDSA-P256", Status::Retired)
401            .unwrap();
402        assert!(r.algorithms.usable("ECDSA-P256").is_none());
403        assert!(r.algorithms.set_status("nope", Status::Active).is_err());
404    }
405
406    #[test]
407    fn published_registry_round_trips() {
408        let mut r = Registry::with_initial_values();
409        r.register_dimension("cnml:instrument-class", "CNML classification");
410        let published = r.publication_bytes().unwrap();
411        let back = Registry::from_published_bytes(&published).unwrap();
412        assert!(back.dimensions.contains("cnml:instrument-class"));
413        assert_eq!(back.publication_bytes().unwrap(), published);
414        assert!(Registry::from_published_bytes(b"not json").is_err());
415    }
416
417    #[test]
418    fn scheme_extension_dimensions() {
419        let mut r = Registry::with_initial_values();
420        r.register_dimension("cnml:instrument-class", "CNML instrument classification");
421        assert!(r.dimensions.contains("cnml:instrument-class"));
422        let bytes = r.publication_bytes().unwrap();
423        assert!(!bytes.is_empty());
424    }
425}