Skip to main content

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}