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