basil_core/core/backend/mod.rs
1// SPDX-FileCopyrightText: 2026 OpenBasil Contributors
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Pluggable signing / key-store backends.
6//!
7//! The agent is a *proxy*: incoming `NEW_KEY` / `SIGN` / `VERIFY` messages are
8//! dispatched to a [`Backend`] trait object. v1 ships a single implementation,
9//! [`vault::VaultBackend`] (a Vault-compatible transit engine: `HashiCorp` Vault or `OpenBao`),
10//! but the trait is deliberately backend-agnostic so additional stores
11//! (db-keystore, 1Password, cloud KMS, an internal TPM-backed signer, …) can be
12//! added later without touching the protocol or the connection handler.
13
14use async_trait::async_trait;
15use basil_proto::{AeadAlgorithm, CiphertextEnvelope, KeyMaterial, KeyType};
16use zeroize::Zeroizing;
17
18#[cfg(feature = "aws-kms")]
19pub mod aws_kms;
20#[cfg(feature = "gcp-kms")]
21pub mod gcp_kms;
22#[cfg(feature = "keystore-backend")]
23pub mod keystore;
24pub mod spiffe;
25pub mod vault;
26
27mod kms_common;
28mod pki;
29mod svid;
30mod transit;
31
32/// A newly created (or imported) key.
33#[derive(Debug, Clone)]
34pub struct NewKey {
35 /// Backend-assigned identifier for the key.
36 pub key_id: String,
37 /// Raw public key bytes (for Ed25519, the 32-byte public key).
38 pub public_key: Vec<u8>,
39}
40
41/// A KV-v2 value read: the stored bytes plus the version they came from.
42///
43/// Returned by [`Backend::kv_get`] for a `value`/`public`-class key. The `value`
44/// is the opaque byte string the broker stored under the `value` field (the
45/// lossless round-trip half of [`Backend::kv_put`]); `version` is the KV-v2
46/// version actually read (the requested one, or the latest when none was asked).
47#[derive(Debug, Clone)]
48pub struct KvValue {
49 /// The raw stored value bytes (never key-crypto material).
50 pub value: Vec<u8>,
51 /// The KV-v2 version these bytes were read from.
52 pub version: u32,
53}
54
55/// A KV-v2 SECRET read: the stored bytes wrapped in [`Zeroizing`] plus the
56/// version they came from.
57///
58/// The secret-custody sibling of [`KvValue`], returned by
59/// [`Backend::kv_get_secret`] for reads whose bytes are secrets (a
60/// materialize-to-use private key, a value-class `get`). The buffer wipes on
61/// drop.
62#[derive(Debug)]
63pub struct KvSecret {
64 /// The raw stored value bytes; wiped on drop.
65 pub value: Zeroizing<Vec<u8>>,
66 /// The KV-v2 version these bytes were read from.
67 pub version: u32,
68}
69
70/// A post-quantum algorithm a [`Backend`] may natively custody and operate on
71/// **in place**: the private seed never leaves the backend.
72///
73/// This is the type the capability probe ([`Backend::supports_native_algorithm`])
74/// and the native PQC operation methods speak. It lives in the backend layer (not
75/// the provider layer) so the [`Backend`] trait carries no dependency on
76/// [`crypto_provider`](crate::core::crypto_provider): the provider layer maps its
77/// `SignatureAlgorithm` onto this enum, never the reverse. Only the ML-DSA family
78/// is modelled today: no shipping backend exposes native ML-KEM transit, so
79/// ML-KEM keys always remain software-custodied.
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum NativeAlgorithm {
82 /// ML-DSA (FIPS 204) signature parameter level 44.
83 MlDsa44,
84 /// ML-DSA (FIPS 204) signature parameter level 65.
85 MlDsa65,
86 /// ML-DSA (FIPS 204) signature parameter level 87.
87 MlDsa87,
88}
89
90/// A key's public material plus the metadata `get_public_key` echoes back.
91///
92/// This is what a `get_public_key` reply needs beyond the raw bytes: the real
93/// algorithm and the current key version (so the handler no longer hardcodes
94/// `ed25519` / `version 1`, the `vault-k3w` OFI).
95#[derive(Debug, Clone)]
96pub struct PublicKey {
97 /// Raw public key bytes (for Ed25519, the 32-byte public key).
98 pub public_key: Vec<u8>,
99 /// The key's algorithm, as the wire reports it.
100 pub key_type: KeyType,
101 /// The latest key version (transit version count).
102 pub version: u32,
103}
104
105/// Value-free metadata for one key, returned by `list`.
106///
107/// Leak-proof: the algorithm and a version *count* only, never the public or
108/// private key bytes (use `get_public_key` for the public half explicitly).
109#[derive(Debug, Clone)]
110pub struct KeyMetadata {
111 /// The key's algorithm, if the backend reports one.
112 pub key_type: Option<KeyType>,
113 /// The latest key version (transit version count).
114 pub latest_version: u32,
115}
116
117/// Backend signing mode for operations whose wire format fixes an algorithm.
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
119pub enum SignOptions {
120 /// Backend default signing behavior.
121 #[default]
122 Default,
123 /// JWS `RS256`: RSASSA-PKCS1-v1_5 over SHA-256.
124 Rs256Pkcs1v15Sha256,
125 /// JWS `ES256`: ECDSA P-256 over SHA-256.
126 Es256,
127 /// JWS `ES384`: ECDSA P-384 over SHA-384.
128 Es384,
129 /// JWS `ES512`: ECDSA P-521 over SHA-512.
130 Es512,
131}
132
133/// An issued X.509-SVID leaf set from a PKI backend.
134#[derive(Debug, Clone)]
135pub struct X509Svid {
136 /// DER-encoded leaf certificate followed by any issuer certificates.
137 pub cert_chain_der: Vec<Vec<u8>>,
138 /// DER-encoded unencrypted PKCS#8 leaf private key.
139 pub leaf_private_key_der: Zeroizing<Vec<u8>>,
140 /// DER-encoded trust-domain bundle certificates.
141 pub bundle_der: Vec<Vec<u8>>,
142}
143
144/// Parameters for a DNS/IP-SAN X.509 leaf issuance (a TLS cert, not a SPIFFE
145/// SVID). Mirrors [`Backend::issue_x509_svid`] but binds DNS/IP SANs and a common
146/// name instead of a SPIFFE URI SAN.
147#[derive(Debug, Clone, Default)]
148pub struct X509CertRequest {
149 /// Certificate common name (and implicit first DNS SAN, per the PKI role).
150 pub common_name: String,
151 /// Additional DNS subject alternative names.
152 pub dns_sans: Vec<String>,
153 /// IP subject alternative names.
154 pub ip_sans: Vec<String>,
155 /// Requested validity in seconds.
156 pub ttl_seconds: u64,
157}
158
159/// Trust-domain X.509 bundle material, ready for Workload API response assembly.
160#[derive(Debug, Clone, Default, PartialEq, Eq)]
161pub struct X509Bundle {
162 /// DER-encoded trust-domain CA certificates.
163 pub bundle_der: Vec<Vec<u8>>,
164 /// DER-encoded CRL bytes, empty when the backend has no CRL to publish.
165 pub crl_der: Vec<u8>,
166}
167
168/// Errors a backend may return. Service adapters map these to canonical gRPC
169/// statuses.
170#[derive(Debug, thiserror::Error)]
171pub enum BackendError {
172 #[error("unsupported key type: {0}")]
173 UnsupportedKeyType(KeyType),
174
175 #[error("key not found: {0}")]
176 KeyNotFound(String),
177
178 /// The op (e.g. `import`/`list`/`key_metadata`) is not supported by this
179 /// backend kind. Distinct from [`BackendError::UnsupportedKeyType`]: the
180 /// *operation* itself has no implementation here.
181 #[error("operation not supported by this backend: {0}")]
182 Unsupported(&'static str),
183
184 /// AEAD authentication failed on `decrypt`: a wrong tag, a wrong key
185 /// version, or mismatched AAD. Deliberately **opaque**: it carries no detail
186 /// distinguishing the cause (no padding/AAD oracle), and the handler maps it
187 /// to the single `decrypt_failed` wire code.
188 #[error("decrypt failed")]
189 DecryptFailed,
190
191 /// The request's algorithm does not match the key's catalog `keyType`
192 /// (e.g. a `chacha20-poly1305` request against an `aes-256-gcm` key), or the
193 /// AEAD suite is otherwise not usable for this key. Maps to the wire
194 /// `unsupported_algorithm` / `invalid_request` per the call site.
195 #[error("unsupported AEAD algorithm: {0}")]
196 UnsupportedAlgorithm(AeadAlgorithm),
197
198 /// Transport / HTTP failure talking to the backend.
199 #[error("backend transport error: {0}")]
200 Transport(String),
201
202 /// The backend was reachable but rejected or failed the operation.
203 #[error("backend error: {0}")]
204 Backend(String),
205
206 /// A response from the backend could not be understood.
207 #[error("malformed backend response: {0}")]
208 Protocol(String),
209}
210
211/// A pluggable key-store + signing backend.
212///
213/// Implementations must be cheap to share across connections (the agent holds
214/// a single `Arc<dyn Backend>` and clones it into every spawned handler).
215#[async_trait]
216pub trait Backend: Send + Sync {
217 /// Short, stable name for this backend (e.g. `"vault"`), used in `STATUS`.
218 fn kind(&self) -> &'static str;
219
220 /// `NEW_KEY` creates a new key of `key_type` and returns its id + public key.
221 async fn new_key(&self, key_type: KeyType) -> Result<NewKey, BackendError>;
222
223 /// Create an **asymmetric** crypto key at a **named path** (`key_id` = the
224 /// catalog transit key name) rather than the server-assigned id [`new_key`]
225 /// uses. This is the startup-reconcile (`vault-zrg`) `generate` path: the key
226 /// must exist at the exact catalog `path` so a later `sign`/`get_public_key`
227 /// resolves to it.
228 ///
229 /// The default returns [`BackendError::Unsupported`]; [`vault`] overrides it.
230 async fn create_named_key(
231 &self,
232 key_id: &str,
233 key_type: KeyType,
234 ) -> Result<NewKey, BackendError> {
235 let _ = (key_id, key_type);
236 Err(BackendError::Unsupported("create_named_key"))
237 }
238
239 /// Create a **symmetric AEAD** crypto key at a **named path**. AEAD suites are
240 /// not wire [`KeyType`]s, so the reconcile `generate` path passes `aead`, the
241 /// catalog algorithm. There is no public half to return; `Ok(())` means the
242 /// key now exists at `key_id`.
243 ///
244 /// The default returns [`BackendError::Unsupported`]; [`vault`] overrides it.
245 async fn create_named_aead(
246 &self,
247 key_id: &str,
248 aead: AeadAlgorithm,
249 ) -> Result<(), BackendError> {
250 let _ = (key_id, aead);
251 Err(BackendError::Unsupported("create_named_aead"))
252 }
253
254 /// Read the raw public key bytes for `key_id` (for Ed25519, 32 bytes).
255 /// Used by credential minters (e.g. to derive a NATS issuer `NKey`).
256 async fn public_key(&self, key_id: &str) -> Result<Vec<u8>, BackendError>;
257
258 /// `GET_PUBLIC_KEY`: read the public half **plus** its metadata (algorithm +
259 /// current version) for `key_id`.
260 ///
261 /// The default returns [`BackendError::Unsupported`] so a backend that only
262 /// signs need not implement metadata; [`vault`] overrides it.
263 async fn public_key_with_meta(&self, key_id: &str) -> Result<PublicKey, BackendError> {
264 let _ = key_id;
265 Err(BackendError::Unsupported("get_public_key"))
266 }
267
268 /// Read value-free metadata (algorithm + latest version) for `key_id`.
269 /// Used by `list` to populate each [`basil_proto::KeyEntry`] without
270 /// ever reading key material.
271 ///
272 /// The default returns [`BackendError::Unsupported`]; [`vault`] overrides it.
273 async fn key_metadata(&self, key_id: &str) -> Result<KeyMetadata, BackendError> {
274 let _ = key_id;
275 Err(BackendError::Unsupported("list"))
276 }
277
278 /// Read **every live version's public key** for `key_id`, keyed by version
279 /// number. For a transit key this is the `data.keys` map, each archived (and
280 /// the latest) version's public half, which is the natural multi-version
281 /// source for a rotation/grace-aware JWKS (`basil-uce.2`): the shared
282 /// generator emits one JWK per version still inside the grace window.
283 ///
284 /// **Public material only**, never any private/secret bytes (the same
285 /// guarantee as [`Backend::public_key`]). The default degrades safely to the
286 /// single latest version (so non-transit backends that only know "the current
287 /// public key" still publish a valid one-key set); [`vault`]/[`spiffe`]
288 /// override it to return the whole version map.
289 async fn public_keys(
290 &self,
291 key_id: &str,
292 ) -> Result<std::collections::BTreeMap<u32, Vec<u8>>, BackendError> {
293 // Fall back to the single latest public key. Probe `key_metadata` for the
294 // version, but a backend that does not implement it (only signs) still
295 // reports a valid set at version 1 rather than failing closed.
296 let version = match self.key_metadata(key_id).await {
297 Ok(meta) => meta.latest_version,
298 Err(BackendError::Unsupported(_)) => 1,
299 Err(e) => return Err(e),
300 };
301 let public = self.public_key(key_id).await?;
302 Ok(std::collections::BTreeMap::from([(version, public)]))
303 }
304
305 /// `IMPORT` (BYOK) creates the key `key_id` from caller-supplied `material`.
306 ///
307 /// Write-only: the private material is consumed to provision the key and the
308 /// reply carries only the public half (never the seed/private bytes). The
309 /// material variant must agree with `key_type`.
310 ///
311 /// The default returns [`BackendError::Unsupported`]; [`vault`] overrides it.
312 async fn import(
313 &self,
314 key_id: &str,
315 key_type: KeyType,
316 material: &KeyMaterial,
317 ) -> Result<NewKey, BackendError> {
318 let _ = (key_id, key_type, material);
319 Err(BackendError::Unsupported("import"))
320 }
321
322 /// `SIGN` signs `message` with `key_id`, returning the raw signature bytes.
323 ///
324 /// For Ed25519 / ed25519-nkey keys the argument is the **raw message**, signed
325 /// directly (`EdDSA` is not pre-hashed): the backend MUST NOT pre-hash it. This is
326 /// what lets a NATS client hand Basil the server-issued nonce verbatim and use
327 /// the returned signature as its connect response (an `async-nats` remote-signer
328 /// callback), so the user seed never leaves the vault.
329 async fn sign(&self, key_id: &str, message: &[u8]) -> Result<Vec<u8>, BackendError>;
330
331 /// `SIGN` with backend-specific algorithm options.
332 ///
333 /// The default mode preserves the normal [`Backend::sign`] behavior. Non-
334 /// default modes fail closed unless a backend explicitly implements them.
335 async fn sign_with_options(
336 &self,
337 key_id: &str,
338 message: &[u8],
339 options: SignOptions,
340 ) -> Result<Vec<u8>, BackendError> {
341 match options {
342 SignOptions::Default => self.sign(key_id, message).await,
343 SignOptions::Rs256Pkcs1v15Sha256 => {
344 let _ = (key_id, message);
345 Err(BackendError::Unsupported("sign rs256_pkcs1v15_sha256"))
346 }
347 SignOptions::Es256 => {
348 let _ = (key_id, message);
349 Err(BackendError::Unsupported("sign es256"))
350 }
351 SignOptions::Es384 => {
352 let _ = (key_id, message);
353 Err(BackendError::Unsupported("sign es384"))
354 }
355 SignOptions::Es512 => {
356 let _ = (key_id, message);
357 Err(BackendError::Unsupported("sign es512"))
358 }
359 }
360 }
361
362 /// `VERIFY` verifies `signature` over `message` with `key_id`.
363 async fn verify(
364 &self,
365 key_id: &str,
366 message: &[u8],
367 signature: &[u8],
368 ) -> Result<bool, BackendError>;
369
370 /// `VERIFY` with backend-specific algorithm options.
371 ///
372 /// The default mode preserves the normal [`Backend::verify`] behavior. Non-
373 /// default modes fail closed unless a backend explicitly implements them.
374 async fn verify_with_options(
375 &self,
376 key_id: &str,
377 message: &[u8],
378 signature: &[u8],
379 options: SignOptions,
380 ) -> Result<bool, BackendError> {
381 match options {
382 SignOptions::Default => self.verify(key_id, message, signature).await,
383 SignOptions::Rs256Pkcs1v15Sha256 => {
384 let _ = (key_id, message, signature);
385 Err(BackendError::Unsupported("verify rs256_pkcs1v15_sha256"))
386 }
387 SignOptions::Es256 => {
388 let _ = (key_id, message, signature);
389 Err(BackendError::Unsupported("verify es256"))
390 }
391 SignOptions::Es384 => {
392 let _ = (key_id, message, signature);
393 Err(BackendError::Unsupported("verify es384"))
394 }
395 SignOptions::Es512 => {
396 let _ = (key_id, message, signature);
397 Err(BackendError::Unsupported("verify es512"))
398 }
399 }
400 }
401
402 /// Whether this backend can perform `algorithm` **natively in place**:
403 /// custody the private key inside the backend and run the operation without
404 /// ever materializing the seed locally.
405 ///
406 /// This is the capability probe behind the `backend-preferred` /
407 /// `backend-required` provider policy (`basil-wuj.10`). It is **cheap,
408 /// synchronous, and fails closed**: the default, and any backend that does
409 /// not override it (every shipping Vault/OpenBao transit engine today, none
410 /// of which has ML-DSA transit), returns `false`, so an unknown or
411 /// unsupported capability never routes key material to a backend that cannot
412 /// custody it. A backend that returns `true` here MUST implement the matching
413 /// native operation methods ([`Self::sign_pqc`], [`Self::verify_pqc`],
414 /// [`Self::create_named_pqc_key`]).
415 fn supports_native_algorithm(&self, algorithm: NativeAlgorithm) -> bool {
416 let _ = algorithm;
417 false
418 }
419
420 /// Provision a new **backend-native** ML-DSA key at the named `key_id`,
421 /// returning only its public half. The private seed is generated and kept
422 /// inside the backend and is never returned. Invoked by the backend-native
423 /// crypto provider when [`Self::supports_native_algorithm`] reports support.
424 ///
425 /// The default returns [`BackendError::Unsupported`]; a backend with native
426 /// ML-DSA transit overrides it.
427 async fn create_named_pqc_key(
428 &self,
429 key_id: &str,
430 algorithm: NativeAlgorithm,
431 ) -> Result<NewKey, BackendError> {
432 let _ = (key_id, algorithm);
433 Err(BackendError::Unsupported("create_named_pqc_key"))
434 }
435
436 /// `SIGN` `message` with a **backend-native** ML-DSA key, returning the raw
437 /// signature bytes. The private seed never leaves the backend.
438 ///
439 /// The default returns [`BackendError::Unsupported`]; a backend with native
440 /// ML-DSA transit overrides it.
441 async fn sign_pqc(
442 &self,
443 key_id: &str,
444 message: &[u8],
445 algorithm: NativeAlgorithm,
446 ) -> Result<Vec<u8>, BackendError> {
447 let _ = (key_id, message, algorithm);
448 Err(BackendError::Unsupported("sign_pqc"))
449 }
450
451 /// `VERIFY` `signature` over `message` with a **backend-native** ML-DSA key,
452 /// using only the public half.
453 ///
454 /// The default returns [`BackendError::Unsupported`]; a backend with native
455 /// ML-DSA transit overrides it.
456 async fn verify_pqc(
457 &self,
458 key_id: &str,
459 message: &[u8],
460 signature: &[u8],
461 algorithm: NativeAlgorithm,
462 ) -> Result<bool, BackendError> {
463 let _ = (key_id, message, signature, algorithm);
464 Err(BackendError::Unsupported("verify_pqc"))
465 }
466
467 /// `ENCRYPT`: AEAD-encrypt `plaintext` under `key_id`'s **latest** version,
468 /// binding `aad` if present, and return a normalized [`CiphertextEnvelope`].
469 ///
470 /// The broker **never** takes a caller nonce: the backend (transit) generates
471 /// a fresh nonce per call. `algorithm` must match the key's catalog `keyType`
472 /// (the manager enforces that before dispatch); the envelope echoes it.
473 ///
474 /// The default returns [`BackendError::Unsupported`]; [`transit`] overrides it.
475 async fn encrypt(
476 &self,
477 key_id: &str,
478 algorithm: AeadAlgorithm,
479 plaintext: &[u8],
480 aad: Option<&[u8]>,
481 ) -> Result<CiphertextEnvelope, BackendError> {
482 let _ = (key_id, algorithm, plaintext, aad);
483 Err(BackendError::Unsupported("encrypt"))
484 }
485
486 /// `DECRYPT`: AEAD-decrypt `envelope` under `key_id`, binding `aad` if
487 /// present, and return the recovered plaintext.
488 ///
489 /// The envelope's `key_version` targets the version that produced it (so a
490 /// ciphertext made before a rotation still decrypts during the grace window).
491 /// A tag/AAD/version mismatch is [`BackendError::DecryptFailed`]: opaque, no
492 /// oracle.
493 ///
494 /// The default returns [`BackendError::Unsupported`]; [`transit`] overrides it.
495 async fn decrypt(
496 &self,
497 key_id: &str,
498 envelope: &CiphertextEnvelope,
499 aad: Option<&[u8]>,
500 ) -> Result<Vec<u8>, BackendError> {
501 let _ = (key_id, envelope, aad);
502 Err(BackendError::Unsupported("decrypt"))
503 }
504
505 /// `ROTATE`: bump `key_id` to a fresh **transit key version**, returning the
506 /// new (now-latest) version number. New encrypt/sign always uses the newest
507 /// version; older versions remain decryptable/verifiable within the grace
508 /// window (see [`Backend::configure_versions`]).
509 ///
510 /// The default returns [`BackendError::Unsupported`]; [`transit`] overrides it.
511 async fn rotate(&self, key_id: &str) -> Result<u32, BackendError> {
512 let _ = key_id;
513 Err(BackendError::Unsupported("rotate"))
514 }
515
516 /// Read a **KV-v2 value** for `key_id`: the stored bytes plus the version they
517 /// came from. `version = None` reads the latest version; `Some(v)` reads that
518 /// specific version. This is the residual value-returning `get` path (§7) and
519 /// is only ever routed to a `value`/`public`-class key by the manager. It
520 /// **never** reads transit (crypto-key) material.
521 ///
522 /// The default returns [`BackendError::Unsupported`]; [`transit`] overrides it.
523 async fn kv_get(&self, key_id: &str, version: Option<u32>) -> Result<KvValue, BackendError> {
524 let _ = (key_id, version);
525 Err(BackendError::Unsupported("get"))
526 }
527
528 /// Read a **KV-v2 value as a SECRET**: the stored bytes wrapped in
529 /// [`Zeroizing`] end-to-end, never landing in a non-zeroizing owner that drops
530 /// un-wiped. Distinct from [`Backend::kv_get`], which returns a plain
531 /// [`KvValue`] for public/probe reads, this path serves the materialize
532 /// reads (`materialize_sealing_private` / `materialize_signing_seed`, where
533 /// the bytes are a private key) and the value-class `get` (where the bytes
534 /// are a stored secret). The returned buffer wipes on drop.
535 ///
536 /// The default returns [`BackendError::Unsupported`]; [`transit`] overrides it.
537 async fn kv_get_secret(
538 &self,
539 key_id: &str,
540 version: Option<u32>,
541 ) -> Result<KvSecret, BackendError> {
542 let _ = (key_id, version);
543 Err(BackendError::Unsupported("kv_get_secret"))
544 }
545
546 /// Write `value` as a fresh **KV-v2 version** of `key_id`, returning the new
547 /// version number (never the value). Used to rotate a *value* key that has a
548 /// catalog `generate` recipe: the broker generates a fresh value and stores it
549 /// as the next version (the `vault-a2p` decision), and to back the `set` op.
550 ///
551 /// The default returns [`BackendError::Unsupported`]; [`transit`] overrides it.
552 async fn kv_put(&self, key_id: &str, value: &[u8]) -> Result<u32, BackendError> {
553 let _ = (key_id, value);
554 Err(BackendError::Unsupported("rotate"))
555 }
556
557 /// Set the transit version window for `key_id`: `min_decryption_version`
558 /// bounds the **grace period** (the oldest version `decrypt`/`verify` may
559 /// still target: `0`/`1` honors all live versions, a higher value rejects
560 /// pre-window ciphertexts), and `min_available_version` is the **retention**
561 /// floor (transit deletes archived key material below it, irreversibly
562 /// pruning expired versions). Both are optional; `None` leaves that field
563 /// untouched.
564 ///
565 /// The default returns [`BackendError::Unsupported`]; [`transit`] overrides it.
566 async fn configure_versions(
567 &self,
568 key_id: &str,
569 min_decryption_version: Option<u32>,
570 min_available_version: Option<u32>,
571 ) -> Result<(), BackendError> {
572 let _ = (key_id, min_decryption_version, min_available_version);
573 Err(BackendError::Unsupported("configure_versions"))
574 }
575
576 /// Issue an X.509-SVID leaf from a provider-native PKI role.
577 ///
578 /// `key_id` is the backend-native issue endpoint from the catalog. For
579 /// Vault PKI this is an absolute path such as `pki/issue/web`, not a
580 /// transit key name. The private key is scoped to this operation and carried
581 /// in a zeroizing buffer until the Workload API response is assembled.
582 async fn issue_x509_svid(
583 &self,
584 key_id: &str,
585 spiffe_id: &str,
586 ttl_seconds: u64,
587 ) -> Result<X509Svid, BackendError> {
588 let _ = (key_id, spiffe_id, ttl_seconds);
589 Err(BackendError::Unsupported("issue_x509_svid"))
590 }
591
592 /// Issue a DNS/IP-SAN X.509 leaf (a TLS cert) from a provider-native PKI role.
593 ///
594 /// `key_id` is the backend-native issue endpoint from the catalog (for
595 /// Vault PKI, an absolute path such as `pki/issue/web`). Like
596 /// [`Backend::issue_x509_svid`] the issuing CA key never leaves the backend;
597 /// unlike an SVID the leaf is bound to DNS/IP SANs, and the leaf private key is
598 /// returned to the caller (a TLS server needs it) in a zeroizing buffer.
599 async fn issue_x509_cert(
600 &self,
601 key_id: &str,
602 request: &X509CertRequest,
603 ) -> Result<X509Svid, BackendError> {
604 let _ = (key_id, request);
605 Err(BackendError::Unsupported("issue_x509_cert"))
606 }
607
608 /// Read trust-domain X.509 bundle material from a provider-native PKI
609 /// issuer path.
610 ///
611 /// `key_id` is the same backend-native locator used by
612 /// [`Backend::issue_x509_svid`]. For Vault PKI this is an issue path such
613 /// as `pki/issue/web`; the backend derives the PKI mount and reads that
614 /// mount's CA bundle and CRL.
615 async fn x509_bundle(&self, key_id: &str) -> Result<X509Bundle, BackendError> {
616 let _ = key_id;
617 Err(BackendError::Unsupported("x509_bundle"))
618 }
619}