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