1use std::time::{Duration, Instant};
16
17use async_trait::async_trait;
18use serde_json::{Value, json};
19use tokio::sync::Mutex;
20use tracing::{debug, info};
21use zeroize::Zeroizing;
22
23use basil_proto::{AeadAlgorithm, CiphertextEnvelope, KeyMaterial, KeyType};
24
25use super::svid::SvidMinter;
26use super::transit::{TransitClient, read_body, transit_aead_type};
27use super::{Backend, BackendError, KeyMetadata, KvValue, NewKey, PublicKey, SignOptions};
28
29const TOKEN_REFRESH_SKEW: Duration = Duration::from_secs(10);
31
32const DEFAULT_LEASE_SECS: u64 = 300;
34
35#[derive(Debug, Clone)]
37pub struct SpiffeConfig {
38 pub vault_addr: String,
40 pub transit_mount: String,
42 pub jwt_auth_mount: String,
44 pub role: String,
46 pub spiffe_id: String,
48 pub audience: String,
50 pub svid_ttl: Duration,
52}
53
54struct CachedToken {
55 token: String,
56 expires_at: Instant,
57}
58
59pub struct SpiffeVaultBackend {
60 http: reqwest::Client,
61 addr: String,
62 auth_mount: String,
63 role: String,
64 transit: TransitClient,
65 minter: SvidMinter,
66 cached: Mutex<Option<CachedToken>>,
67}
68
69impl SpiffeVaultBackend {
70 pub fn new(cfg: SpiffeConfig) -> Result<Self, BackendError> {
74 let minter =
75 SvidMinter::generate(cfg.spiffe_id.clone(), cfg.audience.clone(), cfg.svid_ttl)?;
76 Self::assemble(cfg, minter)
77 }
78
79 pub fn from_signer(key_pem: &str, cfg: SpiffeConfig) -> Result<Self, BackendError> {
86 let minter = SvidMinter::from_pem(
87 key_pem,
88 cfg.spiffe_id.clone(),
89 cfg.audience.clone(),
90 cfg.svid_ttl,
91 )?;
92 Self::assemble(cfg, minter)
93 }
94
95 fn assemble(cfg: SpiffeConfig, minter: SvidMinter) -> Result<Self, BackendError> {
98 crate::ensure_crypto_provider();
99 let http = reqwest::Client::builder()
100 .build()
101 .map_err(|e| BackendError::Transport(e.to_string()))?;
102 let addr = cfg.vault_addr.trim_end_matches('/').to_string();
103 let transit = TransitClient::new(http.clone(), &addr, &cfg.transit_mount);
104 Ok(Self {
105 http,
106 addr,
107 auth_mount: cfg.jwt_auth_mount,
108 role: cfg.role,
109 transit,
110 minter,
111 cached: Mutex::new(None),
112 })
113 }
114
115 #[must_use]
118 pub fn public_key_pem(&self) -> &str {
119 self.minter.public_key_pem()
120 }
121
122 #[must_use]
124 pub fn spiffe_id(&self) -> &str {
125 self.minter.spiffe_id()
126 }
127
128 async fn token(&self) -> Result<String, BackendError> {
131 let mut guard = self.cached.lock().await;
132 if let Some(c) = guard.as_ref()
134 && c.expires_at > Instant::now() + TOKEN_REFRESH_SKEW
136 {
137 return Ok(c.token.clone());
138 }
139 let fresh = self.login().await?;
140 let token = fresh.token.clone();
141 *guard = Some(fresh);
142 drop(guard);
143 Ok(token)
144 }
145
146 async fn login(&self) -> Result<CachedToken, BackendError> {
148 let jwt = self.minter.mint()?;
149 let url = format!("{}/v1/auth/{}/login", self.addr, self.auth_mount);
150 debug!(role = %self.role, spiffe_id = %self.minter.spiffe_id(), "exchanging JWT-SVID for vault token");
151
152 let resp = self
153 .http
154 .post(url)
155 .json(&json!({ "role": self.role, "jwt": jwt }))
156 .send()
157 .await
158 .map_err(|e| BackendError::Transport(e.to_string()))?;
159 let body = read_body(resp)
160 .await?
161 .ok_or_else(|| BackendError::Protocol("empty login response".into()))?;
162
163 let auth = body
164 .get("auth")
165 .ok_or_else(|| BackendError::Backend("login response has no auth block".into()))?;
166 let token = auth
167 .get("client_token")
168 .and_then(Value::as_str)
169 .ok_or_else(|| BackendError::Protocol("no client_token in login response".into()))?
170 .to_string();
171 let lease = auth
172 .get("lease_duration")
173 .and_then(Value::as_u64)
174 .filter(|&l| l > 0)
175 .unwrap_or(DEFAULT_LEASE_SECS);
176
177 info!(lease_seconds = lease, "obtained vault token via JWT-SVID");
178 Ok(CachedToken {
180 token,
182 expires_at: Instant::now() + Duration::from_secs(lease),
183 })
184 }
185}
186
187#[async_trait]
188impl Backend for SpiffeVaultBackend {
189 fn kind(&self) -> &'static str {
190 "spiffe-vault"
191 }
192
193 async fn new_key(&self, key_type: KeyType) -> Result<NewKey, BackendError> {
194 let token = self.token().await?;
195 self.transit.new_key(&token, key_type).await
196 }
197
198 async fn create_named_key(
199 &self,
200 key_id: &str,
201 key_type: KeyType,
202 ) -> Result<NewKey, BackendError> {
203 let token = self.token().await?;
204 self.transit
205 .create_named_key(&token, key_id, key_type)
206 .await
207 }
208
209 async fn create_named_aead(
210 &self,
211 key_id: &str,
212 aead: AeadAlgorithm,
213 ) -> Result<(), BackendError> {
214 let token = self.token().await?;
215 self.transit
216 .create_named_aead(&token, key_id, transit_aead_type(aead))
217 .await
218 }
219
220 async fn public_key(&self, key_id: &str) -> Result<Vec<u8>, BackendError> {
221 let token = self.token().await?;
222 self.transit.read_public_key(&token, key_id).await
223 }
224
225 async fn public_key_with_meta(&self, key_id: &str) -> Result<PublicKey, BackendError> {
226 let token = self.token().await?;
227 self.transit.read_public_key_with_meta(&token, key_id).await
228 }
229
230 async fn key_metadata(&self, key_id: &str) -> Result<KeyMetadata, BackendError> {
231 let token = self.token().await?;
232 self.transit.read_key_metadata(&token, key_id).await
233 }
234
235 async fn public_keys(
236 &self,
237 key_id: &str,
238 ) -> Result<std::collections::BTreeMap<u32, Vec<u8>>, BackendError> {
239 let token = self.token().await?;
240 self.transit.read_public_keys(&token, key_id).await
241 }
242
243 async fn import(
244 &self,
245 key_id: &str,
246 key_type: KeyType,
247 material: &KeyMaterial,
248 ) -> Result<NewKey, BackendError> {
249 let token = self.token().await?;
250 self.transit
251 .import(&token, key_id, key_type, material)
252 .await
253 }
254
255 async fn sign(&self, key_id: &str, message: &[u8]) -> Result<Vec<u8>, BackendError> {
256 let token = self.token().await?;
257 self.transit.sign(&token, key_id, message).await
258 }
259
260 async fn sign_with_options(
261 &self,
262 key_id: &str,
263 message: &[u8],
264 options: SignOptions,
265 ) -> Result<Vec<u8>, BackendError> {
266 let token = self.token().await?;
267 self.transit
268 .sign_with_options(&token, key_id, message, options)
269 .await
270 }
271
272 async fn verify(
273 &self,
274 key_id: &str,
275 message: &[u8],
276 signature: &[u8],
277 ) -> Result<bool, BackendError> {
278 let token = self.token().await?;
279 self.transit
280 .verify(&token, key_id, message, signature)
281 .await
282 }
283
284 async fn verify_with_options(
285 &self,
286 key_id: &str,
287 message: &[u8],
288 signature: &[u8],
289 options: SignOptions,
290 ) -> Result<bool, BackendError> {
291 let token = self.token().await?;
292 self.transit
293 .verify_with_options(&token, key_id, message, signature, options)
294 .await
295 }
296
297 async fn encrypt(
298 &self,
299 key_id: &str,
300 algorithm: AeadAlgorithm,
301 plaintext: &[u8],
302 aad: Option<&[u8]>,
303 ) -> Result<CiphertextEnvelope, BackendError> {
304 let token = self.token().await?;
305 self.transit
306 .encrypt(&token, key_id, algorithm, plaintext, aad)
307 .await
308 }
309
310 async fn decrypt(
311 &self,
312 key_id: &str,
313 envelope: &CiphertextEnvelope,
314 aad: Option<&[u8]>,
315 ) -> Result<Vec<u8>, BackendError> {
316 let token = self.token().await?;
317 self.transit.decrypt(&token, key_id, envelope, aad).await
318 }
319
320 async fn rotate(&self, key_id: &str) -> Result<u32, BackendError> {
321 let token = self.token().await?;
322 self.transit.rotate(&token, key_id).await
323 }
324
325 async fn kv_get(&self, key_id: &str, version: Option<u32>) -> Result<KvValue, BackendError> {
326 let token = self.token().await?;
327 self.transit.kv_get(&token, key_id, version).await
328 }
329
330 async fn kv_get_secret(
331 &self,
332 key_id: &str,
333 version: Option<u32>,
334 ) -> Result<Zeroizing<Vec<u8>>, BackendError> {
335 let token = self.token().await?;
336 self.transit.kv_get_secret(&token, key_id, version).await
337 }
338
339 async fn kv_put(&self, key_id: &str, value: &[u8]) -> Result<u32, BackendError> {
340 let token = self.token().await?;
341 self.transit.kv_put(&token, key_id, value).await
342 }
343
344 async fn configure_versions(
345 &self,
346 key_id: &str,
347 min_decryption_version: Option<u32>,
348 min_available_version: Option<u32>,
349 ) -> Result<(), BackendError> {
350 let token = self.token().await?;
351 self.transit
352 .configure_versions(
353 &token,
354 key_id,
355 min_decryption_version,
356 min_available_version,
357 )
358 .await
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use super::{Backend, Duration, SpiffeConfig, SpiffeVaultBackend};
365 use rsa::RsaPrivateKey;
366 use rsa::pkcs8::{EncodePrivateKey, LineEnding};
367
368 fn config() -> SpiffeConfig {
369 SpiffeConfig {
370 vault_addr: "http://127.0.0.1:8200/".to_string(),
371 transit_mount: "transit".to_string(),
372 jwt_auth_mount: "jwt".to_string(),
373 role: "basil".to_string(),
374 spiffe_id: "spiffe://example.test/basil".to_string(),
375 audience: "openbao".to_string(),
376 svid_ttl: Duration::from_mins(2),
377 }
378 }
379
380 #[test]
383 fn from_signer_builds_from_bundle_pem() {
384 let mut rng = rand::thread_rng();
385 let key = RsaPrivateKey::new(&mut rng, 1024).expect("rsa keygen");
386 let pem = key.to_pkcs8_pem(LineEnding::LF).expect("pkcs8 pem");
387
388 let backend = SpiffeVaultBackend::from_signer(&pem, config())
389 .expect("construct backend from signer cred");
390 assert_eq!(backend.kind(), "spiffe-vault");
391 assert_eq!(backend.spiffe_id(), "spiffe://example.test/basil");
392 assert!(backend.public_key_pem().contains("BEGIN PUBLIC KEY"));
393 assert_eq!(backend.addr, "http://127.0.0.1:8200");
395 }
396
397 #[test]
398 fn from_signer_rejects_invalid_pem() {
399 match SpiffeVaultBackend::from_signer("garbage", config()) {
401 Err(super::BackendError::Backend(_)) => {}
402 Err(other) => panic!("wrong error: {other}"),
403 Ok(_) => panic!("invalid pem must be rejected"),
404 }
405 }
406}