1use std::collections::BTreeMap;
26use std::path::Path;
27use std::process::Command;
28use std::sync::Arc;
29
30use age::secrecy::ExposeSecret as _;
31use basil_cose::Recipient as _;
32use basil_proto::{
33 AeadAlgorithm, CatalogEntry as WireCatalogEntry, CatalogKind, CiphertextEnvelope, KeyMaterial,
34 KeyType,
35};
36use rand::RngCore;
37use uuid::Uuid;
38
39use zeroize::Zeroizing;
40
41use crate::backend::{
42 Backend, BackendError, KvSecret, NativeAlgorithm, NewKey, PublicKey, SignOptions,
43 X509CertRequest, X509Svid,
44};
45use crate::catalog::{BackendRef, Catalog, Class, Engine, GenerateSpec, KeyAlgorithm, KeyEntry};
46use crate::core::crypto_provider::LocalSoftwareProvider;
47use crate::core::crypto_provider::{
48 BackendCryptoProvider, CryptoProvider, CryptoProviderId, CustodyMode,
49 Envelope as ProviderEnvelope, EnvelopeAlgorithm as ProviderEnvelopeAlgorithm,
50 GenerateKey as ProviderGenerateKey, GenerateSealingKey as ProviderGenerateSealingKey,
51 KemAlgorithm as ProviderKemAlgorithm, ProviderError, ProviderMetadata, ProviderPolicy,
52 SignRequest, SignatureAlgorithm, UnwrapEnvelopeRequest as ProviderUnwrapEnvelopeRequest,
53 VerifyRequest, WrapEnvelopeRequest as ProviderWrapEnvelopeRequest, ml_dsa_signature_algorithm,
54 select_provider,
55};
56use crate::ed25519_sign::{self, SignError};
57use crate::state::BrokerLimits;
58use crate::x25519_seal::{self, SealError, SealedEnvelope};
59
60struct GetrandomRng;
61
62impl rand_core::TryRng for GetrandomRng {
63 type Error = getrandom::Error;
64
65 fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
66 let mut bytes = [0u8; 4];
67 self.try_fill_bytes(&mut bytes)?;
68 Ok(u32::from_le_bytes(bytes))
69 }
70
71 fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
72 let mut bytes = [0u8; 8];
73 self.try_fill_bytes(&mut bytes)?;
74 Ok(u64::from_le_bytes(bytes))
75 }
76
77 fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> {
78 getrandom::fill(dst)
79 }
80}
81
82#[derive(Debug, thiserror::Error)]
84pub enum ManagerError {
85 #[error("unknown key: {0}")]
87 UnknownKey(String),
88
89 #[error("key `{key}` references unknown backend `{backend}`")]
92 UnknownBackend {
93 key: String,
95 backend: String,
97 },
98
99 #[error("op `{op}` is not valid for key `{key}` (class {class:?})")]
102 OpNotValidForClass {
103 op: &'static str,
105 key: String,
107 class: Class,
109 },
110
111 #[error("backend `{backend}` does not declare support for {op} key type `{key_type}`")]
114 UnsupportedKeyType {
115 backend: String,
117 op: &'static str,
119 key_type: KeyType,
121 },
122
123 #[error("op `{0}` is recognized but not yet backed by a Backend method")]
126 Unsupported(&'static str),
127
128 #[error("algorithm `{requested}` does not match key `{key}` (catalog type {actual})")]
133 AlgorithmMismatch {
134 key: String,
136 requested: AeadAlgorithm,
138 actual: &'static str,
140 },
141
142 #[error("KEM algorithm `{requested}` does not match key `{key}` (catalog type {actual})")]
145 KemAlgorithmMismatch {
146 key: String,
148 requested: &'static str,
150 actual: &'static str,
152 },
153
154 #[error("value key `{0}` has no generate recipe; rotate via `set` instead")]
158 ValueRotateNeedsSet(String),
159
160 #[error("sealing op failed: {0}")]
164 Sealing(SealingFailure),
165
166 #[error("key `{0}` does not authorize this unseal context")]
174 UnsealContextNotPermitted(String),
175
176 #[error("materialize-to-sign op failed: {0}")]
180 Signing(SigningFailure),
181
182 #[error("key `{0}` has no public_path; its public half cannot be resolved")]
188 MissingPublicPath(String),
189
190 #[error("provider error: {0}")]
195 Provider(#[from] ProviderError),
196
197 #[error("backend error: {0}")]
199 Backend(#[from] BackendError),
200}
201
202#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
208pub enum SealingFailure {
209 #[error("malformed sealing material or envelope")]
212 Malformed,
213
214 #[error("unseal authentication failed")]
217 OpenFailed,
218}
219
220impl SealingFailure {
221 const fn from_seal(err: SealError) -> Self {
225 match err {
226 SealError::OpenFailed => Self::OpenFailed,
227 SealError::BadKeyLength { .. }
228 | SealError::BadNonceLength { .. }
229 | SealError::KdfFailed
230 | SealError::SealFailed => Self::Malformed,
231 }
232 }
233
234 const fn from_cose_open(err: &basil_cose::OpenError) -> Self {
235 match err {
236 basil_cose::OpenError::OpenFailed => Self::OpenFailed,
237 basil_cose::OpenError::Decode(_)
238 | basil_cose::OpenError::RecipientKeyMismatch
239 | basil_cose::OpenError::PartyMismatch
240 | basil_cose::OpenError::Provider { .. } => Self::Malformed,
241 }
242 }
243}
244
245const fn nats_curve_error(err: &basil_nats::Error) -> ManagerError {
246 let failure = match err {
247 basil_nats::Error::XKeyOpenFailed => SealingFailure::OpenFailed,
248 basil_nats::Error::BadPublicKeyLen(_)
249 | basil_nats::Error::BadEncoding
250 | basil_nats::Error::UnsupportedPrefix(_)
251 | basil_nats::Error::UnexpectedPrefix { .. }
252 | basil_nats::Error::UnexpectedXKeyPrefix(_)
253 | basil_nats::Error::BadXKeyVersion
254 | basil_nats::Error::BadXKeyCiphertextLen(_)
255 | basil_nats::Error::XKeyRandomnessFailed
256 | basil_nats::Error::XKeySealFailed
257 | basil_nats::Error::Json(_)
258 | basil_nats::Error::InvalidClaims(_)
259 | basil_nats::Error::MalformedJwt(_)
260 | basil_nats::Error::BadSignatureLen(_) => SealingFailure::Malformed,
261 };
262 ManagerError::Sealing(failure)
263}
264
265#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
271pub enum SigningFailure {
272 #[error("malformed signing seed or verify input")]
275 Malformed,
276}
277
278impl SigningFailure {
279 const fn from_sign(err: SignError) -> Self {
283 match err {
284 SignError::BadSeedLength { .. } | SignError::BadFieldLength { .. } => Self::Malformed,
285 }
286 }
287}
288
289#[derive(Debug, Clone, Copy)]
299pub struct ProviderGate {
300 pub local_software_allowed: bool,
302}
303
304#[derive(Debug, Clone, Copy)]
308pub struct ProviderDispatch {
309 pub provider: CryptoProviderId,
311 pub algorithm: &'static str,
313 pub custody: CustodyMode,
315}
316
317#[derive(Debug, Clone, PartialEq, Eq)]
325pub struct KeyProviderDescriptor {
326 pub policy: ProviderPolicy,
329 pub provider: Option<CryptoProviderId>,
331 pub custody: Option<CustodyMode>,
334 pub version: Option<String>,
336 pub backend_native_available: bool,
340}
341
342#[derive(Debug, Clone, Copy)]
348pub struct MlKemEnvelopeParts<'a> {
349 pub encapsulated_key: &'a [u8],
351 pub nonce: &'a [u8],
353 pub ciphertext: &'a [u8],
355}
356
357pub struct Routed<'a> {
363 pub backend: &'a dyn Backend,
365 pub entry: &'a KeyEntry,
367 pub backend_ref: &'a BackendRef,
369 pub engine: Engine,
371}
372
373impl std::fmt::Debug for Routed<'_> {
376 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
377 f.debug_struct("Routed")
378 .field("backend", &self.backend.kind())
379 .field("entry", &self.entry)
380 .field("backend_ref", &self.backend_ref)
381 .field("engine", &self.engine)
382 .finish()
383 }
384}
385
386impl Routed<'_> {
387 #[must_use]
389 pub fn path(&self) -> &str {
390 &self.entry.path
391 }
392
393 #[must_use]
397 pub fn public_path(&self) -> Option<&str> {
398 self.entry.public_path.as_deref()
399 }
400
401 #[must_use]
403 pub const fn class(&self) -> Class {
404 self.entry.class
405 }
406
407 #[must_use]
409 pub const fn key_type(&self) -> Option<KeyAlgorithm> {
410 self.entry.key_type
411 }
412
413 const fn backend_declares_provides(&self) -> bool {
414 !(self.backend_ref.engines.is_empty()
415 && self.backend_ref.capabilities.is_empty()
416 && self.backend_ref.mint_key_types.is_empty())
417 }
418
419 pub(crate) fn require_mint_key_type(
420 &self,
421 op: &'static str,
422 key_type: KeyType,
423 ) -> Result<(), ManagerError> {
424 let algorithm = KeyAlgorithm::from_wire_key_type(key_type);
425 if !self.backend_declares_provides() || self.backend_ref.mint_key_types.contains(&algorithm)
426 {
427 return Ok(());
428 }
429 Err(ManagerError::UnsupportedKeyType {
430 backend: self.entry.backend.clone(),
431 op,
432 key_type,
433 })
434 }
435}
436
437pub struct BackendManager {
444 catalog: Arc<Catalog>,
445 backends: BTreeMap<String, Box<dyn Backend>>,
446}
447
448impl std::fmt::Debug for BackendManager {
450 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
451 f.debug_struct("BackendManager")
452 .field("backends", &self.backends.keys().collect::<Vec<_>>())
453 .field("keys", &self.catalog.keys.keys().collect::<Vec<_>>())
454 .finish()
455 }
456}
457
458impl BackendManager {
459 pub fn new(
469 catalog: Catalog,
470 backends: BTreeMap<String, Box<dyn Backend>>,
471 ) -> Result<Self, ManagerError> {
472 for (name, entry) in &catalog.keys {
473 if !backends.contains_key(&entry.backend) {
474 return Err(ManagerError::UnknownBackend {
475 key: name.clone(),
476 backend: entry.backend.clone(),
477 });
478 }
479 }
480 Ok(Self {
481 catalog: Arc::new(catalog),
482 backends,
483 })
484 }
485
486 #[must_use]
488 pub fn catalog(&self) -> Arc<Catalog> {
489 Arc::clone(&self.catalog)
490 }
491
492 pub fn resolve(&self, key_id: &str) -> Result<Routed<'_>, ManagerError> {
501 let entry = self
502 .catalog
503 .keys
504 .get(key_id)
505 .ok_or_else(|| ManagerError::UnknownKey(key_id.to_string()))?;
506 let backend_ref = self.catalog.backends.get(&entry.backend).ok_or_else(|| {
507 ManagerError::UnknownBackend {
508 key: key_id.to_string(),
509 backend: entry.backend.clone(),
510 }
511 })?;
512 let backend =
513 self.backends
514 .get(&entry.backend)
515 .ok_or_else(|| ManagerError::UnknownBackend {
516 key: key_id.to_string(),
517 backend: entry.backend.clone(),
518 })?;
519 Ok(Routed {
520 backend: backend.as_ref(),
521 entry,
522 backend_ref,
523 engine: effective_engine(entry),
524 })
525 }
526
527 pub(crate) fn keys(&self) -> impl Iterator<Item = (&String, &KeyEntry)> {
533 self.catalog.keys.iter()
534 }
535
536 pub async fn new_key(&self, key_id: &str, key_type: KeyType) -> Result<NewKey, ManagerError> {
549 let routed = self.resolve(key_id)?;
550 require_class(
551 "new_key",
552 key_id,
553 routed.class(),
554 &[Class::Asymmetric, Class::Symmetric],
555 )?;
556 if routed.class() == Class::Asymmetric {
557 routed.require_mint_key_type("new_key", key_type)?;
558 }
559 Ok(routed.backend.new_key(key_type).await?)
560 }
561
562 pub async fn sign(&self, key_id: &str, message: &[u8]) -> Result<Vec<u8>, ManagerError> {
582 let routed = self.resolve(key_id)?;
583 require_class("sign", key_id, routed.class(), &[Class::Asymmetric])?;
584 if routed.engine == Engine::Kv2 {
585 let seed = self.materialize_signing_seed(key_id, "sign").await?;
588 return Ok(ed25519_sign::sign(&seed, message).to_vec());
589 }
590 Ok(routed
591 .backend
592 .sign_with_options(
593 routed.path(),
594 message,
595 sign_options_for_key(routed.key_type()),
596 )
597 .await?)
598 }
599
600 pub async fn verify(
613 &self,
614 key_id: &str,
615 message: &[u8],
616 signature: &[u8],
617 ) -> Result<bool, ManagerError> {
618 let routed = self.resolve(key_id)?;
619 require_class(
620 "verify",
621 key_id,
622 routed.class(),
623 &[Class::Asymmetric, Class::Public],
624 )?;
625 if routed.class() == Class::Asymmetric && routed.engine == Engine::Kv2 {
626 let public_bytes = self
630 .read_public_half(key_id, "verify", &[Class::Asymmetric])
631 .await?;
632 let public = ed25519_sign::public_from_slice(&public_bytes)
633 .map_err(|e| ManagerError::Signing(SigningFailure::from_sign(e)))?;
634 return ed25519_sign::verify(&public, message, signature)
635 .map_err(|e| ManagerError::Signing(SigningFailure::from_sign(e)));
636 }
637 Ok(routed
638 .backend
639 .verify_with_options(
640 routed.path(),
641 message,
642 signature,
643 sign_options_for_key(routed.key_type()),
644 )
645 .await?)
646 }
647
648 pub async fn get_public_key(&self, key_id: &str) -> Result<PublicKey, ManagerError> {
664 let routed = self.resolve(key_id)?;
665 if routed.class() == Class::Sealing
674 && ProviderMetadata::from_key(routed.entry).custody
675 == Some(CustodyMode::SoftwareEncrypted)
676 && let Some(kem) = routed.key_type().and_then(ml_kem_provider_algorithm)
677 {
678 let public_key = LocalSoftwareProvider::new(routed.backend)
679 .public_key(
680 key_id,
681 routed.path(),
682 routed.entry.labels.get("pqc_storage_key"),
683 kem.token(),
684 )
685 .await?;
686 return Ok(PublicKey {
687 public_key,
688 key_type: ml_kem_wire_key_type(kem),
689 version: 1,
691 });
692 }
693 require_class(
694 "get_public_key",
695 key_id,
696 routed.class(),
697 &[Class::Asymmetric, Class::Public],
698 )?;
699 if ProviderMetadata::from_key(routed.entry).custody == Some(CustodyMode::SoftwareEncrypted)
707 && let Some(algorithm) = routed.key_type().and_then(ml_dsa_signature_algorithm)
708 {
709 let public_key = LocalSoftwareProvider::new(routed.backend)
710 .public_key(
711 key_id,
712 routed.path(),
713 routed.entry.labels.get("pqc_storage_key"),
714 algorithm.token(),
715 )
716 .await?;
717 return Ok(PublicKey {
718 public_key,
719 key_type: ml_dsa_wire_key_type(algorithm),
720 version: 1,
722 });
723 }
724 if routed.class() == Class::Asymmetric && routed.engine == Engine::Kv2 {
725 let public_bytes = self
728 .read_public_half(key_id, "get_public_key", &[Class::Asymmetric])
729 .await?;
730 let public = ed25519_sign::public_from_slice(&public_bytes)
731 .map_err(|e| ManagerError::Signing(SigningFailure::from_sign(e)))?;
732 return Ok(PublicKey {
733 public_key: public.to_vec(),
734 key_type: KeyType::Ed25519,
735 version: 1,
738 });
739 }
740 Ok(routed.backend.public_key_with_meta(routed.path()).await?)
741 }
742
743 #[must_use]
751 pub fn ml_dsa_algorithm_for(&self, key_id: &str) -> Option<SignatureAlgorithm> {
752 let routed = self.resolve(key_id).ok()?;
753 ml_dsa_signature_algorithm(routed.key_type()?)
754 }
755
756 #[must_use]
763 pub fn ml_kem_algorithm_for(&self, key_id: &str) -> Option<ProviderKemAlgorithm> {
764 let routed = self.resolve(key_id).ok()?;
765 ml_kem_provider_algorithm(routed.key_type()?)
766 }
767
768 pub fn describe_provider(&self, key_id: &str) -> Result<KeyProviderDescriptor, ManagerError> {
781 let routed = self.resolve(key_id)?;
782 let metadata = ProviderMetadata::from_key(routed.entry);
783 let native = routed
784 .key_type()
785 .and_then(ml_dsa_signature_algorithm)
786 .and_then(SignatureAlgorithm::native_algorithm);
787 let backend_native_available =
788 native.is_some_and(|algorithm| routed.backend.supports_native_algorithm(algorithm));
789 Ok(KeyProviderDescriptor {
790 policy: metadata.policy,
791 provider: metadata.provider,
792 custody: metadata.custody,
793 version: routed
794 .entry
795 .labels
796 .get("crypto_provider_version")
797 .map(str::to_owned),
798 backend_native_available,
799 })
800 }
801
802 pub async fn provider_sign(
819 &self,
820 key_id: &str,
821 message: &[u8],
822 gate: ProviderGate,
823 ) -> Result<(Vec<u8>, ProviderDispatch), ManagerError> {
824 let routed = self.resolve(key_id)?;
825 require_class("sign", key_id, routed.class(), &[Class::Asymmetric])?;
826 let algorithm = require_ml_dsa(key_id, &routed)?;
827 let provider = select_provider_for(&routed, algorithm.native_algorithm(), gate, "sign")?;
828 let request = SignRequest {
829 key_id,
830 backend_path: routed.path(),
831 algorithm,
832 message,
833 storage_key: routed.entry.labels.get("pqc_storage_key"),
834 };
835 let signature = match provider {
836 CryptoProviderId::VaultTransit => {
837 BackendCryptoProvider::new(routed.backend)
838 .sign(request)
839 .await?
840 }
841 CryptoProviderId::LocalSoftware => {
842 LocalSoftwareProvider::new(routed.backend)
843 .sign(request)
844 .await?
845 }
846 };
847 Ok((signature, provider_dispatch(provider, algorithm)))
848 }
849
850 pub async fn provider_verify(
858 &self,
859 key_id: &str,
860 message: &[u8],
861 signature: &[u8],
862 gate: ProviderGate,
863 ) -> Result<(bool, ProviderDispatch), ManagerError> {
864 let routed = self.resolve(key_id)?;
865 require_class(
866 "verify",
867 key_id,
868 routed.class(),
869 &[Class::Asymmetric, Class::Public],
870 )?;
871 let algorithm = require_ml_dsa(key_id, &routed)?;
872 let provider = select_provider_for(&routed, algorithm.native_algorithm(), gate, "verify")?;
873 let request = VerifyRequest {
874 key_id,
875 backend_path: routed.path(),
876 algorithm,
877 message,
878 signature,
879 storage_key: routed.entry.labels.get("pqc_storage_key"),
880 };
881 let valid = match provider {
882 CryptoProviderId::VaultTransit => {
883 BackendCryptoProvider::new(routed.backend)
884 .verify(request)
885 .await?
886 }
887 CryptoProviderId::LocalSoftware => {
888 LocalSoftwareProvider::new(routed.backend)
889 .verify(request)
890 .await?
891 }
892 };
893 Ok((valid, provider_dispatch(provider, algorithm)))
894 }
895
896 pub async fn provider_generate(
905 &self,
906 key_id: &str,
907 gate: ProviderGate,
908 ) -> Result<(NewKey, ProviderDispatch), ManagerError> {
909 let routed = self.resolve(key_id)?;
910 require_class("new_key", key_id, routed.class(), &[Class::Asymmetric])?;
911 let algorithm = require_ml_dsa(key_id, &routed)?;
912 let provider =
913 select_provider_for(&routed, algorithm.native_algorithm(), gate, "generate")?;
914 let request = ProviderGenerateKey {
915 key_id,
916 backend_path: routed.path(),
917 algorithm,
918 storage_key: routed.entry.labels.get("pqc_storage_key"),
919 };
920 let created = match provider {
921 CryptoProviderId::VaultTransit => {
922 BackendCryptoProvider::new(routed.backend)
923 .generate_key(request)
924 .await?
925 }
926 CryptoProviderId::LocalSoftware => {
927 LocalSoftwareProvider::new(routed.backend)
928 .generate_key(request)
929 .await?
930 }
931 };
932 Ok((created, provider_dispatch(provider, algorithm)))
933 }
934
935 pub async fn provider_generate_sealing(
953 &self,
954 key_id: &str,
955 kem: ProviderKemAlgorithm,
956 gate: ProviderGate,
957 ) -> Result<(NewKey, ProviderDispatch), ManagerError> {
958 let routed = self.resolve(key_id)?;
959 require_class("new_key", key_id, routed.class(), &[Class::Sealing])?;
960 require_ml_kem_sealing_key(key_id, routed.key_type(), kem)?;
961 let provider = select_provider_for(&routed, kem.native_algorithm(), gate, "generate")?;
962 let request = ProviderGenerateSealingKey {
963 key_id,
964 backend_path: routed.path(),
965 algorithm: kem,
966 storage_key: routed.entry.labels.get("pqc_storage_key"),
967 };
968 let created = match provider {
969 CryptoProviderId::VaultTransit => {
970 BackendCryptoProvider::new(routed.backend)
971 .generate_sealing_key(request)
972 .await?
973 }
974 CryptoProviderId::LocalSoftware => {
975 LocalSoftwareProvider::new(routed.backend)
976 .generate_sealing_key(request)
977 .await?
978 }
979 };
980 Ok((created, kem_provider_dispatch(provider, kem)))
981 }
982
983 pub async fn import(
993 &self,
994 key_id: &str,
995 key_type: KeyType,
996 material: &KeyMaterial,
997 ) -> Result<NewKey, ManagerError> {
998 let routed = self.resolve(key_id)?;
999 require_class(
1000 "import",
1001 key_id,
1002 routed.class(),
1003 &[Class::Asymmetric, Class::Symmetric],
1004 )?;
1005 if routed.engine == Engine::Kv2 {
1010 return Err(ManagerError::Unsupported(
1011 "import: a value-store (kv2) crypto key is provisioned out-of-band, not imported via the broker",
1012 ));
1013 }
1014 if routed.class() == Class::Asymmetric {
1015 routed.require_mint_key_type("import", key_type)?;
1016 }
1017 Ok(routed
1020 .backend
1021 .import(routed.path(), key_type, material)
1022 .await?)
1023 }
1024
1025 pub async fn list(
1040 &self,
1041 prefix: Option<&str>,
1042 visible: impl Fn(&str) -> bool,
1043 ) -> Result<Vec<WireCatalogEntry>, ManagerError> {
1044 let mut out = Vec::new();
1045 for (name, entry) in &self.catalog.keys {
1046 if let Some(p) = prefix
1047 && !name.starts_with(p)
1048 {
1049 continue;
1050 }
1051 if !visible(name) {
1052 continue;
1053 }
1054 let meta = match self.resolve(name) {
1058 Ok(routed) => routed.backend.key_metadata(routed.path()).await.ok(),
1059 Err(_) => None,
1060 };
1061 let (key_type, latest_version) = meta.map_or_else(
1062 || (wire_key_type(entry), 0),
1063 |m| (m.key_type, m.latest_version),
1064 );
1065 out.push(WireCatalogEntry {
1066 name: name.clone(),
1067 kind: key_kind(entry),
1068 key_type,
1069 latest_version,
1070 });
1071 }
1072 Ok(out)
1073 }
1074
1075 pub async fn encrypt(
1086 &self,
1087 key_id: &str,
1088 algorithm: AeadAlgorithm,
1089 plaintext: &[u8],
1090 aad: Option<&[u8]>,
1091 ) -> Result<CiphertextEnvelope, ManagerError> {
1092 let routed = self.resolve(key_id)?;
1093 require_class("encrypt", key_id, routed.class(), &[Class::Symmetric])?;
1094 require_aead_match(key_id, routed.key_type(), algorithm)?;
1095 Ok(routed
1096 .backend
1097 .encrypt(routed.path(), algorithm, plaintext, aad)
1098 .await?)
1099 }
1100
1101 pub async fn decrypt(
1114 &self,
1115 key_id: &str,
1116 envelope: &CiphertextEnvelope,
1117 aad: Option<&[u8]>,
1118 ) -> Result<Vec<u8>, ManagerError> {
1119 let routed = self.resolve(key_id)?;
1120 require_class("decrypt", key_id, routed.class(), &[Class::Symmetric])?;
1121 require_aead_match(key_id, routed.key_type(), envelope.alg)?;
1122 Ok(routed.backend.decrypt(routed.path(), envelope, aad).await?)
1123 }
1124
1125 pub async fn wrap_envelope(
1143 &self,
1144 key_id: &str,
1145 plaintext: &[u8],
1146 aad: &[u8],
1147 ) -> Result<SealedEnvelope, ManagerError> {
1148 let routed = self.resolve(key_id)?;
1149 require_class("wrap_envelope", key_id, routed.class(), &[Class::Sealing])?;
1150 require_x25519_sealing_key(key_id, routed.key_type())?;
1151 let public_bytes = self
1154 .read_public_half(key_id, "wrap_envelope", &[Class::Sealing])
1155 .await?;
1156 let recipient_pub = x25519_seal::public_from_slice(&public_bytes)
1157 .map_err(|e| ManagerError::Sealing(SealingFailure::from_seal(e)))?;
1158 x25519_seal::seal(&recipient_pub, plaintext, aad)
1159 .map_err(|e| ManagerError::Sealing(SealingFailure::from_seal(e)))
1160 }
1161
1162 pub async fn unwrap_envelope(
1184 &self,
1185 key_id: &str,
1186 envelope: &SealedEnvelope,
1187 aad: &[u8],
1188 ) -> Result<Zeroizing<Vec<u8>>, ManagerError> {
1189 let private = self
1190 .materialize_sealing_private(key_id, "unwrap_envelope")
1191 .await?;
1192 x25519_seal::open(&private, envelope, aad)
1193 .map_err(|e| ManagerError::Sealing(SealingFailure::from_seal(e)))
1194 }
1195
1196 pub async fn unseal_cose(
1211 &self,
1212 key_id: &str,
1213 cose_encrypt: &[u8],
1214 external_aad: &[u8],
1215 ) -> Result<Zeroizing<Vec<u8>>, ManagerError> {
1216 let expected_parties = {
1223 let routed = self.resolve(key_id)?;
1224 match routed.entry.sealing_pin.as_ref() {
1225 None => None,
1226 Some(pin) => {
1227 if !pin.external_aad_allowed(external_aad) {
1228 return Err(ManagerError::UnsealContextNotPermitted(key_id.to_string()));
1229 }
1230 match pin.parties.as_ref() {
1231 None => None,
1232 Some(parties) => Some(
1233 parties
1234 .to_kdf_parties()
1235 .map_err(|_| ManagerError::Sealing(SealingFailure::Malformed))?,
1236 ),
1237 }
1238 }
1239 }
1240 };
1241
1242 let private = self
1243 .materialize_sealing_private(key_id, "unseal_cose")
1244 .await?;
1245 let cose_key_id = basil_cose::KeyId::from_text(key_id)
1246 .map_err(|_| ManagerError::Sealing(SealingFailure::Malformed))?;
1247 let recipient = basil_cose::X25519Recipient::new(cose_key_id, private);
1248 let aad = basil_cose::ExternalAad::from_bytes(external_aad.to_vec());
1249 let request = basil_cose::OpenRequest {
1250 cose_encrypt,
1251 external_aad: &aad,
1252 expected_parties: expected_parties.as_ref(),
1253 };
1254 recipient.open(&request).await.map_err(|e| match e {
1255 basil_cose::OpenError::PartyMismatch => {
1260 ManagerError::UnsealContextNotPermitted(key_id.to_string())
1261 }
1262 other => ManagerError::Sealing(SealingFailure::from_cose_open(&other)),
1263 })
1264 }
1265
1266 pub async fn encrypt_nats_curve(
1272 &self,
1273 key_id: &str,
1274 recipient_public_xkey: &str,
1275 plaintext: &[u8],
1276 ) -> Result<Vec<u8>, ManagerError> {
1277 let private = self
1278 .materialize_sealing_private(key_id, "encrypt_nats_curve")
1279 .await?;
1280 basil_nats::seal_nats_curve(
1281 &private,
1282 recipient_public_xkey,
1283 plaintext,
1284 &mut GetrandomRng,
1285 )
1286 .map_err(|e| nats_curve_error(&e))
1287 }
1288
1289 pub async fn decrypt_nats_curve(
1294 &self,
1295 key_id: &str,
1296 sender_public_xkey: &str,
1297 ciphertext: &[u8],
1298 ) -> Result<Zeroizing<Vec<u8>>, ManagerError> {
1299 let private = self
1300 .materialize_sealing_private(key_id, "decrypt_nats_curve")
1301 .await?;
1302 basil_nats::open_nats_curve(&private, sender_public_xkey, ciphertext)
1303 .map_err(|e| nats_curve_error(&e))
1304 }
1305
1306 pub async fn provider_wrap_envelope(
1326 &self,
1327 key_id: &str,
1328 kem: ProviderKemAlgorithm,
1329 envelope_algorithm: ProviderEnvelopeAlgorithm,
1330 plaintext: &[u8],
1331 aad: &[u8],
1332 gate: ProviderGate,
1333 ) -> Result<(ProviderEnvelope, ProviderDispatch), ManagerError> {
1334 let routed = self.resolve(key_id)?;
1335 require_class("wrap_envelope", key_id, routed.class(), &[Class::Sealing])?;
1336 require_ml_kem_sealing_key(key_id, routed.key_type(), kem)?;
1337 let provider = select_provider_for(&routed, kem.native_algorithm(), gate, "wrap_envelope")?;
1338 let request = ProviderWrapEnvelopeRequest {
1339 key_id,
1340 backend_path: routed.path(),
1341 kem_algorithm: kem,
1342 envelope_algorithm,
1343 plaintext,
1344 aad: Some(aad),
1345 storage_key: routed.entry.labels.get("pqc_storage_key"),
1346 };
1347 let envelope = match provider {
1348 CryptoProviderId::VaultTransit => {
1349 BackendCryptoProvider::new(routed.backend)
1350 .wrap_envelope(request)
1351 .await?
1352 }
1353 CryptoProviderId::LocalSoftware => {
1354 LocalSoftwareProvider::new(routed.backend)
1355 .wrap_envelope(request)
1356 .await?
1357 }
1358 };
1359 Ok((envelope, kem_provider_dispatch(provider, kem)))
1360 }
1361
1362 pub async fn provider_unwrap_envelope(
1383 &self,
1384 key_id: &str,
1385 kem: ProviderKemAlgorithm,
1386 envelope_algorithm: ProviderEnvelopeAlgorithm,
1387 parts: MlKemEnvelopeParts<'_>,
1388 aad: &[u8],
1389 gate: ProviderGate,
1390 ) -> Result<(Vec<u8>, ProviderDispatch), ManagerError> {
1391 let routed = self.resolve(key_id)?;
1392 require_class("unwrap_envelope", key_id, routed.class(), &[Class::Sealing])?;
1393 require_ml_kem_sealing_key(key_id, routed.key_type(), kem)?;
1394 let provider =
1395 select_provider_for(&routed, kem.native_algorithm(), gate, "unwrap_envelope")?;
1396 let request = ProviderUnwrapEnvelopeRequest {
1397 key_id,
1398 backend_path: routed.path(),
1399 kem_algorithm: kem,
1400 envelope_algorithm,
1401 encapsulated_key: parts.encapsulated_key,
1402 nonce: parts.nonce,
1403 ciphertext: parts.ciphertext,
1404 aad: Some(aad),
1405 storage_key: routed.entry.labels.get("pqc_storage_key"),
1406 };
1407 let plaintext = match provider {
1408 CryptoProviderId::VaultTransit => {
1409 BackendCryptoProvider::new(routed.backend)
1410 .unwrap_envelope(request)
1411 .await?
1412 }
1413 CryptoProviderId::LocalSoftware => {
1414 LocalSoftwareProvider::new(routed.backend)
1415 .unwrap_envelope(request)
1416 .await?
1417 }
1418 };
1419 Ok((plaintext, kem_provider_dispatch(provider, kem)))
1420 }
1421
1422 pub async fn sealing_public_key(
1435 &self,
1436 key_id: &str,
1437 ) -> Result<[u8; x25519_seal::PUBLIC_KEY_LEN], ManagerError> {
1438 let routed = self.resolve(key_id)?;
1439 require_class("get_public_key", key_id, routed.class(), &[Class::Sealing])?;
1440 require_x25519_sealing_key(key_id, routed.key_type())?;
1441 let public_bytes = self
1442 .read_public_half(key_id, "get_public_key", &[Class::Sealing])
1443 .await?;
1444 x25519_seal::public_from_slice(&public_bytes)
1445 .map_err(|e| ManagerError::Sealing(SealingFailure::from_seal(e)))
1446 }
1447
1448 async fn materialize_sealing_private(
1455 &self,
1456 key_id: &str,
1457 op: &'static str,
1458 ) -> Result<Zeroizing<[u8; x25519_seal::PRIVATE_KEY_LEN]>, ManagerError> {
1459 let routed = self.resolve(key_id)?;
1460 require_class(op, key_id, routed.class(), &[Class::Sealing])?;
1461 require_x25519_sealing_key(key_id, routed.key_type())?;
1462 let secret = routed.backend.kv_get_secret(routed.path(), None).await?;
1466 x25519_seal::private_from_slice(&secret.value)
1470 .map_err(|e| ManagerError::Sealing(SealingFailure::from_seal(e)))
1471 }
1472
1473 async fn read_public_half(
1485 &self,
1486 key_id: &str,
1487 op: &'static str,
1488 allowed: &[Class],
1489 ) -> Result<Vec<u8>, ManagerError> {
1490 let routed = self.resolve(key_id)?;
1491 require_class(op, key_id, routed.class(), allowed)?;
1492 let public_path = routed
1493 .public_path()
1494 .ok_or_else(|| ManagerError::MissingPublicPath(key_id.to_string()))?;
1495 let kv = routed.backend.kv_get(public_path, None).await?;
1496 Ok(kv.value)
1497 }
1498
1499 async fn materialize_signing_seed(
1512 &self,
1513 key_id: &str,
1514 op: &'static str,
1515 ) -> Result<Zeroizing<[u8; ed25519_sign::SEED_LEN]>, ManagerError> {
1516 let routed = self.resolve(key_id)?;
1517 require_class(op, key_id, routed.class(), &[Class::Asymmetric])?;
1518 let secret = routed.backend.kv_get_secret(routed.path(), None).await?;
1522 ed25519_sign::seed_from_slice(&secret.value)
1526 .map_err(|e| ManagerError::Signing(SigningFailure::from_sign(e)))
1527 }
1528
1529 pub async fn get(&self, key_id: &str, version: Option<u32>) -> Result<KvSecret, ManagerError> {
1550 let routed = self.resolve(key_id)?;
1551 require_class(
1552 "get",
1553 key_id,
1554 routed.class(),
1555 &[Class::Value, Class::Public],
1556 )?;
1557 if routed.class() == Class::Value {
1558 return Ok(routed.backend.kv_get_secret(routed.path(), version).await?);
1559 }
1560 let kv = routed.backend.kv_get(routed.path(), version).await?;
1561 Ok(KvSecret {
1562 value: Zeroizing::new(kv.value),
1563 version: kv.version,
1564 })
1565 }
1566
1567 pub async fn set(&self, key_id: &str, value: &[u8]) -> Result<u32, ManagerError> {
1579 let routed = self.resolve(key_id)?;
1580 require_class("set", key_id, routed.class(), &[Class::Value])?;
1581 Ok(routed.backend.kv_put(routed.path(), value).await?)
1582 }
1583
1584 pub async fn rotate(&self, key_id: &str, limits: BrokerLimits) -> Result<u32, ManagerError> {
1601 let routed = self.resolve(key_id)?;
1602 require_class(
1603 "rotate",
1604 key_id,
1605 routed.class(),
1606 &[Class::Asymmetric, Class::Symmetric, Class::Value],
1607 )?;
1608
1609 if routed.engine == Engine::Kv2 && routed.class() != Class::Value {
1615 return Err(ManagerError::Unsupported(
1616 "rotate: a value-store (kv2) crypto key is re-provisioned out-of-band, not rotated via the broker",
1617 ));
1618 }
1619
1620 if routed.class() == Class::Value {
1621 let Some(spec) = routed.entry.generate.as_ref() else {
1624 return Err(ManagerError::ValueRotateNeedsSet(key_id.to_string()));
1625 };
1626 let writes = self.generated_writes_for_key(key_id, spec).await?;
1627 let mut rotated_version = None;
1628 for write in writes {
1629 let write_route = self.resolve(&write.key_id)?;
1630 require_class(
1631 "rotate",
1632 &write.key_id,
1633 write_route.class(),
1634 &[Class::Value, Class::Public],
1635 )?;
1636 let version = write_route
1637 .backend
1638 .kv_put(write_route.path(), &write.value)
1639 .await?;
1640 if write.key_id == key_id {
1641 rotated_version = Some(version);
1642 }
1643 }
1644 return rotated_version.ok_or_else(|| {
1645 ManagerError::Backend(BackendError::Backend(
1646 "generate recipe produced no primary value".into(),
1647 ))
1648 });
1649 }
1650
1651 let new_version = routed.backend.rotate(routed.path()).await?;
1654 let floor = limits.grace_floor(new_version);
1655 if let Err(e) = routed
1659 .backend
1660 .configure_versions(routed.path(), Some(floor), None)
1661 .await
1662 && !matches!(e, BackendError::Unsupported(_))
1663 {
1664 return Err(ManagerError::Backend(e));
1665 }
1666 Ok(new_version)
1667 }
1668
1669 pub async fn sweep_retention(
1683 &self,
1684 key_id: &str,
1685 limits: BrokerLimits,
1686 ) -> Result<(), ManagerError> {
1687 let routed = self.resolve(key_id)?;
1688 require_class(
1689 "rotate",
1690 key_id,
1691 routed.class(),
1692 &[Class::Asymmetric, Class::Symmetric],
1693 )?;
1694 let latest = routed
1695 .backend
1696 .key_metadata(routed.path())
1697 .await?
1698 .latest_version;
1699 let Some(floor) = limits.retention_floor(latest) else {
1700 return Ok(()); };
1702 routed
1703 .backend
1704 .configure_versions(routed.path(), None, Some(floor))
1705 .await?;
1706 Ok(())
1707 }
1708
1709 pub async fn sweep_all_retention(&self, limits: BrokerLimits) -> Result<(), ManagerError> {
1718 if limits.retain_versions.is_none() {
1719 return Ok(());
1720 }
1721 let key_ids: Vec<String> = self
1722 .keys()
1723 .filter(|(_, entry)| matches!(entry.class, Class::Asymmetric | Class::Symmetric))
1724 .map(|(name, _)| name.clone())
1725 .collect();
1726 for key_id in key_ids {
1727 self.sweep_retention(&key_id, limits).await?;
1728 }
1729 Ok(())
1730 }
1731
1732 pub async fn issue_x509_svid(
1744 &self,
1745 key_id: &str,
1746 spiffe_id: &str,
1747 ttl_seconds: u64,
1748 ) -> Result<X509Svid, ManagerError> {
1749 let routed = self.resolve(key_id)?;
1750 require_class(
1751 "issue_x509_svid",
1752 key_id,
1753 routed.class(),
1754 &[Class::Asymmetric],
1755 )?;
1756 if routed.engine != Engine::Pki {
1757 return Err(ManagerError::Unsupported("issue_x509_svid"));
1758 }
1759 Ok(routed
1760 .backend
1761 .issue_x509_svid(routed.path(), spiffe_id, ttl_seconds)
1762 .await?)
1763 }
1764
1765 pub async fn issue_x509_cert(
1778 &self,
1779 key_id: &str,
1780 request: &X509CertRequest,
1781 ) -> Result<X509Svid, ManagerError> {
1782 let routed = self.resolve(key_id)?;
1783 require_class(
1784 "issue_x509_cert",
1785 key_id,
1786 routed.class(),
1787 &[Class::Asymmetric],
1788 )?;
1789 if routed.engine != Engine::Pki {
1790 return Err(ManagerError::Unsupported("issue_x509_cert"));
1791 }
1792 Ok(routed
1793 .backend
1794 .issue_x509_cert(routed.path(), request)
1795 .await?)
1796 }
1797
1798 pub(crate) async fn generated_writes_for_key(
1804 &self,
1805 key_id: &str,
1806 spec: &GenerateSpec,
1807 ) -> Result<Vec<GeneratedWrite>, ManagerError> {
1808 match spec {
1809 GenerateSpec::SelfSignedTls {
1810 common_name,
1811 validity,
1812 } => {
1813 if let Some(pair_key) = self.tls_pair_key_for_cert(key_id) {
1814 let existing_key = self.try_existing_value(&pair_key).await?;
1815 let material =
1816 generate_self_signed_tls(common_name, validity, existing_key.as_deref())?;
1817 let mut writes = Vec::new();
1818 if existing_key.is_none() {
1819 writes.push(GeneratedWrite {
1820 key_id: pair_key,
1821 value: material.private_key_pem,
1822 });
1823 }
1824 writes.push(GeneratedWrite {
1825 key_id: key_id.to_string(),
1826 value: material.cert_pem,
1827 });
1828 Ok(writes)
1829 } else {
1830 Ok(vec![GeneratedWrite {
1831 key_id: key_id.to_string(),
1832 value: generate_self_signed_tls(common_name, validity, None)?.cert_pem,
1833 }])
1834 }
1835 }
1836 GenerateSpec::SelfSignedTlsPairOf { pair_of } => {
1837 let (common_name, validity) = self.tls_cert_recipe(pair_of)?;
1838 let material = generate_self_signed_tls(common_name, validity, None)?;
1839 Ok(vec![
1840 GeneratedWrite {
1841 key_id: key_id.to_string(),
1842 value: material.private_key_pem,
1843 },
1844 GeneratedWrite {
1845 key_id: pair_of.clone(),
1846 value: material.cert_pem,
1847 },
1848 ])
1849 }
1850 GenerateSpec::AsciiPrintable { .. }
1851 | GenerateSpec::Base64 { .. }
1852 | GenerateSpec::Hex { .. }
1853 | GenerateSpec::AgeX25519 => Ok(vec![GeneratedWrite {
1854 key_id: key_id.to_string(),
1855 value: generate_value(spec)?,
1856 }]),
1857 }
1858 }
1859
1860 fn tls_pair_key_for_cert(&self, cert_key: &str) -> Option<String> {
1861 self.catalog.keys.iter().find_map(|(name, entry)| {
1862 matches!(
1863 entry.generate.as_ref(),
1864 Some(GenerateSpec::SelfSignedTlsPairOf { pair_of }) if pair_of == cert_key
1867 )
1868 .then(|| name.clone())
1869 })
1870 }
1871
1872 fn tls_cert_recipe(&self, cert_key: &str) -> Result<(&str, &str), ManagerError> {
1873 let entry = self
1874 .catalog
1875 .keys
1876 .get(cert_key)
1877 .ok_or_else(|| ManagerError::UnknownKey(cert_key.to_string()))?;
1878 match entry.generate.as_ref() {
1879 Some(GenerateSpec::SelfSignedTls {
1880 common_name,
1881 validity,
1882 }) => Ok((common_name, validity)),
1883 _ => Err(ManagerError::Backend(BackendError::Backend(format!(
1884 "self-signed-tls-pair-of references non-certificate key `{cert_key}`"
1885 )))),
1886 }
1887 }
1888
1889 async fn try_existing_value(&self, key_id: &str) -> Result<Option<Vec<u8>>, ManagerError> {
1890 let routed = self.resolve(key_id)?;
1891 require_class(
1892 "generate",
1893 key_id,
1894 routed.class(),
1895 &[Class::Value, Class::Public],
1896 )?;
1897 match routed.backend.kv_get(routed.path(), None).await {
1898 Ok(value) => Ok(Some(value.value)),
1899 Err(BackendError::KeyNotFound(_)) => Ok(None),
1900 Err(e) => Err(ManagerError::Backend(e)),
1901 }
1902 }
1903}
1904
1905pub(crate) struct GeneratedWrite {
1907 pub(crate) key_id: String,
1908 pub(crate) value: Vec<u8>,
1909}
1910
1911fn require_aead_match(
1915 key_id: &str,
1916 key_type: Option<KeyAlgorithm>,
1917 algorithm: AeadAlgorithm,
1918) -> Result<(), ManagerError> {
1919 let actual = match key_type {
1920 Some(KeyAlgorithm::Aes256Gcm) => AeadAlgorithm::Aes256Gcm,
1921 Some(KeyAlgorithm::ChaCha20Poly1305) => AeadAlgorithm::Chacha20Poly1305,
1922 _ => {
1926 return Err(ManagerError::AlgorithmMismatch {
1927 key: key_id.to_string(),
1928 requested: algorithm,
1929 actual: "non-aead",
1930 });
1931 }
1932 };
1933 if actual == algorithm {
1934 Ok(())
1935 } else {
1936 Err(ManagerError::AlgorithmMismatch {
1937 key: key_id.to_string(),
1938 requested: algorithm,
1939 actual: match actual {
1940 AeadAlgorithm::Aes256Gcm => "aes-256-gcm",
1941 AeadAlgorithm::Chacha20Poly1305 => "chacha20-poly1305",
1942 },
1943 })
1944 }
1945}
1946
1947const fn sign_options_for_key(key_type: Option<KeyAlgorithm>) -> SignOptions {
1948 match key_type {
1949 Some(KeyAlgorithm::Rsa2048) => SignOptions::Rs256Pkcs1v15Sha256,
1950 Some(KeyAlgorithm::EcdsaP256) => SignOptions::Es256,
1951 Some(KeyAlgorithm::EcdsaP384) => SignOptions::Es384,
1952 Some(KeyAlgorithm::EcdsaP521) => SignOptions::Es512,
1953 _ => SignOptions::Default,
1954 }
1955}
1956
1957fn require_ml_dsa(key_id: &str, routed: &Routed<'_>) -> Result<SignatureAlgorithm, ManagerError> {
1961 routed
1962 .key_type()
1963 .and_then(ml_dsa_signature_algorithm)
1964 .ok_or_else(|| ManagerError::OpNotValidForClass {
1965 op: "provider_dispatch",
1966 key: key_id.to_string(),
1967 class: routed.class(),
1968 })
1969}
1970
1971fn select_provider_for(
1984 routed: &Routed<'_>,
1985 native: Option<NativeAlgorithm>,
1986 gate: ProviderGate,
1987 op: &'static str,
1988) -> Result<CryptoProviderId, ProviderError> {
1989 let metadata = ProviderMetadata::from_key(routed.entry);
1990 let backend_native_supported =
1991 native.is_some_and(|algorithm| routed.backend.supports_native_algorithm(algorithm));
1992 select_provider(
1993 metadata,
1994 backend_native_supported,
1995 gate.local_software_allowed,
1996 op,
1997 )
1998}
1999
2000const fn provider_dispatch(
2002 provider: CryptoProviderId,
2003 algorithm: SignatureAlgorithm,
2004) -> ProviderDispatch {
2005 ProviderDispatch {
2006 provider,
2007 algorithm: algorithm.token(),
2008 custody: provider.custody_mode(),
2009 }
2010}
2011
2012const fn kem_provider_dispatch(
2015 provider: CryptoProviderId,
2016 kem: ProviderKemAlgorithm,
2017) -> ProviderDispatch {
2018 ProviderDispatch {
2019 provider,
2020 algorithm: kem.token(),
2021 custody: provider.custody_mode(),
2022 }
2023}
2024
2025const ASCII_PRINTABLE: &[u8] = b"!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
2028
2029pub(crate) fn generate_value(spec: &GenerateSpec) -> Result<Vec<u8>, ManagerError> {
2032 use base64::Engine as _;
2033 use std::fmt::Write as _;
2034
2035 let mut rng = rand::thread_rng();
2036 match spec {
2037 GenerateSpec::AsciiPrintable { bytes } => {
2038 let mut out = Vec::with_capacity(*bytes as usize);
2039 for _ in 0..*bytes {
2040 let idx = (rng.next_u32() as usize) % ASCII_PRINTABLE.len();
2043 out.push(ASCII_PRINTABLE.get(idx).copied().unwrap_or(b'!'));
2044 }
2045 Ok(out)
2046 }
2047 GenerateSpec::Base64 { bytes } => {
2048 let mut raw = vec![0u8; *bytes as usize];
2049 rng.fill_bytes(&mut raw);
2050 Ok(base64::engine::general_purpose::STANDARD
2051 .encode(&raw)
2052 .into_bytes())
2053 }
2054 GenerateSpec::Hex { bytes } => {
2055 let mut raw = vec![0u8; *bytes as usize];
2056 rng.fill_bytes(&mut raw);
2057 let mut out = String::with_capacity(*bytes as usize * 2);
2058 for b in &raw {
2059 let _ = write!(out, "{b:02x}");
2060 }
2061 Ok(out.into_bytes())
2062 }
2063 GenerateSpec::AgeX25519 => Ok(age::x25519::Identity::generate()
2064 .to_string()
2065 .expose_secret()
2066 .as_bytes()
2067 .to_vec()),
2068 GenerateSpec::SelfSignedTls {
2069 common_name,
2070 validity,
2071 } => Ok(generate_self_signed_tls(common_name, validity, None)?.cert_pem),
2072 GenerateSpec::SelfSignedTlsPairOf { .. } => Err(ManagerError::Backend(
2073 BackendError::Backend("self-signed-tls-pair-of needs catalog pair context".into()),
2074 )),
2075 }
2076}
2077
2078struct TlsMaterial {
2079 cert_pem: Vec<u8>,
2080 private_key_pem: Vec<u8>,
2081}
2082
2083fn generate_self_signed_tls(
2084 common_name: &str,
2085 validity: &str,
2086 existing_key_pem: Option<&[u8]>,
2087) -> Result<TlsMaterial, ManagerError> {
2088 let dir = std::env::temp_dir().join(format!("basil-self-signed-tls-{}", Uuid::new_v4()));
2089 let mut dir_builder = std::fs::DirBuilder::new();
2094 #[cfg(unix)]
2095 {
2096 use std::os::unix::fs::DirBuilderExt as _;
2097 dir_builder.mode(0o700);
2098 }
2099 dir_builder.create(&dir).map_err(|e| {
2100 ManagerError::Backend(BackendError::Backend(format!(
2101 "self-signed-tls tempdir create: {e}"
2102 )))
2103 })?;
2104
2105 let result = generate_self_signed_tls_in_dir(&dir, common_name, validity, existing_key_pem);
2106 let cleanup_result = std::fs::remove_dir_all(&dir);
2107 match (result, cleanup_result) {
2108 (Ok(material), Ok(()) | Err(_)) => Ok(material),
2109 (Err(err), _) => Err(err),
2110 }
2111}
2112
2113fn generate_self_signed_tls_in_dir(
2114 dir: &Path,
2115 common_name: &str,
2116 validity: &str,
2117 existing_key_pem: Option<&[u8]>,
2118) -> Result<TlsMaterial, ManagerError> {
2119 let cert_path = dir.join("cert.pem");
2120 let key_path = dir.join("key.pem");
2121 if let Some(key) = existing_key_pem {
2122 write_secret_file(&key_path, key)?;
2123 }
2124
2125 let mut command = Command::new("step");
2126 command
2127 .arg("certificate")
2128 .arg("create")
2129 .arg(common_name)
2130 .arg(&cert_path);
2131 if existing_key_pem.is_none() {
2132 command.arg(&key_path);
2133 }
2134 command
2135 .arg("--profile")
2136 .arg("self-signed")
2137 .arg("--subtle")
2138 .arg("--not-after")
2139 .arg(validity)
2140 .arg("--force");
2141 if existing_key_pem.is_some() {
2142 command.arg("--key").arg(&key_path);
2143 } else {
2144 command.arg("--no-password").arg("--insecure");
2145 }
2146
2147 let output = command.output().map_err(|e| {
2148 ManagerError::Backend(BackendError::Backend(format!(
2149 "self-signed-tls step execution failed: {e}"
2150 )))
2151 })?;
2152 if !output.status.success() {
2153 let detail = String::from_utf8_lossy(&output.stderr);
2154 return Err(ManagerError::Backend(BackendError::Backend(format!(
2155 "self-signed-tls step failed: {}",
2156 detail.trim()
2157 ))));
2158 }
2159
2160 #[cfg(unix)]
2164 {
2165 use std::os::unix::fs::PermissionsExt as _;
2166 std::fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o600)).map_err(
2167 |e| {
2168 ManagerError::Backend(BackendError::Backend(format!(
2169 "self-signed-tls key perms: {e}"
2170 )))
2171 },
2172 )?;
2173 }
2174
2175 let cert_pem = std::fs::read(&cert_path).map_err(|e| {
2176 ManagerError::Backend(BackendError::Backend(format!(
2177 "self-signed-tls read cert: {e}"
2178 )))
2179 })?;
2180 let private_key_pem = std::fs::read(&key_path).map_err(|e| {
2181 ManagerError::Backend(BackendError::Backend(format!(
2182 "self-signed-tls read key: {e}"
2183 )))
2184 })?;
2185 Ok(TlsMaterial {
2186 cert_pem,
2187 private_key_pem,
2188 })
2189}
2190
2191fn write_secret_file(path: &Path, contents: &[u8]) -> Result<(), ManagerError> {
2192 use std::io::Write as _;
2193 #[cfg(unix)]
2194 use std::os::unix::fs::OpenOptionsExt as _;
2195
2196 let mut options = std::fs::OpenOptions::new();
2197 options.write(true).create_new(true);
2198 #[cfg(unix)]
2199 options.mode(0o600);
2200 let mut file = options.open(path).map_err(|e| {
2201 ManagerError::Backend(BackendError::Backend(format!(
2202 "self-signed-tls write key: {e}"
2203 )))
2204 })?;
2205 file.write_all(contents).map_err(|e| {
2206 ManagerError::Backend(BackendError::Backend(format!(
2207 "self-signed-tls write key: {e}"
2208 )))
2209 })
2210}
2211
2212const fn key_kind(entry: &KeyEntry) -> CatalogKind {
2216 match entry.class {
2217 Class::Asymmetric | Class::Public => CatalogKind::Signing,
2218 Class::Symmetric | Class::Sealing => CatalogKind::Encryption,
2220 Class::Value => CatalogKind::Value,
2221 }
2222}
2223
2224const fn wire_key_type(entry: &KeyEntry) -> Option<KeyType> {
2228 match entry.key_type {
2229 Some(KeyAlgorithm::Ed25519) => Some(KeyType::Ed25519),
2230 Some(KeyAlgorithm::Ed25519Nkey) => Some(KeyType::Ed25519Nkey),
2231 Some(KeyAlgorithm::Rsa2048) => Some(KeyType::Rsa2048),
2232 Some(KeyAlgorithm::EcdsaP256) => Some(KeyType::EcdsaP256),
2233 Some(KeyAlgorithm::EcdsaP384) => Some(KeyType::EcdsaP384),
2234 Some(KeyAlgorithm::EcdsaP521) => Some(KeyType::EcdsaP521),
2235 Some(KeyAlgorithm::MlDsa44) => Some(KeyType::MlDsa44),
2238 Some(KeyAlgorithm::MlDsa65) => Some(KeyType::MlDsa65),
2239 Some(KeyAlgorithm::MlDsa87) => Some(KeyType::MlDsa87),
2240 Some(
2244 KeyAlgorithm::Aes256Gcm
2245 | KeyAlgorithm::ChaCha20Poly1305
2246 | KeyAlgorithm::X25519
2247 | KeyAlgorithm::MlKem512
2248 | KeyAlgorithm::MlKem768
2249 | KeyAlgorithm::MlKem1024,
2250 )
2251 | None => None,
2252 }
2253}
2254
2255fn effective_engine(entry: &KeyEntry) -> Engine {
2258 entry.effective_engine()
2259}
2260
2261fn require_class(
2264 op: &'static str,
2265 key_id: &str,
2266 class: Class,
2267 allowed: &[Class],
2268) -> Result<(), ManagerError> {
2269 if allowed.contains(&class) {
2270 Ok(())
2271 } else {
2272 Err(ManagerError::OpNotValidForClass {
2273 op,
2274 key: key_id.to_string(),
2275 class,
2276 })
2277 }
2278}
2279
2280fn require_x25519_sealing_key(
2281 key_id: &str,
2282 actual: Option<KeyAlgorithm>,
2283) -> Result<(), ManagerError> {
2284 match actual {
2285 Some(KeyAlgorithm::X25519) => Ok(()),
2286 Some(other) => Err(ManagerError::KemAlgorithmMismatch {
2287 key: key_id.to_string(),
2288 requested: "x25519",
2289 actual: other.token(),
2290 }),
2291 None => Err(ManagerError::KemAlgorithmMismatch {
2292 key: key_id.to_string(),
2293 requested: "x25519",
2294 actual: "none",
2295 }),
2296 }
2297}
2298
2299const fn ml_dsa_wire_key_type(algorithm: SignatureAlgorithm) -> KeyType {
2303 match algorithm {
2304 SignatureAlgorithm::MlDsa44 => KeyType::MlDsa44,
2305 SignatureAlgorithm::MlDsa65 => KeyType::MlDsa65,
2306 SignatureAlgorithm::MlDsa87 => KeyType::MlDsa87,
2307 SignatureAlgorithm::Ed25519
2308 | SignatureAlgorithm::Ed25519Nkey
2309 | SignatureAlgorithm::Rs256
2310 | SignatureAlgorithm::Es256 => KeyType::Ed25519,
2311 }
2312}
2313
2314const fn ml_kem_wire_key_type(kem: ProviderKemAlgorithm) -> KeyType {
2318 match kem {
2319 ProviderKemAlgorithm::MlKem512 => KeyType::MlKem512,
2320 ProviderKemAlgorithm::MlKem768 => KeyType::MlKem768,
2321 ProviderKemAlgorithm::MlKem1024 => KeyType::MlKem1024,
2322 }
2323}
2324
2325const fn ml_kem_provider_algorithm(algorithm: KeyAlgorithm) -> Option<ProviderKemAlgorithm> {
2329 match algorithm {
2330 KeyAlgorithm::MlKem512 => Some(ProviderKemAlgorithm::MlKem512),
2331 KeyAlgorithm::MlKem768 => Some(ProviderKemAlgorithm::MlKem768),
2332 KeyAlgorithm::MlKem1024 => Some(ProviderKemAlgorithm::MlKem1024),
2333 KeyAlgorithm::Ed25519
2334 | KeyAlgorithm::Ed25519Nkey
2335 | KeyAlgorithm::Rsa2048
2336 | KeyAlgorithm::EcdsaP256
2337 | KeyAlgorithm::EcdsaP384
2338 | KeyAlgorithm::EcdsaP521
2339 | KeyAlgorithm::Aes256Gcm
2340 | KeyAlgorithm::ChaCha20Poly1305
2341 | KeyAlgorithm::X25519
2342 | KeyAlgorithm::MlDsa44
2343 | KeyAlgorithm::MlDsa65
2344 | KeyAlgorithm::MlDsa87 => None,
2345 }
2346}
2347
2348fn require_ml_kem_sealing_key(
2349 key_id: &str,
2350 actual: Option<KeyAlgorithm>,
2351 requested: ProviderKemAlgorithm,
2352) -> Result<(), ManagerError> {
2353 let expected = match requested {
2354 ProviderKemAlgorithm::MlKem512 => KeyAlgorithm::MlKem512,
2355 ProviderKemAlgorithm::MlKem768 => KeyAlgorithm::MlKem768,
2356 ProviderKemAlgorithm::MlKem1024 => KeyAlgorithm::MlKem1024,
2357 };
2358 match actual {
2359 Some(found) if found == expected => Ok(()),
2360 Some(found) => Err(ManagerError::KemAlgorithmMismatch {
2361 key: key_id.to_string(),
2362 requested: requested.token(),
2363 actual: found.token(),
2364 }),
2365 None => Err(ManagerError::KemAlgorithmMismatch {
2366 key: key_id.to_string(),
2367 requested: requested.token(),
2368 actual: "none",
2369 }),
2370 }
2371}
2372
2373#[cfg(test)]
2374mod tests {
2375 use super::*;
2376 use crate::backend::{KeyMetadata, KvValue};
2377 use crate::core::crypto_provider::{SoftwareCustodyCatalog, encode_record_bytes};
2378 use crate::ml_kem_envelope;
2379 use async_trait::async_trait;
2380 use std::sync::Arc;
2381 use std::sync::atomic::{AtomicUsize, Ordering};
2382
2383 #[derive(Default)]
2386 struct MockBackend {
2387 name: &'static str,
2388 last_path: std::sync::Mutex<Option<String>>,
2389 last_spiffe_id: std::sync::Mutex<Option<String>>,
2390 new_key_calls: AtomicUsize,
2391 rotate_calls: AtomicUsize,
2392 kv_put_calls: AtomicUsize,
2393 latest_version: AtomicUsize,
2395 last_versions_config: std::sync::Mutex<Option<(Option<u32>, Option<u32>)>>,
2399 last_kv_value: std::sync::Mutex<Option<Vec<u8>>>,
2401 last_sign_options: std::sync::Mutex<Option<SignOptions>>,
2402 last_verify_options: std::sync::Mutex<Option<SignOptions>>,
2403 last_new_key_type: std::sync::Mutex<Option<KeyType>>,
2404 kv_store: std::sync::Mutex<BTreeMap<String, Vec<u8>>>,
2411 kv_put_log: std::sync::Mutex<Vec<(String, Vec<u8>)>>,
2412 }
2413
2414 impl MockBackend {
2415 fn new(name: &'static str) -> Arc<Self> {
2416 Arc::new(Self {
2417 name,
2418 last_path: std::sync::Mutex::new(None),
2419 last_spiffe_id: std::sync::Mutex::new(None),
2420 new_key_calls: AtomicUsize::new(0),
2421 rotate_calls: AtomicUsize::new(0),
2422 kv_put_calls: AtomicUsize::new(0),
2423 latest_version: AtomicUsize::new(5),
2424 last_versions_config: std::sync::Mutex::new(None),
2425 last_kv_value: std::sync::Mutex::new(None),
2426 last_sign_options: std::sync::Mutex::new(None),
2427 last_verify_options: std::sync::Mutex::new(None),
2428 last_new_key_type: std::sync::Mutex::new(None),
2429 kv_store: std::sync::Mutex::new(BTreeMap::new()),
2430 kv_put_log: std::sync::Mutex::new(Vec::new()),
2431 })
2432 }
2433
2434 fn seed_kv(&self, path: &str, value: Vec<u8>) {
2437 self.kv_store
2438 .lock()
2439 .unwrap()
2440 .insert(path.to_string(), value);
2441 }
2442
2443 fn kv_lookup(&self, path: &str) -> Vec<u8> {
2447 if let Some(v) = self.kv_store.lock().unwrap().get(path) {
2448 return v.clone();
2449 }
2450 self.last_kv_value
2451 .lock()
2452 .unwrap()
2453 .clone()
2454 .unwrap_or_else(|| b"stored-value".to_vec())
2455 }
2456
2457 fn last_path(&self) -> Option<String> {
2458 self.last_path.lock().unwrap().clone()
2459 }
2460
2461 fn last_versions_config(&self) -> Option<(Option<u32>, Option<u32>)> {
2462 *self.last_versions_config.lock().unwrap()
2463 }
2464
2465 fn last_kv_value(&self) -> Option<Vec<u8>> {
2466 self.last_kv_value.lock().unwrap().clone()
2467 }
2468
2469 fn last_spiffe_id(&self) -> Option<String> {
2470 self.last_spiffe_id.lock().unwrap().clone()
2471 }
2472
2473 fn kv_put_log(&self) -> Vec<(String, Vec<u8>)> {
2474 self.kv_put_log.lock().unwrap().clone()
2475 }
2476
2477 fn last_sign_options(&self) -> Option<SignOptions> {
2478 *self.last_sign_options.lock().unwrap()
2479 }
2480
2481 fn last_verify_options(&self) -> Option<SignOptions> {
2482 *self.last_verify_options.lock().unwrap()
2483 }
2484
2485 fn last_new_key_type(&self) -> Option<KeyType> {
2486 *self.last_new_key_type.lock().unwrap()
2487 }
2488 }
2489
2490 struct MockHandle(Arc<MockBackend>);
2493
2494 #[async_trait]
2495 impl Backend for MockHandle {
2496 fn kind(&self) -> &'static str {
2497 self.0.name
2498 }
2499
2500 async fn new_key(&self, key_type: KeyType) -> Result<NewKey, BackendError> {
2501 self.0.new_key_calls.fetch_add(1, Ordering::SeqCst);
2502 *self.0.last_new_key_type.lock().unwrap() = Some(key_type);
2503 Ok(NewKey {
2504 key_id: format!("{}-newkey", self.0.name),
2505 public_key: vec![1, 2, 3],
2506 })
2507 }
2508
2509 async fn public_key(&self, key_id: &str) -> Result<Vec<u8>, BackendError> {
2510 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2511 Ok(vec![9, 9, 9])
2512 }
2513
2514 async fn public_key_with_meta(&self, key_id: &str) -> Result<PublicKey, BackendError> {
2515 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2516 Ok(PublicKey {
2517 public_key: vec![9, 9, 9],
2518 key_type: KeyType::Ed25519Nkey,
2521 version: 7,
2522 })
2523 }
2524
2525 async fn key_metadata(&self, key_id: &str) -> Result<KeyMetadata, BackendError> {
2526 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2527 Ok(KeyMetadata {
2528 key_type: Some(KeyType::Ed25519),
2529 latest_version: u32::try_from(self.0.latest_version.load(Ordering::SeqCst))
2530 .unwrap_or(u32::MAX),
2531 })
2532 }
2533
2534 async fn import(
2535 &self,
2536 key_id: &str,
2537 _key_type: KeyType,
2538 _material: &KeyMaterial,
2539 ) -> Result<NewKey, BackendError> {
2540 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2541 Ok(NewKey {
2542 key_id: key_id.to_string(),
2543 public_key: vec![0xEE, 0xEE],
2544 })
2545 }
2546
2547 async fn sign(&self, key_id: &str, _message: &[u8]) -> Result<Vec<u8>, BackendError> {
2548 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2549 *self.0.last_sign_options.lock().unwrap() = Some(SignOptions::Default);
2550 Ok(vec![0xAB, 0xCD])
2551 }
2552
2553 async fn sign_with_options(
2554 &self,
2555 key_id: &str,
2556 message: &[u8],
2557 options: SignOptions,
2558 ) -> Result<Vec<u8>, BackendError> {
2559 let mut signature = self.sign(key_id, message).await?;
2560 *self.0.last_sign_options.lock().unwrap() = Some(options);
2561 if options == SignOptions::Rs256Pkcs1v15Sha256 {
2562 signature.push(0x52);
2563 } else if options == SignOptions::Es256 {
2564 signature.push(0x45);
2565 } else if options == SignOptions::Es384 {
2566 signature.push(0x46);
2567 } else if options == SignOptions::Es512 {
2568 signature.push(0x47);
2569 }
2570 Ok(signature)
2571 }
2572
2573 async fn verify(
2574 &self,
2575 key_id: &str,
2576 _message: &[u8],
2577 _signature: &[u8],
2578 ) -> Result<bool, BackendError> {
2579 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2580 *self.0.last_verify_options.lock().unwrap() = Some(SignOptions::Default);
2581 Ok(true)
2582 }
2583
2584 async fn verify_with_options(
2585 &self,
2586 key_id: &str,
2587 message: &[u8],
2588 signature: &[u8],
2589 options: SignOptions,
2590 ) -> Result<bool, BackendError> {
2591 let valid = self.verify(key_id, message, signature).await?;
2592 *self.0.last_verify_options.lock().unwrap() = Some(options);
2593 Ok(valid)
2594 }
2595
2596 async fn encrypt(
2602 &self,
2603 key_id: &str,
2604 algorithm: AeadAlgorithm,
2605 plaintext: &[u8],
2606 aad: Option<&[u8]>,
2607 ) -> Result<CiphertextEnvelope, BackendError> {
2608 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2609 let aad = aad.unwrap_or(&[]);
2610 let mut ciphertext = Vec::new();
2611 ciphertext.push(u8::try_from(aad.len()).unwrap_or(u8::MAX));
2612 ciphertext.extend_from_slice(aad);
2613 ciphertext.extend_from_slice(plaintext);
2614 Ok(CiphertextEnvelope {
2615 alg: algorithm,
2616 key_version: u32::try_from(self.0.latest_version.load(Ordering::SeqCst))
2617 .unwrap_or(u32::MAX),
2618 nonce: Vec::new(),
2619 ciphertext,
2620 })
2621 }
2622
2623 async fn decrypt(
2624 &self,
2625 key_id: &str,
2626 envelope: &CiphertextEnvelope,
2627 aad: Option<&[u8]>,
2628 ) -> Result<Vec<u8>, BackendError> {
2629 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2630 let aad = aad.unwrap_or(&[]);
2631 let ct = &envelope.ciphertext;
2632 let aad_len = *ct.first().ok_or(BackendError::DecryptFailed)? as usize;
2633 let bound = ct.get(1..1 + aad_len).ok_or(BackendError::DecryptFailed)?;
2634 if bound != aad {
2636 return Err(BackendError::DecryptFailed);
2637 }
2638 Ok(ct
2639 .get(1 + aad_len..)
2640 .ok_or(BackendError::DecryptFailed)?
2641 .to_vec())
2642 }
2643
2644 async fn rotate(&self, key_id: &str) -> Result<u32, BackendError> {
2645 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2646 self.0.rotate_calls.fetch_add(1, Ordering::SeqCst);
2647 let v = self.0.latest_version.fetch_add(1, Ordering::SeqCst) + 1;
2648 Ok(u32::try_from(v).unwrap_or(u32::MAX))
2649 }
2650
2651 async fn kv_get(
2652 &self,
2653 key_id: &str,
2654 version: Option<u32>,
2655 ) -> Result<KvValue, BackendError> {
2656 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2657 let value = self.0.kv_lookup(key_id);
2661 let version = version.unwrap_or_else(|| {
2662 u32::try_from(self.0.latest_version.load(Ordering::SeqCst)).unwrap_or(u32::MAX)
2663 });
2664 Ok(KvValue { value, version })
2665 }
2666
2667 async fn kv_get_secret(
2668 &self,
2669 key_id: &str,
2670 version: Option<u32>,
2671 ) -> Result<KvSecret, BackendError> {
2672 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2673 let version = version.unwrap_or_else(|| {
2674 u32::try_from(self.0.latest_version.load(Ordering::SeqCst)).unwrap_or(u32::MAX)
2675 });
2676 Ok(KvSecret {
2677 value: Zeroizing::new(self.0.kv_lookup(key_id)),
2678 version,
2679 })
2680 }
2681
2682 async fn kv_put(&self, key_id: &str, value: &[u8]) -> Result<u32, BackendError> {
2683 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2684 self.0.kv_put_calls.fetch_add(1, Ordering::SeqCst);
2685 *self.0.last_kv_value.lock().unwrap() = Some(value.to_vec());
2686 self.0
2687 .kv_store
2688 .lock()
2689 .unwrap()
2690 .insert(key_id.to_string(), value.to_vec());
2691 self.0
2692 .kv_put_log
2693 .lock()
2694 .unwrap()
2695 .push((key_id.to_string(), value.to_vec()));
2696 let v = self.0.latest_version.fetch_add(1, Ordering::SeqCst) + 1;
2697 Ok(u32::try_from(v).unwrap_or(u32::MAX))
2698 }
2699
2700 async fn configure_versions(
2701 &self,
2702 key_id: &str,
2703 min_decryption_version: Option<u32>,
2704 min_available_version: Option<u32>,
2705 ) -> Result<(), BackendError> {
2706 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2707 *self.0.last_versions_config.lock().unwrap() =
2708 Some((min_decryption_version, min_available_version));
2709 Ok(())
2710 }
2711
2712 async fn issue_x509_svid(
2713 &self,
2714 key_id: &str,
2715 spiffe_id: &str,
2716 _ttl_seconds: u64,
2717 ) -> Result<X509Svid, BackendError> {
2718 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2719 *self.0.last_spiffe_id.lock().unwrap() = Some(spiffe_id.to_string());
2720 Ok(X509Svid {
2721 cert_chain_der: vec![vec![1, 2, 3]],
2722 leaf_private_key_der: zeroize::Zeroizing::new(vec![4, 5, 6]),
2723 bundle_der: vec![vec![7, 8, 9]],
2724 })
2725 }
2726
2727 async fn issue_x509_cert(
2728 &self,
2729 key_id: &str,
2730 request: &X509CertRequest,
2731 ) -> Result<X509Svid, BackendError> {
2732 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2733 *self.0.last_spiffe_id.lock().unwrap() = Some(request.common_name.clone());
2735 Ok(X509Svid {
2736 cert_chain_der: vec![vec![0xCE, 0x27]],
2737 leaf_private_key_der: zeroize::Zeroizing::new(vec![0xDE, 0xAD]),
2738 bundle_der: vec![vec![0xCA, 0xFE]],
2739 })
2740 }
2741 }
2742
2743 const CATALOG: &str = r#"{
2753 "schemaVersion": 1,
2754 "backends": {
2755 "primary": {
2756 "kind": "vault", "addr": "https://127.0.0.1:8200",
2757 "engines": ["transit", "kv2"], "capabilities": [],
2758 "mintKeyTypes": ["ed25519", "ed25519-nkey", "rsa-2048", "ecdsa-p256", "ecdsa-p384", "ecdsa-p521"]
2759 },
2760 "secondary": {
2761 "kind": "vault", "addr": "https://127.0.0.1:8201",
2762 "engines": ["transit", "kv2"], "capabilities": [],
2763 "mintKeyTypes": ["ed25519", "ed25519-nkey", "rsa-2048", "ecdsa-p256"]
2764 }
2765 },
2766 "keys": {
2767 "asym.signer": {
2768 "class": "asymmetric", "keyType": "ed25519", "backend": "primary",
2769 "path": "signer", "writable": true, "missing": "error",
2770 "description": "a signing key"
2771 },
2772 "asym.nkey": {
2773 "class": "asymmetric", "keyType": "ed25519-nkey", "backend": "primary",
2774 "path": "nkey-signer", "writable": true, "missing": "error",
2775 "description": "a NATS nkey signing key"
2776 },
2777 "asym.rsa": {
2778 "class": "asymmetric", "keyType": "rsa-2048", "backend": "primary",
2779 "path": "rsa-signer", "writable": true, "missing": "error",
2780 "description": "an RSA signing key"
2781 },
2782 "asym.ecdsa": {
2783 "class": "asymmetric", "keyType": "ecdsa-p256", "backend": "primary",
2784 "path": "ecdsa-signer", "writable": true, "missing": "error",
2785 "description": "an ECDSA P-256 signing key"
2786 },
2787 "asym.ecdsa384": {
2788 "class": "asymmetric", "keyType": "ecdsa-p384", "backend": "primary",
2789 "path": "ecdsa384-signer", "writable": true, "missing": "error",
2790 "description": "an ECDSA P-384 signing key"
2791 },
2792 "asym.ecdsa521": {
2793 "class": "asymmetric", "keyType": "ecdsa-p521", "backend": "primary",
2794 "path": "ecdsa521-signer", "writable": true, "missing": "error",
2795 "description": "an ECDSA P-521 signing key"
2796 },
2797 "sym.box": {
2798 "class": "symmetric", "keyType": "aes-256-gcm", "backend": "secondary",
2799 "path": "box", "writable": true, "missing": "error",
2800 "description": "a symmetric key"
2801 },
2802 "web.value": {
2803 "class": "value", "backend": "primary", "engine": "kv2",
2804 "path": "secret/data/web/value", "writable": true, "missing": "error",
2805 "description": "an opaque value"
2806 },
2807 "gen.value": {
2808 "class": "value", "backend": "primary", "engine": "kv2",
2809 "path": "secret/data/gen/value", "writable": true, "missing": "generate",
2810 "generate": { "format": "ascii-printable", "bytes": 24 },
2811 "description": "a generate-able value (rotate regenerates)"
2812 },
2813 "tls.cert": {
2814 "class": "public", "backend": "primary", "engine": "kv2",
2815 "path": "secret/data/tls/cert", "writable": false, "missing": "generate",
2816 "generate": { "format": "self-signed-tls", "commonName": "example.test", "validity": "1h" },
2817 "description": "a generated self-signed cert"
2818 },
2819 "tls.key": {
2820 "class": "value", "backend": "primary", "engine": "kv2",
2821 "path": "secret/data/tls/key", "writable": true, "missing": "generate",
2822 "generate": { "format": "self-signed-tls-pair-of", "pairOf": "tls.cert" },
2823 "description": "the private key paired with tls.cert"
2824 },
2825 "web.cert": {
2826 "class": "public", "backend": "secondary", "engine": "kv2",
2827 "path": "secret/data/web/cert", "writable": false, "missing": "warn",
2828 "description": "a public cert"
2829 },
2830 "spiffe.issuer": {
2831 "class": "asymmetric", "keyType": "ed25519", "backend": "secondary", "engine": "pki",
2832 "path": "pki/issue/workload", "writable": false, "missing": "error",
2833 "description": "a pki issuer role"
2834 },
2835 "enroll.sealing": {
2836 "class": "sealing", "keyType": "x25519", "backend": "primary", "engine": "kv2",
2837 "path": "secret/data/enroll/x25519",
2838 "publicPath": "secret/data/enroll/x25519-public",
2839 "writable": true, "missing": "error",
2840 "description": "an x25519 enrollment sealing key"
2841 },
2842 "enroll.mlkem": {
2843 "class": "sealing", "keyType": "ml-kem-768", "backend": "primary", "engine": "kv2",
2844 "path": "secret/data/enroll/ml-kem-768",
2845 "publicPath": "secret/data/enroll/ml-kem-768-public",
2846 "labels": [
2847 "crypto_provider=local-software",
2848 "crypto_provider_version=1",
2849 "pqc_algorithm=ml-kem-768",
2850 "pqc_custody=software-encrypted",
2851 "crypto_provider_policy=local-software",
2852 "pqc_storage_key=pqc/storage/wrap"
2853 ],
2854 "writable": true, "missing": "error",
2855 "description": "an ML-KEM enrollment sealing key"
2856 },
2857 "kv2.signer": {
2858 "class": "asymmetric", "keyType": "ed25519", "backend": "primary", "engine": "kv2",
2859 "path": "secret/data/kv2/signer",
2860 "publicPath": "secret/data/kv2/signer-public",
2861 "writable": true, "missing": "error",
2862 "description": "a value-store Ed25519 materialize-to-sign key"
2863 }
2864 }
2865 }"#;
2866
2867 fn parse_catalog(json: &str) -> Catalog {
2868 serde_json::from_str(json).expect("catalog parses")
2869 }
2870
2871 fn fixture() -> (BackendManager, Arc<MockBackend>, Arc<MockBackend>) {
2874 let primary = MockBackend::new("primary");
2875 let secondary = MockBackend::new("secondary");
2876 let mut backends: BTreeMap<String, Box<dyn Backend>> = BTreeMap::new();
2877 backends.insert("primary".into(), Box::new(MockHandle(primary.clone())));
2878 backends.insert("secondary".into(), Box::new(MockHandle(secondary.clone())));
2879 let mgr = BackendManager::new(parse_catalog(CATALOG), backends)
2880 .expect("manager constructs cleanly");
2881 (mgr, primary, secondary)
2882 }
2883
2884 #[test]
2885 fn new_rejects_key_naming_absent_backend() {
2886 let mut backends: BTreeMap<String, Box<dyn Backend>> = BTreeMap::new();
2888 backends.insert(
2889 "primary".into(),
2890 Box::new(MockHandle(MockBackend::new("primary"))),
2891 );
2892 let err = BackendManager::new(parse_catalog(CATALOG), backends)
2894 .expect_err("missing backend must fail closed");
2895 match err {
2896 ManagerError::UnknownBackend { backend, .. } => assert_eq!(backend, "secondary"),
2897 other => panic!("expected UnknownBackend, got {other:?}"),
2898 }
2899 }
2900
2901 #[test]
2902 fn resolve_maps_key_to_correct_backend_and_path() {
2903 let (mgr, _p, _s) = fixture();
2904
2905 let routed = mgr.resolve("asym.signer").expect("resolves");
2906 assert_eq!(routed.backend.kind(), "primary");
2907 assert_eq!(routed.path(), "signer");
2908 assert_eq!(routed.class(), Class::Asymmetric);
2909 assert_eq!(routed.engine, Engine::Transit);
2910 assert_eq!(routed.key_type(), Some(KeyAlgorithm::Ed25519));
2911
2912 let routed = mgr.resolve("web.cert").expect("resolves");
2913 assert_eq!(routed.backend.kind(), "secondary");
2914 assert_eq!(routed.path(), "secret/data/web/cert");
2915 assert_eq!(routed.class(), Class::Public);
2916 assert_eq!(routed.engine, Engine::Kv2);
2917
2918 let routed = mgr.resolve("spiffe.issuer").expect("resolves");
2919 assert_eq!(routed.backend.kind(), "secondary");
2920 assert_eq!(routed.path(), "pki/issue/workload");
2921 assert_eq!(routed.class(), Class::Asymmetric);
2922 assert_eq!(routed.engine, Engine::Pki);
2923 }
2924
2925 #[test]
2926 fn resolve_infers_engine_when_catalog_omits_it() {
2927 let (mgr, _p, _s) = fixture();
2929 assert_eq!(mgr.resolve("asym.signer").unwrap().engine, Engine::Transit);
2930 assert_eq!(mgr.resolve("sym.box").unwrap().engine, Engine::Transit);
2931 }
2932
2933 #[test]
2934 fn resolve_unknown_key_errors() {
2935 let (mgr, _p, _s) = fixture();
2936 let err = mgr.resolve("does.not.exist").expect_err("unknown key");
2937 match err {
2938 ManagerError::UnknownKey(k) => assert_eq!(k, "does.not.exist"),
2939 other => panic!("expected UnknownKey, got {other:?}"),
2940 }
2941 }
2942
2943 #[tokio::test]
2944 async fn sign_routes_to_correct_backend_with_path() {
2945 let (mgr, primary, secondary) = fixture();
2946 let sig = mgr.sign("asym.signer", &[1, 2, 3]).await.expect("signs");
2947 assert_eq!(sig, vec![0xAB, 0xCD]);
2948 assert_eq!(primary.last_path().as_deref(), Some("signer"));
2951 assert_eq!(
2952 secondary.last_path(),
2953 None,
2954 "the other backend is untouched"
2955 );
2956 }
2957
2958 #[tokio::test]
2959 async fn nkey_sign_uses_raw_ed25519_transit_defaults() {
2960 let (mgr, primary, _secondary) = fixture();
2961 let sig = mgr.sign("asym.nkey", b"nonce").await.expect("signs nkey");
2962 assert_eq!(sig, vec![0xAB, 0xCD]);
2963 assert_eq!(primary.last_path().as_deref(), Some("nkey-signer"));
2964 assert_eq!(primary.last_sign_options(), Some(SignOptions::Default));
2965 }
2966
2967 #[tokio::test]
2968 async fn rsa_sign_and_verify_use_rs256_transit_options() {
2969 let (mgr, primary, _secondary) = fixture();
2970 let sig = mgr.sign("asym.rsa", b"jwt-input").await.expect("signs rsa");
2971 assert_eq!(sig, vec![0xAB, 0xCD, 0x52]);
2972 assert_eq!(primary.last_path().as_deref(), Some("rsa-signer"));
2973 assert_eq!(
2974 primary.last_sign_options(),
2975 Some(SignOptions::Rs256Pkcs1v15Sha256)
2976 );
2977
2978 let valid = mgr
2979 .verify("asym.rsa", b"jwt-input", &sig)
2980 .await
2981 .expect("verifies rsa");
2982 assert!(valid);
2983 assert_eq!(
2984 primary.last_verify_options(),
2985 Some(SignOptions::Rs256Pkcs1v15Sha256)
2986 );
2987 }
2988
2989 #[tokio::test]
2990 async fn ecdsa_sign_and_verify_use_es256_transit_options() {
2991 let (mgr, primary, _secondary) = fixture();
2992 let sig = mgr
2993 .sign("asym.ecdsa", b"jwt-input")
2994 .await
2995 .expect("signs ecdsa");
2996 assert_eq!(sig, vec![0xAB, 0xCD, 0x45]);
2997 assert_eq!(primary.last_path().as_deref(), Some("ecdsa-signer"));
2998 assert_eq!(primary.last_sign_options(), Some(SignOptions::Es256));
2999
3000 let valid = mgr
3001 .verify("asym.ecdsa", b"jwt-input", &sig)
3002 .await
3003 .expect("verifies ecdsa");
3004 assert!(valid);
3005 assert_eq!(primary.last_verify_options(), Some(SignOptions::Es256));
3006 }
3007
3008 #[tokio::test]
3009 async fn ecdsa_p384_sign_and_verify_use_es384_transit_options() {
3010 let (mgr, primary, _secondary) = fixture();
3011 let sig = mgr
3012 .sign("asym.ecdsa384", b"jwt-input")
3013 .await
3014 .expect("signs ecdsa p384");
3015 assert_eq!(sig, vec![0xAB, 0xCD, 0x46]);
3016 assert_eq!(primary.last_path().as_deref(), Some("ecdsa384-signer"));
3017 assert_eq!(primary.last_sign_options(), Some(SignOptions::Es384));
3018
3019 let valid = mgr
3020 .verify("asym.ecdsa384", b"jwt-input", &sig)
3021 .await
3022 .expect("verifies ecdsa p384");
3023 assert!(valid);
3024 assert_eq!(primary.last_verify_options(), Some(SignOptions::Es384));
3025 }
3026
3027 #[tokio::test]
3028 async fn ecdsa_p521_sign_and_verify_use_es512_transit_options() {
3029 let (mgr, primary, _secondary) = fixture();
3030 let sig = mgr
3031 .sign("asym.ecdsa521", b"jwt-input")
3032 .await
3033 .expect("signs ecdsa p521");
3034 assert_eq!(sig, vec![0xAB, 0xCD, 0x47]);
3035 assert_eq!(primary.last_path().as_deref(), Some("ecdsa521-signer"));
3036 assert_eq!(primary.last_sign_options(), Some(SignOptions::Es512));
3037
3038 let valid = mgr
3039 .verify("asym.ecdsa521", b"jwt-input", &sig)
3040 .await
3041 .expect("verifies ecdsa p521");
3042 assert!(valid);
3043 assert_eq!(primary.last_verify_options(), Some(SignOptions::Es512));
3044 }
3045
3046 #[tokio::test]
3047 async fn issue_x509_svid_routes_to_pki_issue_path() {
3048 let (mgr, _primary, secondary) = fixture();
3049 let svid = mgr
3050 .issue_x509_svid("spiffe.issuer", "spiffe://example.test/web", 300)
3051 .await
3052 .expect("issues x509 svid");
3053 assert_eq!(secondary.last_path().as_deref(), Some("pki/issue/workload"));
3054 assert_eq!(
3055 secondary.last_spiffe_id().as_deref(),
3056 Some("spiffe://example.test/web")
3057 );
3058 assert_eq!(svid.cert_chain_der, vec![vec![1, 2, 3]]);
3059 assert_eq!(&*svid.leaf_private_key_der, &[4, 5, 6]);
3060 assert_eq!(svid.bundle_der, vec![vec![7, 8, 9]]);
3061 }
3062
3063 #[tokio::test]
3064 async fn issue_x509_svid_rejects_non_pki_key() {
3065 let (mgr, _primary, _secondary) = fixture();
3066 let err = mgr
3067 .issue_x509_svid("asym.signer", "spiffe://example.test/web", 300)
3068 .await
3069 .expect_err("transit key is not a pki issuer");
3070 assert!(matches!(err, ManagerError::Unsupported("issue_x509_svid")));
3071 }
3072
3073 #[tokio::test]
3074 async fn issue_x509_cert_routes_to_pki_issue_path_with_sans() {
3075 let (mgr, _primary, secondary) = fixture();
3076 let request = X509CertRequest {
3077 common_name: "web.internal".into(),
3078 dns_sans: vec!["web.internal".into(), "alt.internal".into()],
3079 ip_sans: vec!["10.0.0.1".into()],
3080 ttl_seconds: 3600,
3081 };
3082 let issued = mgr
3083 .issue_x509_cert("spiffe.issuer", &request)
3084 .await
3085 .expect("issues x509 cert");
3086 assert_eq!(secondary.last_path().as_deref(), Some("pki/issue/workload"));
3087 assert_eq!(secondary.last_spiffe_id().as_deref(), Some("web.internal"));
3088 assert_eq!(issued.cert_chain_der, vec![vec![0xCE, 0x27]]);
3089 assert_eq!(&*issued.leaf_private_key_der, &[0xDE, 0xAD]);
3090 }
3091
3092 #[tokio::test]
3093 async fn issue_x509_cert_rejects_non_pki_key() {
3094 let (mgr, _primary, _secondary) = fixture();
3095 let request = X509CertRequest {
3096 common_name: "web.internal".into(),
3097 ..X509CertRequest::default()
3098 };
3099 let err = mgr
3100 .issue_x509_cert("asym.signer", &request)
3101 .await
3102 .expect_err("transit key is not a pki issuer");
3103 assert!(matches!(err, ManagerError::Unsupported("issue_x509_cert")));
3104 }
3105
3106 #[tokio::test]
3107 async fn new_key_routes_to_correct_backend() {
3108 let (mgr, primary, secondary) = fixture();
3109 let nk = mgr
3110 .new_key("asym.signer", KeyType::Ed25519)
3111 .await
3112 .expect("new_key");
3113 assert_eq!(nk.key_id, "primary-newkey");
3114 assert_eq!(primary.new_key_calls.load(Ordering::SeqCst), 1);
3115 assert_eq!(primary.last_new_key_type(), Some(KeyType::Ed25519));
3116 assert_eq!(secondary.new_key_calls.load(Ordering::SeqCst), 0);
3117
3118 mgr.new_key("asym.nkey", KeyType::Ed25519Nkey)
3119 .await
3120 .expect("new_key nkey");
3121 assert_eq!(primary.last_new_key_type(), Some(KeyType::Ed25519Nkey));
3122
3123 mgr.new_key("asym.rsa", KeyType::Rsa2048)
3124 .await
3125 .expect("new_key rsa");
3126 assert_eq!(primary.last_new_key_type(), Some(KeyType::Rsa2048));
3127
3128 mgr.new_key("asym.ecdsa", KeyType::EcdsaP256)
3129 .await
3130 .expect("new_key ecdsa");
3131 assert_eq!(primary.last_new_key_type(), Some(KeyType::EcdsaP256));
3132
3133 mgr.new_key("asym.ecdsa384", KeyType::EcdsaP384)
3134 .await
3135 .expect("new_key ecdsa p384");
3136 assert_eq!(primary.last_new_key_type(), Some(KeyType::EcdsaP384));
3137
3138 mgr.new_key("asym.ecdsa521", KeyType::EcdsaP521)
3139 .await
3140 .expect("new_key ecdsa p521");
3141 assert_eq!(primary.last_new_key_type(), Some(KeyType::EcdsaP521));
3142 }
3143
3144 #[tokio::test]
3145 async fn new_key_rejects_key_type_absent_from_static_backend_preset() {
3146 let catalog = CATALOG.replace(
3147 r#""mintKeyTypes": ["ed25519", "ed25519-nkey", "rsa-2048", "ecdsa-p256", "ecdsa-p384", "ecdsa-p521"]"#,
3148 r#""mintKeyTypes": ["ed25519"]"#,
3149 );
3150 let primary = MockBackend::new("primary");
3151 let secondary = MockBackend::new("secondary");
3152 let mut backends: BTreeMap<String, Box<dyn Backend>> = BTreeMap::new();
3153 backends.insert("primary".into(), Box::new(MockHandle(primary.clone())));
3154 backends.insert("secondary".into(), Box::new(MockHandle(secondary)));
3155 let mgr = BackendManager::new(parse_catalog(&catalog), backends).expect("manager builds");
3156
3157 let err = mgr
3158 .new_key("asym.signer", KeyType::Rsa2048)
3159 .await
3160 .expect_err("rsa absent from backend preset");
3161 assert!(matches!(
3162 err,
3163 ManagerError::UnsupportedKeyType {
3164 backend,
3165 op: "new_key",
3166 key_type: KeyType::Rsa2048
3167 } if backend == "primary"
3168 ));
3169 assert_eq!(primary.new_key_calls.load(Ordering::SeqCst), 0);
3170 }
3171
3172 #[tokio::test]
3173 async fn get_public_key_valid_on_public_class_returns_real_metadata() {
3174 let (mgr, _p, secondary) = fixture();
3175 let pk = mgr.get_public_key("web.cert").await.expect("public read");
3176 assert_eq!(pk.public_key, vec![9, 9, 9]);
3177 assert_eq!(pk.key_type, KeyType::Ed25519Nkey);
3179 assert_eq!(pk.version, 7);
3180 assert_eq!(
3181 secondary.last_path().as_deref(),
3182 Some("secret/data/web/cert")
3183 );
3184 }
3185
3186 #[tokio::test]
3187 async fn import_routes_to_backend_path_and_returns_only_public() {
3188 let (mgr, primary, _s) = fixture();
3189 let nk = mgr
3190 .import(
3191 "asym.signer",
3192 KeyType::Ed25519,
3193 &KeyMaterial::Ed25519Seed(vec![7; 32]),
3194 )
3195 .await
3196 .expect("import");
3197 assert_eq!(nk.public_key, vec![0xEE, 0xEE]);
3199 assert_eq!(primary.last_path().as_deref(), Some("signer"));
3201 }
3202
3203 #[tokio::test]
3204 async fn import_rejects_key_type_absent_from_static_backend_preset() {
3205 let catalog = CATALOG.replace(
3206 r#""mintKeyTypes": ["ed25519", "ed25519-nkey", "rsa-2048", "ecdsa-p256", "ecdsa-p384", "ecdsa-p521"]"#,
3207 r#""mintKeyTypes": ["ed25519"]"#,
3208 );
3209 let primary = MockBackend::new("primary");
3210 let secondary = MockBackend::new("secondary");
3211 let mut backends: BTreeMap<String, Box<dyn Backend>> = BTreeMap::new();
3212 backends.insert("primary".into(), Box::new(MockHandle(primary.clone())));
3213 backends.insert("secondary".into(), Box::new(MockHandle(secondary)));
3214 let mgr = BackendManager::new(parse_catalog(&catalog), backends).expect("manager builds");
3215
3216 let err = mgr
3217 .import(
3218 "asym.signer",
3219 KeyType::Rsa2048,
3220 &KeyMaterial::Pkcs8Der(vec![1, 2, 3]),
3221 )
3222 .await
3223 .expect_err("rsa absent from backend preset");
3224 assert!(matches!(
3225 err,
3226 ManagerError::UnsupportedKeyType {
3227 backend,
3228 op: "import",
3229 key_type: KeyType::Rsa2048
3230 } if backend == "primary"
3231 ));
3232 assert!(primary.last_path().is_none());
3233 }
3234
3235 #[tokio::test]
3236 async fn import_on_value_key_is_op_not_valid_for_class() {
3237 let (mgr, _p, _s) = fixture();
3238 let err = mgr
3239 .import(
3240 "web.value",
3241 KeyType::Ed25519,
3242 &KeyMaterial::Ed25519Seed(vec![0; 32]),
3243 )
3244 .await
3245 .expect_err("import on a value key must be rejected");
3246 assert!(matches!(err, ManagerError::OpNotValidForClass { .. }));
3247 }
3248
3249 #[tokio::test]
3250 async fn list_projects_catalog_value_free_and_respects_visibility() {
3251 let (mgr, _p, _s) = fixture();
3252 let entries = mgr
3254 .list(None, |name| name != "web.value")
3257 .await
3258 .expect("list");
3259 let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
3260 assert!(names.contains(&"asym.signer"));
3261 assert!(names.contains(&"sym.box"));
3262 assert!(names.contains(&"web.cert"));
3263 assert!(
3264 !names.contains(&"web.value"),
3265 "invisible key must be filtered"
3266 );
3267 let signer = entries.iter().find(|e| e.name == "asym.signer").unwrap();
3269 assert_eq!(signer.kind, CatalogKind::Signing);
3270 assert_eq!(signer.key_type, Some(KeyType::Ed25519));
3271 assert_eq!(signer.latest_version, 5);
3273 let boxx = entries.iter().find(|e| e.name == "sym.box").unwrap();
3274 assert_eq!(boxx.kind, CatalogKind::Encryption);
3275 let cert = entries.iter().find(|e| e.name == "web.cert").unwrap();
3276 assert_eq!(cert.kind, CatalogKind::Signing);
3277 }
3278
3279 #[tokio::test]
3280 async fn list_filters_by_prefix() {
3281 let (mgr, _p, _s) = fixture();
3282 let entries = mgr.list(Some("web."), |_| true).await.expect("list");
3283 let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
3284 assert_eq!(names, vec!["web.cert", "web.value"]);
3285 }
3286
3287 #[tokio::test]
3288 async fn sign_on_value_key_is_op_not_valid_for_class() {
3289 let (mgr, _p, _s) = fixture();
3290 let err = mgr
3291 .sign("web.value", &[1])
3292 .await
3293 .expect_err("sign on a value key must be rejected");
3294 match err {
3295 ManagerError::OpNotValidForClass { op, key, class } => {
3296 assert_eq!(op, "sign");
3297 assert_eq!(key, "web.value");
3298 assert_eq!(class, Class::Value);
3299 }
3300 other => panic!("expected OpNotValidForClass, got {other:?}"),
3301 }
3302 }
3303
3304 #[tokio::test]
3305 async fn new_key_on_value_key_is_op_not_valid_for_class() {
3306 let (mgr, _p, _s) = fixture();
3307 let err = mgr
3308 .new_key("web.value", KeyType::Ed25519)
3309 .await
3310 .expect_err("new_key on a value key must be rejected");
3311 assert!(matches!(err, ManagerError::OpNotValidForClass { .. }));
3312 }
3313
3314 #[tokio::test]
3315 async fn unbacked_op_still_resolves_first_unknown_key_wins() {
3316 let (mgr, _p, _s) = fixture();
3318 assert!(matches!(
3319 mgr.encrypt("nope", AeadAlgorithm::Aes256Gcm, &[1], None)
3320 .await,
3321 Err(ManagerError::UnknownKey(_))
3322 ));
3323 }
3324
3325 #[tokio::test]
3328 async fn set_writes_value_and_get_round_trips_it() {
3329 let (mgr, primary, _s) = fixture();
3330 let version = mgr.set("web.value", b"super-secret").await.expect("set");
3332 assert_eq!(primary.kv_put_calls.load(Ordering::SeqCst), 1);
3333 assert_eq!(version, 6);
3335 assert_eq!(
3337 primary.last_path().as_deref(),
3338 Some("secret/data/web/value")
3339 );
3340
3341 let kv = mgr.get("web.value", None).await.expect("get");
3344 assert_eq!(kv.value.as_slice(), b"super-secret");
3345 assert_eq!(kv.version, 6);
3346 }
3347
3348 #[tokio::test]
3349 async fn get_reads_a_specific_version() {
3350 let (mgr, _p, _s) = fixture();
3351 let kv = mgr.get("web.value", Some(3)).await.expect("get");
3353 assert_eq!(kv.version, 3);
3354 }
3355
3356 #[tokio::test]
3357 async fn get_valid_on_public_class_key() {
3358 let (mgr, _p, secondary) = fixture();
3359 let kv = mgr.get("web.cert", None).await.expect("get on public key");
3361 assert_eq!(kv.value.as_slice(), b"stored-value");
3362 assert_eq!(
3363 secondary.last_path().as_deref(),
3364 Some("secret/data/web/cert")
3365 );
3366 }
3367
3368 #[tokio::test]
3369 async fn get_on_asymmetric_key_is_op_not_valid_for_class() {
3370 let (mgr, primary, _s) = fixture();
3371 let err = mgr
3374 .get("asym.signer", None)
3375 .await
3376 .expect_err("get on a signing key must be rejected");
3377 match err {
3378 ManagerError::OpNotValidForClass { op, key, class } => {
3379 assert_eq!(op, "get");
3380 assert_eq!(key, "asym.signer");
3381 assert_eq!(class, Class::Asymmetric);
3382 }
3383 other => panic!("expected OpNotValidForClass, got {other:?}"),
3384 }
3385 assert_eq!(primary.last_path(), None);
3387 }
3388
3389 #[tokio::test]
3390 async fn set_on_public_key_is_op_not_valid_for_class() {
3391 let (mgr, _p, secondary) = fixture();
3392 let err = mgr
3394 .set("web.cert", b"x")
3395 .await
3396 .expect_err("set on a public key must be rejected");
3397 assert!(matches!(err, ManagerError::OpNotValidForClass { .. }));
3398 assert_eq!(secondary.last_path(), None);
3399 }
3400
3401 #[tokio::test]
3402 async fn set_on_asymmetric_key_is_op_not_valid_for_class() {
3403 let (mgr, _p, _s) = fixture();
3404 let err = mgr
3405 .set("asym.signer", b"x")
3406 .await
3407 .expect_err("set on a signing key must be rejected");
3408 assert!(matches!(err, ManagerError::OpNotValidForClass { .. }));
3409 }
3410
3411 #[tokio::test]
3412 async fn get_unknown_key_wins_over_class() {
3413 let (mgr, _p, _s) = fixture();
3415 assert!(matches!(
3416 mgr.get("does.not.exist", None).await,
3417 Err(ManagerError::UnknownKey(_))
3418 ));
3419 }
3420
3421 #[tokio::test]
3424 async fn encrypt_then_decrypt_round_trips_with_aad() {
3425 let (mgr, _p, secondary) = fixture();
3426 let pt = b"top secret payload";
3427 let aad = b"context-42";
3428 let env = mgr
3429 .encrypt("sym.box", AeadAlgorithm::Aes256Gcm, pt, Some(aad))
3430 .await
3431 .expect("encrypt");
3432 assert_eq!(env.alg, AeadAlgorithm::Aes256Gcm);
3435 assert!(env.nonce.is_empty());
3436 assert_eq!(secondary.last_path().as_deref(), Some("box"));
3437
3438 let recovered = mgr
3439 .decrypt("sym.box", &env, Some(aad))
3440 .await
3441 .expect("decrypt");
3442 assert_eq!(recovered, pt);
3443 }
3444
3445 #[tokio::test]
3446 async fn decrypt_with_wrong_aad_is_opaque_decrypt_failed() {
3447 let (mgr, _p, _s) = fixture();
3448 let env = mgr
3449 .encrypt("sym.box", AeadAlgorithm::Aes256Gcm, b"x", Some(b"good"))
3450 .await
3451 .expect("encrypt");
3452 let err = mgr
3453 .decrypt("sym.box", &env, Some(b"BAD"))
3454 .await
3455 .expect_err("aad mismatch must fail");
3456 assert!(matches!(
3457 err,
3458 ManagerError::Backend(BackendError::DecryptFailed)
3459 ));
3460 }
3461
3462 #[tokio::test]
3463 async fn encrypt_algorithm_must_match_catalog_key_type() {
3464 let (mgr, _p, _s) = fixture();
3465 let err = mgr
3467 .encrypt("sym.box", AeadAlgorithm::Chacha20Poly1305, b"x", None)
3468 .await
3469 .expect_err("algorithm mismatch must be rejected");
3470 assert!(matches!(err, ManagerError::AlgorithmMismatch { .. }));
3471 }
3472
3473 #[tokio::test]
3474 async fn encrypt_on_non_symmetric_key_is_op_not_valid_for_class() {
3475 let (mgr, _p, _s) = fixture();
3476 let err = mgr
3477 .encrypt("asym.signer", AeadAlgorithm::Aes256Gcm, b"x", None)
3478 .await
3479 .expect_err("encrypt on a signing key must be rejected");
3480 assert!(matches!(err, ManagerError::OpNotValidForClass { .. }));
3481 }
3482
3483 #[tokio::test]
3484 async fn decrypt_targets_the_envelope_key_version() {
3485 let (mgr, _p, _s) = fixture();
3489 let mut env = mgr
3490 .encrypt("sym.box", AeadAlgorithm::Aes256Gcm, b"data", None)
3491 .await
3492 .expect("encrypt");
3493 env.key_version = 2; let recovered = mgr.decrypt("sym.box", &env, None).await.expect("decrypt");
3495 assert_eq!(recovered, b"data");
3496 }
3497
3498 #[tokio::test]
3501 async fn rotate_crypto_key_bumps_version_and_sets_grace_floor() {
3502 let (mgr, _p, secondary) = fixture();
3503 let limits = BrokerLimits::default();
3506 let new_version = mgr.rotate("sym.box", limits).await.expect("rotate");
3507 assert_eq!(new_version, 6);
3508 assert_eq!(secondary.rotate_calls.load(Ordering::SeqCst), 1);
3509 assert_eq!(secondary.last_versions_config(), Some((Some(5), None)));
3510 }
3511
3512 #[tokio::test]
3513 async fn rotate_grace_zero_floors_at_latest() {
3514 let (mgr, primary, _s) = fixture();
3515 let limits = BrokerLimits {
3518 grace_versions: 0,
3519 ..BrokerLimits::default()
3520 };
3521 let new_version = mgr.rotate("asym.signer", limits).await.expect("rotate");
3522 assert_eq!(new_version, 6);
3523 assert_eq!(primary.last_versions_config(), Some((Some(6), None)));
3524 }
3525
3526 #[tokio::test]
3527 async fn rotate_value_key_with_recipe_regenerates_a_new_version() {
3528 let (mgr, primary, _s) = fixture();
3529 let new_version = mgr
3532 .rotate("gen.value", BrokerLimits::default())
3533 .await
3534 .expect("rotate");
3535 assert_eq!(primary.kv_put_calls.load(Ordering::SeqCst), 1);
3536 assert_eq!(new_version, 6);
3537 let value = primary.last_kv_value().expect("a value was written");
3539 assert_eq!(value.len(), 24);
3540 assert!(value.iter().all(|b| (b'!'..=b'~').contains(b)));
3541 }
3542
3543 #[tokio::test]
3544 async fn rotate_tls_pair_key_regenerates_matching_cert_side() {
3545 if !step_available() {
3546 return;
3547 }
3548 let (mgr, primary, _s) = fixture();
3549 let new_version = mgr
3550 .rotate("tls.key", BrokerLimits::default())
3551 .await
3552 .expect("rotate tls key");
3553 assert_eq!(new_version, 6);
3554 let writes = primary.kv_put_log();
3555 assert_eq!(writes.len(), 2);
3556 assert_eq!(writes[0].0, "secret/data/tls/key");
3557 assert_eq!(writes[1].0, "secret/data/tls/cert");
3558 assert!(writes[0].1.starts_with(b"-----BEGIN"));
3559 assert!(
3560 writes[0]
3561 .1
3562 .windows(b"PRIVATE KEY".len())
3563 .any(|w| w == b"PRIVATE KEY")
3564 );
3565 assert!(writes[1].1.starts_with(b"-----BEGIN CERTIFICATE-----"));
3566 }
3567
3568 #[test]
3569 fn age_x25519_generate_recipe_emits_parseable_identity() {
3570 let value = generate_value(&GenerateSpec::AgeX25519).expect("age identity generated");
3571 let identity = String::from_utf8(value).expect("age identity is utf8");
3572 assert!(identity.starts_with("AGE-SECRET-KEY-"));
3573 let parsed = identity.parse::<age::x25519::Identity>();
3574 assert!(parsed.is_ok(), "generated identity must parse");
3575 }
3576
3577 fn step_available() -> bool {
3578 Command::new("step")
3579 .arg("version")
3580 .output()
3581 .is_ok_and(|output| output.status.success())
3582 }
3583
3584 #[tokio::test]
3585 async fn rotate_value_key_without_recipe_is_invalid_request() {
3586 let (mgr, _p, _s) = fixture();
3587 let err = mgr
3590 .rotate("web.value", BrokerLimits::default())
3591 .await
3592 .expect_err("value rotate without recipe must fail");
3593 assert!(matches!(err, ManagerError::ValueRotateNeedsSet(_)));
3594 }
3595
3596 #[tokio::test]
3597 async fn sweep_retention_raises_min_available_version() {
3598 let (mgr, _p, secondary) = fixture();
3599 let limits = BrokerLimits {
3601 retain_versions: Some(2),
3602 ..BrokerLimits::default()
3603 };
3604 mgr.sweep_retention("sym.box", limits).await.expect("sweep");
3605 assert_eq!(secondary.last_versions_config(), Some((None, Some(3))));
3606 }
3607
3608 #[tokio::test]
3609 async fn sweep_all_retention_walks_crypto_keys() {
3610 let (mgr, primary, secondary) = fixture();
3611 let limits = BrokerLimits {
3612 retain_versions: Some(2),
3613 ..BrokerLimits::default()
3614 };
3615 mgr.sweep_all_retention(limits)
3616 .await
3617 .expect("catalog sweep");
3618 assert_eq!(primary.last_versions_config(), Some((None, Some(3))));
3619 assert_eq!(secondary.last_versions_config(), Some((None, Some(3))));
3620 }
3621
3622 #[tokio::test]
3623 async fn sweep_retention_disabled_is_a_noop() {
3624 let (mgr, _p, secondary) = fixture();
3625 mgr.sweep_retention("sym.box", BrokerLimits::default())
3627 .await
3628 .expect("sweep");
3629 assert_eq!(secondary.last_versions_config(), None);
3630 }
3631
3632 fn seed_sealing_private(primary: &Arc<MockBackend>) -> [u8; 32] {
3639 let private = Zeroizing::new([0x11u8; 32]);
3640 let public = x25519_seal::public_from_private(&private);
3641 primary.seed_kv("secret/data/enroll/x25519", private.to_vec());
3642 primary.seed_kv("secret/data/enroll/x25519-public", public.to_vec());
3643 public
3644 }
3645 fn seed_ml_kem_private(primary: &Arc<MockBackend>) -> [u8; ml_kem_envelope::SEED_LEN] {
3646 let seed = [0x42; ml_kem_envelope::SEED_LEN];
3647 primary.seed_kv(
3648 "secret/data/enroll/ml-kem-768",
3649 ml_kem_record_bytes("enroll.mlkem", &seed, 5),
3650 );
3651 primary.seed_kv("secret/data/enroll/ml-kem-768-public", vec![0x7A; 1184]);
3652 seed
3653 }
3654 fn ml_kem_record_bytes(
3655 key_id: &str,
3656 seed: &[u8; ml_kem_envelope::SEED_LEN],
3657 key_version: u32,
3658 ) -> Vec<u8> {
3659 let meta = SoftwareCustodyCatalog {
3662 key_id,
3663 algorithm: ml_kem_envelope::KemAlgorithm::MlKem768.token(),
3664 provider: "local-software",
3665 provider_version: "1",
3666 custody: "software-encrypted",
3667 storage_key: "pqc/storage/wrap",
3668 };
3669 let aad = meta.aad(key_version);
3670 let mut ciphertext = Vec::with_capacity(aad.len() + seed.len() + 1);
3671 ciphertext.push(u8::try_from(aad.len()).expect("test aad fits mock envelope"));
3672 ciphertext.extend_from_slice(&aad);
3673 ciphertext.extend_from_slice(seed);
3674 serde_json::json!({
3675 "schemaVersion": 1,
3676 "keyId": key_id,
3677 "keyVersion": key_version,
3678 "publicKey": encode_record_bytes(&[0x7A; 1184]),
3679 "algorithm": meta.algorithm,
3680 "provider": meta.provider,
3681 "providerVersion": meta.provider_version,
3682 "custody": meta.custody,
3683 "encryptedPrivateKey": {
3684 "wrappingKey": meta.storage_key,
3685 "algorithm": "aes-256-gcm",
3686 "keyVersion": key_version,
3687 "nonce": encode_record_bytes(&[]),
3688 "ciphertext": encode_record_bytes(&ciphertext),
3689 }
3690 })
3691 .to_string()
3692 .into_bytes()
3693 }
3694
3695 #[tokio::test]
3696 async fn sealing_key_rejects_get_and_set() {
3697 let (mgr, _p, _s) = fixture();
3700 let get_err = mgr
3701 .get("enroll.sealing", None)
3702 .await
3703 .expect_err("get must be denied on a sealing key");
3704 assert!(matches!(
3705 get_err,
3706 ManagerError::OpNotValidForClass {
3707 op: "get",
3708 class: Class::Sealing,
3709 ..
3710 }
3711 ));
3712 let set_err = mgr
3713 .set("enroll.sealing", b"anything")
3714 .await
3715 .expect_err("set must be denied on a sealing key");
3716 assert!(matches!(
3717 set_err,
3718 ManagerError::OpNotValidForClass {
3719 op: "set",
3720 class: Class::Sealing,
3721 ..
3722 }
3723 ));
3724 }
3725
3726 #[tokio::test]
3727 async fn sealing_key_rejects_sign_and_rotate() {
3728 let (mgr, _p, _s) = fixture();
3730 assert!(matches!(
3731 mgr.sign("enroll.sealing", b"m").await,
3732 Err(ManagerError::OpNotValidForClass { op: "sign", .. })
3733 ));
3734 assert!(matches!(
3735 mgr.rotate("enroll.sealing", BrokerLimits::default()).await,
3736 Err(ManagerError::OpNotValidForClass { op: "rotate", .. })
3737 ));
3738 }
3739
3740 #[tokio::test]
3741 async fn sealing_get_public_key_reads_out_of_band_public() {
3742 let (mgr, primary, _s) = fixture();
3745 let expected_pub = seed_sealing_private(&primary);
3746 let got = mgr
3747 .sealing_public_key("enroll.sealing")
3748 .await
3749 .expect("reads public");
3750 assert_eq!(got, expected_pub);
3751 assert_eq!(
3753 primary.last_path().as_deref(),
3754 Some("secret/data/enroll/x25519-public")
3755 );
3756 }
3757
3758 #[tokio::test]
3759 async fn sealing_public_ops_never_materialize_private() {
3760 let (mgr, primary, _s) = fixture();
3765 let private = Zeroizing::new([0x11u8; 32]);
3766 let public = x25519_seal::public_from_private(&private);
3767 primary.seed_kv("secret/data/enroll/x25519-public", public.to_vec());
3768 primary.seed_kv("secret/data/enroll/x25519", vec![0xFF; 7]);
3769
3770 let got = mgr
3771 .sealing_public_key("enroll.sealing")
3772 .await
3773 .expect("public read despite garbage private");
3774 assert_eq!(got, public);
3775 mgr.wrap_envelope("enroll.sealing", b"payload", b"ctx")
3778 .await
3779 .expect("wrap reads only the public");
3780 }
3781
3782 #[tokio::test]
3783 async fn sealing_wrap_then_unwrap_round_trips() {
3784 let (mgr, primary, _s) = fixture();
3787 seed_sealing_private(&primary);
3788 let plaintext = b"enrollment-payload";
3789 let aad = b"enroll-ctx";
3790 let env = mgr
3791 .wrap_envelope("enroll.sealing", plaintext, aad)
3792 .await
3793 .expect("wrap");
3794 let recovered = mgr
3795 .unwrap_envelope("enroll.sealing", &env, aad)
3796 .await
3797 .expect("unwrap");
3798 assert_eq!(recovered.as_slice(), plaintext);
3799 }
3800
3801 #[tokio::test]
3802 async fn kv_get_secret_returns_zeroizing_bytes_and_round_trips() {
3803 let backend = MockHandle(MockBackend::new("primary"));
3808 let stored = vec![0x11u8; 32];
3809 backend
3810 .kv_put("secret/data/enroll/x25519", &stored)
3811 .await
3812 .expect("put");
3813 let got = backend
3814 .kv_get_secret("secret/data/enroll/x25519", None)
3815 .await
3816 .expect("secret read");
3817 assert_eq!(got.value.as_slice(), stored.as_slice());
3818 }
3819
3820 #[tokio::test]
3821 async fn sealing_unwrap_opens_externally_sealed_payload() {
3822 let (mgr, primary, _s) = fixture();
3826 let recipient_pub = seed_sealing_private(&primary);
3827 let env = x25519_seal::seal(&recipient_pub, b"sealed-by-sender", b"ctx").expect("seal");
3828 let recovered = mgr
3829 .unwrap_envelope("enroll.sealing", &env, b"ctx")
3830 .await
3831 .expect("unwrap");
3832 assert_eq!(recovered.as_slice(), b"sealed-by-sender");
3833 }
3834
3835 fn ml_kem_parts(env: &ml_kem_envelope::MlKemEnvelope) -> MlKemEnvelopeParts<'_> {
3838 MlKemEnvelopeParts {
3839 encapsulated_key: &env.encapsulated_key,
3840 nonce: &env.nonce,
3841 ciphertext: &env.ciphertext,
3842 }
3843 }
3844 #[tokio::test]
3845 async fn ml_kem_provider_wrap_then_unwrap_round_trips() {
3846 for envelope_algorithm in [
3847 ProviderEnvelopeAlgorithm::Aes256Gcm,
3848 ProviderEnvelopeAlgorithm::ChaCha20Poly1305,
3849 ] {
3850 let (mgr, primary, _s) = fixture();
3851 seed_ml_kem_private(&primary);
3852 let (envelope, dispatch) = mgr
3853 .provider_wrap_envelope(
3854 "enroll.mlkem",
3855 ProviderKemAlgorithm::MlKem768,
3856 envelope_algorithm,
3857 b"top secret",
3858 b"ctx",
3859 ProviderGate {
3860 local_software_allowed: true,
3861 },
3862 )
3863 .await
3864 .expect("wrap");
3865 assert_eq!(envelope.kem_algorithm, ProviderKemAlgorithm::MlKem768);
3867 assert_eq!(envelope.key_version, 5);
3868 assert_eq!(dispatch.algorithm, "ml-kem-768");
3869 assert_eq!(dispatch.provider, CryptoProviderId::LocalSoftware);
3870
3871 let (recovered, _) = mgr
3872 .provider_unwrap_envelope(
3873 "enroll.mlkem",
3874 ProviderKemAlgorithm::MlKem768,
3875 envelope_algorithm,
3876 MlKemEnvelopeParts {
3877 encapsulated_key: &envelope.encapsulated_key,
3878 nonce: &envelope.nonce,
3879 ciphertext: &envelope.ciphertext,
3880 },
3881 b"ctx",
3882 ProviderGate {
3883 local_software_allowed: true,
3884 },
3885 )
3886 .await
3887 .expect("unwrap");
3888 assert_eq!(recovered.as_slice(), b"top secret");
3889 }
3890 }
3891 #[tokio::test]
3892 async fn ml_kem_provider_unwrap_opens_externally_sealed_payload() {
3893 let (mgr, primary, _s) = fixture();
3894 let seed = seed_ml_kem_private(&primary);
3895 let env = ml_kem_envelope::seal(
3898 &seed,
3899 ml_kem_envelope::KemAlgorithm::MlKem768,
3900 ml_kem_envelope::EnvelopeAlgorithm::ChaCha20Poly1305,
3901 b"sealed-by-sender",
3902 b"ctx",
3903 )
3904 .expect("seal");
3905 let (recovered, _) = mgr
3906 .provider_unwrap_envelope(
3907 "enroll.mlkem",
3908 ProviderKemAlgorithm::MlKem768,
3909 ProviderEnvelopeAlgorithm::ChaCha20Poly1305,
3910 ml_kem_parts(&env),
3911 b"ctx",
3912 ProviderGate {
3913 local_software_allowed: true,
3914 },
3915 )
3916 .await
3917 .expect("unwrap");
3918 assert_eq!(recovered.as_slice(), b"sealed-by-sender");
3919 }
3920
3921 fn ml_kem_record_with(public: &[u8], wrapped_seed: &[u8], key_version: u32) -> Vec<u8> {
3925 let meta = SoftwareCustodyCatalog {
3926 key_id: "enroll.mlkem",
3927 algorithm: ml_kem_envelope::KemAlgorithm::MlKem768.token(),
3928 provider: "local-software",
3929 provider_version: "1",
3930 custody: "software-encrypted",
3931 storage_key: "pqc/storage/wrap",
3932 };
3933 serde_json::json!({
3934 "schemaVersion": 1,
3935 "keyId": meta.key_id,
3936 "keyVersion": key_version,
3937 "publicKey": encode_record_bytes(public),
3938 "algorithm": meta.algorithm,
3939 "provider": meta.provider,
3940 "providerVersion": meta.provider_version,
3941 "custody": meta.custody,
3942 "encryptedPrivateKey": {
3943 "wrappingKey": meta.storage_key,
3944 "algorithm": "aes-256-gcm",
3945 "keyVersion": key_version,
3946 "nonce": encode_record_bytes(&[]),
3947 "ciphertext": encode_record_bytes(wrapped_seed),
3948 }
3949 })
3950 .to_string()
3951 .into_bytes()
3952 }
3953 #[tokio::test]
3954 async fn ml_kem_sealing_get_public_key_returns_encapsulation_key_without_seed() {
3955 let (mgr, primary, _s) = fixture();
3963 let encapsulation_key = vec![0xABu8; 1184];
3964 primary.seed_kv(
3965 "secret/data/enroll/ml-kem-768",
3966 ml_kem_record_with(&encapsulation_key, &[], 5),
3969 );
3970
3971 let got = mgr
3972 .get_public_key("enroll.mlkem")
3973 .await
3974 .expect("ML-KEM sealing public-encapsulation-key read");
3975 assert_eq!(got.public_key, encapsulation_key);
3976 assert_eq!(got.key_type, KeyType::MlKem768);
3977 assert_eq!(got.version, 1);
3979 assert_eq!(
3982 primary.last_path().as_deref(),
3983 Some("secret/data/enroll/ml-kem-768")
3984 );
3985 }
3986 #[tokio::test]
3987 async fn get_public_key_rejects_non_ml_kem_sealing_and_value_classes() {
3988 let (mgr, _p, _s) = fixture();
3992 assert!(matches!(
3993 mgr.get_public_key("enroll.sealing").await,
3994 Err(ManagerError::OpNotValidForClass {
3995 op: "get_public_key",
3996 class: Class::Sealing,
3997 ..
3998 })
3999 ));
4000 assert!(matches!(
4001 mgr.get_public_key("web.value").await,
4002 Err(ManagerError::OpNotValidForClass {
4003 op: "get_public_key",
4004 class: Class::Value,
4005 ..
4006 })
4007 ));
4008 }
4009 #[tokio::test]
4010 async fn ml_kem_provider_unwrap_tampered_ciphertext_fails_opaque() {
4011 let (mgr, primary, _s) = fixture();
4012 seed_ml_kem_private(&primary);
4013 let (mut envelope, _) = mgr
4014 .provider_wrap_envelope(
4015 "enroll.mlkem",
4016 ProviderKemAlgorithm::MlKem768,
4017 ProviderEnvelopeAlgorithm::Aes256Gcm,
4018 b"top secret",
4019 b"ctx",
4020 ProviderGate {
4021 local_software_allowed: true,
4022 },
4023 )
4024 .await
4025 .expect("wrap");
4026 if let Some(b) = envelope.ciphertext.first_mut() {
4027 *b ^= 0xFF;
4028 }
4029 let err = mgr
4030 .provider_unwrap_envelope(
4031 "enroll.mlkem",
4032 ProviderKemAlgorithm::MlKem768,
4033 ProviderEnvelopeAlgorithm::Aes256Gcm,
4034 MlKemEnvelopeParts {
4035 encapsulated_key: &envelope.encapsulated_key,
4036 nonce: &envelope.nonce,
4037 ciphertext: &envelope.ciphertext,
4038 },
4039 b"ctx",
4040 ProviderGate {
4041 local_software_allowed: true,
4042 },
4043 )
4044 .await
4045 .expect_err("tampered ciphertext must fail opaque");
4046 assert!(matches!(
4047 err,
4048 ManagerError::Provider(ProviderError::CryptoFailed {
4049 op: "unwrap_envelope",
4050 ..
4051 })
4052 ));
4053 }
4054 #[tokio::test]
4055 async fn ml_kem_provider_unwrap_wrong_aad_fails_opaque() {
4056 let (mgr, primary, _s) = fixture();
4057 seed_ml_kem_private(&primary);
4058 let (envelope, _) = mgr
4059 .provider_wrap_envelope(
4060 "enroll.mlkem",
4061 ProviderKemAlgorithm::MlKem768,
4062 ProviderEnvelopeAlgorithm::ChaCha20Poly1305,
4063 b"top secret",
4064 b"right",
4065 ProviderGate {
4066 local_software_allowed: true,
4067 },
4068 )
4069 .await
4070 .expect("wrap");
4071 let err = mgr
4072 .provider_unwrap_envelope(
4073 "enroll.mlkem",
4074 ProviderKemAlgorithm::MlKem768,
4075 ProviderEnvelopeAlgorithm::ChaCha20Poly1305,
4076 MlKemEnvelopeParts {
4077 encapsulated_key: &envelope.encapsulated_key,
4078 nonce: &envelope.nonce,
4079 ciphertext: &envelope.ciphertext,
4080 },
4081 b"wrong",
4082 ProviderGate {
4083 local_software_allowed: true,
4084 },
4085 )
4086 .await
4087 .expect_err("wrong aad must fail opaque");
4088 assert!(matches!(
4089 err,
4090 ManagerError::Provider(ProviderError::CryptoFailed { .. })
4091 ));
4092 }
4093 #[tokio::test]
4094 async fn ml_kem_software_custody_rejects_raw_seed_storage() {
4095 let (mgr, primary, _s) = fixture();
4096 let seed = [0x42; ml_kem_envelope::SEED_LEN];
4097 primary.seed_kv("secret/data/enroll/ml-kem-768", seed.to_vec());
4098 let env = ml_kem_envelope::seal(
4099 &seed,
4100 ml_kem_envelope::KemAlgorithm::MlKem768,
4101 ml_kem_envelope::EnvelopeAlgorithm::ChaCha20Poly1305,
4102 b"sealed-by-sender",
4103 b"ctx",
4104 )
4105 .expect("seal");
4106 let err = mgr
4107 .provider_unwrap_envelope(
4108 "enroll.mlkem",
4109 ProviderKemAlgorithm::MlKem768,
4110 ProviderEnvelopeAlgorithm::ChaCha20Poly1305,
4111 ml_kem_parts(&env),
4112 b"ctx",
4113 ProviderGate {
4114 local_software_allowed: true,
4115 },
4116 )
4117 .await
4118 .expect_err("raw private seed storage must be rejected");
4119 assert!(matches!(
4120 err,
4121 ManagerError::Provider(ProviderError::CryptoFailed { .. })
4122 ));
4123 assert_eq!(
4124 primary.last_path().as_deref(),
4125 Some("secret/data/enroll/ml-kem-768")
4126 );
4127 }
4128 #[tokio::test]
4129 async fn ml_kem_software_custody_rejects_record_metadata_mismatch() {
4130 let (mgr, primary, _s) = fixture();
4131 let seed = [0x42; ml_kem_envelope::SEED_LEN];
4132 primary.seed_kv(
4134 "secret/data/enroll/ml-kem-768",
4135 ml_kem_record_bytes("enroll.mlkem", &seed, 4),
4136 );
4137 let env = ml_kem_envelope::seal(
4138 &seed,
4139 ml_kem_envelope::KemAlgorithm::MlKem768,
4140 ml_kem_envelope::EnvelopeAlgorithm::ChaCha20Poly1305,
4141 b"sealed-by-sender",
4142 b"ctx",
4143 )
4144 .expect("seal");
4145 let err = mgr
4146 .provider_unwrap_envelope(
4147 "enroll.mlkem",
4148 ProviderKemAlgorithm::MlKem768,
4149 ProviderEnvelopeAlgorithm::ChaCha20Poly1305,
4150 ml_kem_parts(&env),
4151 b"ctx",
4152 ProviderGate {
4153 local_software_allowed: true,
4154 },
4155 )
4156 .await
4157 .expect_err("KV version mismatch must fail before decrypt");
4158 assert!(matches!(
4159 err,
4160 ManagerError::Provider(ProviderError::CryptoFailed { .. })
4161 ));
4162 assert_eq!(
4163 primary.last_path().as_deref(),
4164 Some("secret/data/enroll/ml-kem-768")
4165 );
4166 }
4167
4168 #[tokio::test]
4169 async fn ml_kem_unwrap_rejects_wrong_key_type() {
4170 let (mgr, _primary, _s) = fixture();
4171 let seed = [0x42; ml_kem_envelope::SEED_LEN];
4174 let env = ml_kem_envelope::seal(
4175 &seed,
4176 ml_kem_envelope::KemAlgorithm::MlKem768,
4177 ml_kem_envelope::EnvelopeAlgorithm::ChaCha20Poly1305,
4178 b"sealed-by-sender",
4179 b"ctx",
4180 )
4181 .expect("seal");
4182 let err = mgr
4185 .provider_unwrap_envelope(
4186 "enroll.sealing",
4187 ProviderKemAlgorithm::MlKem768,
4188 ProviderEnvelopeAlgorithm::ChaCha20Poly1305,
4189 ml_kem_parts(&env),
4190 b"ctx",
4191 ProviderGate {
4192 local_software_allowed: true,
4193 },
4194 )
4195 .await
4196 .expect_err("x25519 key cannot unwrap ML-KEM");
4197 assert!(matches!(err, ManagerError::KemAlgorithmMismatch { .. }));
4198 }
4199
4200 #[tokio::test]
4201 async fn ml_kem_wrap_rejects_wrong_kem_param_set() {
4202 let (mgr, _primary, _s) = fixture();
4203 let err = mgr
4206 .provider_wrap_envelope(
4207 "enroll.mlkem",
4208 ProviderKemAlgorithm::MlKem512,
4209 ProviderEnvelopeAlgorithm::ChaCha20Poly1305,
4210 b"top secret",
4211 b"ctx",
4212 ProviderGate {
4213 local_software_allowed: true,
4214 },
4215 )
4216 .await
4217 .expect_err("wrong KEM param set must be rejected");
4218 assert!(matches!(err, ManagerError::KemAlgorithmMismatch { .. }));
4219 }
4220
4221 #[tokio::test]
4222 async fn ml_kem_wrap_and_unwrap_without_grant_denied() {
4223 let (mgr, _primary, _s) = fixture();
4224 let deny = ProviderGate {
4225 local_software_allowed: false,
4226 };
4227 let wrap_err = mgr
4228 .provider_wrap_envelope(
4229 "enroll.mlkem",
4230 ProviderKemAlgorithm::MlKem768,
4231 ProviderEnvelopeAlgorithm::ChaCha20Poly1305,
4232 b"top secret",
4233 b"ctx",
4234 deny,
4235 )
4236 .await
4237 .expect_err("wrap denied without local-software grant");
4238 assert!(matches!(
4239 wrap_err,
4240 ManagerError::Provider(ProviderError::PolicyDenied { .. })
4241 ));
4242 let unwrap_err = mgr
4243 .provider_unwrap_envelope(
4244 "enroll.mlkem",
4245 ProviderKemAlgorithm::MlKem768,
4246 ProviderEnvelopeAlgorithm::ChaCha20Poly1305,
4247 MlKemEnvelopeParts {
4248 encapsulated_key: b"x",
4249 nonce: &[0u8; 12],
4250 ciphertext: b"y",
4251 },
4252 b"ctx",
4253 deny,
4254 )
4255 .await
4256 .expect_err("unwrap denied without local-software grant");
4257 assert!(matches!(
4258 unwrap_err,
4259 ManagerError::Provider(ProviderError::PolicyDenied { .. })
4260 ));
4261 }
4262
4263 #[tokio::test]
4264 async fn sealing_unwrap_wrong_aad_fails_opaque() {
4265 let (mgr, primary, _s) = fixture();
4266 let recipient_pub = seed_sealing_private(&primary);
4267 let env = x25519_seal::seal(&recipient_pub, b"x", b"right").expect("seal");
4268 let err = mgr
4269 .unwrap_envelope("enroll.sealing", &env, b"wrong")
4270 .await
4271 .expect_err("aad mismatch must fail");
4272 assert!(matches!(
4273 err,
4274 ManagerError::Sealing(SealingFailure::OpenFailed)
4275 ));
4276 }
4277
4278 #[tokio::test]
4279 async fn sealing_unwrap_tampered_ciphertext_fails_opaque() {
4280 let (mgr, primary, _s) = fixture();
4281 let recipient_pub = seed_sealing_private(&primary);
4282 let mut env = x25519_seal::seal(&recipient_pub, b"payload", b"aad").expect("seal");
4283 if let Some(b) = env.ciphertext.first_mut() {
4284 *b ^= 0xFF;
4285 }
4286 assert!(matches!(
4287 mgr.unwrap_envelope("enroll.sealing", &env, b"aad").await,
4288 Err(ManagerError::Sealing(SealingFailure::OpenFailed))
4289 ));
4290 }
4291
4292 #[tokio::test]
4293 async fn sealing_unwrap_malformed_private_fails() {
4294 let (mgr, primary, _s) = fixture();
4297 *primary.last_kv_value.lock().unwrap() = Some(vec![0u8; 16]);
4298 let env = SealedEnvelope {
4299 encapsulated_key: [9u8; 32],
4300 nonce: [0u8; 12],
4301 ciphertext: vec![1, 2, 3],
4302 };
4303 assert!(matches!(
4304 mgr.unwrap_envelope("enroll.sealing", &env, b"").await,
4305 Err(ManagerError::Sealing(SealingFailure::Malformed))
4306 ));
4307 }
4308
4309 #[tokio::test]
4310 async fn wrap_unwrap_reject_non_sealing_key() {
4311 let (mgr, _p, _s) = fixture();
4314 assert!(matches!(
4315 mgr.wrap_envelope("sym.box", b"x", b"").await,
4316 Err(ManagerError::OpNotValidForClass {
4317 op: "wrap_envelope",
4318 ..
4319 })
4320 ));
4321 let env = SealedEnvelope {
4322 encapsulated_key: [0u8; 32],
4323 nonce: [0u8; 12],
4324 ciphertext: vec![],
4325 };
4326 assert!(matches!(
4327 mgr.unwrap_envelope("sym.box", &env, b"").await,
4328 Err(ManagerError::OpNotValidForClass {
4329 op: "unwrap_envelope",
4330 ..
4331 })
4332 ));
4333 }
4334
4335 fn seed_signing_seed(primary: &Arc<MockBackend>) -> [u8; 32] {
4342 let seed = Zeroizing::new([0x11u8; 32]);
4343 let public = ed25519_sign::public_from_seed(&seed);
4344 primary.seed_kv("secret/data/kv2/signer", seed.to_vec());
4345 primary.seed_kv("secret/data/kv2/signer-public", public.to_vec());
4346 public
4347 }
4348
4349 #[tokio::test]
4350 async fn kv2_signer_resolves_as_asymmetric_kv2() {
4351 let (mgr, _p, _s) = fixture();
4352 let routed = mgr.resolve("kv2.signer").expect("resolves");
4353 assert_eq!(routed.class(), Class::Asymmetric);
4354 assert_eq!(routed.engine, Engine::Kv2);
4355 assert_eq!(routed.key_type(), Some(KeyAlgorithm::Ed25519));
4356 }
4357
4358 #[tokio::test]
4359 async fn kv2_signer_rejects_get_and_set() {
4360 let (mgr, _p, _s) = fixture();
4365 let get_err = mgr
4366 .get("kv2.signer", None)
4367 .await
4368 .expect_err("get must be denied on a value-store signing key");
4369 assert!(matches!(
4370 get_err,
4371 ManagerError::OpNotValidForClass {
4372 op: "get",
4373 class: Class::Asymmetric,
4374 ..
4375 }
4376 ));
4377 let set_err = mgr
4378 .set("kv2.signer", b"anything")
4379 .await
4380 .expect_err("set must be denied on a value-store signing key");
4381 assert!(matches!(
4382 set_err,
4383 ManagerError::OpNotValidForClass {
4384 op: "set",
4385 class: Class::Asymmetric,
4386 ..
4387 }
4388 ));
4389 }
4390
4391 #[tokio::test]
4392 async fn kv2_signer_rejects_rotate_and_import() {
4393 let (mgr, _p, _s) = fixture();
4398 assert!(matches!(
4399 mgr.rotate("kv2.signer", BrokerLimits::default()).await,
4400 Err(ManagerError::Unsupported(m)) if m.starts_with("rotate")
4401 ));
4402 assert!(matches!(
4403 mgr.import(
4404 "kv2.signer",
4405 KeyType::Ed25519,
4406 &KeyMaterial::Ed25519Seed(vec![0; 32]),
4407 )
4408 .await,
4409 Err(ManagerError::Unsupported(m)) if m.starts_with("import")
4410 ));
4411 }
4412
4413 #[tokio::test]
4414 async fn kv2_signer_materializes_and_signs_matching_in_proc() {
4415 let (mgr, primary, _s) = fixture();
4418 let public = seed_signing_seed(&primary);
4419 let message = b"sign me with a materialized seed";
4420
4421 let sig = mgr.sign("kv2.signer", message).await.expect("sign");
4422 assert_eq!(sig.len(), ed25519_sign::SIGNATURE_LEN);
4423 let expected = ed25519_sign::sign(&Zeroizing::new([0x11u8; 32]), message);
4425 assert_eq!(sig.as_slice(), expected.as_slice());
4426 assert_eq!(ed25519_sign::verify(&public, message, &sig), Ok(true));
4428 assert_eq!(
4431 primary.last_path().as_deref(),
4432 Some("secret/data/kv2/signer")
4433 );
4434 }
4435
4436 #[tokio::test]
4437 async fn kv2_signer_verify_round_trips_in_proc() {
4438 let (mgr, primary, _s) = fixture();
4439 seed_signing_seed(&primary);
4440 let message = b"verify me";
4441 let sig = mgr.sign("kv2.signer", message).await.expect("sign");
4442 assert!(
4443 mgr.verify("kv2.signer", message, &sig)
4444 .await
4445 .expect("verify")
4446 );
4447 assert!(
4449 !mgr.verify("kv2.signer", b"verify-tampered", &sig)
4450 .await
4451 .expect("verify")
4452 );
4453 }
4454
4455 #[tokio::test]
4456 async fn kv2_signer_get_public_key_reads_out_of_band_public() {
4457 let (mgr, primary, _s) = fixture();
4460 let expected_pub = seed_signing_seed(&primary);
4461 let pk = mgr.get_public_key("kv2.signer").await.expect("public");
4462 assert_eq!(pk.public_key.as_slice(), expected_pub.as_slice());
4463 assert_eq!(pk.key_type, KeyType::Ed25519);
4464 assert_eq!(
4466 primary.last_path().as_deref(),
4467 Some("secret/data/kv2/signer-public")
4468 );
4469 }
4470
4471 #[tokio::test]
4472 async fn kv2_signer_public_ops_never_materialize_seed() {
4473 let (mgr, primary, _s) = fixture();
4479 let seed = Zeroizing::new([0x11u8; 32]);
4480 let public = ed25519_sign::public_from_seed(&seed);
4481 primary.seed_kv("secret/data/kv2/signer-public", public.to_vec());
4482 primary.seed_kv("secret/data/kv2/signer", vec![0xFF; 7]);
4483
4484 let pk = mgr
4485 .get_public_key("kv2.signer")
4486 .await
4487 .expect("public read despite garbage seed");
4488 assert_eq!(pk.public_key.as_slice(), public.as_slice());
4489 let sig = ed25519_sign::sign(&seed, b"m");
4492 assert!(
4493 mgr.verify("kv2.signer", b"m", &sig)
4494 .await
4495 .expect("verify reads only the public")
4496 );
4497 }
4498
4499 #[tokio::test]
4500 async fn materialize_public_op_without_public_path_fails_closed() {
4501 const NO_PUB: &str = r#"{
4506 "schemaVersion": 1,
4507 "backends": { "primary": { "kind": "vault", "addr": "https://127.0.0.1:8200" } },
4508 "keys": {
4509 "enroll.sealing": {
4510 "class": "sealing", "keyType": "x25519", "backend": "primary", "engine": "kv2",
4511 "path": "secret/data/enroll/x25519", "writable": true, "missing": "error",
4512 "description": "a sealing key missing its publicPath"
4513 }
4514 }
4515 }"#;
4516 let primary = MockBackend::new("primary");
4517 let mut backends: BTreeMap<String, Box<dyn Backend>> = BTreeMap::new();
4518 backends.insert("primary".into(), Box::new(MockHandle(primary)));
4519 let mgr = BackendManager::new(parse_catalog(NO_PUB), backends).expect("constructs");
4520
4521 assert!(matches!(
4522 mgr.sealing_public_key("enroll.sealing").await,
4523 Err(ManagerError::MissingPublicPath(k)) if k == "enroll.sealing"
4524 ));
4525 assert!(matches!(
4526 mgr.wrap_envelope("enroll.sealing", b"x", b"a").await,
4527 Err(ManagerError::MissingPublicPath(_))
4528 ));
4529 }
4530
4531 #[tokio::test]
4532 async fn kv2_signer_malformed_seed_is_signing_failure_not_panic() {
4533 let (mgr, primary, _s) = fixture();
4536 *primary.last_kv_value.lock().unwrap() = Some(vec![0u8; 16]);
4537 assert!(matches!(
4538 mgr.sign("kv2.signer", b"m").await,
4539 Err(ManagerError::Signing(SigningFailure::Malformed))
4540 ));
4541 }
4542
4543 #[tokio::test]
4544 async fn kv2_signer_verify_rejects_wrong_length_signature_opaquely() {
4545 let (mgr, primary, _s) = fixture();
4548 seed_signing_seed(&primary);
4549 assert!(matches!(
4550 mgr.verify("kv2.signer", b"m", &[0u8; 10]).await,
4551 Err(ManagerError::Signing(SigningFailure::Malformed))
4552 ));
4553 }
4554
4555 #[tokio::test]
4556 async fn transit_signer_still_uses_backend_sign_in_place() {
4557 let (mgr, primary, _s) = fixture();
4560 let sig = mgr.sign("asym.signer", b"m").await.expect("sign");
4561 assert_eq!(sig, vec![0xAB, 0xCD]);
4564 assert_eq!(primary.last_path().as_deref(), Some("signer"));
4565 }
4566}
4567
4568#[cfg(test)]
4569mod pqc_dispatch_tests {
4570 use std::collections::{BTreeMap, HashMap};
4571 use std::sync::Mutex;
4572
4573 use async_trait::async_trait;
4574
4575 use super::{
4576 BackendManager, Catalog, CryptoProviderId, CustodyMode, KeyProviderDescriptor,
4577 ManagerError, ProviderError, ProviderGate, ProviderPolicy,
4578 };
4579 use crate::backend::{Backend, BackendError, KvValue, NativeAlgorithm, NewKey};
4580 use crate::core::crypto_provider::{ProviderAuditEvent, ProviderAuditOutcome};
4581 use crate::core::ml_dsa_sign::{self, MlDsaAlgorithm};
4582 use basil_proto::{AeadAlgorithm, CiphertextEnvelope, KeyType};
4583
4584 #[derive(Default)]
4589 struct PqcBackend {
4590 store: Mutex<HashMap<String, Vec<u8>>>,
4591 }
4592
4593 #[async_trait]
4594 impl Backend for PqcBackend {
4595 fn kind(&self) -> &'static str {
4596 "pqc-dispatch-test"
4597 }
4598
4599 async fn new_key(&self, _key_type: KeyType) -> Result<NewKey, BackendError> {
4600 Err(BackendError::Unsupported("new_key"))
4601 }
4602
4603 async fn public_key(&self, _key_id: &str) -> Result<Vec<u8>, BackendError> {
4604 Err(BackendError::Unsupported("public_key"))
4605 }
4606
4607 async fn sign(&self, _key_id: &str, _message: &[u8]) -> Result<Vec<u8>, BackendError> {
4608 Err(BackendError::Unsupported("sign"))
4609 }
4610
4611 async fn verify(
4612 &self,
4613 _key_id: &str,
4614 _message: &[u8],
4615 _signature: &[u8],
4616 ) -> Result<bool, BackendError> {
4617 Err(BackendError::Unsupported("verify"))
4618 }
4619
4620 async fn encrypt(
4621 &self,
4622 _key_id: &str,
4623 algorithm: AeadAlgorithm,
4624 plaintext: &[u8],
4625 aad: Option<&[u8]>,
4626 ) -> Result<CiphertextEnvelope, BackendError> {
4627 let aad = aad.unwrap_or(&[]);
4628 let mut ciphertext = vec![u8::try_from(aad.len()).unwrap_or(u8::MAX)];
4629 ciphertext.extend_from_slice(aad);
4630 ciphertext.extend_from_slice(plaintext);
4631 Ok(CiphertextEnvelope {
4632 alg: algorithm,
4633 key_version: 1,
4634 nonce: Vec::new(),
4635 ciphertext,
4636 })
4637 }
4638
4639 async fn decrypt(
4640 &self,
4641 _key_id: &str,
4642 envelope: &CiphertextEnvelope,
4643 aad: Option<&[u8]>,
4644 ) -> Result<Vec<u8>, BackendError> {
4645 let aad = aad.unwrap_or(&[]);
4646 let ct = &envelope.ciphertext;
4647 let aad_len = *ct.first().ok_or(BackendError::DecryptFailed)? as usize;
4648 let bound = ct.get(1..1 + aad_len).ok_or(BackendError::DecryptFailed)?;
4649 if bound != aad {
4650 return Err(BackendError::DecryptFailed);
4651 }
4652 Ok(ct
4653 .get(1 + aad_len..)
4654 .ok_or(BackendError::DecryptFailed)?
4655 .to_vec())
4656 }
4657
4658 async fn kv_put(&self, key_id: &str, value: &[u8]) -> Result<u32, BackendError> {
4659 self.store
4660 .lock()
4661 .map_err(|_| BackendError::Unsupported("kv_put"))?
4662 .insert(key_id.to_string(), value.to_vec());
4663 Ok(1)
4664 }
4665
4666 async fn kv_get(
4667 &self,
4668 key_id: &str,
4669 _version: Option<u32>,
4670 ) -> Result<KvValue, BackendError> {
4671 let value = self
4672 .store
4673 .lock()
4674 .map_err(|_| BackendError::Unsupported("kv_get"))?
4675 .get(key_id)
4676 .cloned();
4677 value
4678 .map(|value| KvValue { value, version: 1 })
4679 .ok_or(BackendError::Unsupported("kv_get"))
4680 }
4681 }
4682
4683 #[derive(Default)]
4692 struct NativePqcBackend {
4693 seeds: Mutex<HashMap<String, [u8; 32]>>,
4694 store: Mutex<HashMap<String, Vec<u8>>>,
4695 }
4696
4697 impl NativePqcBackend {
4698 const fn dsa(native: NativeAlgorithm) -> MlDsaAlgorithm {
4699 match native {
4700 NativeAlgorithm::MlDsa44 => MlDsaAlgorithm::MlDsa44,
4701 NativeAlgorithm::MlDsa65 => MlDsaAlgorithm::MlDsa65,
4702 NativeAlgorithm::MlDsa87 => MlDsaAlgorithm::MlDsa87,
4703 }
4704 }
4705
4706 fn seed_for(key_id: &str) -> [u8; 32] {
4709 let mut seed = [7u8; 32];
4710 for (slot, byte) in seed.iter_mut().zip(key_id.bytes()) {
4711 *slot = byte;
4712 }
4713 seed
4714 }
4715 }
4716
4717 #[async_trait]
4718 impl Backend for NativePqcBackend {
4719 fn kind(&self) -> &'static str {
4720 "pqc-native-test"
4721 }
4722
4723 async fn new_key(&self, _key_type: KeyType) -> Result<NewKey, BackendError> {
4724 Err(BackendError::Unsupported("new_key"))
4725 }
4726
4727 async fn public_key(&self, _key_id: &str) -> Result<Vec<u8>, BackendError> {
4728 Err(BackendError::Unsupported("public_key"))
4729 }
4730
4731 async fn sign(&self, _key_id: &str, _message: &[u8]) -> Result<Vec<u8>, BackendError> {
4732 Err(BackendError::Unsupported("sign"))
4733 }
4734
4735 async fn verify(
4736 &self,
4737 _key_id: &str,
4738 _message: &[u8],
4739 _signature: &[u8],
4740 ) -> Result<bool, BackendError> {
4741 Err(BackendError::Unsupported("verify"))
4742 }
4743
4744 fn supports_native_algorithm(&self, algorithm: NativeAlgorithm) -> bool {
4745 matches!(
4746 algorithm,
4747 NativeAlgorithm::MlDsa44 | NativeAlgorithm::MlDsa65 | NativeAlgorithm::MlDsa87
4748 )
4749 }
4750
4751 async fn create_named_pqc_key(
4752 &self,
4753 key_id: &str,
4754 algorithm: NativeAlgorithm,
4755 ) -> Result<NewKey, BackendError> {
4756 let seed = Self::seed_for(key_id);
4757 let public = ml_dsa_sign::public_from_seed(Self::dsa(algorithm), &seed)
4758 .map_err(|_| BackendError::Unsupported("create_named_pqc_key"))?;
4759 self.seeds
4760 .lock()
4761 .map_err(|_| BackendError::Unsupported("create_named_pqc_key"))?
4762 .insert(key_id.to_string(), seed);
4763 Ok(NewKey {
4764 key_id: key_id.to_string(),
4765 public_key: public,
4766 })
4767 }
4768
4769 async fn sign_pqc(
4770 &self,
4771 key_id: &str,
4772 message: &[u8],
4773 algorithm: NativeAlgorithm,
4774 ) -> Result<Vec<u8>, BackendError> {
4775 let seed = self
4776 .seeds
4777 .lock()
4778 .map_err(|_| BackendError::Unsupported("sign_pqc"))?
4779 .get(key_id)
4780 .copied()
4781 .ok_or_else(|| BackendError::KeyNotFound(key_id.to_string()))?;
4782 ml_dsa_sign::sign(Self::dsa(algorithm), &seed, message)
4783 .map_err(|_| BackendError::Unsupported("sign_pqc"))
4784 }
4785
4786 async fn verify_pqc(
4787 &self,
4788 key_id: &str,
4789 message: &[u8],
4790 signature: &[u8],
4791 algorithm: NativeAlgorithm,
4792 ) -> Result<bool, BackendError> {
4793 let seed = self
4794 .seeds
4795 .lock()
4796 .map_err(|_| BackendError::Unsupported("verify_pqc"))?
4797 .get(key_id)
4798 .copied()
4799 .ok_or_else(|| BackendError::KeyNotFound(key_id.to_string()))?;
4800 let public = ml_dsa_sign::public_from_seed(Self::dsa(algorithm), &seed)
4801 .map_err(|_| BackendError::Unsupported("verify_pqc"))?;
4802 ml_dsa_sign::verify(Self::dsa(algorithm), &public, message, signature)
4803 .map_err(|_| BackendError::Unsupported("verify_pqc"))
4804 }
4805
4806 async fn encrypt(
4807 &self,
4808 _key_id: &str,
4809 algorithm: AeadAlgorithm,
4810 plaintext: &[u8],
4811 aad: Option<&[u8]>,
4812 ) -> Result<CiphertextEnvelope, BackendError> {
4813 let aad = aad.unwrap_or(&[]);
4814 let mut ciphertext = vec![u8::try_from(aad.len()).unwrap_or(u8::MAX)];
4815 ciphertext.extend_from_slice(aad);
4816 ciphertext.extend_from_slice(plaintext);
4817 Ok(CiphertextEnvelope {
4818 alg: algorithm,
4819 key_version: 1,
4820 nonce: Vec::new(),
4821 ciphertext,
4822 })
4823 }
4824
4825 async fn decrypt(
4826 &self,
4827 _key_id: &str,
4828 envelope: &CiphertextEnvelope,
4829 aad: Option<&[u8]>,
4830 ) -> Result<Vec<u8>, BackendError> {
4831 let aad = aad.unwrap_or(&[]);
4832 let ct = &envelope.ciphertext;
4833 let aad_len = *ct.first().ok_or(BackendError::DecryptFailed)? as usize;
4834 let bound = ct.get(1..1 + aad_len).ok_or(BackendError::DecryptFailed)?;
4835 if bound != aad {
4836 return Err(BackendError::DecryptFailed);
4837 }
4838 Ok(ct
4839 .get(1 + aad_len..)
4840 .ok_or(BackendError::DecryptFailed)?
4841 .to_vec())
4842 }
4843
4844 async fn kv_put(&self, key_id: &str, value: &[u8]) -> Result<u32, BackendError> {
4845 self.store
4846 .lock()
4847 .map_err(|_| BackendError::Unsupported("kv_put"))?
4848 .insert(key_id.to_string(), value.to_vec());
4849 Ok(1)
4850 }
4851
4852 async fn kv_get(
4853 &self,
4854 key_id: &str,
4855 _version: Option<u32>,
4856 ) -> Result<KvValue, BackendError> {
4857 let value = self
4858 .store
4859 .lock()
4860 .map_err(|_| BackendError::Unsupported("kv_get"))?
4861 .get(key_id)
4862 .cloned();
4863 value
4864 .map(|value| KvValue { value, version: 1 })
4865 .ok_or(BackendError::Unsupported("kv_get"))
4866 }
4867 }
4868
4869 const PQC_CATALOG: &str = r#"{
4870 "schemaVersion": 1,
4871 "backends": { "primary": { "kind": "vault", "addr": "https://127.0.0.1:8200" } },
4872 "keys": {
4873 "pqc.signer44": {
4874 "class": "asymmetric", "keyType": "ml-dsa-44", "backend": "primary",
4875 "path": "secret/data/pqc/44", "writable": true, "missing": "error",
4876 "labels": ["crypto_provider=local-software", "crypto_provider_policy=local-software",
4877 "pqc_custody=software-encrypted", "pqc_storage_key=pqc/aead",
4878 "crypto_provider_version=1"],
4879 "description": "ml-dsa-44 software-custodied signer"
4880 },
4881 "pqc.signer65": {
4882 "class": "asymmetric", "keyType": "ml-dsa-65", "backend": "primary",
4883 "path": "secret/data/pqc/65", "writable": true, "missing": "error",
4884 "labels": ["crypto_provider=local-software", "crypto_provider_policy=local-software",
4885 "pqc_custody=software-encrypted", "pqc_storage_key=pqc/aead",
4886 "crypto_provider_version=1"],
4887 "description": "ml-dsa-65 software-custodied signer"
4888 },
4889 "pqc.signer87": {
4890 "class": "asymmetric", "keyType": "ml-dsa-87", "backend": "primary",
4891 "path": "secret/data/pqc/87", "writable": true, "missing": "error",
4892 "labels": ["crypto_provider=local-software", "crypto_provider_policy=local-software",
4893 "pqc_custody=software-encrypted", "pqc_storage_key=pqc/aead",
4894 "crypto_provider_version=1"],
4895 "description": "ml-dsa-87 software-custodied signer"
4896 },
4897 "pqc.backendreq": {
4898 "class": "asymmetric", "keyType": "ml-dsa-65", "backend": "primary",
4899 "path": "secret/data/pqc/breq", "writable": true, "missing": "error",
4900 "labels": ["crypto_provider_policy=backend-required", "pqc_custody=software-encrypted"],
4901 "description": "backend-required ml-dsa signer (no native provider)"
4902 },
4903 "pqc.preferred_native": {
4904 "class": "asymmetric", "keyType": "ml-dsa-65", "backend": "primary",
4905 "path": "pqc/native65", "writable": true, "missing": "error",
4906 "labels": ["crypto_provider_policy=backend-preferred", "crypto_provider_version=1"],
4907 "description": "backend-preferred ml-dsa signer (native when the backend supports it)"
4908 },
4909 "pqc.preferred_software": {
4910 "class": "asymmetric", "keyType": "ml-dsa-65", "backend": "primary",
4911 "path": "secret/data/pqc/pref-sw", "writable": true, "missing": "error",
4912 "labels": ["crypto_provider_policy=backend-preferred", "pqc_custody=software-encrypted",
4913 "pqc_storage_key=pqc/aead", "crypto_provider_version=1"],
4914 "description": "backend-preferred ml-dsa signer pinned to software custody"
4915 },
4916 "pqc.backendreq_native": {
4917 "class": "asymmetric", "keyType": "ml-dsa-65", "backend": "primary",
4918 "path": "pqc/breq-native", "writable": true, "missing": "error",
4919 "labels": ["crypto_provider_policy=backend-required", "crypto_provider_version=1"],
4920 "description": "backend-required ml-dsa signer served by a native backend"
4921 }
4922 }
4923 }"#;
4924
4925 fn manager() -> BackendManager {
4926 let catalog: Catalog = serde_json::from_str(PQC_CATALOG).expect("catalog parses");
4927 let mut backends: BTreeMap<String, Box<dyn Backend>> = BTreeMap::new();
4928 backends.insert("primary".into(), Box::new(PqcBackend::default()));
4929 BackendManager::new(catalog, backends).expect("manager builds")
4930 }
4931
4932 fn native_manager() -> BackendManager {
4935 let catalog: Catalog = serde_json::from_str(PQC_CATALOG).expect("catalog parses");
4936 let mut backends: BTreeMap<String, Box<dyn Backend>> = BTreeMap::new();
4937 backends.insert("primary".into(), Box::new(NativePqcBackend::default()));
4938 BackendManager::new(catalog, backends).expect("manager builds")
4939 }
4940
4941 const fn allowed() -> ProviderGate {
4942 ProviderGate {
4943 local_software_allowed: true,
4944 }
4945 }
4946
4947 #[tokio::test]
4948 async fn generate_sign_verify_round_trip_all_levels() {
4949 let cases = [
4950 ("pqc.signer44", MlDsaAlgorithm::MlDsa44, "ml-dsa-44"),
4951 ("pqc.signer65", MlDsaAlgorithm::MlDsa65, "ml-dsa-65"),
4952 ("pqc.signer87", MlDsaAlgorithm::MlDsa87, "ml-dsa-87"),
4953 ];
4954 for (key_id, dsa, token) in cases {
4955 let mgr = manager();
4956 let message = b"basil ml-dsa dispatch";
4957
4958 let (created, gen_dispatch) = mgr
4959 .provider_generate(key_id, allowed())
4960 .await
4961 .expect("generate");
4962 assert_eq!(gen_dispatch.provider, CryptoProviderId::LocalSoftware);
4963 assert_eq!(gen_dispatch.algorithm, token);
4964 assert!(!created.public_key.is_empty(), "{token} public");
4965
4966 let (signature, sign_dispatch) = mgr
4967 .provider_sign(key_id, message, allowed())
4968 .await
4969 .expect("sign");
4970 assert_eq!(sign_dispatch.provider, CryptoProviderId::LocalSoftware);
4971 assert_eq!(sign_dispatch.algorithm, token);
4972 assert_eq!(sign_dispatch.custody, CustodyMode::SoftwareEncrypted);
4973
4974 let (valid, _) = mgr
4975 .provider_verify(key_id, message, &signature, allowed())
4976 .await
4977 .expect("verify");
4978 assert!(valid, "{token} verifies through the broker");
4979
4980 assert!(
4982 ml_dsa_sign::verify(dsa, &created.public_key, message, &signature).expect("core"),
4983 "{token} verifies under returned public",
4984 );
4985
4986 let (bad, _) = mgr
4988 .provider_verify(key_id, b"tampered", &signature, allowed())
4989 .await
4990 .expect("verify");
4991 assert!(!bad, "{token} rejects a tampered message");
4992 }
4993 }
4994
4995 #[tokio::test]
4996 async fn backend_required_ml_dsa_fails_closed() {
4997 let mgr = manager();
4998 let err = mgr
5001 .provider_sign("pqc.backendreq", b"m", allowed())
5002 .await
5003 .expect_err("backend-required ml-dsa is unsupported");
5004 assert!(matches!(
5005 err,
5006 ManagerError::Provider(ProviderError::Unsupported { .. })
5007 ));
5008 let gen_err = mgr
5009 .provider_generate("pqc.backendreq", allowed())
5010 .await
5011 .expect_err("backend-required ml-dsa generate is unsupported");
5012 assert!(matches!(
5013 gen_err,
5014 ManagerError::Provider(ProviderError::Unsupported { .. })
5015 ));
5016 }
5017
5018 #[tokio::test]
5019 async fn local_software_without_policy_grant_is_denied() {
5020 let mgr = manager();
5021 let denied = ProviderGate {
5022 local_software_allowed: false,
5023 };
5024 for outcome in [
5025 mgr.provider_sign("pqc.signer65", b"m", denied).await,
5026 mgr.provider_generate("pqc.signer65", denied)
5027 .await
5028 .map(|(key, dispatch)| (key.public_key, dispatch)),
5029 ] {
5030 let err = outcome.expect_err("local software requires an explicit policy grant");
5031 assert!(matches!(
5032 err,
5033 ManagerError::Provider(ProviderError::PolicyDenied { .. })
5034 ));
5035 }
5036 }
5037
5038 #[tokio::test]
5039 async fn dispatch_feeds_a_secret_free_provider_audit_event() {
5040 let mgr = manager();
5041 mgr.provider_generate("pqc.signer87", allowed())
5042 .await
5043 .expect("generate");
5044 let (_signature, dispatch) = mgr
5045 .provider_sign("pqc.signer87", b"m", allowed())
5046 .await
5047 .expect("sign");
5048 let event = ProviderAuditEvent {
5051 op: "sign",
5052 key_id: "pqc.signer87",
5053 key_version: None,
5054 algorithm: dispatch.algorithm,
5055 provider: dispatch.provider,
5056 custody: dispatch.custody,
5057 caller_uid: 4242,
5058 outcome: ProviderAuditOutcome::Success,
5059 reason: "ok",
5060 };
5061 let value = event.to_json_value();
5062 assert_eq!(value["provider"], "local-software");
5063 assert_eq!(value["algorithm"], "ml-dsa-87");
5064 assert_eq!(value["op"], "sign");
5065 assert_eq!(value["caller_uid"], 4242);
5066 assert!(value.get("signature").is_none());
5067 assert!(value.get("private_key").is_none());
5068 }
5069
5070 #[tokio::test]
5071 async fn backend_preferred_routes_to_native_when_backend_supports_it() {
5072 let mgr = native_manager();
5076 let key_id = "pqc.preferred_native";
5077 let message = b"basil native ml-dsa";
5078
5079 let (created, gen_dispatch) = mgr
5080 .provider_generate(key_id, allowed())
5081 .await
5082 .expect("generate");
5083 assert_eq!(gen_dispatch.provider, CryptoProviderId::VaultTransit);
5084 assert_eq!(gen_dispatch.custody, CustodyMode::BackendNative);
5085 assert!(!created.public_key.is_empty(), "native public");
5086
5087 let (signature, sign_dispatch) = mgr
5088 .provider_sign(key_id, message, allowed())
5089 .await
5090 .expect("sign");
5091 assert_eq!(sign_dispatch.provider, CryptoProviderId::VaultTransit);
5092 assert_eq!(sign_dispatch.custody, CustodyMode::BackendNative);
5093
5094 let (valid, _) = mgr
5095 .provider_verify(key_id, message, &signature, allowed())
5096 .await
5097 .expect("verify");
5098 assert!(valid, "native signature verifies through the broker");
5099
5100 let (bad, _) = mgr
5101 .provider_verify(key_id, b"tampered", &signature, allowed())
5102 .await
5103 .expect("verify");
5104 assert!(!bad, "native verify rejects a tampered message");
5105 }
5106
5107 #[tokio::test]
5108 async fn backend_required_routes_to_native_when_backend_supports_it() {
5109 let mgr = native_manager();
5113 let key_id = "pqc.backendreq_native";
5114
5115 let (_created, gen_dispatch) = mgr
5116 .provider_generate(key_id, allowed())
5117 .await
5118 .expect("generate");
5119 assert_eq!(gen_dispatch.provider, CryptoProviderId::VaultTransit);
5120
5121 let (signature, sign_dispatch) = mgr
5122 .provider_sign(key_id, b"m", allowed())
5123 .await
5124 .expect("sign");
5125 assert_eq!(sign_dispatch.provider, CryptoProviderId::VaultTransit);
5126 let (valid, _) = mgr
5127 .provider_verify(key_id, b"m", &signature, allowed())
5128 .await
5129 .expect("verify");
5130 assert!(valid);
5131 }
5132
5133 #[tokio::test]
5134 async fn backend_preferred_falls_back_to_software_without_native_support() {
5135 let mgr = manager();
5138 let key_id = "pqc.preferred_software";
5139
5140 let (_created, gen_dispatch) = mgr
5141 .provider_generate(key_id, allowed())
5142 .await
5143 .expect("generate");
5144 assert_eq!(gen_dispatch.provider, CryptoProviderId::LocalSoftware);
5145 assert_eq!(gen_dispatch.custody, CustodyMode::SoftwareEncrypted);
5146
5147 let (signature, sign_dispatch) = mgr
5148 .provider_sign(key_id, b"m", allowed())
5149 .await
5150 .expect("sign");
5151 assert_eq!(sign_dispatch.provider, CryptoProviderId::LocalSoftware);
5152 let (valid, _) = mgr
5153 .provider_verify(key_id, b"m", &signature, allowed())
5154 .await
5155 .expect("verify");
5156 assert!(valid);
5157 }
5158
5159 #[tokio::test]
5160 async fn software_custodied_key_is_not_rerouted_by_native_probe() {
5161 let mgr = native_manager();
5166 let key_id = "pqc.preferred_software";
5167
5168 let (_created, gen_dispatch) = mgr
5169 .provider_generate(key_id, allowed())
5170 .await
5171 .expect("generate");
5172 assert_eq!(
5173 gen_dispatch.provider,
5174 CryptoProviderId::LocalSoftware,
5175 "software-custodied key stays local-software despite native support",
5176 );
5177
5178 let (_signature, sign_dispatch) = mgr
5179 .provider_sign(key_id, b"m", allowed())
5180 .await
5181 .expect("sign");
5182 assert_eq!(sign_dispatch.provider, CryptoProviderId::LocalSoftware);
5183 assert_eq!(sign_dispatch.custody, CustodyMode::SoftwareEncrypted);
5184 }
5185
5186 #[tokio::test]
5187 async fn describe_provider_surfaces_active_custody_and_migration_availability() {
5188 let native = native_manager();
5191
5192 let software = native
5193 .describe_provider("pqc.preferred_software")
5194 .expect("describe");
5195 let expected_software = KeyProviderDescriptor {
5196 policy: ProviderPolicy::BackendPreferred,
5197 provider: None,
5198 custody: Some(CustodyMode::SoftwareEncrypted),
5199 version: Some("1".to_string()),
5200 backend_native_available: true,
5201 };
5202 assert_eq!(
5203 software, expected_software,
5204 "an admin sees software custody AND that a native migration is available",
5205 );
5206
5207 let preferred = native
5208 .describe_provider("pqc.preferred_native")
5209 .expect("describe");
5210 assert_eq!(preferred.custody, None);
5211 assert!(preferred.backend_native_available);
5212
5213 let classical = manager();
5215 let no_native = classical
5216 .describe_provider("pqc.preferred_software")
5217 .expect("describe");
5218 assert!(!no_native.backend_native_available);
5219
5220 assert!(matches!(
5221 classical.describe_provider("does.not.exist"),
5222 Err(ManagerError::UnknownKey(_))
5223 ));
5224 }
5225}