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