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