khive_runtime/
credentials.rs1use std::collections::BTreeMap;
4use std::fmt;
5use std::sync::Arc;
6
7use thiserror::Error;
8use zeroize::Zeroizing;
9
10mod config;
11pub(crate) use config::read_tables;
12pub use config::{
13 CredentialConfig, CredentialKind, VisibilityReceiptConfig, VisibilityReceiptKeyConfig,
14};
15
16pub(crate) mod receipt_sealer;
17
18#[cfg(test)]
19mod config_tests;
20#[cfg(test)]
21mod tests;
22
23pub struct CredentialMaterial {
35 bytes: Zeroizing<Vec<u8>>,
36}
37
38impl CredentialMaterial {
39 pub fn new(bytes: Vec<u8>) -> Self {
40 Self {
41 bytes: Zeroizing::new(bytes),
42 }
43 }
44}
45
46impl fmt::Debug for CredentialMaterial {
47 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
48 formatter.write_str("CredentialMaterial([REDACTED])")
49 }
50}
51
52#[derive(Debug, Error)]
54pub enum CredentialError {
55 #[error("credential {name:?}: unavailable")]
56 Unavailable { name: String },
57 #[error("credential {name:?}: not configured")]
58 UnknownCredential { name: String },
59 #[error("credential {name:?}: provider {provider:?} is not registered")]
60 UnknownProvider { name: String, provider: String },
61 #[error("credential configuration {name:?}: {reason}")]
62 InvalidConfig { name: String, reason: &'static str },
63 #[error("credential {name:?}: only cookie_jar credentials may be updated")]
64 UpdateNotAllowed { name: String },
65 #[error("credential {name:?}: provider does not support updates")]
66 UpdateUnsupported { name: String },
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum CredentialCacheLifetime {
72 NoCache,
73 Process,
74}
75
76pub trait CredentialProvider: Send + Sync {
78 fn resolve(&self, name: &str) -> Result<CredentialMaterial, CredentialError>;
79
80 fn cache_lifetime(&self) -> CredentialCacheLifetime;
81
82 fn update(&self, name: &str, _material: Zeroizing<Vec<u8>>) -> Result<(), CredentialError> {
84 Err(CredentialError::UpdateUnsupported {
85 name: name.to_owned(),
86 })
87 }
88}
89
90struct EnvCredentialProvider {
91 variables: BTreeMap<String, String>,
92}
93
94impl CredentialProvider for EnvCredentialProvider {
95 fn resolve(&self, name: &str) -> Result<CredentialMaterial, CredentialError> {
96 let unavailable = || CredentialError::Unavailable {
97 name: name.to_owned(),
98 };
99 let variable = self.variables.get(name).ok_or_else(unavailable)?;
100 let value = std::env::var_os(variable).ok_or_else(unavailable)?;
101 let bytes = Zeroizing::new(value.into_encoded_bytes());
102 if bytes.is_empty() || std::str::from_utf8(&bytes).is_err() {
103 return Err(unavailable());
104 }
105 Ok(CredentialMaterial { bytes })
106 }
107
108 fn cache_lifetime(&self) -> CredentialCacheLifetime {
109 CredentialCacheLifetime::NoCache
110 }
111}
112
113pub struct CredentialRegistry {
115 declarations: BTreeMap<String, CredentialConfig>,
116 providers: BTreeMap<String, Arc<dyn CredentialProvider>>,
117}
118
119impl CredentialRegistry {
120 pub fn new(declarations: Vec<CredentialConfig>) -> Result<Self, CredentialError> {
121 CredentialConfig::validate_all(&declarations)?;
122 let variables = declarations
123 .iter()
124 .filter(|entry| entry.provider == "env")
125 .map(|entry| {
126 (
127 entry.name.clone(),
128 entry.env_var.clone().expect("validated env reference"),
129 )
130 })
131 .collect();
132 let mut providers: BTreeMap<String, Arc<dyn CredentialProvider>> = BTreeMap::new();
133 providers.insert(
134 "env".to_owned(),
135 Arc::new(EnvCredentialProvider { variables }),
136 );
137 Ok(Self {
138 declarations: declarations
139 .into_iter()
140 .map(|entry| (entry.name.clone(), entry))
141 .collect(),
142 providers,
143 })
144 }
145
146 pub fn register_provider(
148 &mut self,
149 name: String,
150 provider: Arc<dyn CredentialProvider>,
151 ) -> Result<(), CredentialError> {
152 if name.is_empty() || self.providers.contains_key(&name) {
153 return Err(CredentialError::InvalidConfig {
154 name,
155 reason: "provider name must be nonempty and not already registered",
156 });
157 }
158 self.providers.insert(name, provider);
159 Ok(())
160 }
161
162 pub(crate) fn declarations(&self) -> Vec<CredentialConfig> {
163 self.declarations.values().cloned().collect()
164 }
165
166 pub fn kind(&self, name: &str) -> Result<CredentialKind, CredentialError> {
167 Ok(self.declaration(name)?.kind)
168 }
169
170 pub fn cache_lifetime(&self, name: &str) -> Result<CredentialCacheLifetime, CredentialError> {
171 Ok(self.provider(self.declaration(name)?)?.cache_lifetime())
172 }
173
174 pub fn resolve(&self, name: &str) -> Result<CredentialMaterial, CredentialError> {
175 self.provider(self.declaration(name)?)?
176 .resolve(name)
177 .map_err(|_| CredentialError::Unavailable {
178 name: name.to_owned(),
179 })
180 }
181
182 pub fn update(&self, name: &str, material: Vec<u8>) -> Result<(), CredentialError> {
183 let material = Zeroizing::new(material);
184 let declaration = self.declaration(name)?;
185 if declaration.kind != CredentialKind::CookieJar {
186 return Err(CredentialError::UpdateNotAllowed {
187 name: name.to_owned(),
188 });
189 }
190 self.provider(declaration)?
191 .update(name, material)
192 .map_err(|error| match error {
193 CredentialError::UpdateUnsupported { .. } => CredentialError::UpdateUnsupported {
194 name: name.to_owned(),
195 },
196 _ => CredentialError::Unavailable {
197 name: name.to_owned(),
198 },
199 })
200 }
201
202 fn declaration(&self, name: &str) -> Result<&CredentialConfig, CredentialError> {
203 self.declarations
204 .get(name)
205 .ok_or_else(|| CredentialError::UnknownCredential {
206 name: name.to_owned(),
207 })
208 }
209
210 fn provider(
211 &self,
212 declaration: &CredentialConfig,
213 ) -> Result<&Arc<dyn CredentialProvider>, CredentialError> {
214 self.providers
215 .get(&declaration.provider)
216 .ok_or_else(|| CredentialError::UnknownProvider {
217 name: declaration.name.clone(),
218 provider: declaration.provider.clone(),
219 })
220 }
221}