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