aethel_sdk/purpose.rs
1//! Registered signing purposes.
2//!
3//! A signature made under a purpose is bound to it: it does not verify under
4//! another purpose, nor under the empty context that [`Identity::sign`] uses.
5//! That is what stops a signature made for one kind of statement being replayed
6//! as another.
7//!
8//! The registry lives in aethel-core and is read from the embedded component
9//! (`registered-purposes`), not copied into this crate, so a purpose added to
10//! core reaches the SDK with the next re-vendor and this crate links no
11//! cryptography to know about it. A [`Purpose`] cannot be built from arbitrary
12//! bytes: the only ways to get one are [`Purpose::registered`] and
13//! [`Purpose::from_name`].
14//!
15//! [`Identity::sign`]: crate::Identity::sign
16
17use std::sync::OnceLock;
18
19use crate::component;
20use crate::identity::Error;
21
22/// A purpose from aethel-core's registry.
23///
24/// There is deliberately no constructor from bytes. A signing context that is
25/// not in the registry is an ad hoc context string, which is what the registry
26/// exists to prevent.
27///
28/// ```compile_fail,E0451
29/// use aethel_sdk::Purpose;
30/// let _ = Purpose { name: "MINE".into(), bytes: b"mine/v1".to_vec() };
31/// ```
32///
33/// ```compile_fail,E0599
34/// use aethel_sdk::Purpose;
35/// let _ = Purpose::new(b"mine/v1");
36/// ```
37#[derive(Clone, Debug, PartialEq, Eq)]
38pub struct Purpose {
39 name: String,
40 bytes: Vec<u8>,
41}
42
43static REGISTRY: OnceLock<Vec<Purpose>> = OnceLock::new();
44
45impl Purpose {
46 /// The registry's name for this purpose, for example `AUTH_LOGIN_V1`.
47 pub fn name(&self) -> &str {
48 &self.name
49 }
50
51 /// The exact bytes the component signs and verifies under.
52 pub fn as_bytes(&self) -> &[u8] {
53 &self.bytes
54 }
55
56 /// Every registered purpose, in the registry's declaration order.
57 ///
58 /// Read from the embedded component the first time and kept for the life of
59 /// the process: the registry is fixed by the artifact.
60 pub fn registered() -> Result<&'static [Purpose], Error> {
61 if let Some(registry) = REGISTRY.get() {
62 return Ok(registry);
63 }
64 let (mut store, bindings) = component::load()?;
65 let listed = bindings
66 .aethel_core_identity()
67 .call_registered_purposes(&mut store)?;
68 let registry = listed
69 .into_iter()
70 .map(|p| Purpose {
71 name: p.name,
72 bytes: p.bytes,
73 })
74 .collect();
75 Ok(REGISTRY.get_or_init(|| registry))
76 }
77
78 /// Look a purpose up by its registry name.
79 ///
80 /// Returns [`Error::UnknownPurpose`] for a name that is not registered.
81 pub fn from_name(name: &str) -> Result<Purpose, Error> {
82 Self::registered()?
83 .iter()
84 .find(|p| p.name == name)
85 .cloned()
86 .ok_or_else(|| Error::UnknownPurpose(name.to_string()))
87 }
88}