1use serde::{Deserialize, Serialize};
11
12use crate::error::{SignatifError, SignatifResult};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum Status {
18 Active,
20 Deprecated,
22 Retired,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct Entry {
29 pub name: String,
31 pub description: String,
33 pub status: Status,
35 pub reference: String,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
41pub struct DimensionTag(String);
42
43impl DimensionTag {
44 pub const DATA: &'static str = "data";
46 pub const PERSON: &'static str = "person";
48 pub const TIME: &'static str = "time";
50 pub const LOCATION: &'static str = "location";
52 pub const ENVIRONMENT: &'static str = "environment";
54 pub const AUTHORIZATION: &'static str = "authorization";
56 pub const IDENTITY: &'static str = "identity";
58 pub const ORACLE: &'static str = "oracle";
60
61 pub fn data() -> Self {
63 Self(Self::DATA.into())
64 }
65 pub fn person() -> Self {
67 Self(Self::PERSON.into())
68 }
69 pub fn time() -> Self {
71 Self(Self::TIME.into())
72 }
73 pub fn location() -> Self {
75 Self(Self::LOCATION.into())
76 }
77 pub fn environment() -> Self {
79 Self(Self::ENVIRONMENT.into())
80 }
81 pub fn authorization() -> Self {
83 Self(Self::AUTHORIZATION.into())
84 }
85 pub fn identity() -> Self {
87 Self(Self::IDENTITY.into())
88 }
89 pub fn oracle() -> Self {
91 Self(Self::ORACLE.into())
92 }
93
94 pub fn custom(name: &str) -> Self {
96 Self(name.into())
97 }
98
99 pub fn as_str(&self) -> &str {
101 &self.0
102 }
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct ValueRegistry {
108 pub registry_name: String,
110 pub entries: Vec<Entry>,
112}
113
114impl ValueRegistry {
115 pub fn new(name: &str) -> Self {
117 Self {
118 registry_name: name.to_string(),
119 entries: Vec::new(),
120 }
121 }
122
123 pub fn register(&mut self, entry: Entry) {
125 self.entries.push(entry);
126 }
127
128 pub fn get(&self, name: &str) -> Option<&Entry> {
130 self.entries.iter().find(|e| e.name == name)
131 }
132
133 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 pub fn usable(&self, name: &str) -> Option<&Entry> {
144 self.get(name).filter(|e| e.status != Status::Retired)
145 }
146
147 pub fn status(&self, name: &str) -> Option<Status> {
149 self.get(name).map(|e| e.status)
150 }
151
152 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#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct Registry {
174 pub dimensions: ValueRegistry,
176 pub algorithms: ValueRegistry,
179 pub ceremony_types: ValueRegistry,
181 pub format_profiles: ValueRegistry,
183 pub scope_dimensions: ValueRegistry,
185}
186
187impl Registry {
188 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 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 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 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}