Skip to main content

smolvm_protocol/
secrets.rs

1//! Secret reference types shared across smolvm surfaces.
2//!
3//! A [`SecretRef`] is a *pointer* to a secret. Refs travel across trust
4//! boundaries (HTTP request bodies, persisted VM records, `.smolmachine`
5//! pack manifests); resolved plaintext values do not.
6//!
7//! This crate carries only the *shape* of a ref — the on-the-wire and
8//! on-disk representation plus trivial introspection. The validation
9//! policy (which source kinds are allowed at which trust boundary)
10//! lives in the host crate alongside the code that enforces it. See
11//! `smolvm::secrets` for `ResolutionScope` and `validate_ref`.
12
13use serde::{Deserialize, Serialize};
14use std::path::PathBuf;
15
16/// Which source a [`SecretRef`] points at, independent of the data
17/// inside. Used by audit logging so the logger never sees the path or
18/// env-var name (which can themselves be revealing).
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum SecretSourceKind {
21    /// The ref points at an entry in the host secret store.
22    Store,
23    /// The ref points at a host environment variable.
24    Env,
25    /// The ref points at a host file path.
26    File,
27}
28
29impl SecretSourceKind {
30    /// Human-readable label.
31    pub fn as_str(self) -> &'static str {
32        match self {
33            Self::Store => "store",
34            Self::Env => "env",
35            Self::File => "file",
36        }
37    }
38}
39
40/// A reference to a secret. Exactly one of the three sources must be
41/// populated; validation is performed by the host crate's
42/// `validate_ref` (policy lives where it's enforced).
43///
44/// Round-trips through `serde_json` for persistence in the VM record DB
45/// and in `.smolmachine` pack manifests. Refs are not sensitive; the
46/// resolved plaintext is produced only at the workload launch site and
47/// never touches any of these stores.
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49#[serde(deny_unknown_fields)]
50pub struct SecretRef {
51    /// Look up the secret by name in the host secret store.
52    #[serde(default, skip_serializing_if = "Option::is_none")]
53    pub from_store: Option<String>,
54
55    /// Read the secret from a host environment variable.
56    #[serde(default, skip_serializing_if = "Option::is_none")]
57    pub from_env: Option<String>,
58
59    /// Read the secret from a host file path (must be absolute).
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub from_file: Option<PathBuf>,
62}
63
64impl SecretRef {
65    /// Return the source kind for this ref, if exactly one source is set.
66    ///
67    /// Returns `None` for structurally invalid refs (0 or >1 sources).
68    /// Callers are expected to have already validated with the host
69    /// crate's `validate_ref` before calling this; this function is
70    /// primarily for audit logging of a known-good ref.
71    pub fn source_kind(&self) -> Option<SecretSourceKind> {
72        match (
73            self.from_store.is_some(),
74            self.from_env.is_some(),
75            self.from_file.is_some(),
76        ) {
77            (true, false, false) => Some(SecretSourceKind::Store),
78            (false, true, false) => Some(SecretSourceKind::Env),
79            (false, false, true) => Some(SecretSourceKind::File),
80            _ => None,
81        }
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn source_kind_reports_variant() {
91        assert_eq!(
92            SecretRef {
93                from_store: Some("x".into()),
94                from_env: None,
95                from_file: None,
96            }
97            .source_kind(),
98            Some(SecretSourceKind::Store)
99        );
100        assert_eq!(
101            SecretRef {
102                from_store: None,
103                from_env: Some("Y".into()),
104                from_file: None,
105            }
106            .source_kind(),
107            Some(SecretSourceKind::Env)
108        );
109        assert_eq!(
110            SecretRef {
111                from_store: None,
112                from_env: None,
113                from_file: Some(PathBuf::from("/z")),
114            }
115            .source_kind(),
116            Some(SecretSourceKind::File)
117        );
118        let empty = SecretRef {
119            from_store: None,
120            from_env: None,
121            from_file: None,
122        };
123        assert_eq!(empty.source_kind(), None);
124    }
125
126    #[test]
127    fn deny_unknown_fields() {
128        let bad = r#"{ "from_stor": "typo" }"#;
129        let res: Result<SecretRef, _> = serde_json::from_str(bad);
130        assert!(res.is_err());
131    }
132
133    #[test]
134    fn roundtrip_json() {
135        let r = SecretRef {
136            from_store: Some("API_KEY".into()),
137            from_env: None,
138            from_file: None,
139        };
140        let json = serde_json::to_string(&r).unwrap();
141        let back: SecretRef = serde_json::from_str(&json).unwrap();
142        assert_eq!(r, back);
143    }
144
145    #[test]
146    fn serialization_omits_empty_fields() {
147        let r = SecretRef {
148            from_store: Some("X".into()),
149            from_env: None,
150            from_file: None,
151        };
152        let json = serde_json::to_string(&r).unwrap();
153        assert!(json.contains("from_store"));
154        assert!(!json.contains("from_env"));
155        assert!(!json.contains("from_file"));
156    }
157}