Skip to main content

mnemo_mesh/
identity.rs

1//! SPIFFE-style identity types (v0.4.0 P0-2).
2//!
3//! Mesh issues each workload a SPIFFE-compatible ID
4//! (`spiffe://<trust-domain>/<workload-path>`) plus an attestation
5//! token signed by the Mesh control plane. Mnemo doesn't validate
6//! the token cryptographically yet — that lives in a future
7//! `MeshTokenValidator` trait wired to whatever signing scheme the
8//! operator's Mesh trust bundle uses. For now we expose the data
9//! shape so downstream policy code is forwards-compatible.
10
11use serde::{Deserialize, Serialize};
12use thiserror::Error;
13
14/// One Mesh-attested workload. Carries the SPIFFE ID + the raw
15/// attestation token bytes. Validation is the policy enforcer's job.
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub struct MeshIdentity {
18    pub workload_spiffe_id: String,
19    pub attestation: AttestationToken,
20}
21
22impl MeshIdentity {
23    pub fn new(spiffe_id: impl Into<String>, token: AttestationToken) -> Self {
24        Self {
25            workload_spiffe_id: spiffe_id.into(),
26            attestation: token,
27        }
28    }
29
30    /// Extract `(trust_domain, workload_path)` from the SPIFFE ID.
31    /// Returns `None` for malformed ids.
32    pub fn split_spiffe(&self) -> Option<(&str, &str)> {
33        let rest = self.workload_spiffe_id.strip_prefix("spiffe://")?;
34        let slash = rest.find('/')?;
35        Some((&rest[..slash], &rest[slash + 1..]))
36    }
37
38    pub fn trust_domain(&self) -> Option<&str> {
39        self.split_spiffe().map(|(td, _)| td)
40    }
41}
42
43/// Opaque token bytes the Mesh control plane signs. We hold the
44/// envelope but defer signature validation to a follow-up validator
45/// trait. The token is included in the audit envelope so an offline
46/// auditor can re-verify against the historical trust bundle.
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48pub struct AttestationToken {
49    pub raw: Vec<u8>,
50    /// Operator-defined kid pointing at which Mesh trust-bundle key
51    /// signed `raw`.
52    pub kid: String,
53}
54
55impl AttestationToken {
56    pub fn new(raw: impl Into<Vec<u8>>, kid: impl Into<String>) -> Self {
57        Self {
58            raw: raw.into(),
59            kid: kid.into(),
60        }
61    }
62}
63
64#[derive(Debug, Error, PartialEq)]
65pub enum IdentityError {
66    #[error("malformed SPIFFE ID: {0:?}")]
67    MalformedSpiffe(String),
68    #[error("attestation token is empty — refuse to authorize against an empty token")]
69    EmptyToken,
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn split_spiffe_extracts_components() {
78        let id = MeshIdentity::new(
79            "spiffe://prod.mnemo.io/agent/runner-42",
80            AttestationToken::new(vec![1, 2, 3], "k1"),
81        );
82        assert_eq!(
83            id.split_spiffe(),
84            Some(("prod.mnemo.io", "agent/runner-42"))
85        );
86        assert_eq!(id.trust_domain(), Some("prod.mnemo.io"));
87    }
88
89    #[test]
90    fn malformed_spiffe_returns_none() {
91        let id = MeshIdentity::new("not-a-spiffe-id", AttestationToken::new(vec![1], "k"));
92        assert!(id.split_spiffe().is_none());
93        assert!(id.trust_domain().is_none());
94    }
95
96    #[test]
97    fn malformed_no_workload_path_returns_none() {
98        let id = MeshIdentity::new(
99            "spiffe://only-trust-domain",
100            AttestationToken::new(vec![1], "k"),
101        );
102        assert!(id.split_spiffe().is_none());
103    }
104}