1use std::collections::BTreeMap;
22use std::path::Path;
23use std::process::Command;
24use std::sync::Arc;
25
26use age::secrecy::ExposeSecret as _;
27use basil_cose::Recipient as _;
28use basil_proto::{
29 AeadAlgorithm, CatalogEntry as WireCatalogEntry, CatalogKind, CiphertextEnvelope, KeyMaterial,
30 KeyType,
31};
32use rand::RngCore;
33use uuid::Uuid;
34
35use zeroize::Zeroizing;
36
37use crate::backend::{
38 Backend, BackendError, KvValue, NativeAlgorithm, NewKey, PublicKey, SignOptions,
39 X509CertRequest, X509Svid,
40};
41use crate::catalog::{BackendRef, Catalog, Class, Engine, GenerateSpec, KeyAlgorithm, KeyEntry};
42use crate::core::crypto_provider::LocalSoftwareProvider;
43use crate::core::crypto_provider::{
44 BackendCryptoProvider, CryptoProvider, CryptoProviderId, CustodyMode,
45 Envelope as ProviderEnvelope, EnvelopeAlgorithm as ProviderEnvelopeAlgorithm,
46 GenerateKey as ProviderGenerateKey, GenerateSealingKey as ProviderGenerateSealingKey,
47 KemAlgorithm as ProviderKemAlgorithm, ProviderError, ProviderMetadata, ProviderPolicy,
48 SignRequest, SignatureAlgorithm, UnwrapEnvelopeRequest as ProviderUnwrapEnvelopeRequest,
49 VerifyRequest, WrapEnvelopeRequest as ProviderWrapEnvelopeRequest, ml_dsa_signature_algorithm,
50 select_provider,
51};
52use crate::ed25519_sign::{self, SignError};
53use crate::state::BrokerLimits;
54use crate::x25519_seal::{self, SealError, SealedEnvelope};
55
56#[derive(Debug, thiserror::Error)]
58pub enum ManagerError {
59 #[error("unknown key: {0}")]
61 UnknownKey(String),
62
63 #[error("key `{key}` references unknown backend `{backend}`")]
66 UnknownBackend {
67 key: String,
69 backend: String,
71 },
72
73 #[error("op `{op}` is not valid for key `{key}` (class {class:?})")]
76 OpNotValidForClass {
77 op: &'static str,
79 key: String,
81 class: Class,
83 },
84
85 #[error("backend `{backend}` does not declare support for {op} key type `{key_type}`")]
88 UnsupportedKeyType {
89 backend: String,
91 op: &'static str,
93 key_type: KeyType,
95 },
96
97 #[error("op `{0}` is recognized but not yet backed by a Backend method")]
100 Unsupported(&'static str),
101
102 #[error("algorithm `{requested}` does not match key `{key}` (catalog type {actual})")]
107 AlgorithmMismatch {
108 key: String,
110 requested: AeadAlgorithm,
112 actual: &'static str,
114 },
115
116 #[error("KEM algorithm `{requested}` does not match key `{key}` (catalog type {actual})")]
119 KemAlgorithmMismatch {
120 key: String,
122 requested: &'static str,
124 actual: &'static str,
126 },
127
128 #[error("value key `{0}` has no generate recipe; rotate via `set` instead")]
132 ValueRotateNeedsSet(String),
133
134 #[error("sealing op failed: {0}")]
138 Sealing(SealingFailure),
139
140 #[error("key `{0}` does not authorize this unseal context")]
148 UnsealContextNotPermitted(String),
149
150 #[error("materialize-to-sign op failed: {0}")]
154 Signing(SigningFailure),
155
156 #[error("key `{0}` has no public_path; its public half cannot be resolved")]
162 MissingPublicPath(String),
163
164 #[error("provider error: {0}")]
169 Provider(#[from] ProviderError),
170
171 #[error("backend error: {0}")]
173 Backend(#[from] BackendError),
174}
175
176#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
182pub enum SealingFailure {
183 #[error("malformed sealing material or envelope")]
186 Malformed,
187
188 #[error("unseal authentication failed")]
191 OpenFailed,
192}
193
194impl SealingFailure {
195 const fn from_seal(err: SealError) -> Self {
199 match err {
200 SealError::OpenFailed => Self::OpenFailed,
201 SealError::BadKeyLength { .. }
202 | SealError::BadNonceLength { .. }
203 | SealError::KdfFailed
204 | SealError::SealFailed => Self::Malformed,
205 }
206 }
207
208 const fn from_cose_open(err: &basil_cose::OpenError) -> Self {
209 match err {
210 basil_cose::OpenError::OpenFailed => Self::OpenFailed,
211 basil_cose::OpenError::Decode(_)
212 | basil_cose::OpenError::RecipientKeyMismatch
213 | basil_cose::OpenError::PartyMismatch
214 | basil_cose::OpenError::Provider { .. } => Self::Malformed,
215 }
216 }
217}
218
219const fn nats_curve_error(err: &basil_nats::Error) -> ManagerError {
220 let failure = match err {
221 basil_nats::Error::XKeyOpenFailed => SealingFailure::OpenFailed,
222 basil_nats::Error::BadPublicKeyLen(_)
223 | basil_nats::Error::UnsupportedPrefix(_)
224 | basil_nats::Error::UnexpectedPrefix { .. }
225 | basil_nats::Error::UnexpectedXKeyPrefix(_)
226 | basil_nats::Error::BadXKeyVersion
227 | basil_nats::Error::BadXKeyCiphertextLen(_)
228 | basil_nats::Error::XKeySealFailed
229 | basil_nats::Error::Json(_)
230 | basil_nats::Error::InvalidClaims(_)
231 | basil_nats::Error::MalformedJwt(_)
232 | basil_nats::Error::BadSignatureLen(_) => SealingFailure::Malformed,
233 };
234 ManagerError::Sealing(failure)
235}
236
237#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
243pub enum SigningFailure {
244 #[error("malformed signing seed or verify input")]
247 Malformed,
248}
249
250impl SigningFailure {
251 const fn from_sign(err: SignError) -> Self {
255 match err {
256 SignError::BadSeedLength { .. } | SignError::BadFieldLength { .. } => Self::Malformed,
257 }
258 }
259}
260
261#[derive(Debug, Clone, Copy)]
271pub struct ProviderGate {
272 pub local_software_allowed: bool,
274}
275
276#[derive(Debug, Clone, Copy)]
280pub struct ProviderDispatch {
281 pub provider: CryptoProviderId,
283 pub algorithm: &'static str,
285 pub custody: CustodyMode,
287}
288
289#[derive(Debug, Clone, PartialEq, Eq)]
297pub struct KeyProviderDescriptor {
298 pub policy: ProviderPolicy,
301 pub provider: Option<CryptoProviderId>,
303 pub custody: Option<CustodyMode>,
306 pub version: Option<String>,
308 pub backend_native_available: bool,
312}
313
314#[derive(Debug, Clone, Copy)]
320pub struct MlKemEnvelopeParts<'a> {
321 pub encapsulated_key: &'a [u8],
323 pub nonce: &'a [u8],
325 pub ciphertext: &'a [u8],
327}
328
329pub struct Routed<'a> {
335 pub backend: &'a dyn Backend,
337 pub entry: &'a KeyEntry,
339 pub backend_ref: &'a BackendRef,
341 pub engine: Engine,
343}
344
345impl std::fmt::Debug for Routed<'_> {
348 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
349 f.debug_struct("Routed")
350 .field("backend", &self.backend.kind())
351 .field("entry", &self.entry)
352 .field("backend_ref", &self.backend_ref)
353 .field("engine", &self.engine)
354 .finish()
355 }
356}
357
358impl Routed<'_> {
359 #[must_use]
361 pub fn path(&self) -> &str {
362 &self.entry.path
363 }
364
365 #[must_use]
369 pub fn public_path(&self) -> Option<&str> {
370 self.entry.public_path.as_deref()
371 }
372
373 #[must_use]
375 pub const fn class(&self) -> Class {
376 self.entry.class
377 }
378
379 #[must_use]
381 pub const fn key_type(&self) -> Option<KeyAlgorithm> {
382 self.entry.key_type
383 }
384
385 const fn backend_declares_provides(&self) -> bool {
386 !(self.backend_ref.engines.is_empty()
387 && self.backend_ref.capabilities.is_empty()
388 && self.backend_ref.mint_key_types.is_empty())
389 }
390
391 pub(crate) fn require_mint_key_type(
392 &self,
393 op: &'static str,
394 key_type: KeyType,
395 ) -> Result<(), ManagerError> {
396 let algorithm = KeyAlgorithm::from_wire_key_type(key_type);
397 if !self.backend_declares_provides() || self.backend_ref.mint_key_types.contains(&algorithm)
398 {
399 return Ok(());
400 }
401 Err(ManagerError::UnsupportedKeyType {
402 backend: self.entry.backend.clone(),
403 op,
404 key_type,
405 })
406 }
407}
408
409pub struct BackendManager {
416 catalog: Arc<Catalog>,
417 backends: BTreeMap<String, Box<dyn Backend>>,
418}
419
420impl std::fmt::Debug for BackendManager {
422 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
423 f.debug_struct("BackendManager")
424 .field("backends", &self.backends.keys().collect::<Vec<_>>())
425 .field("keys", &self.catalog.keys.keys().collect::<Vec<_>>())
426 .finish()
427 }
428}
429
430impl BackendManager {
431 pub fn new(
441 catalog: Catalog,
442 backends: BTreeMap<String, Box<dyn Backend>>,
443 ) -> Result<Self, ManagerError> {
444 for (name, entry) in &catalog.keys {
445 if !backends.contains_key(&entry.backend) {
446 return Err(ManagerError::UnknownBackend {
447 key: name.clone(),
448 backend: entry.backend.clone(),
449 });
450 }
451 }
452 Ok(Self {
453 catalog: Arc::new(catalog),
454 backends,
455 })
456 }
457
458 #[must_use]
460 pub fn catalog(&self) -> Arc<Catalog> {
461 Arc::clone(&self.catalog)
462 }
463
464 pub fn resolve(&self, key_id: &str) -> Result<Routed<'_>, ManagerError> {
473 let entry = self
474 .catalog
475 .keys
476 .get(key_id)
477 .ok_or_else(|| ManagerError::UnknownKey(key_id.to_string()))?;
478 let backend_ref = self.catalog.backends.get(&entry.backend).ok_or_else(|| {
479 ManagerError::UnknownBackend {
480 key: key_id.to_string(),
481 backend: entry.backend.clone(),
482 }
483 })?;
484 let backend =
485 self.backends
486 .get(&entry.backend)
487 .ok_or_else(|| ManagerError::UnknownBackend {
488 key: key_id.to_string(),
489 backend: entry.backend.clone(),
490 })?;
491 Ok(Routed {
492 backend: backend.as_ref(),
493 entry,
494 backend_ref,
495 engine: effective_engine(entry),
496 })
497 }
498
499 pub(crate) fn keys(&self) -> impl Iterator<Item = (&String, &KeyEntry)> {
505 self.catalog.keys.iter()
506 }
507
508 pub async fn new_key(&self, key_id: &str, key_type: KeyType) -> Result<NewKey, ManagerError> {
521 let routed = self.resolve(key_id)?;
522 require_class(
523 "new_key",
524 key_id,
525 routed.class(),
526 &[Class::Asymmetric, Class::Symmetric],
527 )?;
528 if routed.class() == Class::Asymmetric {
529 routed.require_mint_key_type("new_key", key_type)?;
530 }
531 Ok(routed.backend.new_key(key_type).await?)
532 }
533
534 pub async fn sign(&self, key_id: &str, message: &[u8]) -> Result<Vec<u8>, ManagerError> {
554 let routed = self.resolve(key_id)?;
555 require_class("sign", key_id, routed.class(), &[Class::Asymmetric])?;
556 if routed.engine == Engine::Kv2 {
557 let seed = self.materialize_signing_seed(key_id, "sign").await?;
560 return Ok(ed25519_sign::sign(&seed, message).to_vec());
561 }
562 Ok(routed
563 .backend
564 .sign_with_options(
565 routed.path(),
566 message,
567 sign_options_for_key(routed.key_type()),
568 )
569 .await?)
570 }
571
572 pub async fn verify(
585 &self,
586 key_id: &str,
587 message: &[u8],
588 signature: &[u8],
589 ) -> Result<bool, ManagerError> {
590 let routed = self.resolve(key_id)?;
591 require_class(
592 "verify",
593 key_id,
594 routed.class(),
595 &[Class::Asymmetric, Class::Public],
596 )?;
597 if routed.class() == Class::Asymmetric && routed.engine == Engine::Kv2 {
598 let public_bytes = self
602 .read_public_half(key_id, "verify", &[Class::Asymmetric])
603 .await?;
604 let public = ed25519_sign::public_from_slice(&public_bytes)
605 .map_err(|e| ManagerError::Signing(SigningFailure::from_sign(e)))?;
606 return ed25519_sign::verify(&public, message, signature)
607 .map_err(|e| ManagerError::Signing(SigningFailure::from_sign(e)));
608 }
609 Ok(routed
610 .backend
611 .verify_with_options(
612 routed.path(),
613 message,
614 signature,
615 sign_options_for_key(routed.key_type()),
616 )
617 .await?)
618 }
619
620 pub async fn get_public_key(&self, key_id: &str) -> Result<PublicKey, ManagerError> {
636 let routed = self.resolve(key_id)?;
637 if routed.class() == Class::Sealing
646 && ProviderMetadata::from_key(routed.entry).custody
647 == Some(CustodyMode::SoftwareEncrypted)
648 && let Some(kem) = routed.key_type().and_then(ml_kem_provider_algorithm)
649 {
650 let public_key = LocalSoftwareProvider::new(routed.backend)
651 .public_key(key_id, routed.path(), kem.token())
652 .await?;
653 return Ok(PublicKey {
654 public_key,
655 key_type: ml_kem_wire_key_type(kem),
656 version: 1,
658 });
659 }
660 require_class(
661 "get_public_key",
662 key_id,
663 routed.class(),
664 &[Class::Asymmetric, Class::Public],
665 )?;
666 if ProviderMetadata::from_key(routed.entry).custody == Some(CustodyMode::SoftwareEncrypted)
674 && let Some(algorithm) = routed.key_type().and_then(ml_dsa_signature_algorithm)
675 {
676 let public_key = LocalSoftwareProvider::new(routed.backend)
677 .public_key(key_id, routed.path(), algorithm.token())
678 .await?;
679 return Ok(PublicKey {
680 public_key,
681 key_type: ml_dsa_wire_key_type(algorithm),
682 version: 1,
684 });
685 }
686 if routed.class() == Class::Asymmetric && routed.engine == Engine::Kv2 {
687 let public_bytes = self
690 .read_public_half(key_id, "get_public_key", &[Class::Asymmetric])
691 .await?;
692 let public = ed25519_sign::public_from_slice(&public_bytes)
693 .map_err(|e| ManagerError::Signing(SigningFailure::from_sign(e)))?;
694 return Ok(PublicKey {
695 public_key: public.to_vec(),
696 key_type: KeyType::Ed25519,
697 version: 1,
700 });
701 }
702 Ok(routed.backend.public_key_with_meta(routed.path()).await?)
703 }
704
705 #[must_use]
713 pub fn ml_dsa_algorithm_for(&self, key_id: &str) -> Option<SignatureAlgorithm> {
714 let routed = self.resolve(key_id).ok()?;
715 ml_dsa_signature_algorithm(routed.key_type()?)
716 }
717
718 #[must_use]
725 pub fn ml_kem_algorithm_for(&self, key_id: &str) -> Option<ProviderKemAlgorithm> {
726 let routed = self.resolve(key_id).ok()?;
727 ml_kem_provider_algorithm(routed.key_type()?)
728 }
729
730 pub fn describe_provider(&self, key_id: &str) -> Result<KeyProviderDescriptor, ManagerError> {
743 let routed = self.resolve(key_id)?;
744 let metadata = ProviderMetadata::from_key(routed.entry);
745 let native = routed
746 .key_type()
747 .and_then(ml_dsa_signature_algorithm)
748 .and_then(SignatureAlgorithm::native_algorithm);
749 let backend_native_available =
750 native.is_some_and(|algorithm| routed.backend.supports_native_algorithm(algorithm));
751 Ok(KeyProviderDescriptor {
752 policy: metadata.policy,
753 provider: metadata.provider,
754 custody: metadata.custody,
755 version: routed
756 .entry
757 .labels
758 .get("crypto_provider_version")
759 .map(str::to_owned),
760 backend_native_available,
761 })
762 }
763
764 pub async fn provider_sign(
781 &self,
782 key_id: &str,
783 message: &[u8],
784 gate: ProviderGate,
785 ) -> Result<(Vec<u8>, ProviderDispatch), ManagerError> {
786 let routed = self.resolve(key_id)?;
787 require_class("sign", key_id, routed.class(), &[Class::Asymmetric])?;
788 let algorithm = require_ml_dsa(key_id, &routed)?;
789 let provider = select_provider_for(&routed, algorithm.native_algorithm(), gate, "sign")?;
790 let request = SignRequest {
791 key_id,
792 backend_path: routed.path(),
793 algorithm,
794 message,
795 };
796 let signature = match provider {
797 CryptoProviderId::VaultTransit => {
798 BackendCryptoProvider::new(routed.backend)
799 .sign(request)
800 .await?
801 }
802 CryptoProviderId::LocalSoftware => {
803 LocalSoftwareProvider::new(routed.backend)
804 .sign(request)
805 .await?
806 }
807 };
808 Ok((signature, provider_dispatch(provider, algorithm)))
809 }
810
811 pub async fn provider_verify(
819 &self,
820 key_id: &str,
821 message: &[u8],
822 signature: &[u8],
823 gate: ProviderGate,
824 ) -> Result<(bool, ProviderDispatch), ManagerError> {
825 let routed = self.resolve(key_id)?;
826 require_class(
827 "verify",
828 key_id,
829 routed.class(),
830 &[Class::Asymmetric, Class::Public],
831 )?;
832 let algorithm = require_ml_dsa(key_id, &routed)?;
833 let provider = select_provider_for(&routed, algorithm.native_algorithm(), gate, "verify")?;
834 let request = VerifyRequest {
835 key_id,
836 backend_path: routed.path(),
837 algorithm,
838 message,
839 signature,
840 };
841 let valid = match provider {
842 CryptoProviderId::VaultTransit => {
843 BackendCryptoProvider::new(routed.backend)
844 .verify(request)
845 .await?
846 }
847 CryptoProviderId::LocalSoftware => {
848 LocalSoftwareProvider::new(routed.backend)
849 .verify(request)
850 .await?
851 }
852 };
853 Ok((valid, provider_dispatch(provider, algorithm)))
854 }
855
856 pub async fn provider_generate(
865 &self,
866 key_id: &str,
867 gate: ProviderGate,
868 ) -> Result<(NewKey, ProviderDispatch), ManagerError> {
869 let routed = self.resolve(key_id)?;
870 require_class("new_key", key_id, routed.class(), &[Class::Asymmetric])?;
871 let algorithm = require_ml_dsa(key_id, &routed)?;
872 let provider =
873 select_provider_for(&routed, algorithm.native_algorithm(), gate, "generate")?;
874 let request = ProviderGenerateKey {
875 key_id,
876 backend_path: routed.path(),
877 algorithm,
878 storage_key: routed.entry.labels.get("pqc_storage_key"),
879 };
880 let created = match provider {
881 CryptoProviderId::VaultTransit => {
882 BackendCryptoProvider::new(routed.backend)
883 .generate_key(request)
884 .await?
885 }
886 CryptoProviderId::LocalSoftware => {
887 LocalSoftwareProvider::new(routed.backend)
888 .generate_key(request)
889 .await?
890 }
891 };
892 Ok((created, provider_dispatch(provider, algorithm)))
893 }
894
895 pub async fn provider_generate_sealing(
913 &self,
914 key_id: &str,
915 kem: ProviderKemAlgorithm,
916 gate: ProviderGate,
917 ) -> Result<(NewKey, ProviderDispatch), ManagerError> {
918 let routed = self.resolve(key_id)?;
919 require_class("new_key", key_id, routed.class(), &[Class::Sealing])?;
920 require_ml_kem_sealing_key(key_id, routed.key_type(), kem)?;
921 let provider = select_provider_for(&routed, kem.native_algorithm(), gate, "generate")?;
922 let request = ProviderGenerateSealingKey {
923 key_id,
924 backend_path: routed.path(),
925 algorithm: kem,
926 storage_key: routed.entry.labels.get("pqc_storage_key"),
927 };
928 let created = match provider {
929 CryptoProviderId::VaultTransit => {
930 BackendCryptoProvider::new(routed.backend)
931 .generate_sealing_key(request)
932 .await?
933 }
934 CryptoProviderId::LocalSoftware => {
935 LocalSoftwareProvider::new(routed.backend)
936 .generate_sealing_key(request)
937 .await?
938 }
939 };
940 Ok((created, kem_provider_dispatch(provider, kem)))
941 }
942
943 pub async fn import(
953 &self,
954 key_id: &str,
955 key_type: KeyType,
956 material: &KeyMaterial,
957 ) -> Result<NewKey, ManagerError> {
958 let routed = self.resolve(key_id)?;
959 require_class(
960 "import",
961 key_id,
962 routed.class(),
963 &[Class::Asymmetric, Class::Symmetric],
964 )?;
965 if routed.engine == Engine::Kv2 {
970 return Err(ManagerError::Unsupported(
971 "import: a value-store (kv2) crypto key is provisioned out-of-band, not imported via the broker",
972 ));
973 }
974 if routed.class() == Class::Asymmetric {
975 routed.require_mint_key_type("import", key_type)?;
976 }
977 Ok(routed
980 .backend
981 .import(routed.path(), key_type, material)
982 .await?)
983 }
984
985 pub async fn list(
1000 &self,
1001 prefix: Option<&str>,
1002 visible: impl Fn(&str) -> bool,
1003 ) -> Result<Vec<WireCatalogEntry>, ManagerError> {
1004 let mut out = Vec::new();
1005 for (name, entry) in &self.catalog.keys {
1006 if let Some(p) = prefix
1007 && !name.starts_with(p)
1008 {
1009 continue;
1010 }
1011 if !visible(name) {
1012 continue;
1013 }
1014 let meta = match self.resolve(name) {
1018 Ok(routed) => routed.backend.key_metadata(routed.path()).await.ok(),
1019 Err(_) => None,
1020 };
1021 let (key_type, latest_version) = meta.map_or_else(
1022 || (wire_key_type(entry), 0),
1023 |m| (m.key_type, m.latest_version),
1024 );
1025 out.push(WireCatalogEntry {
1026 name: name.clone(),
1027 kind: key_kind(entry),
1028 key_type,
1029 latest_version,
1030 });
1031 }
1032 Ok(out)
1033 }
1034
1035 pub async fn encrypt(
1046 &self,
1047 key_id: &str,
1048 algorithm: AeadAlgorithm,
1049 plaintext: &[u8],
1050 aad: Option<&[u8]>,
1051 ) -> Result<CiphertextEnvelope, ManagerError> {
1052 let routed = self.resolve(key_id)?;
1053 require_class("encrypt", key_id, routed.class(), &[Class::Symmetric])?;
1054 require_aead_match(key_id, routed.key_type(), algorithm)?;
1055 Ok(routed
1056 .backend
1057 .encrypt(routed.path(), algorithm, plaintext, aad)
1058 .await?)
1059 }
1060
1061 pub async fn decrypt(
1074 &self,
1075 key_id: &str,
1076 envelope: &CiphertextEnvelope,
1077 aad: Option<&[u8]>,
1078 ) -> Result<Vec<u8>, ManagerError> {
1079 let routed = self.resolve(key_id)?;
1080 require_class("decrypt", key_id, routed.class(), &[Class::Symmetric])?;
1081 require_aead_match(key_id, routed.key_type(), envelope.alg)?;
1082 Ok(routed.backend.decrypt(routed.path(), envelope, aad).await?)
1083 }
1084
1085 pub async fn wrap_envelope(
1103 &self,
1104 key_id: &str,
1105 plaintext: &[u8],
1106 aad: &[u8],
1107 ) -> Result<SealedEnvelope, ManagerError> {
1108 let routed = self.resolve(key_id)?;
1109 require_class("wrap_envelope", key_id, routed.class(), &[Class::Sealing])?;
1110 require_x25519_sealing_key(key_id, routed.key_type())?;
1111 let public_bytes = self
1114 .read_public_half(key_id, "wrap_envelope", &[Class::Sealing])
1115 .await?;
1116 let recipient_pub = x25519_seal::public_from_slice(&public_bytes)
1117 .map_err(|e| ManagerError::Sealing(SealingFailure::from_seal(e)))?;
1118 x25519_seal::seal(&recipient_pub, plaintext, aad)
1119 .map_err(|e| ManagerError::Sealing(SealingFailure::from_seal(e)))
1120 }
1121
1122 pub async fn unwrap_envelope(
1144 &self,
1145 key_id: &str,
1146 envelope: &SealedEnvelope,
1147 aad: &[u8],
1148 ) -> Result<Zeroizing<Vec<u8>>, ManagerError> {
1149 let private = self
1150 .materialize_sealing_private(key_id, "unwrap_envelope")
1151 .await?;
1152 x25519_seal::open(&private, envelope, aad)
1153 .map_err(|e| ManagerError::Sealing(SealingFailure::from_seal(e)))
1154 }
1155
1156 pub async fn unseal_cose(
1171 &self,
1172 key_id: &str,
1173 cose_encrypt: &[u8],
1174 external_aad: &[u8],
1175 ) -> Result<Zeroizing<Vec<u8>>, ManagerError> {
1176 let expected_parties = {
1183 let routed = self.resolve(key_id)?;
1184 match routed.entry.sealing_pin.as_ref() {
1185 None => None,
1186 Some(pin) => {
1187 if !pin.external_aad_allowed(external_aad) {
1188 return Err(ManagerError::UnsealContextNotPermitted(key_id.to_string()));
1189 }
1190 match pin.parties.as_ref() {
1191 None => None,
1192 Some(parties) => Some(
1193 parties
1194 .to_kdf_parties()
1195 .map_err(|_| ManagerError::Sealing(SealingFailure::Malformed))?,
1196 ),
1197 }
1198 }
1199 }
1200 };
1201
1202 let private = self
1203 .materialize_sealing_private(key_id, "unseal_cose")
1204 .await?;
1205 let cose_key_id = basil_cose::KeyId::from_text(key_id)
1206 .map_err(|_| ManagerError::Sealing(SealingFailure::Malformed))?;
1207 let recipient = basil_cose::X25519Recipient::new(cose_key_id, private);
1208 let aad = basil_cose::ExternalAad::from_bytes(external_aad.to_vec());
1209 let request = basil_cose::OpenRequest {
1210 cose_encrypt,
1211 external_aad: &aad,
1212 expected_parties: expected_parties.as_ref(),
1213 };
1214 recipient.open(&request).await.map_err(|e| match e {
1215 basil_cose::OpenError::PartyMismatch => {
1220 ManagerError::UnsealContextNotPermitted(key_id.to_string())
1221 }
1222 other => ManagerError::Sealing(SealingFailure::from_cose_open(&other)),
1223 })
1224 }
1225
1226 pub async fn encrypt_nats_curve(
1232 &self,
1233 key_id: &str,
1234 recipient_public_xkey: &str,
1235 plaintext: &[u8],
1236 ) -> Result<Vec<u8>, ManagerError> {
1237 let private = self
1238 .materialize_sealing_private(key_id, "encrypt_nats_curve")
1239 .await?;
1240 basil_nats::seal_nats_curve(&private, recipient_public_xkey, plaintext)
1241 .map_err(|e| nats_curve_error(&e))
1242 }
1243
1244 pub async fn decrypt_nats_curve(
1249 &self,
1250 key_id: &str,
1251 sender_public_xkey: &str,
1252 ciphertext: &[u8],
1253 ) -> Result<Zeroizing<Vec<u8>>, ManagerError> {
1254 let private = self
1255 .materialize_sealing_private(key_id, "decrypt_nats_curve")
1256 .await?;
1257 basil_nats::open_nats_curve(&private, sender_public_xkey, ciphertext)
1258 .map_err(|e| nats_curve_error(&e))
1259 }
1260
1261 pub async fn provider_wrap_envelope(
1281 &self,
1282 key_id: &str,
1283 kem: ProviderKemAlgorithm,
1284 envelope_algorithm: ProviderEnvelopeAlgorithm,
1285 plaintext: &[u8],
1286 aad: &[u8],
1287 gate: ProviderGate,
1288 ) -> Result<(ProviderEnvelope, ProviderDispatch), ManagerError> {
1289 let routed = self.resolve(key_id)?;
1290 require_class("wrap_envelope", key_id, routed.class(), &[Class::Sealing])?;
1291 require_ml_kem_sealing_key(key_id, routed.key_type(), kem)?;
1292 let provider = select_provider_for(&routed, kem.native_algorithm(), gate, "wrap_envelope")?;
1293 let request = ProviderWrapEnvelopeRequest {
1294 key_id,
1295 backend_path: routed.path(),
1296 kem_algorithm: kem,
1297 envelope_algorithm,
1298 plaintext,
1299 aad: Some(aad),
1300 };
1301 let envelope = match provider {
1302 CryptoProviderId::VaultTransit => {
1303 BackendCryptoProvider::new(routed.backend)
1304 .wrap_envelope(request)
1305 .await?
1306 }
1307 CryptoProviderId::LocalSoftware => {
1308 LocalSoftwareProvider::new(routed.backend)
1309 .wrap_envelope(request)
1310 .await?
1311 }
1312 };
1313 Ok((envelope, kem_provider_dispatch(provider, kem)))
1314 }
1315
1316 pub async fn provider_unwrap_envelope(
1337 &self,
1338 key_id: &str,
1339 kem: ProviderKemAlgorithm,
1340 envelope_algorithm: ProviderEnvelopeAlgorithm,
1341 parts: MlKemEnvelopeParts<'_>,
1342 aad: &[u8],
1343 gate: ProviderGate,
1344 ) -> Result<(Vec<u8>, ProviderDispatch), ManagerError> {
1345 let routed = self.resolve(key_id)?;
1346 require_class("unwrap_envelope", key_id, routed.class(), &[Class::Sealing])?;
1347 require_ml_kem_sealing_key(key_id, routed.key_type(), kem)?;
1348 let provider =
1349 select_provider_for(&routed, kem.native_algorithm(), gate, "unwrap_envelope")?;
1350 let request = ProviderUnwrapEnvelopeRequest {
1351 key_id,
1352 backend_path: routed.path(),
1353 kem_algorithm: kem,
1354 envelope_algorithm,
1355 encapsulated_key: parts.encapsulated_key,
1356 nonce: parts.nonce,
1357 ciphertext: parts.ciphertext,
1358 aad: Some(aad),
1359 };
1360 let plaintext = match provider {
1361 CryptoProviderId::VaultTransit => {
1362 BackendCryptoProvider::new(routed.backend)
1363 .unwrap_envelope(request)
1364 .await?
1365 }
1366 CryptoProviderId::LocalSoftware => {
1367 LocalSoftwareProvider::new(routed.backend)
1368 .unwrap_envelope(request)
1369 .await?
1370 }
1371 };
1372 Ok((plaintext, kem_provider_dispatch(provider, kem)))
1373 }
1374
1375 pub async fn sealing_public_key(
1388 &self,
1389 key_id: &str,
1390 ) -> Result<[u8; x25519_seal::PUBLIC_KEY_LEN], ManagerError> {
1391 let routed = self.resolve(key_id)?;
1392 require_class("get_public_key", key_id, routed.class(), &[Class::Sealing])?;
1393 require_x25519_sealing_key(key_id, routed.key_type())?;
1394 let public_bytes = self
1395 .read_public_half(key_id, "get_public_key", &[Class::Sealing])
1396 .await?;
1397 x25519_seal::public_from_slice(&public_bytes)
1398 .map_err(|e| ManagerError::Sealing(SealingFailure::from_seal(e)))
1399 }
1400
1401 async fn materialize_sealing_private(
1408 &self,
1409 key_id: &str,
1410 op: &'static str,
1411 ) -> Result<Zeroizing<[u8; x25519_seal::PRIVATE_KEY_LEN]>, ManagerError> {
1412 let routed = self.resolve(key_id)?;
1413 require_class(op, key_id, routed.class(), &[Class::Sealing])?;
1414 require_x25519_sealing_key(key_id, routed.key_type())?;
1415 let secret = routed.backend.kv_get_secret(routed.path(), None).await?;
1419 x25519_seal::private_from_slice(&secret)
1423 .map_err(|e| ManagerError::Sealing(SealingFailure::from_seal(e)))
1424 }
1425
1426 async fn read_public_half(
1438 &self,
1439 key_id: &str,
1440 op: &'static str,
1441 allowed: &[Class],
1442 ) -> Result<Vec<u8>, ManagerError> {
1443 let routed = self.resolve(key_id)?;
1444 require_class(op, key_id, routed.class(), allowed)?;
1445 let public_path = routed
1446 .public_path()
1447 .ok_or_else(|| ManagerError::MissingPublicPath(key_id.to_string()))?;
1448 let kv = routed.backend.kv_get(public_path, None).await?;
1449 Ok(kv.value)
1450 }
1451
1452 async fn materialize_signing_seed(
1465 &self,
1466 key_id: &str,
1467 op: &'static str,
1468 ) -> Result<Zeroizing<[u8; ed25519_sign::SEED_LEN]>, ManagerError> {
1469 let routed = self.resolve(key_id)?;
1470 require_class(op, key_id, routed.class(), &[Class::Asymmetric])?;
1471 let secret = routed.backend.kv_get_secret(routed.path(), None).await?;
1475 ed25519_sign::seed_from_slice(&secret)
1479 .map_err(|e| ManagerError::Signing(SigningFailure::from_sign(e)))
1480 }
1481
1482 pub async fn get(&self, key_id: &str, version: Option<u32>) -> Result<KvValue, ManagerError> {
1496 let routed = self.resolve(key_id)?;
1497 require_class(
1498 "get",
1499 key_id,
1500 routed.class(),
1501 &[Class::Value, Class::Public],
1502 )?;
1503 Ok(routed.backend.kv_get(routed.path(), version).await?)
1504 }
1505
1506 pub async fn set(&self, key_id: &str, value: &[u8]) -> Result<u32, ManagerError> {
1518 let routed = self.resolve(key_id)?;
1519 require_class("set", key_id, routed.class(), &[Class::Value])?;
1520 Ok(routed.backend.kv_put(routed.path(), value).await?)
1521 }
1522
1523 pub async fn rotate(&self, key_id: &str, limits: BrokerLimits) -> Result<u32, ManagerError> {
1540 let routed = self.resolve(key_id)?;
1541 require_class(
1542 "rotate",
1543 key_id,
1544 routed.class(),
1545 &[Class::Asymmetric, Class::Symmetric, Class::Value],
1546 )?;
1547
1548 if routed.engine == Engine::Kv2 && routed.class() != Class::Value {
1554 return Err(ManagerError::Unsupported(
1555 "rotate: a value-store (kv2) crypto key is re-provisioned out-of-band, not rotated via the broker",
1556 ));
1557 }
1558
1559 if routed.class() == Class::Value {
1560 let Some(spec) = routed.entry.generate.as_ref() else {
1563 return Err(ManagerError::ValueRotateNeedsSet(key_id.to_string()));
1564 };
1565 let writes = self.generated_writes_for_key(key_id, spec).await?;
1566 let mut rotated_version = None;
1567 for write in writes {
1568 let write_route = self.resolve(&write.key_id)?;
1569 require_class(
1570 "rotate",
1571 &write.key_id,
1572 write_route.class(),
1573 &[Class::Value, Class::Public],
1574 )?;
1575 let version = write_route
1576 .backend
1577 .kv_put(write_route.path(), &write.value)
1578 .await?;
1579 if write.key_id == key_id {
1580 rotated_version = Some(version);
1581 }
1582 }
1583 return rotated_version.ok_or_else(|| {
1584 ManagerError::Backend(BackendError::Backend(
1585 "generate recipe produced no primary value".into(),
1586 ))
1587 });
1588 }
1589
1590 let new_version = routed.backend.rotate(routed.path()).await?;
1593 let floor = limits.grace_floor(new_version);
1594 if let Err(e) = routed
1598 .backend
1599 .configure_versions(routed.path(), Some(floor), None)
1600 .await
1601 && !matches!(e, BackendError::Unsupported(_))
1602 {
1603 return Err(ManagerError::Backend(e));
1604 }
1605 Ok(new_version)
1606 }
1607
1608 pub async fn sweep_retention(
1622 &self,
1623 key_id: &str,
1624 limits: BrokerLimits,
1625 ) -> Result<(), ManagerError> {
1626 let routed = self.resolve(key_id)?;
1627 require_class(
1628 "rotate",
1629 key_id,
1630 routed.class(),
1631 &[Class::Asymmetric, Class::Symmetric],
1632 )?;
1633 let latest = routed
1634 .backend
1635 .key_metadata(routed.path())
1636 .await?
1637 .latest_version;
1638 let Some(floor) = limits.retention_floor(latest) else {
1639 return Ok(()); };
1641 routed
1642 .backend
1643 .configure_versions(routed.path(), None, Some(floor))
1644 .await?;
1645 Ok(())
1646 }
1647
1648 pub async fn sweep_all_retention(&self, limits: BrokerLimits) -> Result<(), ManagerError> {
1657 if limits.retain_versions.is_none() {
1658 return Ok(());
1659 }
1660 let key_ids: Vec<String> = self
1661 .keys()
1662 .filter(|(_, entry)| matches!(entry.class, Class::Asymmetric | Class::Symmetric))
1663 .map(|(name, _)| name.clone())
1664 .collect();
1665 for key_id in key_ids {
1666 self.sweep_retention(&key_id, limits).await?;
1667 }
1668 Ok(())
1669 }
1670
1671 pub async fn issue_x509_svid(
1683 &self,
1684 key_id: &str,
1685 spiffe_id: &str,
1686 ttl_seconds: u64,
1687 ) -> Result<X509Svid, ManagerError> {
1688 let routed = self.resolve(key_id)?;
1689 require_class(
1690 "issue_x509_svid",
1691 key_id,
1692 routed.class(),
1693 &[Class::Asymmetric],
1694 )?;
1695 if routed.engine != Engine::Pki {
1696 return Err(ManagerError::Unsupported("issue_x509_svid"));
1697 }
1698 Ok(routed
1699 .backend
1700 .issue_x509_svid(routed.path(), spiffe_id, ttl_seconds)
1701 .await?)
1702 }
1703
1704 pub async fn issue_x509_cert(
1717 &self,
1718 key_id: &str,
1719 request: &X509CertRequest,
1720 ) -> Result<X509Svid, ManagerError> {
1721 let routed = self.resolve(key_id)?;
1722 require_class(
1723 "issue_x509_cert",
1724 key_id,
1725 routed.class(),
1726 &[Class::Asymmetric],
1727 )?;
1728 if routed.engine != Engine::Pki {
1729 return Err(ManagerError::Unsupported("issue_x509_cert"));
1730 }
1731 Ok(routed
1732 .backend
1733 .issue_x509_cert(routed.path(), request)
1734 .await?)
1735 }
1736
1737 pub(crate) async fn generated_writes_for_key(
1743 &self,
1744 key_id: &str,
1745 spec: &GenerateSpec,
1746 ) -> Result<Vec<GeneratedWrite>, ManagerError> {
1747 match spec {
1748 GenerateSpec::SelfSignedTls {
1749 common_name,
1750 validity,
1751 } => {
1752 if let Some(pair_key) = self.tls_pair_key_for_cert(key_id) {
1753 let existing_key = self.try_existing_value(&pair_key).await?;
1754 let material =
1755 generate_self_signed_tls(common_name, validity, existing_key.as_deref())?;
1756 let mut writes = Vec::new();
1757 if existing_key.is_none() {
1758 writes.push(GeneratedWrite {
1759 key_id: pair_key,
1760 value: material.private_key_pem,
1761 });
1762 }
1763 writes.push(GeneratedWrite {
1764 key_id: key_id.to_string(),
1765 value: material.cert_pem,
1766 });
1767 Ok(writes)
1768 } else {
1769 Ok(vec![GeneratedWrite {
1770 key_id: key_id.to_string(),
1771 value: generate_self_signed_tls(common_name, validity, None)?.cert_pem,
1772 }])
1773 }
1774 }
1775 GenerateSpec::SelfSignedTlsPairOf { pair_of } => {
1776 let (common_name, validity) = self.tls_cert_recipe(pair_of)?;
1777 let material = generate_self_signed_tls(common_name, validity, None)?;
1778 Ok(vec![
1779 GeneratedWrite {
1780 key_id: key_id.to_string(),
1781 value: material.private_key_pem,
1782 },
1783 GeneratedWrite {
1784 key_id: pair_of.clone(),
1785 value: material.cert_pem,
1786 },
1787 ])
1788 }
1789 GenerateSpec::AsciiPrintable { .. }
1790 | GenerateSpec::Base64 { .. }
1791 | GenerateSpec::Hex { .. }
1792 | GenerateSpec::AgeX25519 => Ok(vec![GeneratedWrite {
1793 key_id: key_id.to_string(),
1794 value: generate_value(spec)?,
1795 }]),
1796 }
1797 }
1798
1799 fn tls_pair_key_for_cert(&self, cert_key: &str) -> Option<String> {
1800 self.catalog.keys.iter().find_map(|(name, entry)| {
1801 matches!(
1802 entry.generate.as_ref(),
1803 Some(GenerateSpec::SelfSignedTlsPairOf { pair_of }) if pair_of == cert_key
1806 )
1807 .then(|| name.clone())
1808 })
1809 }
1810
1811 fn tls_cert_recipe(&self, cert_key: &str) -> Result<(&str, &str), ManagerError> {
1812 let entry = self
1813 .catalog
1814 .keys
1815 .get(cert_key)
1816 .ok_or_else(|| ManagerError::UnknownKey(cert_key.to_string()))?;
1817 match entry.generate.as_ref() {
1818 Some(GenerateSpec::SelfSignedTls {
1819 common_name,
1820 validity,
1821 }) => Ok((common_name, validity)),
1822 _ => Err(ManagerError::Backend(BackendError::Backend(format!(
1823 "self-signed-tls-pair-of references non-certificate key `{cert_key}`"
1824 )))),
1825 }
1826 }
1827
1828 async fn try_existing_value(&self, key_id: &str) -> Result<Option<Vec<u8>>, ManagerError> {
1829 let routed = self.resolve(key_id)?;
1830 require_class(
1831 "generate",
1832 key_id,
1833 routed.class(),
1834 &[Class::Value, Class::Public],
1835 )?;
1836 match routed.backend.kv_get(routed.path(), None).await {
1837 Ok(value) => Ok(Some(value.value)),
1838 Err(BackendError::KeyNotFound(_)) => Ok(None),
1839 Err(e) => Err(ManagerError::Backend(e)),
1840 }
1841 }
1842}
1843
1844pub(crate) struct GeneratedWrite {
1846 pub(crate) key_id: String,
1847 pub(crate) value: Vec<u8>,
1848}
1849
1850fn require_aead_match(
1854 key_id: &str,
1855 key_type: Option<KeyAlgorithm>,
1856 algorithm: AeadAlgorithm,
1857) -> Result<(), ManagerError> {
1858 let actual = match key_type {
1859 Some(KeyAlgorithm::Aes256Gcm) => AeadAlgorithm::Aes256Gcm,
1860 Some(KeyAlgorithm::ChaCha20Poly1305) => AeadAlgorithm::Chacha20Poly1305,
1861 _ => {
1865 return Err(ManagerError::AlgorithmMismatch {
1866 key: key_id.to_string(),
1867 requested: algorithm,
1868 actual: "non-aead",
1869 });
1870 }
1871 };
1872 if actual == algorithm {
1873 Ok(())
1874 } else {
1875 Err(ManagerError::AlgorithmMismatch {
1876 key: key_id.to_string(),
1877 requested: algorithm,
1878 actual: match actual {
1879 AeadAlgorithm::Aes256Gcm => "aes-256-gcm",
1880 AeadAlgorithm::Chacha20Poly1305 => "chacha20-poly1305",
1881 },
1882 })
1883 }
1884}
1885
1886const fn sign_options_for_key(key_type: Option<KeyAlgorithm>) -> SignOptions {
1887 match key_type {
1888 Some(KeyAlgorithm::Rsa2048) => SignOptions::Rs256Pkcs1v15Sha256,
1889 Some(KeyAlgorithm::EcdsaP256) => SignOptions::Es256,
1890 Some(KeyAlgorithm::EcdsaP384) => SignOptions::Es384,
1891 Some(KeyAlgorithm::EcdsaP521) => SignOptions::Es512,
1892 _ => SignOptions::Default,
1893 }
1894}
1895
1896fn require_ml_dsa(key_id: &str, routed: &Routed<'_>) -> Result<SignatureAlgorithm, ManagerError> {
1900 routed
1901 .key_type()
1902 .and_then(ml_dsa_signature_algorithm)
1903 .ok_or_else(|| ManagerError::OpNotValidForClass {
1904 op: "provider_dispatch",
1905 key: key_id.to_string(),
1906 class: routed.class(),
1907 })
1908}
1909
1910fn select_provider_for(
1923 routed: &Routed<'_>,
1924 native: Option<NativeAlgorithm>,
1925 gate: ProviderGate,
1926 op: &'static str,
1927) -> Result<CryptoProviderId, ProviderError> {
1928 let metadata = ProviderMetadata::from_key(routed.entry);
1929 let backend_native_supported =
1930 native.is_some_and(|algorithm| routed.backend.supports_native_algorithm(algorithm));
1931 select_provider(
1932 metadata,
1933 backend_native_supported,
1934 gate.local_software_allowed,
1935 op,
1936 )
1937}
1938
1939const fn provider_dispatch(
1941 provider: CryptoProviderId,
1942 algorithm: SignatureAlgorithm,
1943) -> ProviderDispatch {
1944 ProviderDispatch {
1945 provider,
1946 algorithm: algorithm.token(),
1947 custody: provider.custody_mode(),
1948 }
1949}
1950
1951const fn kem_provider_dispatch(
1954 provider: CryptoProviderId,
1955 kem: ProviderKemAlgorithm,
1956) -> ProviderDispatch {
1957 ProviderDispatch {
1958 provider,
1959 algorithm: kem.token(),
1960 custody: provider.custody_mode(),
1961 }
1962}
1963
1964const ASCII_PRINTABLE: &[u8] = b"!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
1967
1968pub(crate) fn generate_value(spec: &GenerateSpec) -> Result<Vec<u8>, ManagerError> {
1971 use base64::Engine as _;
1972 use std::fmt::Write as _;
1973
1974 let mut rng = rand::thread_rng();
1975 match spec {
1976 GenerateSpec::AsciiPrintable { bytes } => {
1977 let mut out = Vec::with_capacity(*bytes as usize);
1978 for _ in 0..*bytes {
1979 let idx = (rng.next_u32() as usize) % ASCII_PRINTABLE.len();
1982 out.push(ASCII_PRINTABLE.get(idx).copied().unwrap_or(b'!'));
1983 }
1984 Ok(out)
1985 }
1986 GenerateSpec::Base64 { bytes } => {
1987 let mut raw = vec![0u8; *bytes as usize];
1988 rng.fill_bytes(&mut raw);
1989 Ok(base64::engine::general_purpose::STANDARD
1990 .encode(&raw)
1991 .into_bytes())
1992 }
1993 GenerateSpec::Hex { bytes } => {
1994 let mut raw = vec![0u8; *bytes as usize];
1995 rng.fill_bytes(&mut raw);
1996 let mut out = String::with_capacity(*bytes as usize * 2);
1997 for b in &raw {
1998 let _ = write!(out, "{b:02x}");
1999 }
2000 Ok(out.into_bytes())
2001 }
2002 GenerateSpec::AgeX25519 => Ok(age::x25519::Identity::generate()
2003 .to_string()
2004 .expose_secret()
2005 .as_bytes()
2006 .to_vec()),
2007 GenerateSpec::SelfSignedTls {
2008 common_name,
2009 validity,
2010 } => Ok(generate_self_signed_tls(common_name, validity, None)?.cert_pem),
2011 GenerateSpec::SelfSignedTlsPairOf { .. } => Err(ManagerError::Backend(
2012 BackendError::Backend("self-signed-tls-pair-of needs catalog pair context".into()),
2013 )),
2014 }
2015}
2016
2017struct TlsMaterial {
2018 cert_pem: Vec<u8>,
2019 private_key_pem: Vec<u8>,
2020}
2021
2022fn generate_self_signed_tls(
2023 common_name: &str,
2024 validity: &str,
2025 existing_key_pem: Option<&[u8]>,
2026) -> Result<TlsMaterial, ManagerError> {
2027 let dir = std::env::temp_dir().join(format!("basil-self-signed-tls-{}", Uuid::new_v4()));
2028 std::fs::create_dir(&dir).map_err(|e| {
2029 ManagerError::Backend(BackendError::Backend(format!(
2030 "self-signed-tls tempdir create: {e}"
2031 )))
2032 })?;
2033
2034 let result = generate_self_signed_tls_in_dir(&dir, common_name, validity, existing_key_pem);
2035 let cleanup_result = std::fs::remove_dir_all(&dir);
2036 match (result, cleanup_result) {
2037 (Ok(material), Ok(()) | Err(_)) => Ok(material),
2038 (Err(err), _) => Err(err),
2039 }
2040}
2041
2042fn generate_self_signed_tls_in_dir(
2043 dir: &Path,
2044 common_name: &str,
2045 validity: &str,
2046 existing_key_pem: Option<&[u8]>,
2047) -> Result<TlsMaterial, ManagerError> {
2048 let cert_path = dir.join("cert.pem");
2049 let key_path = dir.join("key.pem");
2050 if let Some(key) = existing_key_pem {
2051 write_secret_file(&key_path, key)?;
2052 }
2053
2054 let mut command = Command::new("step");
2055 command
2056 .arg("certificate")
2057 .arg("create")
2058 .arg(common_name)
2059 .arg(&cert_path);
2060 if existing_key_pem.is_none() {
2061 command.arg(&key_path);
2062 }
2063 command
2064 .arg("--profile")
2065 .arg("self-signed")
2066 .arg("--subtle")
2067 .arg("--not-after")
2068 .arg(validity)
2069 .arg("--force");
2070 if existing_key_pem.is_some() {
2071 command.arg("--key").arg(&key_path);
2072 } else {
2073 command.arg("--no-password").arg("--insecure");
2074 }
2075
2076 let output = command.output().map_err(|e| {
2077 ManagerError::Backend(BackendError::Backend(format!(
2078 "self-signed-tls step execution failed: {e}"
2079 )))
2080 })?;
2081 if !output.status.success() {
2082 let detail = String::from_utf8_lossy(&output.stderr);
2083 return Err(ManagerError::Backend(BackendError::Backend(format!(
2084 "self-signed-tls step failed: {}",
2085 detail.trim()
2086 ))));
2087 }
2088
2089 let cert_pem = std::fs::read(&cert_path).map_err(|e| {
2090 ManagerError::Backend(BackendError::Backend(format!(
2091 "self-signed-tls read cert: {e}"
2092 )))
2093 })?;
2094 let private_key_pem = std::fs::read(&key_path).map_err(|e| {
2095 ManagerError::Backend(BackendError::Backend(format!(
2096 "self-signed-tls read key: {e}"
2097 )))
2098 })?;
2099 Ok(TlsMaterial {
2100 cert_pem,
2101 private_key_pem,
2102 })
2103}
2104
2105fn write_secret_file(path: &Path, contents: &[u8]) -> Result<(), ManagerError> {
2106 use std::io::Write as _;
2107 #[cfg(unix)]
2108 use std::os::unix::fs::OpenOptionsExt as _;
2109
2110 let mut options = std::fs::OpenOptions::new();
2111 options.write(true).create_new(true);
2112 #[cfg(unix)]
2113 options.mode(0o600);
2114 let mut file = options.open(path).map_err(|e| {
2115 ManagerError::Backend(BackendError::Backend(format!(
2116 "self-signed-tls write key: {e}"
2117 )))
2118 })?;
2119 file.write_all(contents).map_err(|e| {
2120 ManagerError::Backend(BackendError::Backend(format!(
2121 "self-signed-tls write key: {e}"
2122 )))
2123 })
2124}
2125
2126const fn key_kind(entry: &KeyEntry) -> CatalogKind {
2130 match entry.class {
2131 Class::Asymmetric | Class::Public => CatalogKind::Signing,
2132 Class::Symmetric | Class::Sealing => CatalogKind::Encryption,
2134 Class::Value => CatalogKind::Value,
2135 }
2136}
2137
2138const fn wire_key_type(entry: &KeyEntry) -> Option<KeyType> {
2142 match entry.key_type {
2143 Some(KeyAlgorithm::Ed25519) => Some(KeyType::Ed25519),
2144 Some(KeyAlgorithm::Ed25519Nkey) => Some(KeyType::Ed25519Nkey),
2145 Some(KeyAlgorithm::Rsa2048) => Some(KeyType::Rsa2048),
2146 Some(KeyAlgorithm::EcdsaP256) => Some(KeyType::EcdsaP256),
2147 Some(KeyAlgorithm::EcdsaP384) => Some(KeyType::EcdsaP384),
2148 Some(KeyAlgorithm::EcdsaP521) => Some(KeyType::EcdsaP521),
2149 Some(KeyAlgorithm::MlDsa44) => Some(KeyType::MlDsa44),
2152 Some(KeyAlgorithm::MlDsa65) => Some(KeyType::MlDsa65),
2153 Some(KeyAlgorithm::MlDsa87) => Some(KeyType::MlDsa87),
2154 Some(
2158 KeyAlgorithm::Aes256Gcm
2159 | KeyAlgorithm::ChaCha20Poly1305
2160 | KeyAlgorithm::X25519
2161 | KeyAlgorithm::MlKem512
2162 | KeyAlgorithm::MlKem768
2163 | KeyAlgorithm::MlKem1024,
2164 )
2165 | None => None,
2166 }
2167}
2168
2169fn effective_engine(entry: &KeyEntry) -> Engine {
2172 entry.effective_engine()
2173}
2174
2175fn require_class(
2178 op: &'static str,
2179 key_id: &str,
2180 class: Class,
2181 allowed: &[Class],
2182) -> Result<(), ManagerError> {
2183 if allowed.contains(&class) {
2184 Ok(())
2185 } else {
2186 Err(ManagerError::OpNotValidForClass {
2187 op,
2188 key: key_id.to_string(),
2189 class,
2190 })
2191 }
2192}
2193
2194fn require_x25519_sealing_key(
2195 key_id: &str,
2196 actual: Option<KeyAlgorithm>,
2197) -> Result<(), ManagerError> {
2198 match actual {
2199 Some(KeyAlgorithm::X25519) => Ok(()),
2200 Some(other) => Err(ManagerError::KemAlgorithmMismatch {
2201 key: key_id.to_string(),
2202 requested: "x25519",
2203 actual: other.token(),
2204 }),
2205 None => Err(ManagerError::KemAlgorithmMismatch {
2206 key: key_id.to_string(),
2207 requested: "x25519",
2208 actual: "none",
2209 }),
2210 }
2211}
2212
2213const fn ml_dsa_wire_key_type(algorithm: SignatureAlgorithm) -> KeyType {
2217 match algorithm {
2218 SignatureAlgorithm::MlDsa44 => KeyType::MlDsa44,
2219 SignatureAlgorithm::MlDsa65 => KeyType::MlDsa65,
2220 SignatureAlgorithm::MlDsa87 => KeyType::MlDsa87,
2221 SignatureAlgorithm::Ed25519
2222 | SignatureAlgorithm::Ed25519Nkey
2223 | SignatureAlgorithm::Rs256
2224 | SignatureAlgorithm::Es256 => KeyType::Ed25519,
2225 }
2226}
2227
2228const fn ml_kem_wire_key_type(kem: ProviderKemAlgorithm) -> KeyType {
2232 match kem {
2233 ProviderKemAlgorithm::MlKem512 => KeyType::MlKem512,
2234 ProviderKemAlgorithm::MlKem768 => KeyType::MlKem768,
2235 ProviderKemAlgorithm::MlKem1024 => KeyType::MlKem1024,
2236 }
2237}
2238
2239const fn ml_kem_provider_algorithm(algorithm: KeyAlgorithm) -> Option<ProviderKemAlgorithm> {
2243 match algorithm {
2244 KeyAlgorithm::MlKem512 => Some(ProviderKemAlgorithm::MlKem512),
2245 KeyAlgorithm::MlKem768 => Some(ProviderKemAlgorithm::MlKem768),
2246 KeyAlgorithm::MlKem1024 => Some(ProviderKemAlgorithm::MlKem1024),
2247 KeyAlgorithm::Ed25519
2248 | KeyAlgorithm::Ed25519Nkey
2249 | KeyAlgorithm::Rsa2048
2250 | KeyAlgorithm::EcdsaP256
2251 | KeyAlgorithm::EcdsaP384
2252 | KeyAlgorithm::EcdsaP521
2253 | KeyAlgorithm::Aes256Gcm
2254 | KeyAlgorithm::ChaCha20Poly1305
2255 | KeyAlgorithm::X25519
2256 | KeyAlgorithm::MlDsa44
2257 | KeyAlgorithm::MlDsa65
2258 | KeyAlgorithm::MlDsa87 => None,
2259 }
2260}
2261
2262fn require_ml_kem_sealing_key(
2263 key_id: &str,
2264 actual: Option<KeyAlgorithm>,
2265 requested: ProviderKemAlgorithm,
2266) -> Result<(), ManagerError> {
2267 let expected = match requested {
2268 ProviderKemAlgorithm::MlKem512 => KeyAlgorithm::MlKem512,
2269 ProviderKemAlgorithm::MlKem768 => KeyAlgorithm::MlKem768,
2270 ProviderKemAlgorithm::MlKem1024 => KeyAlgorithm::MlKem1024,
2271 };
2272 match actual {
2273 Some(found) if found == expected => Ok(()),
2274 Some(found) => Err(ManagerError::KemAlgorithmMismatch {
2275 key: key_id.to_string(),
2276 requested: requested.token(),
2277 actual: found.token(),
2278 }),
2279 None => Err(ManagerError::KemAlgorithmMismatch {
2280 key: key_id.to_string(),
2281 requested: requested.token(),
2282 actual: "none",
2283 }),
2284 }
2285}
2286
2287#[cfg(test)]
2288mod tests {
2289 use super::*;
2290 use crate::backend::{KeyMetadata, KvValue};
2291 use crate::core::crypto_provider::{SoftwareCustodyCatalog, encode_record_bytes};
2292 use crate::ml_kem_envelope;
2293 use async_trait::async_trait;
2294 use std::sync::Arc;
2295 use std::sync::atomic::{AtomicUsize, Ordering};
2296
2297 #[derive(Default)]
2300 struct MockBackend {
2301 name: &'static str,
2302 last_path: std::sync::Mutex<Option<String>>,
2303 last_spiffe_id: std::sync::Mutex<Option<String>>,
2304 new_key_calls: AtomicUsize,
2305 rotate_calls: AtomicUsize,
2306 kv_put_calls: AtomicUsize,
2307 latest_version: AtomicUsize,
2309 last_versions_config: std::sync::Mutex<Option<(Option<u32>, Option<u32>)>>,
2313 last_kv_value: std::sync::Mutex<Option<Vec<u8>>>,
2315 last_sign_options: std::sync::Mutex<Option<SignOptions>>,
2316 last_verify_options: std::sync::Mutex<Option<SignOptions>>,
2317 last_new_key_type: std::sync::Mutex<Option<KeyType>>,
2318 kv_store: std::sync::Mutex<BTreeMap<String, Vec<u8>>>,
2325 kv_put_log: std::sync::Mutex<Vec<(String, Vec<u8>)>>,
2326 }
2327
2328 impl MockBackend {
2329 fn new(name: &'static str) -> Arc<Self> {
2330 Arc::new(Self {
2331 name,
2332 last_path: std::sync::Mutex::new(None),
2333 last_spiffe_id: std::sync::Mutex::new(None),
2334 new_key_calls: AtomicUsize::new(0),
2335 rotate_calls: AtomicUsize::new(0),
2336 kv_put_calls: AtomicUsize::new(0),
2337 latest_version: AtomicUsize::new(5),
2338 last_versions_config: std::sync::Mutex::new(None),
2339 last_kv_value: std::sync::Mutex::new(None),
2340 last_sign_options: std::sync::Mutex::new(None),
2341 last_verify_options: std::sync::Mutex::new(None),
2342 last_new_key_type: std::sync::Mutex::new(None),
2343 kv_store: std::sync::Mutex::new(BTreeMap::new()),
2344 kv_put_log: std::sync::Mutex::new(Vec::new()),
2345 })
2346 }
2347
2348 fn seed_kv(&self, path: &str, value: Vec<u8>) {
2351 self.kv_store
2352 .lock()
2353 .unwrap()
2354 .insert(path.to_string(), value);
2355 }
2356
2357 fn kv_lookup(&self, path: &str) -> Vec<u8> {
2361 if let Some(v) = self.kv_store.lock().unwrap().get(path) {
2362 return v.clone();
2363 }
2364 self.last_kv_value
2365 .lock()
2366 .unwrap()
2367 .clone()
2368 .unwrap_or_else(|| b"stored-value".to_vec())
2369 }
2370
2371 fn last_path(&self) -> Option<String> {
2372 self.last_path.lock().unwrap().clone()
2373 }
2374
2375 fn last_versions_config(&self) -> Option<(Option<u32>, Option<u32>)> {
2376 *self.last_versions_config.lock().unwrap()
2377 }
2378
2379 fn last_kv_value(&self) -> Option<Vec<u8>> {
2380 self.last_kv_value.lock().unwrap().clone()
2381 }
2382
2383 fn last_spiffe_id(&self) -> Option<String> {
2384 self.last_spiffe_id.lock().unwrap().clone()
2385 }
2386
2387 fn kv_put_log(&self) -> Vec<(String, Vec<u8>)> {
2388 self.kv_put_log.lock().unwrap().clone()
2389 }
2390
2391 fn last_sign_options(&self) -> Option<SignOptions> {
2392 *self.last_sign_options.lock().unwrap()
2393 }
2394
2395 fn last_verify_options(&self) -> Option<SignOptions> {
2396 *self.last_verify_options.lock().unwrap()
2397 }
2398
2399 fn last_new_key_type(&self) -> Option<KeyType> {
2400 *self.last_new_key_type.lock().unwrap()
2401 }
2402 }
2403
2404 struct MockHandle(Arc<MockBackend>);
2407
2408 #[async_trait]
2409 impl Backend for MockHandle {
2410 fn kind(&self) -> &'static str {
2411 self.0.name
2412 }
2413
2414 async fn new_key(&self, key_type: KeyType) -> Result<NewKey, BackendError> {
2415 self.0.new_key_calls.fetch_add(1, Ordering::SeqCst);
2416 *self.0.last_new_key_type.lock().unwrap() = Some(key_type);
2417 Ok(NewKey {
2418 key_id: format!("{}-newkey", self.0.name),
2419 public_key: vec![1, 2, 3],
2420 })
2421 }
2422
2423 async fn public_key(&self, key_id: &str) -> Result<Vec<u8>, BackendError> {
2424 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2425 Ok(vec![9, 9, 9])
2426 }
2427
2428 async fn public_key_with_meta(&self, key_id: &str) -> Result<PublicKey, BackendError> {
2429 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2430 Ok(PublicKey {
2431 public_key: vec![9, 9, 9],
2432 key_type: KeyType::Ed25519Nkey,
2435 version: 7,
2436 })
2437 }
2438
2439 async fn key_metadata(&self, key_id: &str) -> Result<KeyMetadata, BackendError> {
2440 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2441 Ok(KeyMetadata {
2442 key_type: Some(KeyType::Ed25519),
2443 latest_version: u32::try_from(self.0.latest_version.load(Ordering::SeqCst))
2444 .unwrap_or(u32::MAX),
2445 })
2446 }
2447
2448 async fn import(
2449 &self,
2450 key_id: &str,
2451 _key_type: KeyType,
2452 _material: &KeyMaterial,
2453 ) -> Result<NewKey, BackendError> {
2454 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2455 Ok(NewKey {
2456 key_id: key_id.to_string(),
2457 public_key: vec![0xEE, 0xEE],
2458 })
2459 }
2460
2461 async fn sign(&self, key_id: &str, _message: &[u8]) -> Result<Vec<u8>, BackendError> {
2462 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2463 *self.0.last_sign_options.lock().unwrap() = Some(SignOptions::Default);
2464 Ok(vec![0xAB, 0xCD])
2465 }
2466
2467 async fn sign_with_options(
2468 &self,
2469 key_id: &str,
2470 message: &[u8],
2471 options: SignOptions,
2472 ) -> Result<Vec<u8>, BackendError> {
2473 let mut signature = self.sign(key_id, message).await?;
2474 *self.0.last_sign_options.lock().unwrap() = Some(options);
2475 if options == SignOptions::Rs256Pkcs1v15Sha256 {
2476 signature.push(0x52);
2477 } else if options == SignOptions::Es256 {
2478 signature.push(0x45);
2479 } else if options == SignOptions::Es384 {
2480 signature.push(0x46);
2481 } else if options == SignOptions::Es512 {
2482 signature.push(0x47);
2483 }
2484 Ok(signature)
2485 }
2486
2487 async fn verify(
2488 &self,
2489 key_id: &str,
2490 _message: &[u8],
2491 _signature: &[u8],
2492 ) -> Result<bool, BackendError> {
2493 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2494 *self.0.last_verify_options.lock().unwrap() = Some(SignOptions::Default);
2495 Ok(true)
2496 }
2497
2498 async fn verify_with_options(
2499 &self,
2500 key_id: &str,
2501 message: &[u8],
2502 signature: &[u8],
2503 options: SignOptions,
2504 ) -> Result<bool, BackendError> {
2505 let valid = self.verify(key_id, message, signature).await?;
2506 *self.0.last_verify_options.lock().unwrap() = Some(options);
2507 Ok(valid)
2508 }
2509
2510 async fn encrypt(
2516 &self,
2517 key_id: &str,
2518 algorithm: AeadAlgorithm,
2519 plaintext: &[u8],
2520 aad: Option<&[u8]>,
2521 ) -> Result<CiphertextEnvelope, BackendError> {
2522 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2523 let aad = aad.unwrap_or(&[]);
2524 let mut ciphertext = Vec::new();
2525 ciphertext.push(u8::try_from(aad.len()).unwrap_or(u8::MAX));
2526 ciphertext.extend_from_slice(aad);
2527 ciphertext.extend_from_slice(plaintext);
2528 Ok(CiphertextEnvelope {
2529 alg: algorithm,
2530 key_version: u32::try_from(self.0.latest_version.load(Ordering::SeqCst))
2531 .unwrap_or(u32::MAX),
2532 nonce: Vec::new(),
2533 ciphertext,
2534 })
2535 }
2536
2537 async fn decrypt(
2538 &self,
2539 key_id: &str,
2540 envelope: &CiphertextEnvelope,
2541 aad: Option<&[u8]>,
2542 ) -> Result<Vec<u8>, BackendError> {
2543 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2544 let aad = aad.unwrap_or(&[]);
2545 let ct = &envelope.ciphertext;
2546 let aad_len = *ct.first().ok_or(BackendError::DecryptFailed)? as usize;
2547 let bound = ct.get(1..1 + aad_len).ok_or(BackendError::DecryptFailed)?;
2548 if bound != aad {
2550 return Err(BackendError::DecryptFailed);
2551 }
2552 Ok(ct
2553 .get(1 + aad_len..)
2554 .ok_or(BackendError::DecryptFailed)?
2555 .to_vec())
2556 }
2557
2558 async fn rotate(&self, key_id: &str) -> Result<u32, BackendError> {
2559 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2560 self.0.rotate_calls.fetch_add(1, Ordering::SeqCst);
2561 let v = self.0.latest_version.fetch_add(1, Ordering::SeqCst) + 1;
2562 Ok(u32::try_from(v).unwrap_or(u32::MAX))
2563 }
2564
2565 async fn kv_get(
2566 &self,
2567 key_id: &str,
2568 version: Option<u32>,
2569 ) -> Result<KvValue, BackendError> {
2570 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2571 let value = self.0.kv_lookup(key_id);
2575 let version = version.unwrap_or_else(|| {
2576 u32::try_from(self.0.latest_version.load(Ordering::SeqCst)).unwrap_or(u32::MAX)
2577 });
2578 Ok(KvValue { value, version })
2579 }
2580
2581 async fn kv_get_secret(
2582 &self,
2583 key_id: &str,
2584 _version: Option<u32>,
2585 ) -> Result<Zeroizing<Vec<u8>>, BackendError> {
2586 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2587 Ok(Zeroizing::new(self.0.kv_lookup(key_id)))
2588 }
2589
2590 async fn kv_put(&self, key_id: &str, value: &[u8]) -> Result<u32, BackendError> {
2591 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2592 self.0.kv_put_calls.fetch_add(1, Ordering::SeqCst);
2593 *self.0.last_kv_value.lock().unwrap() = Some(value.to_vec());
2594 self.0
2595 .kv_store
2596 .lock()
2597 .unwrap()
2598 .insert(key_id.to_string(), value.to_vec());
2599 self.0
2600 .kv_put_log
2601 .lock()
2602 .unwrap()
2603 .push((key_id.to_string(), value.to_vec()));
2604 let v = self.0.latest_version.fetch_add(1, Ordering::SeqCst) + 1;
2605 Ok(u32::try_from(v).unwrap_or(u32::MAX))
2606 }
2607
2608 async fn configure_versions(
2609 &self,
2610 key_id: &str,
2611 min_decryption_version: Option<u32>,
2612 min_available_version: Option<u32>,
2613 ) -> Result<(), BackendError> {
2614 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2615 *self.0.last_versions_config.lock().unwrap() =
2616 Some((min_decryption_version, min_available_version));
2617 Ok(())
2618 }
2619
2620 async fn issue_x509_svid(
2621 &self,
2622 key_id: &str,
2623 spiffe_id: &str,
2624 _ttl_seconds: u64,
2625 ) -> Result<X509Svid, BackendError> {
2626 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2627 *self.0.last_spiffe_id.lock().unwrap() = Some(spiffe_id.to_string());
2628 Ok(X509Svid {
2629 cert_chain_der: vec![vec![1, 2, 3]],
2630 leaf_private_key_der: zeroize::Zeroizing::new(vec![4, 5, 6]),
2631 bundle_der: vec![vec![7, 8, 9]],
2632 })
2633 }
2634
2635 async fn issue_x509_cert(
2636 &self,
2637 key_id: &str,
2638 request: &X509CertRequest,
2639 ) -> Result<X509Svid, BackendError> {
2640 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2641 *self.0.last_spiffe_id.lock().unwrap() = Some(request.common_name.clone());
2643 Ok(X509Svid {
2644 cert_chain_der: vec![vec![0xCE, 0x27]],
2645 leaf_private_key_der: zeroize::Zeroizing::new(vec![0xDE, 0xAD]),
2646 bundle_der: vec![vec![0xCA, 0xFE]],
2647 })
2648 }
2649 }
2650
2651 const CATALOG: &str = r#"{
2661 "schemaVersion": 1,
2662 "backends": {
2663 "primary": {
2664 "kind": "vault", "addr": "https://127.0.0.1:8200",
2665 "engines": ["transit", "kv2"], "capabilities": [],
2666 "mintKeyTypes": ["ed25519", "ed25519-nkey", "rsa-2048", "ecdsa-p256", "ecdsa-p384", "ecdsa-p521"]
2667 },
2668 "secondary": {
2669 "kind": "vault", "addr": "https://127.0.0.1:8201",
2670 "engines": ["transit", "kv2"], "capabilities": [],
2671 "mintKeyTypes": ["ed25519", "ed25519-nkey", "rsa-2048", "ecdsa-p256"]
2672 }
2673 },
2674 "keys": {
2675 "asym.signer": {
2676 "class": "asymmetric", "keyType": "ed25519", "backend": "primary",
2677 "path": "signer", "writable": true, "missing": "error",
2678 "description": "a signing key"
2679 },
2680 "asym.nkey": {
2681 "class": "asymmetric", "keyType": "ed25519-nkey", "backend": "primary",
2682 "path": "nkey-signer", "writable": true, "missing": "error",
2683 "description": "a NATS nkey signing key"
2684 },
2685 "asym.rsa": {
2686 "class": "asymmetric", "keyType": "rsa-2048", "backend": "primary",
2687 "path": "rsa-signer", "writable": true, "missing": "error",
2688 "description": "an RSA signing key"
2689 },
2690 "asym.ecdsa": {
2691 "class": "asymmetric", "keyType": "ecdsa-p256", "backend": "primary",
2692 "path": "ecdsa-signer", "writable": true, "missing": "error",
2693 "description": "an ECDSA P-256 signing key"
2694 },
2695 "asym.ecdsa384": {
2696 "class": "asymmetric", "keyType": "ecdsa-p384", "backend": "primary",
2697 "path": "ecdsa384-signer", "writable": true, "missing": "error",
2698 "description": "an ECDSA P-384 signing key"
2699 },
2700 "asym.ecdsa521": {
2701 "class": "asymmetric", "keyType": "ecdsa-p521", "backend": "primary",
2702 "path": "ecdsa521-signer", "writable": true, "missing": "error",
2703 "description": "an ECDSA P-521 signing key"
2704 },
2705 "sym.box": {
2706 "class": "symmetric", "keyType": "aes-256-gcm", "backend": "secondary",
2707 "path": "box", "writable": true, "missing": "error",
2708 "description": "a symmetric key"
2709 },
2710 "web.value": {
2711 "class": "value", "backend": "primary", "engine": "kv2",
2712 "path": "secret/data/web/value", "writable": true, "missing": "error",
2713 "description": "an opaque value"
2714 },
2715 "gen.value": {
2716 "class": "value", "backend": "primary", "engine": "kv2",
2717 "path": "secret/data/gen/value", "writable": true, "missing": "generate",
2718 "generate": { "format": "ascii-printable", "bytes": 24 },
2719 "description": "a generate-able value (rotate regenerates)"
2720 },
2721 "tls.cert": {
2722 "class": "public", "backend": "primary", "engine": "kv2",
2723 "path": "secret/data/tls/cert", "writable": false, "missing": "generate",
2724 "generate": { "format": "self-signed-tls", "commonName": "example.test", "validity": "1h" },
2725 "description": "a generated self-signed cert"
2726 },
2727 "tls.key": {
2728 "class": "value", "backend": "primary", "engine": "kv2",
2729 "path": "secret/data/tls/key", "writable": true, "missing": "generate",
2730 "generate": { "format": "self-signed-tls-pair-of", "pairOf": "tls.cert" },
2731 "description": "the private key paired with tls.cert"
2732 },
2733 "web.cert": {
2734 "class": "public", "backend": "secondary", "engine": "kv2",
2735 "path": "secret/data/web/cert", "writable": false, "missing": "warn",
2736 "description": "a public cert"
2737 },
2738 "spiffe.issuer": {
2739 "class": "asymmetric", "keyType": "ed25519", "backend": "secondary", "engine": "pki",
2740 "path": "pki/issue/workload", "writable": false, "missing": "error",
2741 "description": "a pki issuer role"
2742 },
2743 "enroll.sealing": {
2744 "class": "sealing", "keyType": "x25519", "backend": "primary", "engine": "kv2",
2745 "path": "secret/data/enroll/x25519",
2746 "publicPath": "secret/data/enroll/x25519-public",
2747 "writable": true, "missing": "error",
2748 "description": "an x25519 enrollment sealing key"
2749 },
2750 "enroll.mlkem": {
2751 "class": "sealing", "keyType": "ml-kem-768", "backend": "primary", "engine": "kv2",
2752 "path": "secret/data/enroll/ml-kem-768",
2753 "publicPath": "secret/data/enroll/ml-kem-768-public",
2754 "labels": [
2755 "crypto_provider=local-software",
2756 "crypto_provider_version=1",
2757 "pqc_algorithm=ml-kem-768",
2758 "pqc_custody=software-encrypted",
2759 "crypto_provider_policy=local-software",
2760 "pqc_storage_key=pqc/storage/wrap"
2761 ],
2762 "writable": true, "missing": "error",
2763 "description": "an ML-KEM enrollment sealing key"
2764 },
2765 "kv2.signer": {
2766 "class": "asymmetric", "keyType": "ed25519", "backend": "primary", "engine": "kv2",
2767 "path": "secret/data/kv2/signer",
2768 "publicPath": "secret/data/kv2/signer-public",
2769 "writable": true, "missing": "error",
2770 "description": "a value-store Ed25519 materialize-to-sign key"
2771 }
2772 }
2773 }"#;
2774
2775 fn parse_catalog(json: &str) -> Catalog {
2776 serde_json::from_str(json).expect("catalog parses")
2777 }
2778
2779 fn fixture() -> (BackendManager, Arc<MockBackend>, Arc<MockBackend>) {
2782 let primary = MockBackend::new("primary");
2783 let secondary = MockBackend::new("secondary");
2784 let mut backends: BTreeMap<String, Box<dyn Backend>> = BTreeMap::new();
2785 backends.insert("primary".into(), Box::new(MockHandle(primary.clone())));
2786 backends.insert("secondary".into(), Box::new(MockHandle(secondary.clone())));
2787 let mgr = BackendManager::new(parse_catalog(CATALOG), backends)
2788 .expect("manager constructs cleanly");
2789 (mgr, primary, secondary)
2790 }
2791
2792 #[test]
2793 fn new_rejects_key_naming_absent_backend() {
2794 let mut backends: BTreeMap<String, Box<dyn Backend>> = BTreeMap::new();
2796 backends.insert(
2797 "primary".into(),
2798 Box::new(MockHandle(MockBackend::new("primary"))),
2799 );
2800 let err = BackendManager::new(parse_catalog(CATALOG), backends)
2802 .expect_err("missing backend must fail closed");
2803 match err {
2804 ManagerError::UnknownBackend { backend, .. } => assert_eq!(backend, "secondary"),
2805 other => panic!("expected UnknownBackend, got {other:?}"),
2806 }
2807 }
2808
2809 #[test]
2810 fn resolve_maps_key_to_correct_backend_and_path() {
2811 let (mgr, _p, _s) = fixture();
2812
2813 let routed = mgr.resolve("asym.signer").expect("resolves");
2814 assert_eq!(routed.backend.kind(), "primary");
2815 assert_eq!(routed.path(), "signer");
2816 assert_eq!(routed.class(), Class::Asymmetric);
2817 assert_eq!(routed.engine, Engine::Transit);
2818 assert_eq!(routed.key_type(), Some(KeyAlgorithm::Ed25519));
2819
2820 let routed = mgr.resolve("web.cert").expect("resolves");
2821 assert_eq!(routed.backend.kind(), "secondary");
2822 assert_eq!(routed.path(), "secret/data/web/cert");
2823 assert_eq!(routed.class(), Class::Public);
2824 assert_eq!(routed.engine, Engine::Kv2);
2825
2826 let routed = mgr.resolve("spiffe.issuer").expect("resolves");
2827 assert_eq!(routed.backend.kind(), "secondary");
2828 assert_eq!(routed.path(), "pki/issue/workload");
2829 assert_eq!(routed.class(), Class::Asymmetric);
2830 assert_eq!(routed.engine, Engine::Pki);
2831 }
2832
2833 #[test]
2834 fn resolve_infers_engine_when_catalog_omits_it() {
2835 let (mgr, _p, _s) = fixture();
2837 assert_eq!(mgr.resolve("asym.signer").unwrap().engine, Engine::Transit);
2838 assert_eq!(mgr.resolve("sym.box").unwrap().engine, Engine::Transit);
2839 }
2840
2841 #[test]
2842 fn resolve_unknown_key_errors() {
2843 let (mgr, _p, _s) = fixture();
2844 let err = mgr.resolve("does.not.exist").expect_err("unknown key");
2845 match err {
2846 ManagerError::UnknownKey(k) => assert_eq!(k, "does.not.exist"),
2847 other => panic!("expected UnknownKey, got {other:?}"),
2848 }
2849 }
2850
2851 #[tokio::test]
2852 async fn sign_routes_to_correct_backend_with_path() {
2853 let (mgr, primary, secondary) = fixture();
2854 let sig = mgr.sign("asym.signer", &[1, 2, 3]).await.expect("signs");
2855 assert_eq!(sig, vec![0xAB, 0xCD]);
2856 assert_eq!(primary.last_path().as_deref(), Some("signer"));
2859 assert_eq!(
2860 secondary.last_path(),
2861 None,
2862 "the other backend is untouched"
2863 );
2864 }
2865
2866 #[tokio::test]
2867 async fn nkey_sign_uses_raw_ed25519_transit_defaults() {
2868 let (mgr, primary, _secondary) = fixture();
2869 let sig = mgr.sign("asym.nkey", b"nonce").await.expect("signs nkey");
2870 assert_eq!(sig, vec![0xAB, 0xCD]);
2871 assert_eq!(primary.last_path().as_deref(), Some("nkey-signer"));
2872 assert_eq!(primary.last_sign_options(), Some(SignOptions::Default));
2873 }
2874
2875 #[tokio::test]
2876 async fn rsa_sign_and_verify_use_rs256_transit_options() {
2877 let (mgr, primary, _secondary) = fixture();
2878 let sig = mgr.sign("asym.rsa", b"jwt-input").await.expect("signs rsa");
2879 assert_eq!(sig, vec![0xAB, 0xCD, 0x52]);
2880 assert_eq!(primary.last_path().as_deref(), Some("rsa-signer"));
2881 assert_eq!(
2882 primary.last_sign_options(),
2883 Some(SignOptions::Rs256Pkcs1v15Sha256)
2884 );
2885
2886 let valid = mgr
2887 .verify("asym.rsa", b"jwt-input", &sig)
2888 .await
2889 .expect("verifies rsa");
2890 assert!(valid);
2891 assert_eq!(
2892 primary.last_verify_options(),
2893 Some(SignOptions::Rs256Pkcs1v15Sha256)
2894 );
2895 }
2896
2897 #[tokio::test]
2898 async fn ecdsa_sign_and_verify_use_es256_transit_options() {
2899 let (mgr, primary, _secondary) = fixture();
2900 let sig = mgr
2901 .sign("asym.ecdsa", b"jwt-input")
2902 .await
2903 .expect("signs ecdsa");
2904 assert_eq!(sig, vec![0xAB, 0xCD, 0x45]);
2905 assert_eq!(primary.last_path().as_deref(), Some("ecdsa-signer"));
2906 assert_eq!(primary.last_sign_options(), Some(SignOptions::Es256));
2907
2908 let valid = mgr
2909 .verify("asym.ecdsa", b"jwt-input", &sig)
2910 .await
2911 .expect("verifies ecdsa");
2912 assert!(valid);
2913 assert_eq!(primary.last_verify_options(), Some(SignOptions::Es256));
2914 }
2915
2916 #[tokio::test]
2917 async fn ecdsa_p384_sign_and_verify_use_es384_transit_options() {
2918 let (mgr, primary, _secondary) = fixture();
2919 let sig = mgr
2920 .sign("asym.ecdsa384", b"jwt-input")
2921 .await
2922 .expect("signs ecdsa p384");
2923 assert_eq!(sig, vec![0xAB, 0xCD, 0x46]);
2924 assert_eq!(primary.last_path().as_deref(), Some("ecdsa384-signer"));
2925 assert_eq!(primary.last_sign_options(), Some(SignOptions::Es384));
2926
2927 let valid = mgr
2928 .verify("asym.ecdsa384", b"jwt-input", &sig)
2929 .await
2930 .expect("verifies ecdsa p384");
2931 assert!(valid);
2932 assert_eq!(primary.last_verify_options(), Some(SignOptions::Es384));
2933 }
2934
2935 #[tokio::test]
2936 async fn ecdsa_p521_sign_and_verify_use_es512_transit_options() {
2937 let (mgr, primary, _secondary) = fixture();
2938 let sig = mgr
2939 .sign("asym.ecdsa521", b"jwt-input")
2940 .await
2941 .expect("signs ecdsa p521");
2942 assert_eq!(sig, vec![0xAB, 0xCD, 0x47]);
2943 assert_eq!(primary.last_path().as_deref(), Some("ecdsa521-signer"));
2944 assert_eq!(primary.last_sign_options(), Some(SignOptions::Es512));
2945
2946 let valid = mgr
2947 .verify("asym.ecdsa521", b"jwt-input", &sig)
2948 .await
2949 .expect("verifies ecdsa p521");
2950 assert!(valid);
2951 assert_eq!(primary.last_verify_options(), Some(SignOptions::Es512));
2952 }
2953
2954 #[tokio::test]
2955 async fn issue_x509_svid_routes_to_pki_issue_path() {
2956 let (mgr, _primary, secondary) = fixture();
2957 let svid = mgr
2958 .issue_x509_svid("spiffe.issuer", "spiffe://example.test/web", 300)
2959 .await
2960 .expect("issues x509 svid");
2961 assert_eq!(secondary.last_path().as_deref(), Some("pki/issue/workload"));
2962 assert_eq!(
2963 secondary.last_spiffe_id().as_deref(),
2964 Some("spiffe://example.test/web")
2965 );
2966 assert_eq!(svid.cert_chain_der, vec![vec![1, 2, 3]]);
2967 assert_eq!(&*svid.leaf_private_key_der, &[4, 5, 6]);
2968 assert_eq!(svid.bundle_der, vec![vec![7, 8, 9]]);
2969 }
2970
2971 #[tokio::test]
2972 async fn issue_x509_svid_rejects_non_pki_key() {
2973 let (mgr, _primary, _secondary) = fixture();
2974 let err = mgr
2975 .issue_x509_svid("asym.signer", "spiffe://example.test/web", 300)
2976 .await
2977 .expect_err("transit key is not a pki issuer");
2978 assert!(matches!(err, ManagerError::Unsupported("issue_x509_svid")));
2979 }
2980
2981 #[tokio::test]
2982 async fn issue_x509_cert_routes_to_pki_issue_path_with_sans() {
2983 let (mgr, _primary, secondary) = fixture();
2984 let request = X509CertRequest {
2985 common_name: "web.internal".into(),
2986 dns_sans: vec!["web.internal".into(), "alt.internal".into()],
2987 ip_sans: vec!["10.0.0.1".into()],
2988 ttl_seconds: 3600,
2989 };
2990 let issued = mgr
2991 .issue_x509_cert("spiffe.issuer", &request)
2992 .await
2993 .expect("issues x509 cert");
2994 assert_eq!(secondary.last_path().as_deref(), Some("pki/issue/workload"));
2995 assert_eq!(secondary.last_spiffe_id().as_deref(), Some("web.internal"));
2996 assert_eq!(issued.cert_chain_der, vec![vec![0xCE, 0x27]]);
2997 assert_eq!(&*issued.leaf_private_key_der, &[0xDE, 0xAD]);
2998 }
2999
3000 #[tokio::test]
3001 async fn issue_x509_cert_rejects_non_pki_key() {
3002 let (mgr, _primary, _secondary) = fixture();
3003 let request = X509CertRequest {
3004 common_name: "web.internal".into(),
3005 ..X509CertRequest::default()
3006 };
3007 let err = mgr
3008 .issue_x509_cert("asym.signer", &request)
3009 .await
3010 .expect_err("transit key is not a pki issuer");
3011 assert!(matches!(err, ManagerError::Unsupported("issue_x509_cert")));
3012 }
3013
3014 #[tokio::test]
3015 async fn new_key_routes_to_correct_backend() {
3016 let (mgr, primary, secondary) = fixture();
3017 let nk = mgr
3018 .new_key("asym.signer", KeyType::Ed25519)
3019 .await
3020 .expect("new_key");
3021 assert_eq!(nk.key_id, "primary-newkey");
3022 assert_eq!(primary.new_key_calls.load(Ordering::SeqCst), 1);
3023 assert_eq!(primary.last_new_key_type(), Some(KeyType::Ed25519));
3024 assert_eq!(secondary.new_key_calls.load(Ordering::SeqCst), 0);
3025
3026 mgr.new_key("asym.nkey", KeyType::Ed25519Nkey)
3027 .await
3028 .expect("new_key nkey");
3029 assert_eq!(primary.last_new_key_type(), Some(KeyType::Ed25519Nkey));
3030
3031 mgr.new_key("asym.rsa", KeyType::Rsa2048)
3032 .await
3033 .expect("new_key rsa");
3034 assert_eq!(primary.last_new_key_type(), Some(KeyType::Rsa2048));
3035
3036 mgr.new_key("asym.ecdsa", KeyType::EcdsaP256)
3037 .await
3038 .expect("new_key ecdsa");
3039 assert_eq!(primary.last_new_key_type(), Some(KeyType::EcdsaP256));
3040
3041 mgr.new_key("asym.ecdsa384", KeyType::EcdsaP384)
3042 .await
3043 .expect("new_key ecdsa p384");
3044 assert_eq!(primary.last_new_key_type(), Some(KeyType::EcdsaP384));
3045
3046 mgr.new_key("asym.ecdsa521", KeyType::EcdsaP521)
3047 .await
3048 .expect("new_key ecdsa p521");
3049 assert_eq!(primary.last_new_key_type(), Some(KeyType::EcdsaP521));
3050 }
3051
3052 #[tokio::test]
3053 async fn new_key_rejects_key_type_absent_from_static_backend_preset() {
3054 let catalog = CATALOG.replace(
3055 r#""mintKeyTypes": ["ed25519", "ed25519-nkey", "rsa-2048", "ecdsa-p256", "ecdsa-p384", "ecdsa-p521"]"#,
3056 r#""mintKeyTypes": ["ed25519"]"#,
3057 );
3058 let primary = MockBackend::new("primary");
3059 let secondary = MockBackend::new("secondary");
3060 let mut backends: BTreeMap<String, Box<dyn Backend>> = BTreeMap::new();
3061 backends.insert("primary".into(), Box::new(MockHandle(primary.clone())));
3062 backends.insert("secondary".into(), Box::new(MockHandle(secondary)));
3063 let mgr = BackendManager::new(parse_catalog(&catalog), backends).expect("manager builds");
3064
3065 let err = mgr
3066 .new_key("asym.signer", KeyType::Rsa2048)
3067 .await
3068 .expect_err("rsa absent from backend preset");
3069 assert!(matches!(
3070 err,
3071 ManagerError::UnsupportedKeyType {
3072 backend,
3073 op: "new_key",
3074 key_type: KeyType::Rsa2048
3075 } if backend == "primary"
3076 ));
3077 assert_eq!(primary.new_key_calls.load(Ordering::SeqCst), 0);
3078 }
3079
3080 #[tokio::test]
3081 async fn get_public_key_valid_on_public_class_returns_real_metadata() {
3082 let (mgr, _p, secondary) = fixture();
3083 let pk = mgr.get_public_key("web.cert").await.expect("public read");
3084 assert_eq!(pk.public_key, vec![9, 9, 9]);
3085 assert_eq!(pk.key_type, KeyType::Ed25519Nkey);
3087 assert_eq!(pk.version, 7);
3088 assert_eq!(
3089 secondary.last_path().as_deref(),
3090 Some("secret/data/web/cert")
3091 );
3092 }
3093
3094 #[tokio::test]
3095 async fn import_routes_to_backend_path_and_returns_only_public() {
3096 let (mgr, primary, _s) = fixture();
3097 let nk = mgr
3098 .import(
3099 "asym.signer",
3100 KeyType::Ed25519,
3101 &KeyMaterial::Ed25519Seed(vec![7; 32]),
3102 )
3103 .await
3104 .expect("import");
3105 assert_eq!(nk.public_key, vec![0xEE, 0xEE]);
3107 assert_eq!(primary.last_path().as_deref(), Some("signer"));
3109 }
3110
3111 #[tokio::test]
3112 async fn import_rejects_key_type_absent_from_static_backend_preset() {
3113 let catalog = CATALOG.replace(
3114 r#""mintKeyTypes": ["ed25519", "ed25519-nkey", "rsa-2048", "ecdsa-p256", "ecdsa-p384", "ecdsa-p521"]"#,
3115 r#""mintKeyTypes": ["ed25519"]"#,
3116 );
3117 let primary = MockBackend::new("primary");
3118 let secondary = MockBackend::new("secondary");
3119 let mut backends: BTreeMap<String, Box<dyn Backend>> = BTreeMap::new();
3120 backends.insert("primary".into(), Box::new(MockHandle(primary.clone())));
3121 backends.insert("secondary".into(), Box::new(MockHandle(secondary)));
3122 let mgr = BackendManager::new(parse_catalog(&catalog), backends).expect("manager builds");
3123
3124 let err = mgr
3125 .import(
3126 "asym.signer",
3127 KeyType::Rsa2048,
3128 &KeyMaterial::Pkcs8Der(vec![1, 2, 3]),
3129 )
3130 .await
3131 .expect_err("rsa absent from backend preset");
3132 assert!(matches!(
3133 err,
3134 ManagerError::UnsupportedKeyType {
3135 backend,
3136 op: "import",
3137 key_type: KeyType::Rsa2048
3138 } if backend == "primary"
3139 ));
3140 assert!(primary.last_path().is_none());
3141 }
3142
3143 #[tokio::test]
3144 async fn import_on_value_key_is_op_not_valid_for_class() {
3145 let (mgr, _p, _s) = fixture();
3146 let err = mgr
3147 .import(
3148 "web.value",
3149 KeyType::Ed25519,
3150 &KeyMaterial::Ed25519Seed(vec![0; 32]),
3151 )
3152 .await
3153 .expect_err("import on a value key must be rejected");
3154 assert!(matches!(err, ManagerError::OpNotValidForClass { .. }));
3155 }
3156
3157 #[tokio::test]
3158 async fn list_projects_catalog_value_free_and_respects_visibility() {
3159 let (mgr, _p, _s) = fixture();
3160 let entries = mgr
3162 .list(None, |name| name != "web.value")
3165 .await
3166 .expect("list");
3167 let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
3168 assert!(names.contains(&"asym.signer"));
3169 assert!(names.contains(&"sym.box"));
3170 assert!(names.contains(&"web.cert"));
3171 assert!(
3172 !names.contains(&"web.value"),
3173 "invisible key must be filtered"
3174 );
3175 let signer = entries.iter().find(|e| e.name == "asym.signer").unwrap();
3177 assert_eq!(signer.kind, CatalogKind::Signing);
3178 assert_eq!(signer.key_type, Some(KeyType::Ed25519));
3179 assert_eq!(signer.latest_version, 5);
3181 let boxx = entries.iter().find(|e| e.name == "sym.box").unwrap();
3182 assert_eq!(boxx.kind, CatalogKind::Encryption);
3183 let cert = entries.iter().find(|e| e.name == "web.cert").unwrap();
3184 assert_eq!(cert.kind, CatalogKind::Signing);
3185 }
3186
3187 #[tokio::test]
3188 async fn list_filters_by_prefix() {
3189 let (mgr, _p, _s) = fixture();
3190 let entries = mgr.list(Some("web."), |_| true).await.expect("list");
3191 let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
3192 assert_eq!(names, vec!["web.cert", "web.value"]);
3193 }
3194
3195 #[tokio::test]
3196 async fn sign_on_value_key_is_op_not_valid_for_class() {
3197 let (mgr, _p, _s) = fixture();
3198 let err = mgr
3199 .sign("web.value", &[1])
3200 .await
3201 .expect_err("sign on a value key must be rejected");
3202 match err {
3203 ManagerError::OpNotValidForClass { op, key, class } => {
3204 assert_eq!(op, "sign");
3205 assert_eq!(key, "web.value");
3206 assert_eq!(class, Class::Value);
3207 }
3208 other => panic!("expected OpNotValidForClass, got {other:?}"),
3209 }
3210 }
3211
3212 #[tokio::test]
3213 async fn new_key_on_value_key_is_op_not_valid_for_class() {
3214 let (mgr, _p, _s) = fixture();
3215 let err = mgr
3216 .new_key("web.value", KeyType::Ed25519)
3217 .await
3218 .expect_err("new_key on a value key must be rejected");
3219 assert!(matches!(err, ManagerError::OpNotValidForClass { .. }));
3220 }
3221
3222 #[tokio::test]
3223 async fn unbacked_op_still_resolves_first_unknown_key_wins() {
3224 let (mgr, _p, _s) = fixture();
3226 assert!(matches!(
3227 mgr.encrypt("nope", AeadAlgorithm::Aes256Gcm, &[1], None)
3228 .await,
3229 Err(ManagerError::UnknownKey(_))
3230 ));
3231 }
3232
3233 #[tokio::test]
3236 async fn set_writes_value_and_get_round_trips_it() {
3237 let (mgr, primary, _s) = fixture();
3238 let version = mgr.set("web.value", b"super-secret").await.expect("set");
3240 assert_eq!(primary.kv_put_calls.load(Ordering::SeqCst), 1);
3241 assert_eq!(version, 6);
3243 assert_eq!(
3245 primary.last_path().as_deref(),
3246 Some("secret/data/web/value")
3247 );
3248
3249 let kv = mgr.get("web.value", None).await.expect("get");
3251 assert_eq!(kv.value, b"super-secret");
3252 assert_eq!(kv.version, 6);
3253 }
3254
3255 #[tokio::test]
3256 async fn get_reads_a_specific_version() {
3257 let (mgr, _p, _s) = fixture();
3258 let kv = mgr.get("web.value", Some(3)).await.expect("get");
3260 assert_eq!(kv.version, 3);
3261 }
3262
3263 #[tokio::test]
3264 async fn get_valid_on_public_class_key() {
3265 let (mgr, _p, secondary) = fixture();
3266 let kv = mgr.get("web.cert", None).await.expect("get on public key");
3268 assert_eq!(kv.value, b"stored-value");
3269 assert_eq!(
3270 secondary.last_path().as_deref(),
3271 Some("secret/data/web/cert")
3272 );
3273 }
3274
3275 #[tokio::test]
3276 async fn get_on_asymmetric_key_is_op_not_valid_for_class() {
3277 let (mgr, primary, _s) = fixture();
3278 let err = mgr
3281 .get("asym.signer", None)
3282 .await
3283 .expect_err("get on a signing key must be rejected");
3284 match err {
3285 ManagerError::OpNotValidForClass { op, key, class } => {
3286 assert_eq!(op, "get");
3287 assert_eq!(key, "asym.signer");
3288 assert_eq!(class, Class::Asymmetric);
3289 }
3290 other => panic!("expected OpNotValidForClass, got {other:?}"),
3291 }
3292 assert_eq!(primary.last_path(), None);
3294 }
3295
3296 #[tokio::test]
3297 async fn set_on_public_key_is_op_not_valid_for_class() {
3298 let (mgr, _p, secondary) = fixture();
3299 let err = mgr
3301 .set("web.cert", b"x")
3302 .await
3303 .expect_err("set on a public key must be rejected");
3304 assert!(matches!(err, ManagerError::OpNotValidForClass { .. }));
3305 assert_eq!(secondary.last_path(), None);
3306 }
3307
3308 #[tokio::test]
3309 async fn set_on_asymmetric_key_is_op_not_valid_for_class() {
3310 let (mgr, _p, _s) = fixture();
3311 let err = mgr
3312 .set("asym.signer", b"x")
3313 .await
3314 .expect_err("set on a signing key must be rejected");
3315 assert!(matches!(err, ManagerError::OpNotValidForClass { .. }));
3316 }
3317
3318 #[tokio::test]
3319 async fn get_unknown_key_wins_over_class() {
3320 let (mgr, _p, _s) = fixture();
3322 assert!(matches!(
3323 mgr.get("does.not.exist", None).await,
3324 Err(ManagerError::UnknownKey(_))
3325 ));
3326 }
3327
3328 #[tokio::test]
3331 async fn encrypt_then_decrypt_round_trips_with_aad() {
3332 let (mgr, _p, secondary) = fixture();
3333 let pt = b"top secret payload";
3334 let aad = b"context-42";
3335 let env = mgr
3336 .encrypt("sym.box", AeadAlgorithm::Aes256Gcm, pt, Some(aad))
3337 .await
3338 .expect("encrypt");
3339 assert_eq!(env.alg, AeadAlgorithm::Aes256Gcm);
3342 assert!(env.nonce.is_empty());
3343 assert_eq!(secondary.last_path().as_deref(), Some("box"));
3344
3345 let recovered = mgr
3346 .decrypt("sym.box", &env, Some(aad))
3347 .await
3348 .expect("decrypt");
3349 assert_eq!(recovered, pt);
3350 }
3351
3352 #[tokio::test]
3353 async fn decrypt_with_wrong_aad_is_opaque_decrypt_failed() {
3354 let (mgr, _p, _s) = fixture();
3355 let env = mgr
3356 .encrypt("sym.box", AeadAlgorithm::Aes256Gcm, b"x", Some(b"good"))
3357 .await
3358 .expect("encrypt");
3359 let err = mgr
3360 .decrypt("sym.box", &env, Some(b"BAD"))
3361 .await
3362 .expect_err("aad mismatch must fail");
3363 assert!(matches!(
3364 err,
3365 ManagerError::Backend(BackendError::DecryptFailed)
3366 ));
3367 }
3368
3369 #[tokio::test]
3370 async fn encrypt_algorithm_must_match_catalog_key_type() {
3371 let (mgr, _p, _s) = fixture();
3372 let err = mgr
3374 .encrypt("sym.box", AeadAlgorithm::Chacha20Poly1305, b"x", None)
3375 .await
3376 .expect_err("algorithm mismatch must be rejected");
3377 assert!(matches!(err, ManagerError::AlgorithmMismatch { .. }));
3378 }
3379
3380 #[tokio::test]
3381 async fn encrypt_on_non_symmetric_key_is_op_not_valid_for_class() {
3382 let (mgr, _p, _s) = fixture();
3383 let err = mgr
3384 .encrypt("asym.signer", AeadAlgorithm::Aes256Gcm, b"x", None)
3385 .await
3386 .expect_err("encrypt on a signing key must be rejected");
3387 assert!(matches!(err, ManagerError::OpNotValidForClass { .. }));
3388 }
3389
3390 #[tokio::test]
3391 async fn decrypt_targets_the_envelope_key_version() {
3392 let (mgr, _p, _s) = fixture();
3396 let mut env = mgr
3397 .encrypt("sym.box", AeadAlgorithm::Aes256Gcm, b"data", None)
3398 .await
3399 .expect("encrypt");
3400 env.key_version = 2; let recovered = mgr.decrypt("sym.box", &env, None).await.expect("decrypt");
3402 assert_eq!(recovered, b"data");
3403 }
3404
3405 #[tokio::test]
3408 async fn rotate_crypto_key_bumps_version_and_sets_grace_floor() {
3409 let (mgr, _p, secondary) = fixture();
3410 let limits = BrokerLimits::default();
3413 let new_version = mgr.rotate("sym.box", limits).await.expect("rotate");
3414 assert_eq!(new_version, 6);
3415 assert_eq!(secondary.rotate_calls.load(Ordering::SeqCst), 1);
3416 assert_eq!(secondary.last_versions_config(), Some((Some(5), None)));
3417 }
3418
3419 #[tokio::test]
3420 async fn rotate_grace_zero_floors_at_latest() {
3421 let (mgr, primary, _s) = fixture();
3422 let limits = BrokerLimits {
3425 grace_versions: 0,
3426 ..BrokerLimits::default()
3427 };
3428 let new_version = mgr.rotate("asym.signer", limits).await.expect("rotate");
3429 assert_eq!(new_version, 6);
3430 assert_eq!(primary.last_versions_config(), Some((Some(6), None)));
3431 }
3432
3433 #[tokio::test]
3434 async fn rotate_value_key_with_recipe_regenerates_a_new_version() {
3435 let (mgr, primary, _s) = fixture();
3436 let new_version = mgr
3439 .rotate("gen.value", BrokerLimits::default())
3440 .await
3441 .expect("rotate");
3442 assert_eq!(primary.kv_put_calls.load(Ordering::SeqCst), 1);
3443 assert_eq!(new_version, 6);
3444 let value = primary.last_kv_value().expect("a value was written");
3446 assert_eq!(value.len(), 24);
3447 assert!(value.iter().all(|b| (b'!'..=b'~').contains(b)));
3448 }
3449
3450 #[tokio::test]
3451 async fn rotate_tls_pair_key_regenerates_matching_cert_side() {
3452 if !step_available() {
3453 return;
3454 }
3455 let (mgr, primary, _s) = fixture();
3456 let new_version = mgr
3457 .rotate("tls.key", BrokerLimits::default())
3458 .await
3459 .expect("rotate tls key");
3460 assert_eq!(new_version, 6);
3461 let writes = primary.kv_put_log();
3462 assert_eq!(writes.len(), 2);
3463 assert_eq!(writes[0].0, "secret/data/tls/key");
3464 assert_eq!(writes[1].0, "secret/data/tls/cert");
3465 assert!(writes[0].1.starts_with(b"-----BEGIN"));
3466 assert!(
3467 writes[0]
3468 .1
3469 .windows(b"PRIVATE KEY".len())
3470 .any(|w| w == b"PRIVATE KEY")
3471 );
3472 assert!(writes[1].1.starts_with(b"-----BEGIN CERTIFICATE-----"));
3473 }
3474
3475 #[test]
3476 fn age_x25519_generate_recipe_emits_parseable_identity() {
3477 let value = generate_value(&GenerateSpec::AgeX25519).expect("age identity generated");
3478 let identity = String::from_utf8(value).expect("age identity is utf8");
3479 assert!(identity.starts_with("AGE-SECRET-KEY-"));
3480 let parsed = identity.parse::<age::x25519::Identity>();
3481 assert!(parsed.is_ok(), "generated identity must parse");
3482 }
3483
3484 fn step_available() -> bool {
3485 Command::new("step")
3486 .arg("version")
3487 .output()
3488 .is_ok_and(|output| output.status.success())
3489 }
3490
3491 #[tokio::test]
3492 async fn rotate_value_key_without_recipe_is_invalid_request() {
3493 let (mgr, _p, _s) = fixture();
3494 let err = mgr
3497 .rotate("web.value", BrokerLimits::default())
3498 .await
3499 .expect_err("value rotate without recipe must fail");
3500 assert!(matches!(err, ManagerError::ValueRotateNeedsSet(_)));
3501 }
3502
3503 #[tokio::test]
3504 async fn sweep_retention_raises_min_available_version() {
3505 let (mgr, _p, secondary) = fixture();
3506 let limits = BrokerLimits {
3508 retain_versions: Some(2),
3509 ..BrokerLimits::default()
3510 };
3511 mgr.sweep_retention("sym.box", limits).await.expect("sweep");
3512 assert_eq!(secondary.last_versions_config(), Some((None, Some(3))));
3513 }
3514
3515 #[tokio::test]
3516 async fn sweep_all_retention_walks_crypto_keys() {
3517 let (mgr, primary, secondary) = fixture();
3518 let limits = BrokerLimits {
3519 retain_versions: Some(2),
3520 ..BrokerLimits::default()
3521 };
3522 mgr.sweep_all_retention(limits)
3523 .await
3524 .expect("catalog sweep");
3525 assert_eq!(primary.last_versions_config(), Some((None, Some(3))));
3526 assert_eq!(secondary.last_versions_config(), Some((None, Some(3))));
3527 }
3528
3529 #[tokio::test]
3530 async fn sweep_retention_disabled_is_a_noop() {
3531 let (mgr, _p, secondary) = fixture();
3532 mgr.sweep_retention("sym.box", BrokerLimits::default())
3534 .await
3535 .expect("sweep");
3536 assert_eq!(secondary.last_versions_config(), None);
3537 }
3538
3539 fn seed_sealing_private(primary: &Arc<MockBackend>) -> [u8; 32] {
3546 let private = Zeroizing::new([0x11u8; 32]);
3547 let public = x25519_seal::public_from_private(&private);
3548 primary.seed_kv("secret/data/enroll/x25519", private.to_vec());
3549 primary.seed_kv("secret/data/enroll/x25519-public", public.to_vec());
3550 public
3551 }
3552 fn seed_ml_kem_private(primary: &Arc<MockBackend>) -> [u8; ml_kem_envelope::SEED_LEN] {
3553 let seed = [0x42; ml_kem_envelope::SEED_LEN];
3554 primary.seed_kv(
3555 "secret/data/enroll/ml-kem-768",
3556 ml_kem_record_bytes("enroll.mlkem", &seed, 5),
3557 );
3558 primary.seed_kv("secret/data/enroll/ml-kem-768-public", vec![0x7A; 1184]);
3559 seed
3560 }
3561 fn ml_kem_record_bytes(
3562 key_id: &str,
3563 seed: &[u8; ml_kem_envelope::SEED_LEN],
3564 key_version: u32,
3565 ) -> Vec<u8> {
3566 let meta = SoftwareCustodyCatalog {
3569 key_id,
3570 algorithm: ml_kem_envelope::KemAlgorithm::MlKem768.token(),
3571 provider: "local-software",
3572 provider_version: "1",
3573 custody: "software-encrypted",
3574 storage_key: "pqc/storage/wrap",
3575 };
3576 let aad = meta.aad(key_version);
3577 let mut ciphertext = Vec::with_capacity(aad.len() + seed.len() + 1);
3578 ciphertext.push(u8::try_from(aad.len()).expect("test aad fits mock envelope"));
3579 ciphertext.extend_from_slice(&aad);
3580 ciphertext.extend_from_slice(seed);
3581 serde_json::json!({
3582 "schemaVersion": 1,
3583 "keyId": key_id,
3584 "keyVersion": key_version,
3585 "publicKey": encode_record_bytes(&[0x7A; 1184]),
3586 "algorithm": meta.algorithm,
3587 "provider": meta.provider,
3588 "providerVersion": meta.provider_version,
3589 "custody": meta.custody,
3590 "encryptedPrivateKey": {
3591 "wrappingKey": meta.storage_key,
3592 "algorithm": "aes-256-gcm",
3593 "keyVersion": key_version,
3594 "nonce": encode_record_bytes(&[]),
3595 "ciphertext": encode_record_bytes(&ciphertext),
3596 }
3597 })
3598 .to_string()
3599 .into_bytes()
3600 }
3601
3602 #[tokio::test]
3603 async fn sealing_key_rejects_get_and_set() {
3604 let (mgr, _p, _s) = fixture();
3607 let get_err = mgr
3608 .get("enroll.sealing", None)
3609 .await
3610 .expect_err("get must be denied on a sealing key");
3611 assert!(matches!(
3612 get_err,
3613 ManagerError::OpNotValidForClass {
3614 op: "get",
3615 class: Class::Sealing,
3616 ..
3617 }
3618 ));
3619 let set_err = mgr
3620 .set("enroll.sealing", b"anything")
3621 .await
3622 .expect_err("set must be denied on a sealing key");
3623 assert!(matches!(
3624 set_err,
3625 ManagerError::OpNotValidForClass {
3626 op: "set",
3627 class: Class::Sealing,
3628 ..
3629 }
3630 ));
3631 }
3632
3633 #[tokio::test]
3634 async fn sealing_key_rejects_sign_and_rotate() {
3635 let (mgr, _p, _s) = fixture();
3637 assert!(matches!(
3638 mgr.sign("enroll.sealing", b"m").await,
3639 Err(ManagerError::OpNotValidForClass { op: "sign", .. })
3640 ));
3641 assert!(matches!(
3642 mgr.rotate("enroll.sealing", BrokerLimits::default()).await,
3643 Err(ManagerError::OpNotValidForClass { op: "rotate", .. })
3644 ));
3645 }
3646
3647 #[tokio::test]
3648 async fn sealing_get_public_key_reads_out_of_band_public() {
3649 let (mgr, primary, _s) = fixture();
3652 let expected_pub = seed_sealing_private(&primary);
3653 let got = mgr
3654 .sealing_public_key("enroll.sealing")
3655 .await
3656 .expect("reads public");
3657 assert_eq!(got, expected_pub);
3658 assert_eq!(
3660 primary.last_path().as_deref(),
3661 Some("secret/data/enroll/x25519-public")
3662 );
3663 }
3664
3665 #[tokio::test]
3666 async fn sealing_public_ops_never_materialize_private() {
3667 let (mgr, primary, _s) = fixture();
3672 let private = Zeroizing::new([0x11u8; 32]);
3673 let public = x25519_seal::public_from_private(&private);
3674 primary.seed_kv("secret/data/enroll/x25519-public", public.to_vec());
3675 primary.seed_kv("secret/data/enroll/x25519", vec![0xFF; 7]);
3676
3677 let got = mgr
3678 .sealing_public_key("enroll.sealing")
3679 .await
3680 .expect("public read despite garbage private");
3681 assert_eq!(got, public);
3682 mgr.wrap_envelope("enroll.sealing", b"payload", b"ctx")
3685 .await
3686 .expect("wrap reads only the public");
3687 }
3688
3689 #[tokio::test]
3690 async fn sealing_wrap_then_unwrap_round_trips() {
3691 let (mgr, primary, _s) = fixture();
3694 seed_sealing_private(&primary);
3695 let plaintext = b"enrollment-payload";
3696 let aad = b"enroll-ctx";
3697 let env = mgr
3698 .wrap_envelope("enroll.sealing", plaintext, aad)
3699 .await
3700 .expect("wrap");
3701 let recovered = mgr
3702 .unwrap_envelope("enroll.sealing", &env, aad)
3703 .await
3704 .expect("unwrap");
3705 assert_eq!(recovered.as_slice(), plaintext);
3706 }
3707
3708 #[tokio::test]
3709 async fn kv_get_secret_returns_zeroizing_bytes_and_round_trips() {
3710 let backend = MockHandle(MockBackend::new("primary"));
3714 let stored = vec![0x11u8; 32];
3715 backend
3716 .kv_put("secret/data/enroll/x25519", &stored)
3717 .await
3718 .expect("put");
3719 let got: Zeroizing<Vec<u8>> = backend
3720 .kv_get_secret("secret/data/enroll/x25519", None)
3721 .await
3722 .expect("secret read");
3723 assert_eq!(got.as_slice(), stored.as_slice());
3724 }
3725
3726 #[tokio::test]
3727 async fn sealing_unwrap_opens_externally_sealed_payload() {
3728 let (mgr, primary, _s) = fixture();
3732 let recipient_pub = seed_sealing_private(&primary);
3733 let env = x25519_seal::seal(&recipient_pub, b"sealed-by-sender", b"ctx").expect("seal");
3734 let recovered = mgr
3735 .unwrap_envelope("enroll.sealing", &env, b"ctx")
3736 .await
3737 .expect("unwrap");
3738 assert_eq!(recovered.as_slice(), b"sealed-by-sender");
3739 }
3740
3741 fn ml_kem_parts(env: &ml_kem_envelope::MlKemEnvelope) -> MlKemEnvelopeParts<'_> {
3744 MlKemEnvelopeParts {
3745 encapsulated_key: &env.encapsulated_key,
3746 nonce: &env.nonce,
3747 ciphertext: &env.ciphertext,
3748 }
3749 }
3750 #[tokio::test]
3751 async fn ml_kem_provider_wrap_then_unwrap_round_trips() {
3752 for envelope_algorithm in [
3753 ProviderEnvelopeAlgorithm::Aes256Gcm,
3754 ProviderEnvelopeAlgorithm::ChaCha20Poly1305,
3755 ] {
3756 let (mgr, primary, _s) = fixture();
3757 seed_ml_kem_private(&primary);
3758 let (envelope, dispatch) = mgr
3759 .provider_wrap_envelope(
3760 "enroll.mlkem",
3761 ProviderKemAlgorithm::MlKem768,
3762 envelope_algorithm,
3763 b"top secret",
3764 b"ctx",
3765 ProviderGate {
3766 local_software_allowed: true,
3767 },
3768 )
3769 .await
3770 .expect("wrap");
3771 assert_eq!(envelope.kem_algorithm, ProviderKemAlgorithm::MlKem768);
3773 assert_eq!(envelope.key_version, 5);
3774 assert_eq!(dispatch.algorithm, "ml-kem-768");
3775 assert_eq!(dispatch.provider, CryptoProviderId::LocalSoftware);
3776
3777 let (recovered, _) = mgr
3778 .provider_unwrap_envelope(
3779 "enroll.mlkem",
3780 ProviderKemAlgorithm::MlKem768,
3781 envelope_algorithm,
3782 MlKemEnvelopeParts {
3783 encapsulated_key: &envelope.encapsulated_key,
3784 nonce: &envelope.nonce,
3785 ciphertext: &envelope.ciphertext,
3786 },
3787 b"ctx",
3788 ProviderGate {
3789 local_software_allowed: true,
3790 },
3791 )
3792 .await
3793 .expect("unwrap");
3794 assert_eq!(recovered.as_slice(), b"top secret");
3795 }
3796 }
3797 #[tokio::test]
3798 async fn ml_kem_provider_unwrap_opens_externally_sealed_payload() {
3799 let (mgr, primary, _s) = fixture();
3800 let seed = seed_ml_kem_private(&primary);
3801 let env = ml_kem_envelope::seal(
3804 &seed,
3805 ml_kem_envelope::KemAlgorithm::MlKem768,
3806 ml_kem_envelope::EnvelopeAlgorithm::ChaCha20Poly1305,
3807 b"sealed-by-sender",
3808 b"ctx",
3809 )
3810 .expect("seal");
3811 let (recovered, _) = mgr
3812 .provider_unwrap_envelope(
3813 "enroll.mlkem",
3814 ProviderKemAlgorithm::MlKem768,
3815 ProviderEnvelopeAlgorithm::ChaCha20Poly1305,
3816 ml_kem_parts(&env),
3817 b"ctx",
3818 ProviderGate {
3819 local_software_allowed: true,
3820 },
3821 )
3822 .await
3823 .expect("unwrap");
3824 assert_eq!(recovered.as_slice(), b"sealed-by-sender");
3825 }
3826
3827 fn ml_kem_record_with(public: &[u8], wrapped_seed: &[u8], key_version: u32) -> Vec<u8> {
3831 let meta = SoftwareCustodyCatalog {
3832 key_id: "enroll.mlkem",
3833 algorithm: ml_kem_envelope::KemAlgorithm::MlKem768.token(),
3834 provider: "local-software",
3835 provider_version: "1",
3836 custody: "software-encrypted",
3837 storage_key: "pqc/storage/wrap",
3838 };
3839 serde_json::json!({
3840 "schemaVersion": 1,
3841 "keyId": meta.key_id,
3842 "keyVersion": key_version,
3843 "publicKey": encode_record_bytes(public),
3844 "algorithm": meta.algorithm,
3845 "provider": meta.provider,
3846 "providerVersion": meta.provider_version,
3847 "custody": meta.custody,
3848 "encryptedPrivateKey": {
3849 "wrappingKey": meta.storage_key,
3850 "algorithm": "aes-256-gcm",
3851 "keyVersion": key_version,
3852 "nonce": encode_record_bytes(&[]),
3853 "ciphertext": encode_record_bytes(wrapped_seed),
3854 }
3855 })
3856 .to_string()
3857 .into_bytes()
3858 }
3859 #[tokio::test]
3860 async fn ml_kem_sealing_get_public_key_returns_encapsulation_key_without_seed() {
3861 let (mgr, primary, _s) = fixture();
3869 let encapsulation_key = vec![0xABu8; 1184];
3870 primary.seed_kv(
3871 "secret/data/enroll/ml-kem-768",
3872 ml_kem_record_with(&encapsulation_key, &[], 5),
3875 );
3876
3877 let got = mgr
3878 .get_public_key("enroll.mlkem")
3879 .await
3880 .expect("ML-KEM sealing public-encapsulation-key read");
3881 assert_eq!(got.public_key, encapsulation_key);
3882 assert_eq!(got.key_type, KeyType::MlKem768);
3883 assert_eq!(got.version, 1);
3885 assert_eq!(
3888 primary.last_path().as_deref(),
3889 Some("secret/data/enroll/ml-kem-768")
3890 );
3891 }
3892 #[tokio::test]
3893 async fn get_public_key_rejects_non_ml_kem_sealing_and_value_classes() {
3894 let (mgr, _p, _s) = fixture();
3898 assert!(matches!(
3899 mgr.get_public_key("enroll.sealing").await,
3900 Err(ManagerError::OpNotValidForClass {
3901 op: "get_public_key",
3902 class: Class::Sealing,
3903 ..
3904 })
3905 ));
3906 assert!(matches!(
3907 mgr.get_public_key("web.value").await,
3908 Err(ManagerError::OpNotValidForClass {
3909 op: "get_public_key",
3910 class: Class::Value,
3911 ..
3912 })
3913 ));
3914 }
3915 #[tokio::test]
3916 async fn ml_kem_provider_unwrap_tampered_ciphertext_fails_opaque() {
3917 let (mgr, primary, _s) = fixture();
3918 seed_ml_kem_private(&primary);
3919 let (mut envelope, _) = mgr
3920 .provider_wrap_envelope(
3921 "enroll.mlkem",
3922 ProviderKemAlgorithm::MlKem768,
3923 ProviderEnvelopeAlgorithm::Aes256Gcm,
3924 b"top secret",
3925 b"ctx",
3926 ProviderGate {
3927 local_software_allowed: true,
3928 },
3929 )
3930 .await
3931 .expect("wrap");
3932 if let Some(b) = envelope.ciphertext.first_mut() {
3933 *b ^= 0xFF;
3934 }
3935 let err = mgr
3936 .provider_unwrap_envelope(
3937 "enroll.mlkem",
3938 ProviderKemAlgorithm::MlKem768,
3939 ProviderEnvelopeAlgorithm::Aes256Gcm,
3940 MlKemEnvelopeParts {
3941 encapsulated_key: &envelope.encapsulated_key,
3942 nonce: &envelope.nonce,
3943 ciphertext: &envelope.ciphertext,
3944 },
3945 b"ctx",
3946 ProviderGate {
3947 local_software_allowed: true,
3948 },
3949 )
3950 .await
3951 .expect_err("tampered ciphertext must fail opaque");
3952 assert!(matches!(
3953 err,
3954 ManagerError::Provider(ProviderError::CryptoFailed {
3955 op: "unwrap_envelope",
3956 ..
3957 })
3958 ));
3959 }
3960 #[tokio::test]
3961 async fn ml_kem_provider_unwrap_wrong_aad_fails_opaque() {
3962 let (mgr, primary, _s) = fixture();
3963 seed_ml_kem_private(&primary);
3964 let (envelope, _) = mgr
3965 .provider_wrap_envelope(
3966 "enroll.mlkem",
3967 ProviderKemAlgorithm::MlKem768,
3968 ProviderEnvelopeAlgorithm::ChaCha20Poly1305,
3969 b"top secret",
3970 b"right",
3971 ProviderGate {
3972 local_software_allowed: true,
3973 },
3974 )
3975 .await
3976 .expect("wrap");
3977 let err = mgr
3978 .provider_unwrap_envelope(
3979 "enroll.mlkem",
3980 ProviderKemAlgorithm::MlKem768,
3981 ProviderEnvelopeAlgorithm::ChaCha20Poly1305,
3982 MlKemEnvelopeParts {
3983 encapsulated_key: &envelope.encapsulated_key,
3984 nonce: &envelope.nonce,
3985 ciphertext: &envelope.ciphertext,
3986 },
3987 b"wrong",
3988 ProviderGate {
3989 local_software_allowed: true,
3990 },
3991 )
3992 .await
3993 .expect_err("wrong aad must fail opaque");
3994 assert!(matches!(
3995 err,
3996 ManagerError::Provider(ProviderError::CryptoFailed { .. })
3997 ));
3998 }
3999 #[tokio::test]
4000 async fn ml_kem_software_custody_rejects_raw_seed_storage() {
4001 let (mgr, primary, _s) = fixture();
4002 let seed = [0x42; ml_kem_envelope::SEED_LEN];
4003 primary.seed_kv("secret/data/enroll/ml-kem-768", seed.to_vec());
4004 let env = ml_kem_envelope::seal(
4005 &seed,
4006 ml_kem_envelope::KemAlgorithm::MlKem768,
4007 ml_kem_envelope::EnvelopeAlgorithm::ChaCha20Poly1305,
4008 b"sealed-by-sender",
4009 b"ctx",
4010 )
4011 .expect("seal");
4012 let err = mgr
4013 .provider_unwrap_envelope(
4014 "enroll.mlkem",
4015 ProviderKemAlgorithm::MlKem768,
4016 ProviderEnvelopeAlgorithm::ChaCha20Poly1305,
4017 ml_kem_parts(&env),
4018 b"ctx",
4019 ProviderGate {
4020 local_software_allowed: true,
4021 },
4022 )
4023 .await
4024 .expect_err("raw private seed storage must be rejected");
4025 assert!(matches!(
4026 err,
4027 ManagerError::Provider(ProviderError::CryptoFailed { .. })
4028 ));
4029 assert_eq!(
4030 primary.last_path().as_deref(),
4031 Some("secret/data/enroll/ml-kem-768")
4032 );
4033 }
4034 #[tokio::test]
4035 async fn ml_kem_software_custody_rejects_record_metadata_mismatch() {
4036 let (mgr, primary, _s) = fixture();
4037 let seed = [0x42; ml_kem_envelope::SEED_LEN];
4038 primary.seed_kv(
4040 "secret/data/enroll/ml-kem-768",
4041 ml_kem_record_bytes("enroll.mlkem", &seed, 4),
4042 );
4043 let env = ml_kem_envelope::seal(
4044 &seed,
4045 ml_kem_envelope::KemAlgorithm::MlKem768,
4046 ml_kem_envelope::EnvelopeAlgorithm::ChaCha20Poly1305,
4047 b"sealed-by-sender",
4048 b"ctx",
4049 )
4050 .expect("seal");
4051 let err = mgr
4052 .provider_unwrap_envelope(
4053 "enroll.mlkem",
4054 ProviderKemAlgorithm::MlKem768,
4055 ProviderEnvelopeAlgorithm::ChaCha20Poly1305,
4056 ml_kem_parts(&env),
4057 b"ctx",
4058 ProviderGate {
4059 local_software_allowed: true,
4060 },
4061 )
4062 .await
4063 .expect_err("KV version mismatch must fail before decrypt");
4064 assert!(matches!(
4065 err,
4066 ManagerError::Provider(ProviderError::CryptoFailed { .. })
4067 ));
4068 assert_eq!(
4069 primary.last_path().as_deref(),
4070 Some("secret/data/enroll/ml-kem-768")
4071 );
4072 }
4073
4074 #[tokio::test]
4075 async fn ml_kem_unwrap_rejects_wrong_key_type() {
4076 let (mgr, _primary, _s) = fixture();
4077 let seed = [0x42; ml_kem_envelope::SEED_LEN];
4080 let env = ml_kem_envelope::seal(
4081 &seed,
4082 ml_kem_envelope::KemAlgorithm::MlKem768,
4083 ml_kem_envelope::EnvelopeAlgorithm::ChaCha20Poly1305,
4084 b"sealed-by-sender",
4085 b"ctx",
4086 )
4087 .expect("seal");
4088 let err = mgr
4091 .provider_unwrap_envelope(
4092 "enroll.sealing",
4093 ProviderKemAlgorithm::MlKem768,
4094 ProviderEnvelopeAlgorithm::ChaCha20Poly1305,
4095 ml_kem_parts(&env),
4096 b"ctx",
4097 ProviderGate {
4098 local_software_allowed: true,
4099 },
4100 )
4101 .await
4102 .expect_err("x25519 key cannot unwrap ML-KEM");
4103 assert!(matches!(err, ManagerError::KemAlgorithmMismatch { .. }));
4104 }
4105
4106 #[tokio::test]
4107 async fn ml_kem_wrap_rejects_wrong_kem_param_set() {
4108 let (mgr, _primary, _s) = fixture();
4109 let err = mgr
4112 .provider_wrap_envelope(
4113 "enroll.mlkem",
4114 ProviderKemAlgorithm::MlKem512,
4115 ProviderEnvelopeAlgorithm::ChaCha20Poly1305,
4116 b"top secret",
4117 b"ctx",
4118 ProviderGate {
4119 local_software_allowed: true,
4120 },
4121 )
4122 .await
4123 .expect_err("wrong KEM param set must be rejected");
4124 assert!(matches!(err, ManagerError::KemAlgorithmMismatch { .. }));
4125 }
4126
4127 #[tokio::test]
4128 async fn ml_kem_wrap_and_unwrap_without_grant_denied() {
4129 let (mgr, _primary, _s) = fixture();
4130 let deny = ProviderGate {
4131 local_software_allowed: false,
4132 };
4133 let wrap_err = mgr
4134 .provider_wrap_envelope(
4135 "enroll.mlkem",
4136 ProviderKemAlgorithm::MlKem768,
4137 ProviderEnvelopeAlgorithm::ChaCha20Poly1305,
4138 b"top secret",
4139 b"ctx",
4140 deny,
4141 )
4142 .await
4143 .expect_err("wrap denied without local-software grant");
4144 assert!(matches!(
4145 wrap_err,
4146 ManagerError::Provider(ProviderError::PolicyDenied { .. })
4147 ));
4148 let unwrap_err = mgr
4149 .provider_unwrap_envelope(
4150 "enroll.mlkem",
4151 ProviderKemAlgorithm::MlKem768,
4152 ProviderEnvelopeAlgorithm::ChaCha20Poly1305,
4153 MlKemEnvelopeParts {
4154 encapsulated_key: b"x",
4155 nonce: &[0u8; 12],
4156 ciphertext: b"y",
4157 },
4158 b"ctx",
4159 deny,
4160 )
4161 .await
4162 .expect_err("unwrap denied without local-software grant");
4163 assert!(matches!(
4164 unwrap_err,
4165 ManagerError::Provider(ProviderError::PolicyDenied { .. })
4166 ));
4167 }
4168
4169 #[tokio::test]
4170 async fn sealing_unwrap_wrong_aad_fails_opaque() {
4171 let (mgr, primary, _s) = fixture();
4172 let recipient_pub = seed_sealing_private(&primary);
4173 let env = x25519_seal::seal(&recipient_pub, b"x", b"right").expect("seal");
4174 let err = mgr
4175 .unwrap_envelope("enroll.sealing", &env, b"wrong")
4176 .await
4177 .expect_err("aad mismatch must fail");
4178 assert!(matches!(
4179 err,
4180 ManagerError::Sealing(SealingFailure::OpenFailed)
4181 ));
4182 }
4183
4184 #[tokio::test]
4185 async fn sealing_unwrap_tampered_ciphertext_fails_opaque() {
4186 let (mgr, primary, _s) = fixture();
4187 let recipient_pub = seed_sealing_private(&primary);
4188 let mut env = x25519_seal::seal(&recipient_pub, b"payload", b"aad").expect("seal");
4189 if let Some(b) = env.ciphertext.first_mut() {
4190 *b ^= 0xFF;
4191 }
4192 assert!(matches!(
4193 mgr.unwrap_envelope("enroll.sealing", &env, b"aad").await,
4194 Err(ManagerError::Sealing(SealingFailure::OpenFailed))
4195 ));
4196 }
4197
4198 #[tokio::test]
4199 async fn sealing_unwrap_malformed_private_fails() {
4200 let (mgr, primary, _s) = fixture();
4203 *primary.last_kv_value.lock().unwrap() = Some(vec![0u8; 16]);
4204 let env = SealedEnvelope {
4205 encapsulated_key: [9u8; 32],
4206 nonce: [0u8; 12],
4207 ciphertext: vec![1, 2, 3],
4208 };
4209 assert!(matches!(
4210 mgr.unwrap_envelope("enroll.sealing", &env, b"").await,
4211 Err(ManagerError::Sealing(SealingFailure::Malformed))
4212 ));
4213 }
4214
4215 #[tokio::test]
4216 async fn wrap_unwrap_reject_non_sealing_key() {
4217 let (mgr, _p, _s) = fixture();
4220 assert!(matches!(
4221 mgr.wrap_envelope("sym.box", b"x", b"").await,
4222 Err(ManagerError::OpNotValidForClass {
4223 op: "wrap_envelope",
4224 ..
4225 })
4226 ));
4227 let env = SealedEnvelope {
4228 encapsulated_key: [0u8; 32],
4229 nonce: [0u8; 12],
4230 ciphertext: vec![],
4231 };
4232 assert!(matches!(
4233 mgr.unwrap_envelope("sym.box", &env, b"").await,
4234 Err(ManagerError::OpNotValidForClass {
4235 op: "unwrap_envelope",
4236 ..
4237 })
4238 ));
4239 }
4240
4241 fn seed_signing_seed(primary: &Arc<MockBackend>) -> [u8; 32] {
4248 let seed = Zeroizing::new([0x11u8; 32]);
4249 let public = ed25519_sign::public_from_seed(&seed);
4250 primary.seed_kv("secret/data/kv2/signer", seed.to_vec());
4251 primary.seed_kv("secret/data/kv2/signer-public", public.to_vec());
4252 public
4253 }
4254
4255 #[tokio::test]
4256 async fn kv2_signer_resolves_as_asymmetric_kv2() {
4257 let (mgr, _p, _s) = fixture();
4258 let routed = mgr.resolve("kv2.signer").expect("resolves");
4259 assert_eq!(routed.class(), Class::Asymmetric);
4260 assert_eq!(routed.engine, Engine::Kv2);
4261 assert_eq!(routed.key_type(), Some(KeyAlgorithm::Ed25519));
4262 }
4263
4264 #[tokio::test]
4265 async fn kv2_signer_rejects_get_and_set() {
4266 let (mgr, _p, _s) = fixture();
4271 let get_err = mgr
4272 .get("kv2.signer", None)
4273 .await
4274 .expect_err("get must be denied on a value-store signing key");
4275 assert!(matches!(
4276 get_err,
4277 ManagerError::OpNotValidForClass {
4278 op: "get",
4279 class: Class::Asymmetric,
4280 ..
4281 }
4282 ));
4283 let set_err = mgr
4284 .set("kv2.signer", b"anything")
4285 .await
4286 .expect_err("set must be denied on a value-store signing key");
4287 assert!(matches!(
4288 set_err,
4289 ManagerError::OpNotValidForClass {
4290 op: "set",
4291 class: Class::Asymmetric,
4292 ..
4293 }
4294 ));
4295 }
4296
4297 #[tokio::test]
4298 async fn kv2_signer_rejects_rotate_and_import() {
4299 let (mgr, _p, _s) = fixture();
4304 assert!(matches!(
4305 mgr.rotate("kv2.signer", BrokerLimits::default()).await,
4306 Err(ManagerError::Unsupported(m)) if m.starts_with("rotate")
4307 ));
4308 assert!(matches!(
4309 mgr.import(
4310 "kv2.signer",
4311 KeyType::Ed25519,
4312 &KeyMaterial::Ed25519Seed(vec![0; 32]),
4313 )
4314 .await,
4315 Err(ManagerError::Unsupported(m)) if m.starts_with("import")
4316 ));
4317 }
4318
4319 #[tokio::test]
4320 async fn kv2_signer_materializes_and_signs_matching_in_proc() {
4321 let (mgr, primary, _s) = fixture();
4324 let public = seed_signing_seed(&primary);
4325 let message = b"sign me with a materialized seed";
4326
4327 let sig = mgr.sign("kv2.signer", message).await.expect("sign");
4328 assert_eq!(sig.len(), ed25519_sign::SIGNATURE_LEN);
4329 let expected = ed25519_sign::sign(&Zeroizing::new([0x11u8; 32]), message);
4331 assert_eq!(sig.as_slice(), expected.as_slice());
4332 assert_eq!(ed25519_sign::verify(&public, message, &sig), Ok(true));
4334 assert_eq!(
4337 primary.last_path().as_deref(),
4338 Some("secret/data/kv2/signer")
4339 );
4340 }
4341
4342 #[tokio::test]
4343 async fn kv2_signer_verify_round_trips_in_proc() {
4344 let (mgr, primary, _s) = fixture();
4345 seed_signing_seed(&primary);
4346 let message = b"verify me";
4347 let sig = mgr.sign("kv2.signer", message).await.expect("sign");
4348 assert!(
4349 mgr.verify("kv2.signer", message, &sig)
4350 .await
4351 .expect("verify")
4352 );
4353 assert!(
4355 !mgr.verify("kv2.signer", b"verify-tampered", &sig)
4356 .await
4357 .expect("verify")
4358 );
4359 }
4360
4361 #[tokio::test]
4362 async fn kv2_signer_get_public_key_reads_out_of_band_public() {
4363 let (mgr, primary, _s) = fixture();
4366 let expected_pub = seed_signing_seed(&primary);
4367 let pk = mgr.get_public_key("kv2.signer").await.expect("public");
4368 assert_eq!(pk.public_key.as_slice(), expected_pub.as_slice());
4369 assert_eq!(pk.key_type, KeyType::Ed25519);
4370 assert_eq!(
4372 primary.last_path().as_deref(),
4373 Some("secret/data/kv2/signer-public")
4374 );
4375 }
4376
4377 #[tokio::test]
4378 async fn kv2_signer_public_ops_never_materialize_seed() {
4379 let (mgr, primary, _s) = fixture();
4385 let seed = Zeroizing::new([0x11u8; 32]);
4386 let public = ed25519_sign::public_from_seed(&seed);
4387 primary.seed_kv("secret/data/kv2/signer-public", public.to_vec());
4388 primary.seed_kv("secret/data/kv2/signer", vec![0xFF; 7]);
4389
4390 let pk = mgr
4391 .get_public_key("kv2.signer")
4392 .await
4393 .expect("public read despite garbage seed");
4394 assert_eq!(pk.public_key.as_slice(), public.as_slice());
4395 let sig = ed25519_sign::sign(&seed, b"m");
4398 assert!(
4399 mgr.verify("kv2.signer", b"m", &sig)
4400 .await
4401 .expect("verify reads only the public")
4402 );
4403 }
4404
4405 #[tokio::test]
4406 async fn materialize_public_op_without_public_path_fails_closed() {
4407 const NO_PUB: &str = r#"{
4412 "schemaVersion": 1,
4413 "backends": { "primary": { "kind": "vault", "addr": "https://127.0.0.1:8200" } },
4414 "keys": {
4415 "enroll.sealing": {
4416 "class": "sealing", "keyType": "x25519", "backend": "primary", "engine": "kv2",
4417 "path": "secret/data/enroll/x25519", "writable": true, "missing": "error",
4418 "description": "a sealing key missing its publicPath"
4419 }
4420 }
4421 }"#;
4422 let primary = MockBackend::new("primary");
4423 let mut backends: BTreeMap<String, Box<dyn Backend>> = BTreeMap::new();
4424 backends.insert("primary".into(), Box::new(MockHandle(primary)));
4425 let mgr = BackendManager::new(parse_catalog(NO_PUB), backends).expect("constructs");
4426
4427 assert!(matches!(
4428 mgr.sealing_public_key("enroll.sealing").await,
4429 Err(ManagerError::MissingPublicPath(k)) if k == "enroll.sealing"
4430 ));
4431 assert!(matches!(
4432 mgr.wrap_envelope("enroll.sealing", b"x", b"a").await,
4433 Err(ManagerError::MissingPublicPath(_))
4434 ));
4435 }
4436
4437 #[tokio::test]
4438 async fn kv2_signer_malformed_seed_is_signing_failure_not_panic() {
4439 let (mgr, primary, _s) = fixture();
4442 *primary.last_kv_value.lock().unwrap() = Some(vec![0u8; 16]);
4443 assert!(matches!(
4444 mgr.sign("kv2.signer", b"m").await,
4445 Err(ManagerError::Signing(SigningFailure::Malformed))
4446 ));
4447 }
4448
4449 #[tokio::test]
4450 async fn kv2_signer_verify_rejects_wrong_length_signature_opaquely() {
4451 let (mgr, primary, _s) = fixture();
4454 seed_signing_seed(&primary);
4455 assert!(matches!(
4456 mgr.verify("kv2.signer", b"m", &[0u8; 10]).await,
4457 Err(ManagerError::Signing(SigningFailure::Malformed))
4458 ));
4459 }
4460
4461 #[tokio::test]
4462 async fn transit_signer_still_uses_backend_sign_in_place() {
4463 let (mgr, primary, _s) = fixture();
4466 let sig = mgr.sign("asym.signer", b"m").await.expect("sign");
4467 assert_eq!(sig, vec![0xAB, 0xCD]);
4470 assert_eq!(primary.last_path().as_deref(), Some("signer"));
4471 }
4472}
4473
4474#[cfg(test)]
4475mod pqc_dispatch_tests {
4476 use std::collections::{BTreeMap, HashMap};
4477 use std::sync::Mutex;
4478
4479 use async_trait::async_trait;
4480
4481 use super::{
4482 BackendManager, Catalog, CryptoProviderId, CustodyMode, KeyProviderDescriptor,
4483 ManagerError, ProviderError, ProviderGate, ProviderPolicy,
4484 };
4485 use crate::backend::{Backend, BackendError, KvValue, NativeAlgorithm, NewKey};
4486 use crate::core::crypto_provider::{ProviderAuditEvent, ProviderAuditOutcome};
4487 use crate::core::ml_dsa_sign::{self, MlDsaAlgorithm};
4488 use basil_proto::{AeadAlgorithm, CiphertextEnvelope, KeyType};
4489
4490 #[derive(Default)]
4495 struct PqcBackend {
4496 store: Mutex<HashMap<String, Vec<u8>>>,
4497 }
4498
4499 #[async_trait]
4500 impl Backend for PqcBackend {
4501 fn kind(&self) -> &'static str {
4502 "pqc-dispatch-test"
4503 }
4504
4505 async fn new_key(&self, _key_type: KeyType) -> Result<NewKey, BackendError> {
4506 Err(BackendError::Unsupported("new_key"))
4507 }
4508
4509 async fn public_key(&self, _key_id: &str) -> Result<Vec<u8>, BackendError> {
4510 Err(BackendError::Unsupported("public_key"))
4511 }
4512
4513 async fn sign(&self, _key_id: &str, _message: &[u8]) -> Result<Vec<u8>, BackendError> {
4514 Err(BackendError::Unsupported("sign"))
4515 }
4516
4517 async fn verify(
4518 &self,
4519 _key_id: &str,
4520 _message: &[u8],
4521 _signature: &[u8],
4522 ) -> Result<bool, BackendError> {
4523 Err(BackendError::Unsupported("verify"))
4524 }
4525
4526 async fn encrypt(
4527 &self,
4528 _key_id: &str,
4529 algorithm: AeadAlgorithm,
4530 plaintext: &[u8],
4531 aad: Option<&[u8]>,
4532 ) -> Result<CiphertextEnvelope, BackendError> {
4533 let aad = aad.unwrap_or(&[]);
4534 let mut ciphertext = vec![u8::try_from(aad.len()).unwrap_or(u8::MAX)];
4535 ciphertext.extend_from_slice(aad);
4536 ciphertext.extend_from_slice(plaintext);
4537 Ok(CiphertextEnvelope {
4538 alg: algorithm,
4539 key_version: 1,
4540 nonce: Vec::new(),
4541 ciphertext,
4542 })
4543 }
4544
4545 async fn decrypt(
4546 &self,
4547 _key_id: &str,
4548 envelope: &CiphertextEnvelope,
4549 aad: Option<&[u8]>,
4550 ) -> Result<Vec<u8>, BackendError> {
4551 let aad = aad.unwrap_or(&[]);
4552 let ct = &envelope.ciphertext;
4553 let aad_len = *ct.first().ok_or(BackendError::DecryptFailed)? as usize;
4554 let bound = ct.get(1..1 + aad_len).ok_or(BackendError::DecryptFailed)?;
4555 if bound != aad {
4556 return Err(BackendError::DecryptFailed);
4557 }
4558 Ok(ct
4559 .get(1 + aad_len..)
4560 .ok_or(BackendError::DecryptFailed)?
4561 .to_vec())
4562 }
4563
4564 async fn kv_put(&self, key_id: &str, value: &[u8]) -> Result<u32, BackendError> {
4565 self.store
4566 .lock()
4567 .map_err(|_| BackendError::Unsupported("kv_put"))?
4568 .insert(key_id.to_string(), value.to_vec());
4569 Ok(1)
4570 }
4571
4572 async fn kv_get(
4573 &self,
4574 key_id: &str,
4575 _version: Option<u32>,
4576 ) -> Result<KvValue, BackendError> {
4577 let value = self
4578 .store
4579 .lock()
4580 .map_err(|_| BackendError::Unsupported("kv_get"))?
4581 .get(key_id)
4582 .cloned();
4583 value
4584 .map(|value| KvValue { value, version: 1 })
4585 .ok_or(BackendError::Unsupported("kv_get"))
4586 }
4587 }
4588
4589 #[derive(Default)]
4598 struct NativePqcBackend {
4599 seeds: Mutex<HashMap<String, [u8; 32]>>,
4600 store: Mutex<HashMap<String, Vec<u8>>>,
4601 }
4602
4603 impl NativePqcBackend {
4604 const fn dsa(native: NativeAlgorithm) -> MlDsaAlgorithm {
4605 match native {
4606 NativeAlgorithm::MlDsa44 => MlDsaAlgorithm::MlDsa44,
4607 NativeAlgorithm::MlDsa65 => MlDsaAlgorithm::MlDsa65,
4608 NativeAlgorithm::MlDsa87 => MlDsaAlgorithm::MlDsa87,
4609 }
4610 }
4611
4612 fn seed_for(key_id: &str) -> [u8; 32] {
4615 let mut seed = [7u8; 32];
4616 for (slot, byte) in seed.iter_mut().zip(key_id.bytes()) {
4617 *slot = byte;
4618 }
4619 seed
4620 }
4621 }
4622
4623 #[async_trait]
4624 impl Backend for NativePqcBackend {
4625 fn kind(&self) -> &'static str {
4626 "pqc-native-test"
4627 }
4628
4629 async fn new_key(&self, _key_type: KeyType) -> Result<NewKey, BackendError> {
4630 Err(BackendError::Unsupported("new_key"))
4631 }
4632
4633 async fn public_key(&self, _key_id: &str) -> Result<Vec<u8>, BackendError> {
4634 Err(BackendError::Unsupported("public_key"))
4635 }
4636
4637 async fn sign(&self, _key_id: &str, _message: &[u8]) -> Result<Vec<u8>, BackendError> {
4638 Err(BackendError::Unsupported("sign"))
4639 }
4640
4641 async fn verify(
4642 &self,
4643 _key_id: &str,
4644 _message: &[u8],
4645 _signature: &[u8],
4646 ) -> Result<bool, BackendError> {
4647 Err(BackendError::Unsupported("verify"))
4648 }
4649
4650 fn supports_native_algorithm(&self, algorithm: NativeAlgorithm) -> bool {
4651 matches!(
4652 algorithm,
4653 NativeAlgorithm::MlDsa44 | NativeAlgorithm::MlDsa65 | NativeAlgorithm::MlDsa87
4654 )
4655 }
4656
4657 async fn create_named_pqc_key(
4658 &self,
4659 key_id: &str,
4660 algorithm: NativeAlgorithm,
4661 ) -> Result<NewKey, BackendError> {
4662 let seed = Self::seed_for(key_id);
4663 let public = ml_dsa_sign::public_from_seed(Self::dsa(algorithm), &seed)
4664 .map_err(|_| BackendError::Unsupported("create_named_pqc_key"))?;
4665 self.seeds
4666 .lock()
4667 .map_err(|_| BackendError::Unsupported("create_named_pqc_key"))?
4668 .insert(key_id.to_string(), seed);
4669 Ok(NewKey {
4670 key_id: key_id.to_string(),
4671 public_key: public,
4672 })
4673 }
4674
4675 async fn sign_pqc(
4676 &self,
4677 key_id: &str,
4678 message: &[u8],
4679 algorithm: NativeAlgorithm,
4680 ) -> Result<Vec<u8>, BackendError> {
4681 let seed = self
4682 .seeds
4683 .lock()
4684 .map_err(|_| BackendError::Unsupported("sign_pqc"))?
4685 .get(key_id)
4686 .copied()
4687 .ok_or_else(|| BackendError::KeyNotFound(key_id.to_string()))?;
4688 ml_dsa_sign::sign(Self::dsa(algorithm), &seed, message)
4689 .map_err(|_| BackendError::Unsupported("sign_pqc"))
4690 }
4691
4692 async fn verify_pqc(
4693 &self,
4694 key_id: &str,
4695 message: &[u8],
4696 signature: &[u8],
4697 algorithm: NativeAlgorithm,
4698 ) -> Result<bool, BackendError> {
4699 let seed = self
4700 .seeds
4701 .lock()
4702 .map_err(|_| BackendError::Unsupported("verify_pqc"))?
4703 .get(key_id)
4704 .copied()
4705 .ok_or_else(|| BackendError::KeyNotFound(key_id.to_string()))?;
4706 let public = ml_dsa_sign::public_from_seed(Self::dsa(algorithm), &seed)
4707 .map_err(|_| BackendError::Unsupported("verify_pqc"))?;
4708 ml_dsa_sign::verify(Self::dsa(algorithm), &public, message, signature)
4709 .map_err(|_| BackendError::Unsupported("verify_pqc"))
4710 }
4711
4712 async fn encrypt(
4713 &self,
4714 _key_id: &str,
4715 algorithm: AeadAlgorithm,
4716 plaintext: &[u8],
4717 aad: Option<&[u8]>,
4718 ) -> Result<CiphertextEnvelope, BackendError> {
4719 let aad = aad.unwrap_or(&[]);
4720 let mut ciphertext = vec![u8::try_from(aad.len()).unwrap_or(u8::MAX)];
4721 ciphertext.extend_from_slice(aad);
4722 ciphertext.extend_from_slice(plaintext);
4723 Ok(CiphertextEnvelope {
4724 alg: algorithm,
4725 key_version: 1,
4726 nonce: Vec::new(),
4727 ciphertext,
4728 })
4729 }
4730
4731 async fn decrypt(
4732 &self,
4733 _key_id: &str,
4734 envelope: &CiphertextEnvelope,
4735 aad: Option<&[u8]>,
4736 ) -> Result<Vec<u8>, BackendError> {
4737 let aad = aad.unwrap_or(&[]);
4738 let ct = &envelope.ciphertext;
4739 let aad_len = *ct.first().ok_or(BackendError::DecryptFailed)? as usize;
4740 let bound = ct.get(1..1 + aad_len).ok_or(BackendError::DecryptFailed)?;
4741 if bound != aad {
4742 return Err(BackendError::DecryptFailed);
4743 }
4744 Ok(ct
4745 .get(1 + aad_len..)
4746 .ok_or(BackendError::DecryptFailed)?
4747 .to_vec())
4748 }
4749
4750 async fn kv_put(&self, key_id: &str, value: &[u8]) -> Result<u32, BackendError> {
4751 self.store
4752 .lock()
4753 .map_err(|_| BackendError::Unsupported("kv_put"))?
4754 .insert(key_id.to_string(), value.to_vec());
4755 Ok(1)
4756 }
4757
4758 async fn kv_get(
4759 &self,
4760 key_id: &str,
4761 _version: Option<u32>,
4762 ) -> Result<KvValue, BackendError> {
4763 let value = self
4764 .store
4765 .lock()
4766 .map_err(|_| BackendError::Unsupported("kv_get"))?
4767 .get(key_id)
4768 .cloned();
4769 value
4770 .map(|value| KvValue { value, version: 1 })
4771 .ok_or(BackendError::Unsupported("kv_get"))
4772 }
4773 }
4774
4775 const PQC_CATALOG: &str = r#"{
4776 "schemaVersion": 1,
4777 "backends": { "primary": { "kind": "vault", "addr": "https://127.0.0.1:8200" } },
4778 "keys": {
4779 "pqc.signer44": {
4780 "class": "asymmetric", "keyType": "ml-dsa-44", "backend": "primary",
4781 "path": "secret/data/pqc/44", "writable": true, "missing": "error",
4782 "labels": ["crypto_provider=local-software", "crypto_provider_policy=local-software",
4783 "pqc_custody=software-encrypted", "pqc_storage_key=pqc/aead",
4784 "crypto_provider_version=1"],
4785 "description": "ml-dsa-44 software-custodied signer"
4786 },
4787 "pqc.signer65": {
4788 "class": "asymmetric", "keyType": "ml-dsa-65", "backend": "primary",
4789 "path": "secret/data/pqc/65", "writable": true, "missing": "error",
4790 "labels": ["crypto_provider=local-software", "crypto_provider_policy=local-software",
4791 "pqc_custody=software-encrypted", "pqc_storage_key=pqc/aead",
4792 "crypto_provider_version=1"],
4793 "description": "ml-dsa-65 software-custodied signer"
4794 },
4795 "pqc.signer87": {
4796 "class": "asymmetric", "keyType": "ml-dsa-87", "backend": "primary",
4797 "path": "secret/data/pqc/87", "writable": true, "missing": "error",
4798 "labels": ["crypto_provider=local-software", "crypto_provider_policy=local-software",
4799 "pqc_custody=software-encrypted", "pqc_storage_key=pqc/aead",
4800 "crypto_provider_version=1"],
4801 "description": "ml-dsa-87 software-custodied signer"
4802 },
4803 "pqc.backendreq": {
4804 "class": "asymmetric", "keyType": "ml-dsa-65", "backend": "primary",
4805 "path": "secret/data/pqc/breq", "writable": true, "missing": "error",
4806 "labels": ["crypto_provider_policy=backend-required", "pqc_custody=software-encrypted"],
4807 "description": "backend-required ml-dsa signer (no native provider)"
4808 },
4809 "pqc.preferred_native": {
4810 "class": "asymmetric", "keyType": "ml-dsa-65", "backend": "primary",
4811 "path": "pqc/native65", "writable": true, "missing": "error",
4812 "labels": ["crypto_provider_policy=backend-preferred", "crypto_provider_version=1"],
4813 "description": "backend-preferred ml-dsa signer (native when the backend supports it)"
4814 },
4815 "pqc.preferred_software": {
4816 "class": "asymmetric", "keyType": "ml-dsa-65", "backend": "primary",
4817 "path": "secret/data/pqc/pref-sw", "writable": true, "missing": "error",
4818 "labels": ["crypto_provider_policy=backend-preferred", "pqc_custody=software-encrypted",
4819 "pqc_storage_key=pqc/aead", "crypto_provider_version=1"],
4820 "description": "backend-preferred ml-dsa signer pinned to software custody"
4821 },
4822 "pqc.backendreq_native": {
4823 "class": "asymmetric", "keyType": "ml-dsa-65", "backend": "primary",
4824 "path": "pqc/breq-native", "writable": true, "missing": "error",
4825 "labels": ["crypto_provider_policy=backend-required", "crypto_provider_version=1"],
4826 "description": "backend-required ml-dsa signer served by a native backend"
4827 }
4828 }
4829 }"#;
4830
4831 fn manager() -> BackendManager {
4832 let catalog: Catalog = serde_json::from_str(PQC_CATALOG).expect("catalog parses");
4833 let mut backends: BTreeMap<String, Box<dyn Backend>> = BTreeMap::new();
4834 backends.insert("primary".into(), Box::new(PqcBackend::default()));
4835 BackendManager::new(catalog, backends).expect("manager builds")
4836 }
4837
4838 fn native_manager() -> BackendManager {
4841 let catalog: Catalog = serde_json::from_str(PQC_CATALOG).expect("catalog parses");
4842 let mut backends: BTreeMap<String, Box<dyn Backend>> = BTreeMap::new();
4843 backends.insert("primary".into(), Box::new(NativePqcBackend::default()));
4844 BackendManager::new(catalog, backends).expect("manager builds")
4845 }
4846
4847 const fn allowed() -> ProviderGate {
4848 ProviderGate {
4849 local_software_allowed: true,
4850 }
4851 }
4852
4853 #[tokio::test]
4854 async fn generate_sign_verify_round_trip_all_levels() {
4855 let cases = [
4856 ("pqc.signer44", MlDsaAlgorithm::MlDsa44, "ml-dsa-44"),
4857 ("pqc.signer65", MlDsaAlgorithm::MlDsa65, "ml-dsa-65"),
4858 ("pqc.signer87", MlDsaAlgorithm::MlDsa87, "ml-dsa-87"),
4859 ];
4860 for (key_id, dsa, token) in cases {
4861 let mgr = manager();
4862 let message = b"basil ml-dsa dispatch";
4863
4864 let (created, gen_dispatch) = mgr
4865 .provider_generate(key_id, allowed())
4866 .await
4867 .expect("generate");
4868 assert_eq!(gen_dispatch.provider, CryptoProviderId::LocalSoftware);
4869 assert_eq!(gen_dispatch.algorithm, token);
4870 assert!(!created.public_key.is_empty(), "{token} public");
4871
4872 let (signature, sign_dispatch) = mgr
4873 .provider_sign(key_id, message, allowed())
4874 .await
4875 .expect("sign");
4876 assert_eq!(sign_dispatch.provider, CryptoProviderId::LocalSoftware);
4877 assert_eq!(sign_dispatch.algorithm, token);
4878 assert_eq!(sign_dispatch.custody, CustodyMode::SoftwareEncrypted);
4879
4880 let (valid, _) = mgr
4881 .provider_verify(key_id, message, &signature, allowed())
4882 .await
4883 .expect("verify");
4884 assert!(valid, "{token} verifies through the broker");
4885
4886 assert!(
4888 ml_dsa_sign::verify(dsa, &created.public_key, message, &signature).expect("core"),
4889 "{token} verifies under returned public",
4890 );
4891
4892 let (bad, _) = mgr
4894 .provider_verify(key_id, b"tampered", &signature, allowed())
4895 .await
4896 .expect("verify");
4897 assert!(!bad, "{token} rejects a tampered message");
4898 }
4899 }
4900
4901 #[tokio::test]
4902 async fn backend_required_ml_dsa_fails_closed() {
4903 let mgr = manager();
4904 let err = mgr
4907 .provider_sign("pqc.backendreq", b"m", allowed())
4908 .await
4909 .expect_err("backend-required ml-dsa is unsupported");
4910 assert!(matches!(
4911 err,
4912 ManagerError::Provider(ProviderError::Unsupported { .. })
4913 ));
4914 let gen_err = mgr
4915 .provider_generate("pqc.backendreq", allowed())
4916 .await
4917 .expect_err("backend-required ml-dsa generate is unsupported");
4918 assert!(matches!(
4919 gen_err,
4920 ManagerError::Provider(ProviderError::Unsupported { .. })
4921 ));
4922 }
4923
4924 #[tokio::test]
4925 async fn local_software_without_policy_grant_is_denied() {
4926 let mgr = manager();
4927 let denied = ProviderGate {
4928 local_software_allowed: false,
4929 };
4930 for outcome in [
4931 mgr.provider_sign("pqc.signer65", b"m", denied).await,
4932 mgr.provider_generate("pqc.signer65", denied)
4933 .await
4934 .map(|(key, dispatch)| (key.public_key, dispatch)),
4935 ] {
4936 let err = outcome.expect_err("local software requires an explicit policy grant");
4937 assert!(matches!(
4938 err,
4939 ManagerError::Provider(ProviderError::PolicyDenied { .. })
4940 ));
4941 }
4942 }
4943
4944 #[tokio::test]
4945 async fn dispatch_feeds_a_secret_free_provider_audit_event() {
4946 let mgr = manager();
4947 mgr.provider_generate("pqc.signer87", allowed())
4948 .await
4949 .expect("generate");
4950 let (_signature, dispatch) = mgr
4951 .provider_sign("pqc.signer87", b"m", allowed())
4952 .await
4953 .expect("sign");
4954 let event = ProviderAuditEvent {
4957 op: "sign",
4958 key_id: "pqc.signer87",
4959 key_version: None,
4960 algorithm: dispatch.algorithm,
4961 provider: dispatch.provider,
4962 custody: dispatch.custody,
4963 caller_uid: 4242,
4964 outcome: ProviderAuditOutcome::Success,
4965 reason: "ok",
4966 };
4967 let value = event.to_json_value();
4968 assert_eq!(value["provider"], "local-software");
4969 assert_eq!(value["algorithm"], "ml-dsa-87");
4970 assert_eq!(value["op"], "sign");
4971 assert_eq!(value["caller_uid"], 4242);
4972 assert!(value.get("signature").is_none());
4973 assert!(value.get("private_key").is_none());
4974 }
4975
4976 #[tokio::test]
4977 async fn backend_preferred_routes_to_native_when_backend_supports_it() {
4978 let mgr = native_manager();
4982 let key_id = "pqc.preferred_native";
4983 let message = b"basil native ml-dsa";
4984
4985 let (created, gen_dispatch) = mgr
4986 .provider_generate(key_id, allowed())
4987 .await
4988 .expect("generate");
4989 assert_eq!(gen_dispatch.provider, CryptoProviderId::VaultTransit);
4990 assert_eq!(gen_dispatch.custody, CustodyMode::BackendNative);
4991 assert!(!created.public_key.is_empty(), "native public");
4992
4993 let (signature, sign_dispatch) = mgr
4994 .provider_sign(key_id, message, allowed())
4995 .await
4996 .expect("sign");
4997 assert_eq!(sign_dispatch.provider, CryptoProviderId::VaultTransit);
4998 assert_eq!(sign_dispatch.custody, CustodyMode::BackendNative);
4999
5000 let (valid, _) = mgr
5001 .provider_verify(key_id, message, &signature, allowed())
5002 .await
5003 .expect("verify");
5004 assert!(valid, "native signature verifies through the broker");
5005
5006 let (bad, _) = mgr
5007 .provider_verify(key_id, b"tampered", &signature, allowed())
5008 .await
5009 .expect("verify");
5010 assert!(!bad, "native verify rejects a tampered message");
5011 }
5012
5013 #[tokio::test]
5014 async fn backend_required_routes_to_native_when_backend_supports_it() {
5015 let mgr = native_manager();
5019 let key_id = "pqc.backendreq_native";
5020
5021 let (_created, gen_dispatch) = mgr
5022 .provider_generate(key_id, allowed())
5023 .await
5024 .expect("generate");
5025 assert_eq!(gen_dispatch.provider, CryptoProviderId::VaultTransit);
5026
5027 let (signature, sign_dispatch) = mgr
5028 .provider_sign(key_id, b"m", allowed())
5029 .await
5030 .expect("sign");
5031 assert_eq!(sign_dispatch.provider, CryptoProviderId::VaultTransit);
5032 let (valid, _) = mgr
5033 .provider_verify(key_id, b"m", &signature, allowed())
5034 .await
5035 .expect("verify");
5036 assert!(valid);
5037 }
5038
5039 #[tokio::test]
5040 async fn backend_preferred_falls_back_to_software_without_native_support() {
5041 let mgr = manager();
5044 let key_id = "pqc.preferred_software";
5045
5046 let (_created, gen_dispatch) = mgr
5047 .provider_generate(key_id, allowed())
5048 .await
5049 .expect("generate");
5050 assert_eq!(gen_dispatch.provider, CryptoProviderId::LocalSoftware);
5051 assert_eq!(gen_dispatch.custody, CustodyMode::SoftwareEncrypted);
5052
5053 let (signature, sign_dispatch) = mgr
5054 .provider_sign(key_id, b"m", allowed())
5055 .await
5056 .expect("sign");
5057 assert_eq!(sign_dispatch.provider, CryptoProviderId::LocalSoftware);
5058 let (valid, _) = mgr
5059 .provider_verify(key_id, b"m", &signature, allowed())
5060 .await
5061 .expect("verify");
5062 assert!(valid);
5063 }
5064
5065 #[tokio::test]
5066 async fn software_custodied_key_is_not_rerouted_by_native_probe() {
5067 let mgr = native_manager();
5072 let key_id = "pqc.preferred_software";
5073
5074 let (_created, gen_dispatch) = mgr
5075 .provider_generate(key_id, allowed())
5076 .await
5077 .expect("generate");
5078 assert_eq!(
5079 gen_dispatch.provider,
5080 CryptoProviderId::LocalSoftware,
5081 "software-custodied key stays local-software despite native support",
5082 );
5083
5084 let (_signature, sign_dispatch) = mgr
5085 .provider_sign(key_id, b"m", allowed())
5086 .await
5087 .expect("sign");
5088 assert_eq!(sign_dispatch.provider, CryptoProviderId::LocalSoftware);
5089 assert_eq!(sign_dispatch.custody, CustodyMode::SoftwareEncrypted);
5090 }
5091
5092 #[tokio::test]
5093 async fn describe_provider_surfaces_active_custody_and_migration_availability() {
5094 let native = native_manager();
5097
5098 let software = native
5099 .describe_provider("pqc.preferred_software")
5100 .expect("describe");
5101 let expected_software = KeyProviderDescriptor {
5102 policy: ProviderPolicy::BackendPreferred,
5103 provider: None,
5104 custody: Some(CustodyMode::SoftwareEncrypted),
5105 version: Some("1".to_string()),
5106 backend_native_available: true,
5107 };
5108 assert_eq!(
5109 software, expected_software,
5110 "an admin sees software custody AND that a native migration is available",
5111 );
5112
5113 let preferred = native
5114 .describe_provider("pqc.preferred_native")
5115 .expect("describe");
5116 assert_eq!(preferred.custody, None);
5117 assert!(preferred.backend_native_available);
5118
5119 let classical = manager();
5121 let no_native = classical
5122 .describe_provider("pqc.preferred_software")
5123 .expect("describe");
5124 assert!(!no_native.backend_native_available);
5125
5126 assert!(matches!(
5127 classical.describe_provider("does.not.exist"),
5128 Err(ManagerError::UnknownKey(_))
5129 ));
5130 }
5131}