Skip to main content

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}