1use std::collections::BTreeMap;
26use std::path::Path;
27use std::process::Command;
28use std::sync::Arc;
29
30use age::secrecy::ExposeSecret as _;
31use basil_cose::Recipient as _;
32use basil_proto::{
33 AeadAlgorithm, CatalogEntry as WireCatalogEntry, CatalogKind, CiphertextEnvelope, KeyMaterial,
34 KeyType,
35};
36use rand::RngCore;
37use uuid::Uuid;
38
39use zeroize::Zeroizing;
40
41use crate::backend::{
42 Backend, BackendError, KvValue, NativeAlgorithm, NewKey, PublicKey, SignOptions,
43 X509CertRequest, X509Svid,
44};
45use crate::catalog::{BackendRef, Catalog, Class, Engine, GenerateSpec, KeyAlgorithm, KeyEntry};
46use crate::core::crypto_provider::LocalSoftwareProvider;
47use crate::core::crypto_provider::{
48 BackendCryptoProvider, CryptoProvider, CryptoProviderId, CustodyMode,
49 Envelope as ProviderEnvelope, EnvelopeAlgorithm as ProviderEnvelopeAlgorithm,
50 GenerateKey as ProviderGenerateKey, GenerateSealingKey as ProviderGenerateSealingKey,
51 KemAlgorithm as ProviderKemAlgorithm, ProviderError, ProviderMetadata, ProviderPolicy,
52 SignRequest, SignatureAlgorithm, UnwrapEnvelopeRequest as ProviderUnwrapEnvelopeRequest,
53 VerifyRequest, WrapEnvelopeRequest as ProviderWrapEnvelopeRequest, ml_dsa_signature_algorithm,
54 select_provider,
55};
56use crate::ed25519_sign::{self, SignError};
57use crate::state::BrokerLimits;
58use crate::x25519_seal::{self, SealError, SealedEnvelope};
59
60struct GetrandomRng;
61
62impl rand_core::TryRng for GetrandomRng {
63 type Error = getrandom::Error;
64
65 fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
66 let mut bytes = [0u8; 4];
67 self.try_fill_bytes(&mut bytes)?;
68 Ok(u32::from_le_bytes(bytes))
69 }
70
71 fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
72 let mut bytes = [0u8; 8];
73 self.try_fill_bytes(&mut bytes)?;
74 Ok(u64::from_le_bytes(bytes))
75 }
76
77 fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> {
78 getrandom::fill(dst)
79 }
80}
81
82#[derive(Debug, thiserror::Error)]
84pub enum ManagerError {
85 #[error("unknown key: {0}")]
87 UnknownKey(String),
88
89 #[error("key `{key}` references unknown backend `{backend}`")]
92 UnknownBackend {
93 key: String,
95 backend: String,
97 },
98
99 #[error("op `{op}` is not valid for key `{key}` (class {class:?})")]
102 OpNotValidForClass {
103 op: &'static str,
105 key: String,
107 class: Class,
109 },
110
111 #[error("backend `{backend}` does not declare support for {op} key type `{key_type}`")]
114 UnsupportedKeyType {
115 backend: String,
117 op: &'static str,
119 key_type: KeyType,
121 },
122
123 #[error("op `{0}` is recognized but not yet backed by a Backend method")]
126 Unsupported(&'static str),
127
128 #[error("algorithm `{requested}` does not match key `{key}` (catalog type {actual})")]
133 AlgorithmMismatch {
134 key: String,
136 requested: AeadAlgorithm,
138 actual: &'static str,
140 },
141
142 #[error("KEM algorithm `{requested}` does not match key `{key}` (catalog type {actual})")]
145 KemAlgorithmMismatch {
146 key: String,
148 requested: &'static str,
150 actual: &'static str,
152 },
153
154 #[error("value key `{0}` has no generate recipe; rotate via `set` instead")]
158 ValueRotateNeedsSet(String),
159
160 #[error("sealing op failed: {0}")]
164 Sealing(SealingFailure),
165
166 #[error("key `{0}` does not authorize this unseal context")]
174 UnsealContextNotPermitted(String),
175
176 #[error("materialize-to-sign op failed: {0}")]
180 Signing(SigningFailure),
181
182 #[error("key `{0}` has no public_path; its public half cannot be resolved")]
188 MissingPublicPath(String),
189
190 #[error("provider error: {0}")]
195 Provider(#[from] ProviderError),
196
197 #[error("backend error: {0}")]
199 Backend(#[from] BackendError),
200}
201
202#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
208pub enum SealingFailure {
209 #[error("malformed sealing material or envelope")]
212 Malformed,
213
214 #[error("unseal authentication failed")]
217 OpenFailed,
218}
219
220impl SealingFailure {
221 const fn from_seal(err: SealError) -> Self {
225 match err {
226 SealError::OpenFailed => Self::OpenFailed,
227 SealError::BadKeyLength { .. }
228 | SealError::BadNonceLength { .. }
229 | SealError::KdfFailed
230 | SealError::SealFailed => Self::Malformed,
231 }
232 }
233
234 const fn from_cose_open(err: &basil_cose::OpenError) -> Self {
235 match err {
236 basil_cose::OpenError::OpenFailed => Self::OpenFailed,
237 basil_cose::OpenError::Decode(_)
238 | basil_cose::OpenError::RecipientKeyMismatch
239 | basil_cose::OpenError::PartyMismatch
240 | basil_cose::OpenError::Provider { .. } => Self::Malformed,
241 }
242 }
243}
244
245const fn nats_curve_error(err: &basil_nats::Error) -> ManagerError {
246 let failure = match err {
247 basil_nats::Error::XKeyOpenFailed => SealingFailure::OpenFailed,
248 basil_nats::Error::BadPublicKeyLen(_)
249 | basil_nats::Error::UnsupportedPrefix(_)
250 | basil_nats::Error::UnexpectedPrefix { .. }
251 | basil_nats::Error::UnexpectedXKeyPrefix(_)
252 | basil_nats::Error::BadXKeyVersion
253 | basil_nats::Error::BadXKeyCiphertextLen(_)
254 | basil_nats::Error::XKeyRandomnessFailed
255 | basil_nats::Error::XKeySealFailed
256 | basil_nats::Error::Json(_)
257 | basil_nats::Error::InvalidClaims(_)
258 | basil_nats::Error::MalformedJwt(_)
259 | basil_nats::Error::BadSignatureLen(_) => SealingFailure::Malformed,
260 };
261 ManagerError::Sealing(failure)
262}
263
264#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
270pub enum SigningFailure {
271 #[error("malformed signing seed or verify input")]
274 Malformed,
275}
276
277impl SigningFailure {
278 const fn from_sign(err: SignError) -> Self {
282 match err {
283 SignError::BadSeedLength { .. } | SignError::BadFieldLength { .. } => Self::Malformed,
284 }
285 }
286}
287
288#[derive(Debug, Clone, Copy)]
298pub struct ProviderGate {
299 pub local_software_allowed: bool,
301}
302
303#[derive(Debug, Clone, Copy)]
307pub struct ProviderDispatch {
308 pub provider: CryptoProviderId,
310 pub algorithm: &'static str,
312 pub custody: CustodyMode,
314}
315
316#[derive(Debug, Clone, PartialEq, Eq)]
324pub struct KeyProviderDescriptor {
325 pub policy: ProviderPolicy,
328 pub provider: Option<CryptoProviderId>,
330 pub custody: Option<CustodyMode>,
333 pub version: Option<String>,
335 pub backend_native_available: bool,
339}
340
341#[derive(Debug, Clone, Copy)]
347pub struct MlKemEnvelopeParts<'a> {
348 pub encapsulated_key: &'a [u8],
350 pub nonce: &'a [u8],
352 pub ciphertext: &'a [u8],
354}
355
356pub struct Routed<'a> {
362 pub backend: &'a dyn Backend,
364 pub entry: &'a KeyEntry,
366 pub backend_ref: &'a BackendRef,
368 pub engine: Engine,
370}
371
372impl std::fmt::Debug for Routed<'_> {
375 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
376 f.debug_struct("Routed")
377 .field("backend", &self.backend.kind())
378 .field("entry", &self.entry)
379 .field("backend_ref", &self.backend_ref)
380 .field("engine", &self.engine)
381 .finish()
382 }
383}
384
385impl Routed<'_> {
386 #[must_use]
388 pub fn path(&self) -> &str {
389 &self.entry.path
390 }
391
392 #[must_use]
396 pub fn public_path(&self) -> Option<&str> {
397 self.entry.public_path.as_deref()
398 }
399
400 #[must_use]
402 pub const fn class(&self) -> Class {
403 self.entry.class
404 }
405
406 #[must_use]
408 pub const fn key_type(&self) -> Option<KeyAlgorithm> {
409 self.entry.key_type
410 }
411
412 const fn backend_declares_provides(&self) -> bool {
413 !(self.backend_ref.engines.is_empty()
414 && self.backend_ref.capabilities.is_empty()
415 && self.backend_ref.mint_key_types.is_empty())
416 }
417
418 pub(crate) fn require_mint_key_type(
419 &self,
420 op: &'static str,
421 key_type: KeyType,
422 ) -> Result<(), ManagerError> {
423 let algorithm = KeyAlgorithm::from_wire_key_type(key_type);
424 if !self.backend_declares_provides() || self.backend_ref.mint_key_types.contains(&algorithm)
425 {
426 return Ok(());
427 }
428 Err(ManagerError::UnsupportedKeyType {
429 backend: self.entry.backend.clone(),
430 op,
431 key_type,
432 })
433 }
434}
435
436pub struct BackendManager {
443 catalog: Arc<Catalog>,
444 backends: BTreeMap<String, Box<dyn Backend>>,
445}
446
447impl std::fmt::Debug for BackendManager {
449 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
450 f.debug_struct("BackendManager")
451 .field("backends", &self.backends.keys().collect::<Vec<_>>())
452 .field("keys", &self.catalog.keys.keys().collect::<Vec<_>>())
453 .finish()
454 }
455}
456
457impl BackendManager {
458 pub fn new(
468 catalog: Catalog,
469 backends: BTreeMap<String, Box<dyn Backend>>,
470 ) -> Result<Self, ManagerError> {
471 for (name, entry) in &catalog.keys {
472 if !backends.contains_key(&entry.backend) {
473 return Err(ManagerError::UnknownBackend {
474 key: name.clone(),
475 backend: entry.backend.clone(),
476 });
477 }
478 }
479 Ok(Self {
480 catalog: Arc::new(catalog),
481 backends,
482 })
483 }
484
485 #[must_use]
487 pub fn catalog(&self) -> Arc<Catalog> {
488 Arc::clone(&self.catalog)
489 }
490
491 pub fn resolve(&self, key_id: &str) -> Result<Routed<'_>, ManagerError> {
500 let entry = self
501 .catalog
502 .keys
503 .get(key_id)
504 .ok_or_else(|| ManagerError::UnknownKey(key_id.to_string()))?;
505 let backend_ref = self.catalog.backends.get(&entry.backend).ok_or_else(|| {
506 ManagerError::UnknownBackend {
507 key: key_id.to_string(),
508 backend: entry.backend.clone(),
509 }
510 })?;
511 let backend =
512 self.backends
513 .get(&entry.backend)
514 .ok_or_else(|| ManagerError::UnknownBackend {
515 key: key_id.to_string(),
516 backend: entry.backend.clone(),
517 })?;
518 Ok(Routed {
519 backend: backend.as_ref(),
520 entry,
521 backend_ref,
522 engine: effective_engine(entry),
523 })
524 }
525
526 pub(crate) fn keys(&self) -> impl Iterator<Item = (&String, &KeyEntry)> {
532 self.catalog.keys.iter()
533 }
534
535 pub async fn new_key(&self, key_id: &str, key_type: KeyType) -> Result<NewKey, ManagerError> {
548 let routed = self.resolve(key_id)?;
549 require_class(
550 "new_key",
551 key_id,
552 routed.class(),
553 &[Class::Asymmetric, Class::Symmetric],
554 )?;
555 if routed.class() == Class::Asymmetric {
556 routed.require_mint_key_type("new_key", key_type)?;
557 }
558 Ok(routed.backend.new_key(key_type).await?)
559 }
560
561 pub async fn sign(&self, key_id: &str, message: &[u8]) -> Result<Vec<u8>, ManagerError> {
581 let routed = self.resolve(key_id)?;
582 require_class("sign", key_id, routed.class(), &[Class::Asymmetric])?;
583 if routed.engine == Engine::Kv2 {
584 let seed = self.materialize_signing_seed(key_id, "sign").await?;
587 return Ok(ed25519_sign::sign(&seed, message).to_vec());
588 }
589 Ok(routed
590 .backend
591 .sign_with_options(
592 routed.path(),
593 message,
594 sign_options_for_key(routed.key_type()),
595 )
596 .await?)
597 }
598
599 pub async fn verify(
612 &self,
613 key_id: &str,
614 message: &[u8],
615 signature: &[u8],
616 ) -> Result<bool, ManagerError> {
617 let routed = self.resolve(key_id)?;
618 require_class(
619 "verify",
620 key_id,
621 routed.class(),
622 &[Class::Asymmetric, Class::Public],
623 )?;
624 if routed.class() == Class::Asymmetric && routed.engine == Engine::Kv2 {
625 let public_bytes = self
629 .read_public_half(key_id, "verify", &[Class::Asymmetric])
630 .await?;
631 let public = ed25519_sign::public_from_slice(&public_bytes)
632 .map_err(|e| ManagerError::Signing(SigningFailure::from_sign(e)))?;
633 return ed25519_sign::verify(&public, message, signature)
634 .map_err(|e| ManagerError::Signing(SigningFailure::from_sign(e)));
635 }
636 Ok(routed
637 .backend
638 .verify_with_options(
639 routed.path(),
640 message,
641 signature,
642 sign_options_for_key(routed.key_type()),
643 )
644 .await?)
645 }
646
647 pub async fn get_public_key(&self, key_id: &str) -> Result<PublicKey, ManagerError> {
663 let routed = self.resolve(key_id)?;
664 if routed.class() == Class::Sealing
673 && ProviderMetadata::from_key(routed.entry).custody
674 == Some(CustodyMode::SoftwareEncrypted)
675 && let Some(kem) = routed.key_type().and_then(ml_kem_provider_algorithm)
676 {
677 let public_key = LocalSoftwareProvider::new(routed.backend)
678 .public_key(key_id, routed.path(), kem.token())
679 .await?;
680 return Ok(PublicKey {
681 public_key,
682 key_type: ml_kem_wire_key_type(kem),
683 version: 1,
685 });
686 }
687 require_class(
688 "get_public_key",
689 key_id,
690 routed.class(),
691 &[Class::Asymmetric, Class::Public],
692 )?;
693 if ProviderMetadata::from_key(routed.entry).custody == Some(CustodyMode::SoftwareEncrypted)
701 && let Some(algorithm) = routed.key_type().and_then(ml_dsa_signature_algorithm)
702 {
703 let public_key = LocalSoftwareProvider::new(routed.backend)
704 .public_key(key_id, routed.path(), algorithm.token())
705 .await?;
706 return Ok(PublicKey {
707 public_key,
708 key_type: ml_dsa_wire_key_type(algorithm),
709 version: 1,
711 });
712 }
713 if routed.class() == Class::Asymmetric && routed.engine == Engine::Kv2 {
714 let public_bytes = self
717 .read_public_half(key_id, "get_public_key", &[Class::Asymmetric])
718 .await?;
719 let public = ed25519_sign::public_from_slice(&public_bytes)
720 .map_err(|e| ManagerError::Signing(SigningFailure::from_sign(e)))?;
721 return Ok(PublicKey {
722 public_key: public.to_vec(),
723 key_type: KeyType::Ed25519,
724 version: 1,
727 });
728 }
729 Ok(routed.backend.public_key_with_meta(routed.path()).await?)
730 }
731
732 #[must_use]
740 pub fn ml_dsa_algorithm_for(&self, key_id: &str) -> Option<SignatureAlgorithm> {
741 let routed = self.resolve(key_id).ok()?;
742 ml_dsa_signature_algorithm(routed.key_type()?)
743 }
744
745 #[must_use]
752 pub fn ml_kem_algorithm_for(&self, key_id: &str) -> Option<ProviderKemAlgorithm> {
753 let routed = self.resolve(key_id).ok()?;
754 ml_kem_provider_algorithm(routed.key_type()?)
755 }
756
757 pub fn describe_provider(&self, key_id: &str) -> Result<KeyProviderDescriptor, ManagerError> {
770 let routed = self.resolve(key_id)?;
771 let metadata = ProviderMetadata::from_key(routed.entry);
772 let native = routed
773 .key_type()
774 .and_then(ml_dsa_signature_algorithm)
775 .and_then(SignatureAlgorithm::native_algorithm);
776 let backend_native_available =
777 native.is_some_and(|algorithm| routed.backend.supports_native_algorithm(algorithm));
778 Ok(KeyProviderDescriptor {
779 policy: metadata.policy,
780 provider: metadata.provider,
781 custody: metadata.custody,
782 version: routed
783 .entry
784 .labels
785 .get("crypto_provider_version")
786 .map(str::to_owned),
787 backend_native_available,
788 })
789 }
790
791 pub async fn provider_sign(
808 &self,
809 key_id: &str,
810 message: &[u8],
811 gate: ProviderGate,
812 ) -> Result<(Vec<u8>, ProviderDispatch), ManagerError> {
813 let routed = self.resolve(key_id)?;
814 require_class("sign", key_id, routed.class(), &[Class::Asymmetric])?;
815 let algorithm = require_ml_dsa(key_id, &routed)?;
816 let provider = select_provider_for(&routed, algorithm.native_algorithm(), gate, "sign")?;
817 let request = SignRequest {
818 key_id,
819 backend_path: routed.path(),
820 algorithm,
821 message,
822 };
823 let signature = match provider {
824 CryptoProviderId::VaultTransit => {
825 BackendCryptoProvider::new(routed.backend)
826 .sign(request)
827 .await?
828 }
829 CryptoProviderId::LocalSoftware => {
830 LocalSoftwareProvider::new(routed.backend)
831 .sign(request)
832 .await?
833 }
834 };
835 Ok((signature, provider_dispatch(provider, algorithm)))
836 }
837
838 pub async fn provider_verify(
846 &self,
847 key_id: &str,
848 message: &[u8],
849 signature: &[u8],
850 gate: ProviderGate,
851 ) -> Result<(bool, ProviderDispatch), ManagerError> {
852 let routed = self.resolve(key_id)?;
853 require_class(
854 "verify",
855 key_id,
856 routed.class(),
857 &[Class::Asymmetric, Class::Public],
858 )?;
859 let algorithm = require_ml_dsa(key_id, &routed)?;
860 let provider = select_provider_for(&routed, algorithm.native_algorithm(), gate, "verify")?;
861 let request = VerifyRequest {
862 key_id,
863 backend_path: routed.path(),
864 algorithm,
865 message,
866 signature,
867 };
868 let valid = match provider {
869 CryptoProviderId::VaultTransit => {
870 BackendCryptoProvider::new(routed.backend)
871 .verify(request)
872 .await?
873 }
874 CryptoProviderId::LocalSoftware => {
875 LocalSoftwareProvider::new(routed.backend)
876 .verify(request)
877 .await?
878 }
879 };
880 Ok((valid, provider_dispatch(provider, algorithm)))
881 }
882
883 pub async fn provider_generate(
892 &self,
893 key_id: &str,
894 gate: ProviderGate,
895 ) -> Result<(NewKey, ProviderDispatch), ManagerError> {
896 let routed = self.resolve(key_id)?;
897 require_class("new_key", key_id, routed.class(), &[Class::Asymmetric])?;
898 let algorithm = require_ml_dsa(key_id, &routed)?;
899 let provider =
900 select_provider_for(&routed, algorithm.native_algorithm(), gate, "generate")?;
901 let request = ProviderGenerateKey {
902 key_id,
903 backend_path: routed.path(),
904 algorithm,
905 storage_key: routed.entry.labels.get("pqc_storage_key"),
906 };
907 let created = match provider {
908 CryptoProviderId::VaultTransit => {
909 BackendCryptoProvider::new(routed.backend)
910 .generate_key(request)
911 .await?
912 }
913 CryptoProviderId::LocalSoftware => {
914 LocalSoftwareProvider::new(routed.backend)
915 .generate_key(request)
916 .await?
917 }
918 };
919 Ok((created, provider_dispatch(provider, algorithm)))
920 }
921
922 pub async fn provider_generate_sealing(
940 &self,
941 key_id: &str,
942 kem: ProviderKemAlgorithm,
943 gate: ProviderGate,
944 ) -> Result<(NewKey, ProviderDispatch), ManagerError> {
945 let routed = self.resolve(key_id)?;
946 require_class("new_key", key_id, routed.class(), &[Class::Sealing])?;
947 require_ml_kem_sealing_key(key_id, routed.key_type(), kem)?;
948 let provider = select_provider_for(&routed, kem.native_algorithm(), gate, "generate")?;
949 let request = ProviderGenerateSealingKey {
950 key_id,
951 backend_path: routed.path(),
952 algorithm: kem,
953 storage_key: routed.entry.labels.get("pqc_storage_key"),
954 };
955 let created = match provider {
956 CryptoProviderId::VaultTransit => {
957 BackendCryptoProvider::new(routed.backend)
958 .generate_sealing_key(request)
959 .await?
960 }
961 CryptoProviderId::LocalSoftware => {
962 LocalSoftwareProvider::new(routed.backend)
963 .generate_sealing_key(request)
964 .await?
965 }
966 };
967 Ok((created, kem_provider_dispatch(provider, kem)))
968 }
969
970 pub async fn import(
980 &self,
981 key_id: &str,
982 key_type: KeyType,
983 material: &KeyMaterial,
984 ) -> Result<NewKey, ManagerError> {
985 let routed = self.resolve(key_id)?;
986 require_class(
987 "import",
988 key_id,
989 routed.class(),
990 &[Class::Asymmetric, Class::Symmetric],
991 )?;
992 if routed.engine == Engine::Kv2 {
997 return Err(ManagerError::Unsupported(
998 "import: a value-store (kv2) crypto key is provisioned out-of-band, not imported via the broker",
999 ));
1000 }
1001 if routed.class() == Class::Asymmetric {
1002 routed.require_mint_key_type("import", key_type)?;
1003 }
1004 Ok(routed
1007 .backend
1008 .import(routed.path(), key_type, material)
1009 .await?)
1010 }
1011
1012 pub async fn list(
1027 &self,
1028 prefix: Option<&str>,
1029 visible: impl Fn(&str) -> bool,
1030 ) -> Result<Vec<WireCatalogEntry>, ManagerError> {
1031 let mut out = Vec::new();
1032 for (name, entry) in &self.catalog.keys {
1033 if let Some(p) = prefix
1034 && !name.starts_with(p)
1035 {
1036 continue;
1037 }
1038 if !visible(name) {
1039 continue;
1040 }
1041 let meta = match self.resolve(name) {
1045 Ok(routed) => routed.backend.key_metadata(routed.path()).await.ok(),
1046 Err(_) => None,
1047 };
1048 let (key_type, latest_version) = meta.map_or_else(
1049 || (wire_key_type(entry), 0),
1050 |m| (m.key_type, m.latest_version),
1051 );
1052 out.push(WireCatalogEntry {
1053 name: name.clone(),
1054 kind: key_kind(entry),
1055 key_type,
1056 latest_version,
1057 });
1058 }
1059 Ok(out)
1060 }
1061
1062 pub async fn encrypt(
1073 &self,
1074 key_id: &str,
1075 algorithm: AeadAlgorithm,
1076 plaintext: &[u8],
1077 aad: Option<&[u8]>,
1078 ) -> Result<CiphertextEnvelope, ManagerError> {
1079 let routed = self.resolve(key_id)?;
1080 require_class("encrypt", key_id, routed.class(), &[Class::Symmetric])?;
1081 require_aead_match(key_id, routed.key_type(), algorithm)?;
1082 Ok(routed
1083 .backend
1084 .encrypt(routed.path(), algorithm, plaintext, aad)
1085 .await?)
1086 }
1087
1088 pub async fn decrypt(
1101 &self,
1102 key_id: &str,
1103 envelope: &CiphertextEnvelope,
1104 aad: Option<&[u8]>,
1105 ) -> Result<Vec<u8>, ManagerError> {
1106 let routed = self.resolve(key_id)?;
1107 require_class("decrypt", key_id, routed.class(), &[Class::Symmetric])?;
1108 require_aead_match(key_id, routed.key_type(), envelope.alg)?;
1109 Ok(routed.backend.decrypt(routed.path(), envelope, aad).await?)
1110 }
1111
1112 pub async fn wrap_envelope(
1130 &self,
1131 key_id: &str,
1132 plaintext: &[u8],
1133 aad: &[u8],
1134 ) -> Result<SealedEnvelope, ManagerError> {
1135 let routed = self.resolve(key_id)?;
1136 require_class("wrap_envelope", key_id, routed.class(), &[Class::Sealing])?;
1137 require_x25519_sealing_key(key_id, routed.key_type())?;
1138 let public_bytes = self
1141 .read_public_half(key_id, "wrap_envelope", &[Class::Sealing])
1142 .await?;
1143 let recipient_pub = x25519_seal::public_from_slice(&public_bytes)
1144 .map_err(|e| ManagerError::Sealing(SealingFailure::from_seal(e)))?;
1145 x25519_seal::seal(&recipient_pub, plaintext, aad)
1146 .map_err(|e| ManagerError::Sealing(SealingFailure::from_seal(e)))
1147 }
1148
1149 pub async fn unwrap_envelope(
1171 &self,
1172 key_id: &str,
1173 envelope: &SealedEnvelope,
1174 aad: &[u8],
1175 ) -> Result<Zeroizing<Vec<u8>>, ManagerError> {
1176 let private = self
1177 .materialize_sealing_private(key_id, "unwrap_envelope")
1178 .await?;
1179 x25519_seal::open(&private, envelope, aad)
1180 .map_err(|e| ManagerError::Sealing(SealingFailure::from_seal(e)))
1181 }
1182
1183 pub async fn unseal_cose(
1198 &self,
1199 key_id: &str,
1200 cose_encrypt: &[u8],
1201 external_aad: &[u8],
1202 ) -> Result<Zeroizing<Vec<u8>>, ManagerError> {
1203 let expected_parties = {
1210 let routed = self.resolve(key_id)?;
1211 match routed.entry.sealing_pin.as_ref() {
1212 None => None,
1213 Some(pin) => {
1214 if !pin.external_aad_allowed(external_aad) {
1215 return Err(ManagerError::UnsealContextNotPermitted(key_id.to_string()));
1216 }
1217 match pin.parties.as_ref() {
1218 None => None,
1219 Some(parties) => Some(
1220 parties
1221 .to_kdf_parties()
1222 .map_err(|_| ManagerError::Sealing(SealingFailure::Malformed))?,
1223 ),
1224 }
1225 }
1226 }
1227 };
1228
1229 let private = self
1230 .materialize_sealing_private(key_id, "unseal_cose")
1231 .await?;
1232 let cose_key_id = basil_cose::KeyId::from_text(key_id)
1233 .map_err(|_| ManagerError::Sealing(SealingFailure::Malformed))?;
1234 let recipient = basil_cose::X25519Recipient::new(cose_key_id, private);
1235 let aad = basil_cose::ExternalAad::from_bytes(external_aad.to_vec());
1236 let request = basil_cose::OpenRequest {
1237 cose_encrypt,
1238 external_aad: &aad,
1239 expected_parties: expected_parties.as_ref(),
1240 };
1241 recipient.open(&request).await.map_err(|e| match e {
1242 basil_cose::OpenError::PartyMismatch => {
1247 ManagerError::UnsealContextNotPermitted(key_id.to_string())
1248 }
1249 other => ManagerError::Sealing(SealingFailure::from_cose_open(&other)),
1250 })
1251 }
1252
1253 pub async fn encrypt_nats_curve(
1259 &self,
1260 key_id: &str,
1261 recipient_public_xkey: &str,
1262 plaintext: &[u8],
1263 ) -> Result<Vec<u8>, ManagerError> {
1264 let private = self
1265 .materialize_sealing_private(key_id, "encrypt_nats_curve")
1266 .await?;
1267 basil_nats::seal_nats_curve(
1268 &private,
1269 recipient_public_xkey,
1270 plaintext,
1271 &mut GetrandomRng,
1272 )
1273 .map_err(|e| nats_curve_error(&e))
1274 }
1275
1276 pub async fn decrypt_nats_curve(
1281 &self,
1282 key_id: &str,
1283 sender_public_xkey: &str,
1284 ciphertext: &[u8],
1285 ) -> Result<Zeroizing<Vec<u8>>, ManagerError> {
1286 let private = self
1287 .materialize_sealing_private(key_id, "decrypt_nats_curve")
1288 .await?;
1289 basil_nats::open_nats_curve(&private, sender_public_xkey, ciphertext)
1290 .map_err(|e| nats_curve_error(&e))
1291 }
1292
1293 pub async fn provider_wrap_envelope(
1313 &self,
1314 key_id: &str,
1315 kem: ProviderKemAlgorithm,
1316 envelope_algorithm: ProviderEnvelopeAlgorithm,
1317 plaintext: &[u8],
1318 aad: &[u8],
1319 gate: ProviderGate,
1320 ) -> Result<(ProviderEnvelope, ProviderDispatch), ManagerError> {
1321 let routed = self.resolve(key_id)?;
1322 require_class("wrap_envelope", key_id, routed.class(), &[Class::Sealing])?;
1323 require_ml_kem_sealing_key(key_id, routed.key_type(), kem)?;
1324 let provider = select_provider_for(&routed, kem.native_algorithm(), gate, "wrap_envelope")?;
1325 let request = ProviderWrapEnvelopeRequest {
1326 key_id,
1327 backend_path: routed.path(),
1328 kem_algorithm: kem,
1329 envelope_algorithm,
1330 plaintext,
1331 aad: Some(aad),
1332 };
1333 let envelope = match provider {
1334 CryptoProviderId::VaultTransit => {
1335 BackendCryptoProvider::new(routed.backend)
1336 .wrap_envelope(request)
1337 .await?
1338 }
1339 CryptoProviderId::LocalSoftware => {
1340 LocalSoftwareProvider::new(routed.backend)
1341 .wrap_envelope(request)
1342 .await?
1343 }
1344 };
1345 Ok((envelope, kem_provider_dispatch(provider, kem)))
1346 }
1347
1348 pub async fn provider_unwrap_envelope(
1369 &self,
1370 key_id: &str,
1371 kem: ProviderKemAlgorithm,
1372 envelope_algorithm: ProviderEnvelopeAlgorithm,
1373 parts: MlKemEnvelopeParts<'_>,
1374 aad: &[u8],
1375 gate: ProviderGate,
1376 ) -> Result<(Vec<u8>, ProviderDispatch), ManagerError> {
1377 let routed = self.resolve(key_id)?;
1378 require_class("unwrap_envelope", key_id, routed.class(), &[Class::Sealing])?;
1379 require_ml_kem_sealing_key(key_id, routed.key_type(), kem)?;
1380 let provider =
1381 select_provider_for(&routed, kem.native_algorithm(), gate, "unwrap_envelope")?;
1382 let request = ProviderUnwrapEnvelopeRequest {
1383 key_id,
1384 backend_path: routed.path(),
1385 kem_algorithm: kem,
1386 envelope_algorithm,
1387 encapsulated_key: parts.encapsulated_key,
1388 nonce: parts.nonce,
1389 ciphertext: parts.ciphertext,
1390 aad: Some(aad),
1391 };
1392 let plaintext = match provider {
1393 CryptoProviderId::VaultTransit => {
1394 BackendCryptoProvider::new(routed.backend)
1395 .unwrap_envelope(request)
1396 .await?
1397 }
1398 CryptoProviderId::LocalSoftware => {
1399 LocalSoftwareProvider::new(routed.backend)
1400 .unwrap_envelope(request)
1401 .await?
1402 }
1403 };
1404 Ok((plaintext, kem_provider_dispatch(provider, kem)))
1405 }
1406
1407 pub async fn sealing_public_key(
1420 &self,
1421 key_id: &str,
1422 ) -> Result<[u8; x25519_seal::PUBLIC_KEY_LEN], ManagerError> {
1423 let routed = self.resolve(key_id)?;
1424 require_class("get_public_key", key_id, routed.class(), &[Class::Sealing])?;
1425 require_x25519_sealing_key(key_id, routed.key_type())?;
1426 let public_bytes = self
1427 .read_public_half(key_id, "get_public_key", &[Class::Sealing])
1428 .await?;
1429 x25519_seal::public_from_slice(&public_bytes)
1430 .map_err(|e| ManagerError::Sealing(SealingFailure::from_seal(e)))
1431 }
1432
1433 async fn materialize_sealing_private(
1440 &self,
1441 key_id: &str,
1442 op: &'static str,
1443 ) -> Result<Zeroizing<[u8; x25519_seal::PRIVATE_KEY_LEN]>, ManagerError> {
1444 let routed = self.resolve(key_id)?;
1445 require_class(op, key_id, routed.class(), &[Class::Sealing])?;
1446 require_x25519_sealing_key(key_id, routed.key_type())?;
1447 let secret = routed.backend.kv_get_secret(routed.path(), None).await?;
1451 x25519_seal::private_from_slice(&secret)
1455 .map_err(|e| ManagerError::Sealing(SealingFailure::from_seal(e)))
1456 }
1457
1458 async fn read_public_half(
1470 &self,
1471 key_id: &str,
1472 op: &'static str,
1473 allowed: &[Class],
1474 ) -> Result<Vec<u8>, ManagerError> {
1475 let routed = self.resolve(key_id)?;
1476 require_class(op, key_id, routed.class(), allowed)?;
1477 let public_path = routed
1478 .public_path()
1479 .ok_or_else(|| ManagerError::MissingPublicPath(key_id.to_string()))?;
1480 let kv = routed.backend.kv_get(public_path, None).await?;
1481 Ok(kv.value)
1482 }
1483
1484 async fn materialize_signing_seed(
1497 &self,
1498 key_id: &str,
1499 op: &'static str,
1500 ) -> Result<Zeroizing<[u8; ed25519_sign::SEED_LEN]>, ManagerError> {
1501 let routed = self.resolve(key_id)?;
1502 require_class(op, key_id, routed.class(), &[Class::Asymmetric])?;
1503 let secret = routed.backend.kv_get_secret(routed.path(), None).await?;
1507 ed25519_sign::seed_from_slice(&secret)
1511 .map_err(|e| ManagerError::Signing(SigningFailure::from_sign(e)))
1512 }
1513
1514 pub async fn get(&self, key_id: &str, version: Option<u32>) -> Result<KvValue, ManagerError> {
1528 let routed = self.resolve(key_id)?;
1529 require_class(
1530 "get",
1531 key_id,
1532 routed.class(),
1533 &[Class::Value, Class::Public],
1534 )?;
1535 Ok(routed.backend.kv_get(routed.path(), version).await?)
1536 }
1537
1538 pub async fn set(&self, key_id: &str, value: &[u8]) -> Result<u32, ManagerError> {
1550 let routed = self.resolve(key_id)?;
1551 require_class("set", key_id, routed.class(), &[Class::Value])?;
1552 Ok(routed.backend.kv_put(routed.path(), value).await?)
1553 }
1554
1555 pub async fn rotate(&self, key_id: &str, limits: BrokerLimits) -> Result<u32, ManagerError> {
1572 let routed = self.resolve(key_id)?;
1573 require_class(
1574 "rotate",
1575 key_id,
1576 routed.class(),
1577 &[Class::Asymmetric, Class::Symmetric, Class::Value],
1578 )?;
1579
1580 if routed.engine == Engine::Kv2 && routed.class() != Class::Value {
1586 return Err(ManagerError::Unsupported(
1587 "rotate: a value-store (kv2) crypto key is re-provisioned out-of-band, not rotated via the broker",
1588 ));
1589 }
1590
1591 if routed.class() == Class::Value {
1592 let Some(spec) = routed.entry.generate.as_ref() else {
1595 return Err(ManagerError::ValueRotateNeedsSet(key_id.to_string()));
1596 };
1597 let writes = self.generated_writes_for_key(key_id, spec).await?;
1598 let mut rotated_version = None;
1599 for write in writes {
1600 let write_route = self.resolve(&write.key_id)?;
1601 require_class(
1602 "rotate",
1603 &write.key_id,
1604 write_route.class(),
1605 &[Class::Value, Class::Public],
1606 )?;
1607 let version = write_route
1608 .backend
1609 .kv_put(write_route.path(), &write.value)
1610 .await?;
1611 if write.key_id == key_id {
1612 rotated_version = Some(version);
1613 }
1614 }
1615 return rotated_version.ok_or_else(|| {
1616 ManagerError::Backend(BackendError::Backend(
1617 "generate recipe produced no primary value".into(),
1618 ))
1619 });
1620 }
1621
1622 let new_version = routed.backend.rotate(routed.path()).await?;
1625 let floor = limits.grace_floor(new_version);
1626 if let Err(e) = routed
1630 .backend
1631 .configure_versions(routed.path(), Some(floor), None)
1632 .await
1633 && !matches!(e, BackendError::Unsupported(_))
1634 {
1635 return Err(ManagerError::Backend(e));
1636 }
1637 Ok(new_version)
1638 }
1639
1640 pub async fn sweep_retention(
1654 &self,
1655 key_id: &str,
1656 limits: BrokerLimits,
1657 ) -> Result<(), ManagerError> {
1658 let routed = self.resolve(key_id)?;
1659 require_class(
1660 "rotate",
1661 key_id,
1662 routed.class(),
1663 &[Class::Asymmetric, Class::Symmetric],
1664 )?;
1665 let latest = routed
1666 .backend
1667 .key_metadata(routed.path())
1668 .await?
1669 .latest_version;
1670 let Some(floor) = limits.retention_floor(latest) else {
1671 return Ok(()); };
1673 routed
1674 .backend
1675 .configure_versions(routed.path(), None, Some(floor))
1676 .await?;
1677 Ok(())
1678 }
1679
1680 pub async fn sweep_all_retention(&self, limits: BrokerLimits) -> Result<(), ManagerError> {
1689 if limits.retain_versions.is_none() {
1690 return Ok(());
1691 }
1692 let key_ids: Vec<String> = self
1693 .keys()
1694 .filter(|(_, entry)| matches!(entry.class, Class::Asymmetric | Class::Symmetric))
1695 .map(|(name, _)| name.clone())
1696 .collect();
1697 for key_id in key_ids {
1698 self.sweep_retention(&key_id, limits).await?;
1699 }
1700 Ok(())
1701 }
1702
1703 pub async fn issue_x509_svid(
1715 &self,
1716 key_id: &str,
1717 spiffe_id: &str,
1718 ttl_seconds: u64,
1719 ) -> Result<X509Svid, ManagerError> {
1720 let routed = self.resolve(key_id)?;
1721 require_class(
1722 "issue_x509_svid",
1723 key_id,
1724 routed.class(),
1725 &[Class::Asymmetric],
1726 )?;
1727 if routed.engine != Engine::Pki {
1728 return Err(ManagerError::Unsupported("issue_x509_svid"));
1729 }
1730 Ok(routed
1731 .backend
1732 .issue_x509_svid(routed.path(), spiffe_id, ttl_seconds)
1733 .await?)
1734 }
1735
1736 pub async fn issue_x509_cert(
1749 &self,
1750 key_id: &str,
1751 request: &X509CertRequest,
1752 ) -> Result<X509Svid, ManagerError> {
1753 let routed = self.resolve(key_id)?;
1754 require_class(
1755 "issue_x509_cert",
1756 key_id,
1757 routed.class(),
1758 &[Class::Asymmetric],
1759 )?;
1760 if routed.engine != Engine::Pki {
1761 return Err(ManagerError::Unsupported("issue_x509_cert"));
1762 }
1763 Ok(routed
1764 .backend
1765 .issue_x509_cert(routed.path(), request)
1766 .await?)
1767 }
1768
1769 pub(crate) async fn generated_writes_for_key(
1775 &self,
1776 key_id: &str,
1777 spec: &GenerateSpec,
1778 ) -> Result<Vec<GeneratedWrite>, ManagerError> {
1779 match spec {
1780 GenerateSpec::SelfSignedTls {
1781 common_name,
1782 validity,
1783 } => {
1784 if let Some(pair_key) = self.tls_pair_key_for_cert(key_id) {
1785 let existing_key = self.try_existing_value(&pair_key).await?;
1786 let material =
1787 generate_self_signed_tls(common_name, validity, existing_key.as_deref())?;
1788 let mut writes = Vec::new();
1789 if existing_key.is_none() {
1790 writes.push(GeneratedWrite {
1791 key_id: pair_key,
1792 value: material.private_key_pem,
1793 });
1794 }
1795 writes.push(GeneratedWrite {
1796 key_id: key_id.to_string(),
1797 value: material.cert_pem,
1798 });
1799 Ok(writes)
1800 } else {
1801 Ok(vec![GeneratedWrite {
1802 key_id: key_id.to_string(),
1803 value: generate_self_signed_tls(common_name, validity, None)?.cert_pem,
1804 }])
1805 }
1806 }
1807 GenerateSpec::SelfSignedTlsPairOf { pair_of } => {
1808 let (common_name, validity) = self.tls_cert_recipe(pair_of)?;
1809 let material = generate_self_signed_tls(common_name, validity, None)?;
1810 Ok(vec![
1811 GeneratedWrite {
1812 key_id: key_id.to_string(),
1813 value: material.private_key_pem,
1814 },
1815 GeneratedWrite {
1816 key_id: pair_of.clone(),
1817 value: material.cert_pem,
1818 },
1819 ])
1820 }
1821 GenerateSpec::AsciiPrintable { .. }
1822 | GenerateSpec::Base64 { .. }
1823 | GenerateSpec::Hex { .. }
1824 | GenerateSpec::AgeX25519 => Ok(vec![GeneratedWrite {
1825 key_id: key_id.to_string(),
1826 value: generate_value(spec)?,
1827 }]),
1828 }
1829 }
1830
1831 fn tls_pair_key_for_cert(&self, cert_key: &str) -> Option<String> {
1832 self.catalog.keys.iter().find_map(|(name, entry)| {
1833 matches!(
1834 entry.generate.as_ref(),
1835 Some(GenerateSpec::SelfSignedTlsPairOf { pair_of }) if pair_of == cert_key
1838 )
1839 .then(|| name.clone())
1840 })
1841 }
1842
1843 fn tls_cert_recipe(&self, cert_key: &str) -> Result<(&str, &str), ManagerError> {
1844 let entry = self
1845 .catalog
1846 .keys
1847 .get(cert_key)
1848 .ok_or_else(|| ManagerError::UnknownKey(cert_key.to_string()))?;
1849 match entry.generate.as_ref() {
1850 Some(GenerateSpec::SelfSignedTls {
1851 common_name,
1852 validity,
1853 }) => Ok((common_name, validity)),
1854 _ => Err(ManagerError::Backend(BackendError::Backend(format!(
1855 "self-signed-tls-pair-of references non-certificate key `{cert_key}`"
1856 )))),
1857 }
1858 }
1859
1860 async fn try_existing_value(&self, key_id: &str) -> Result<Option<Vec<u8>>, ManagerError> {
1861 let routed = self.resolve(key_id)?;
1862 require_class(
1863 "generate",
1864 key_id,
1865 routed.class(),
1866 &[Class::Value, Class::Public],
1867 )?;
1868 match routed.backend.kv_get(routed.path(), None).await {
1869 Ok(value) => Ok(Some(value.value)),
1870 Err(BackendError::KeyNotFound(_)) => Ok(None),
1871 Err(e) => Err(ManagerError::Backend(e)),
1872 }
1873 }
1874}
1875
1876pub(crate) struct GeneratedWrite {
1878 pub(crate) key_id: String,
1879 pub(crate) value: Vec<u8>,
1880}
1881
1882fn require_aead_match(
1886 key_id: &str,
1887 key_type: Option<KeyAlgorithm>,
1888 algorithm: AeadAlgorithm,
1889) -> Result<(), ManagerError> {
1890 let actual = match key_type {
1891 Some(KeyAlgorithm::Aes256Gcm) => AeadAlgorithm::Aes256Gcm,
1892 Some(KeyAlgorithm::ChaCha20Poly1305) => AeadAlgorithm::Chacha20Poly1305,
1893 _ => {
1897 return Err(ManagerError::AlgorithmMismatch {
1898 key: key_id.to_string(),
1899 requested: algorithm,
1900 actual: "non-aead",
1901 });
1902 }
1903 };
1904 if actual == algorithm {
1905 Ok(())
1906 } else {
1907 Err(ManagerError::AlgorithmMismatch {
1908 key: key_id.to_string(),
1909 requested: algorithm,
1910 actual: match actual {
1911 AeadAlgorithm::Aes256Gcm => "aes-256-gcm",
1912 AeadAlgorithm::Chacha20Poly1305 => "chacha20-poly1305",
1913 },
1914 })
1915 }
1916}
1917
1918const fn sign_options_for_key(key_type: Option<KeyAlgorithm>) -> SignOptions {
1919 match key_type {
1920 Some(KeyAlgorithm::Rsa2048) => SignOptions::Rs256Pkcs1v15Sha256,
1921 Some(KeyAlgorithm::EcdsaP256) => SignOptions::Es256,
1922 Some(KeyAlgorithm::EcdsaP384) => SignOptions::Es384,
1923 Some(KeyAlgorithm::EcdsaP521) => SignOptions::Es512,
1924 _ => SignOptions::Default,
1925 }
1926}
1927
1928fn require_ml_dsa(key_id: &str, routed: &Routed<'_>) -> Result<SignatureAlgorithm, ManagerError> {
1932 routed
1933 .key_type()
1934 .and_then(ml_dsa_signature_algorithm)
1935 .ok_or_else(|| ManagerError::OpNotValidForClass {
1936 op: "provider_dispatch",
1937 key: key_id.to_string(),
1938 class: routed.class(),
1939 })
1940}
1941
1942fn select_provider_for(
1955 routed: &Routed<'_>,
1956 native: Option<NativeAlgorithm>,
1957 gate: ProviderGate,
1958 op: &'static str,
1959) -> Result<CryptoProviderId, ProviderError> {
1960 let metadata = ProviderMetadata::from_key(routed.entry);
1961 let backend_native_supported =
1962 native.is_some_and(|algorithm| routed.backend.supports_native_algorithm(algorithm));
1963 select_provider(
1964 metadata,
1965 backend_native_supported,
1966 gate.local_software_allowed,
1967 op,
1968 )
1969}
1970
1971const fn provider_dispatch(
1973 provider: CryptoProviderId,
1974 algorithm: SignatureAlgorithm,
1975) -> ProviderDispatch {
1976 ProviderDispatch {
1977 provider,
1978 algorithm: algorithm.token(),
1979 custody: provider.custody_mode(),
1980 }
1981}
1982
1983const fn kem_provider_dispatch(
1986 provider: CryptoProviderId,
1987 kem: ProviderKemAlgorithm,
1988) -> ProviderDispatch {
1989 ProviderDispatch {
1990 provider,
1991 algorithm: kem.token(),
1992 custody: provider.custody_mode(),
1993 }
1994}
1995
1996const ASCII_PRINTABLE: &[u8] = b"!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
1999
2000pub(crate) fn generate_value(spec: &GenerateSpec) -> Result<Vec<u8>, ManagerError> {
2003 use base64::Engine as _;
2004 use std::fmt::Write as _;
2005
2006 let mut rng = rand::thread_rng();
2007 match spec {
2008 GenerateSpec::AsciiPrintable { bytes } => {
2009 let mut out = Vec::with_capacity(*bytes as usize);
2010 for _ in 0..*bytes {
2011 let idx = (rng.next_u32() as usize) % ASCII_PRINTABLE.len();
2014 out.push(ASCII_PRINTABLE.get(idx).copied().unwrap_or(b'!'));
2015 }
2016 Ok(out)
2017 }
2018 GenerateSpec::Base64 { bytes } => {
2019 let mut raw = vec![0u8; *bytes as usize];
2020 rng.fill_bytes(&mut raw);
2021 Ok(base64::engine::general_purpose::STANDARD
2022 .encode(&raw)
2023 .into_bytes())
2024 }
2025 GenerateSpec::Hex { bytes } => {
2026 let mut raw = vec![0u8; *bytes as usize];
2027 rng.fill_bytes(&mut raw);
2028 let mut out = String::with_capacity(*bytes as usize * 2);
2029 for b in &raw {
2030 let _ = write!(out, "{b:02x}");
2031 }
2032 Ok(out.into_bytes())
2033 }
2034 GenerateSpec::AgeX25519 => Ok(age::x25519::Identity::generate()
2035 .to_string()
2036 .expose_secret()
2037 .as_bytes()
2038 .to_vec()),
2039 GenerateSpec::SelfSignedTls {
2040 common_name,
2041 validity,
2042 } => Ok(generate_self_signed_tls(common_name, validity, None)?.cert_pem),
2043 GenerateSpec::SelfSignedTlsPairOf { .. } => Err(ManagerError::Backend(
2044 BackendError::Backend("self-signed-tls-pair-of needs catalog pair context".into()),
2045 )),
2046 }
2047}
2048
2049struct TlsMaterial {
2050 cert_pem: Vec<u8>,
2051 private_key_pem: Vec<u8>,
2052}
2053
2054fn generate_self_signed_tls(
2055 common_name: &str,
2056 validity: &str,
2057 existing_key_pem: Option<&[u8]>,
2058) -> Result<TlsMaterial, ManagerError> {
2059 let dir = std::env::temp_dir().join(format!("basil-self-signed-tls-{}", Uuid::new_v4()));
2060 std::fs::create_dir(&dir).map_err(|e| {
2061 ManagerError::Backend(BackendError::Backend(format!(
2062 "self-signed-tls tempdir create: {e}"
2063 )))
2064 })?;
2065
2066 let result = generate_self_signed_tls_in_dir(&dir, common_name, validity, existing_key_pem);
2067 let cleanup_result = std::fs::remove_dir_all(&dir);
2068 match (result, cleanup_result) {
2069 (Ok(material), Ok(()) | Err(_)) => Ok(material),
2070 (Err(err), _) => Err(err),
2071 }
2072}
2073
2074fn generate_self_signed_tls_in_dir(
2075 dir: &Path,
2076 common_name: &str,
2077 validity: &str,
2078 existing_key_pem: Option<&[u8]>,
2079) -> Result<TlsMaterial, ManagerError> {
2080 let cert_path = dir.join("cert.pem");
2081 let key_path = dir.join("key.pem");
2082 if let Some(key) = existing_key_pem {
2083 write_secret_file(&key_path, key)?;
2084 }
2085
2086 let mut command = Command::new("step");
2087 command
2088 .arg("certificate")
2089 .arg("create")
2090 .arg(common_name)
2091 .arg(&cert_path);
2092 if existing_key_pem.is_none() {
2093 command.arg(&key_path);
2094 }
2095 command
2096 .arg("--profile")
2097 .arg("self-signed")
2098 .arg("--subtle")
2099 .arg("--not-after")
2100 .arg(validity)
2101 .arg("--force");
2102 if existing_key_pem.is_some() {
2103 command.arg("--key").arg(&key_path);
2104 } else {
2105 command.arg("--no-password").arg("--insecure");
2106 }
2107
2108 let output = command.output().map_err(|e| {
2109 ManagerError::Backend(BackendError::Backend(format!(
2110 "self-signed-tls step execution failed: {e}"
2111 )))
2112 })?;
2113 if !output.status.success() {
2114 let detail = String::from_utf8_lossy(&output.stderr);
2115 return Err(ManagerError::Backend(BackendError::Backend(format!(
2116 "self-signed-tls step failed: {}",
2117 detail.trim()
2118 ))));
2119 }
2120
2121 let cert_pem = std::fs::read(&cert_path).map_err(|e| {
2122 ManagerError::Backend(BackendError::Backend(format!(
2123 "self-signed-tls read cert: {e}"
2124 )))
2125 })?;
2126 let private_key_pem = std::fs::read(&key_path).map_err(|e| {
2127 ManagerError::Backend(BackendError::Backend(format!(
2128 "self-signed-tls read key: {e}"
2129 )))
2130 })?;
2131 Ok(TlsMaterial {
2132 cert_pem,
2133 private_key_pem,
2134 })
2135}
2136
2137fn write_secret_file(path: &Path, contents: &[u8]) -> Result<(), ManagerError> {
2138 use std::io::Write as _;
2139 #[cfg(unix)]
2140 use std::os::unix::fs::OpenOptionsExt as _;
2141
2142 let mut options = std::fs::OpenOptions::new();
2143 options.write(true).create_new(true);
2144 #[cfg(unix)]
2145 options.mode(0o600);
2146 let mut file = options.open(path).map_err(|e| {
2147 ManagerError::Backend(BackendError::Backend(format!(
2148 "self-signed-tls write key: {e}"
2149 )))
2150 })?;
2151 file.write_all(contents).map_err(|e| {
2152 ManagerError::Backend(BackendError::Backend(format!(
2153 "self-signed-tls write key: {e}"
2154 )))
2155 })
2156}
2157
2158const fn key_kind(entry: &KeyEntry) -> CatalogKind {
2162 match entry.class {
2163 Class::Asymmetric | Class::Public => CatalogKind::Signing,
2164 Class::Symmetric | Class::Sealing => CatalogKind::Encryption,
2166 Class::Value => CatalogKind::Value,
2167 }
2168}
2169
2170const fn wire_key_type(entry: &KeyEntry) -> Option<KeyType> {
2174 match entry.key_type {
2175 Some(KeyAlgorithm::Ed25519) => Some(KeyType::Ed25519),
2176 Some(KeyAlgorithm::Ed25519Nkey) => Some(KeyType::Ed25519Nkey),
2177 Some(KeyAlgorithm::Rsa2048) => Some(KeyType::Rsa2048),
2178 Some(KeyAlgorithm::EcdsaP256) => Some(KeyType::EcdsaP256),
2179 Some(KeyAlgorithm::EcdsaP384) => Some(KeyType::EcdsaP384),
2180 Some(KeyAlgorithm::EcdsaP521) => Some(KeyType::EcdsaP521),
2181 Some(KeyAlgorithm::MlDsa44) => Some(KeyType::MlDsa44),
2184 Some(KeyAlgorithm::MlDsa65) => Some(KeyType::MlDsa65),
2185 Some(KeyAlgorithm::MlDsa87) => Some(KeyType::MlDsa87),
2186 Some(
2190 KeyAlgorithm::Aes256Gcm
2191 | KeyAlgorithm::ChaCha20Poly1305
2192 | KeyAlgorithm::X25519
2193 | KeyAlgorithm::MlKem512
2194 | KeyAlgorithm::MlKem768
2195 | KeyAlgorithm::MlKem1024,
2196 )
2197 | None => None,
2198 }
2199}
2200
2201fn effective_engine(entry: &KeyEntry) -> Engine {
2204 entry.effective_engine()
2205}
2206
2207fn require_class(
2210 op: &'static str,
2211 key_id: &str,
2212 class: Class,
2213 allowed: &[Class],
2214) -> Result<(), ManagerError> {
2215 if allowed.contains(&class) {
2216 Ok(())
2217 } else {
2218 Err(ManagerError::OpNotValidForClass {
2219 op,
2220 key: key_id.to_string(),
2221 class,
2222 })
2223 }
2224}
2225
2226fn require_x25519_sealing_key(
2227 key_id: &str,
2228 actual: Option<KeyAlgorithm>,
2229) -> Result<(), ManagerError> {
2230 match actual {
2231 Some(KeyAlgorithm::X25519) => Ok(()),
2232 Some(other) => Err(ManagerError::KemAlgorithmMismatch {
2233 key: key_id.to_string(),
2234 requested: "x25519",
2235 actual: other.token(),
2236 }),
2237 None => Err(ManagerError::KemAlgorithmMismatch {
2238 key: key_id.to_string(),
2239 requested: "x25519",
2240 actual: "none",
2241 }),
2242 }
2243}
2244
2245const fn ml_dsa_wire_key_type(algorithm: SignatureAlgorithm) -> KeyType {
2249 match algorithm {
2250 SignatureAlgorithm::MlDsa44 => KeyType::MlDsa44,
2251 SignatureAlgorithm::MlDsa65 => KeyType::MlDsa65,
2252 SignatureAlgorithm::MlDsa87 => KeyType::MlDsa87,
2253 SignatureAlgorithm::Ed25519
2254 | SignatureAlgorithm::Ed25519Nkey
2255 | SignatureAlgorithm::Rs256
2256 | SignatureAlgorithm::Es256 => KeyType::Ed25519,
2257 }
2258}
2259
2260const fn ml_kem_wire_key_type(kem: ProviderKemAlgorithm) -> KeyType {
2264 match kem {
2265 ProviderKemAlgorithm::MlKem512 => KeyType::MlKem512,
2266 ProviderKemAlgorithm::MlKem768 => KeyType::MlKem768,
2267 ProviderKemAlgorithm::MlKem1024 => KeyType::MlKem1024,
2268 }
2269}
2270
2271const fn ml_kem_provider_algorithm(algorithm: KeyAlgorithm) -> Option<ProviderKemAlgorithm> {
2275 match algorithm {
2276 KeyAlgorithm::MlKem512 => Some(ProviderKemAlgorithm::MlKem512),
2277 KeyAlgorithm::MlKem768 => Some(ProviderKemAlgorithm::MlKem768),
2278 KeyAlgorithm::MlKem1024 => Some(ProviderKemAlgorithm::MlKem1024),
2279 KeyAlgorithm::Ed25519
2280 | KeyAlgorithm::Ed25519Nkey
2281 | KeyAlgorithm::Rsa2048
2282 | KeyAlgorithm::EcdsaP256
2283 | KeyAlgorithm::EcdsaP384
2284 | KeyAlgorithm::EcdsaP521
2285 | KeyAlgorithm::Aes256Gcm
2286 | KeyAlgorithm::ChaCha20Poly1305
2287 | KeyAlgorithm::X25519
2288 | KeyAlgorithm::MlDsa44
2289 | KeyAlgorithm::MlDsa65
2290 | KeyAlgorithm::MlDsa87 => None,
2291 }
2292}
2293
2294fn require_ml_kem_sealing_key(
2295 key_id: &str,
2296 actual: Option<KeyAlgorithm>,
2297 requested: ProviderKemAlgorithm,
2298) -> Result<(), ManagerError> {
2299 let expected = match requested {
2300 ProviderKemAlgorithm::MlKem512 => KeyAlgorithm::MlKem512,
2301 ProviderKemAlgorithm::MlKem768 => KeyAlgorithm::MlKem768,
2302 ProviderKemAlgorithm::MlKem1024 => KeyAlgorithm::MlKem1024,
2303 };
2304 match actual {
2305 Some(found) if found == expected => Ok(()),
2306 Some(found) => Err(ManagerError::KemAlgorithmMismatch {
2307 key: key_id.to_string(),
2308 requested: requested.token(),
2309 actual: found.token(),
2310 }),
2311 None => Err(ManagerError::KemAlgorithmMismatch {
2312 key: key_id.to_string(),
2313 requested: requested.token(),
2314 actual: "none",
2315 }),
2316 }
2317}
2318
2319#[cfg(test)]
2320mod tests {
2321 use super::*;
2322 use crate::backend::{KeyMetadata, KvValue};
2323 use crate::core::crypto_provider::{SoftwareCustodyCatalog, encode_record_bytes};
2324 use crate::ml_kem_envelope;
2325 use async_trait::async_trait;
2326 use std::sync::Arc;
2327 use std::sync::atomic::{AtomicUsize, Ordering};
2328
2329 #[derive(Default)]
2332 struct MockBackend {
2333 name: &'static str,
2334 last_path: std::sync::Mutex<Option<String>>,
2335 last_spiffe_id: std::sync::Mutex<Option<String>>,
2336 new_key_calls: AtomicUsize,
2337 rotate_calls: AtomicUsize,
2338 kv_put_calls: AtomicUsize,
2339 latest_version: AtomicUsize,
2341 last_versions_config: std::sync::Mutex<Option<(Option<u32>, Option<u32>)>>,
2345 last_kv_value: std::sync::Mutex<Option<Vec<u8>>>,
2347 last_sign_options: std::sync::Mutex<Option<SignOptions>>,
2348 last_verify_options: std::sync::Mutex<Option<SignOptions>>,
2349 last_new_key_type: std::sync::Mutex<Option<KeyType>>,
2350 kv_store: std::sync::Mutex<BTreeMap<String, Vec<u8>>>,
2357 kv_put_log: std::sync::Mutex<Vec<(String, Vec<u8>)>>,
2358 }
2359
2360 impl MockBackend {
2361 fn new(name: &'static str) -> Arc<Self> {
2362 Arc::new(Self {
2363 name,
2364 last_path: std::sync::Mutex::new(None),
2365 last_spiffe_id: std::sync::Mutex::new(None),
2366 new_key_calls: AtomicUsize::new(0),
2367 rotate_calls: AtomicUsize::new(0),
2368 kv_put_calls: AtomicUsize::new(0),
2369 latest_version: AtomicUsize::new(5),
2370 last_versions_config: std::sync::Mutex::new(None),
2371 last_kv_value: std::sync::Mutex::new(None),
2372 last_sign_options: std::sync::Mutex::new(None),
2373 last_verify_options: std::sync::Mutex::new(None),
2374 last_new_key_type: std::sync::Mutex::new(None),
2375 kv_store: std::sync::Mutex::new(BTreeMap::new()),
2376 kv_put_log: std::sync::Mutex::new(Vec::new()),
2377 })
2378 }
2379
2380 fn seed_kv(&self, path: &str, value: Vec<u8>) {
2383 self.kv_store
2384 .lock()
2385 .unwrap()
2386 .insert(path.to_string(), value);
2387 }
2388
2389 fn kv_lookup(&self, path: &str) -> Vec<u8> {
2393 if let Some(v) = self.kv_store.lock().unwrap().get(path) {
2394 return v.clone();
2395 }
2396 self.last_kv_value
2397 .lock()
2398 .unwrap()
2399 .clone()
2400 .unwrap_or_else(|| b"stored-value".to_vec())
2401 }
2402
2403 fn last_path(&self) -> Option<String> {
2404 self.last_path.lock().unwrap().clone()
2405 }
2406
2407 fn last_versions_config(&self) -> Option<(Option<u32>, Option<u32>)> {
2408 *self.last_versions_config.lock().unwrap()
2409 }
2410
2411 fn last_kv_value(&self) -> Option<Vec<u8>> {
2412 self.last_kv_value.lock().unwrap().clone()
2413 }
2414
2415 fn last_spiffe_id(&self) -> Option<String> {
2416 self.last_spiffe_id.lock().unwrap().clone()
2417 }
2418
2419 fn kv_put_log(&self) -> Vec<(String, Vec<u8>)> {
2420 self.kv_put_log.lock().unwrap().clone()
2421 }
2422
2423 fn last_sign_options(&self) -> Option<SignOptions> {
2424 *self.last_sign_options.lock().unwrap()
2425 }
2426
2427 fn last_verify_options(&self) -> Option<SignOptions> {
2428 *self.last_verify_options.lock().unwrap()
2429 }
2430
2431 fn last_new_key_type(&self) -> Option<KeyType> {
2432 *self.last_new_key_type.lock().unwrap()
2433 }
2434 }
2435
2436 struct MockHandle(Arc<MockBackend>);
2439
2440 #[async_trait]
2441 impl Backend for MockHandle {
2442 fn kind(&self) -> &'static str {
2443 self.0.name
2444 }
2445
2446 async fn new_key(&self, key_type: KeyType) -> Result<NewKey, BackendError> {
2447 self.0.new_key_calls.fetch_add(1, Ordering::SeqCst);
2448 *self.0.last_new_key_type.lock().unwrap() = Some(key_type);
2449 Ok(NewKey {
2450 key_id: format!("{}-newkey", self.0.name),
2451 public_key: vec![1, 2, 3],
2452 })
2453 }
2454
2455 async fn public_key(&self, key_id: &str) -> Result<Vec<u8>, BackendError> {
2456 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2457 Ok(vec![9, 9, 9])
2458 }
2459
2460 async fn public_key_with_meta(&self, key_id: &str) -> Result<PublicKey, BackendError> {
2461 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2462 Ok(PublicKey {
2463 public_key: vec![9, 9, 9],
2464 key_type: KeyType::Ed25519Nkey,
2467 version: 7,
2468 })
2469 }
2470
2471 async fn key_metadata(&self, key_id: &str) -> Result<KeyMetadata, BackendError> {
2472 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2473 Ok(KeyMetadata {
2474 key_type: Some(KeyType::Ed25519),
2475 latest_version: u32::try_from(self.0.latest_version.load(Ordering::SeqCst))
2476 .unwrap_or(u32::MAX),
2477 })
2478 }
2479
2480 async fn import(
2481 &self,
2482 key_id: &str,
2483 _key_type: KeyType,
2484 _material: &KeyMaterial,
2485 ) -> Result<NewKey, BackendError> {
2486 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2487 Ok(NewKey {
2488 key_id: key_id.to_string(),
2489 public_key: vec![0xEE, 0xEE],
2490 })
2491 }
2492
2493 async fn sign(&self, key_id: &str, _message: &[u8]) -> Result<Vec<u8>, BackendError> {
2494 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2495 *self.0.last_sign_options.lock().unwrap() = Some(SignOptions::Default);
2496 Ok(vec![0xAB, 0xCD])
2497 }
2498
2499 async fn sign_with_options(
2500 &self,
2501 key_id: &str,
2502 message: &[u8],
2503 options: SignOptions,
2504 ) -> Result<Vec<u8>, BackendError> {
2505 let mut signature = self.sign(key_id, message).await?;
2506 *self.0.last_sign_options.lock().unwrap() = Some(options);
2507 if options == SignOptions::Rs256Pkcs1v15Sha256 {
2508 signature.push(0x52);
2509 } else if options == SignOptions::Es256 {
2510 signature.push(0x45);
2511 } else if options == SignOptions::Es384 {
2512 signature.push(0x46);
2513 } else if options == SignOptions::Es512 {
2514 signature.push(0x47);
2515 }
2516 Ok(signature)
2517 }
2518
2519 async fn verify(
2520 &self,
2521 key_id: &str,
2522 _message: &[u8],
2523 _signature: &[u8],
2524 ) -> Result<bool, BackendError> {
2525 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2526 *self.0.last_verify_options.lock().unwrap() = Some(SignOptions::Default);
2527 Ok(true)
2528 }
2529
2530 async fn verify_with_options(
2531 &self,
2532 key_id: &str,
2533 message: &[u8],
2534 signature: &[u8],
2535 options: SignOptions,
2536 ) -> Result<bool, BackendError> {
2537 let valid = self.verify(key_id, message, signature).await?;
2538 *self.0.last_verify_options.lock().unwrap() = Some(options);
2539 Ok(valid)
2540 }
2541
2542 async fn encrypt(
2548 &self,
2549 key_id: &str,
2550 algorithm: AeadAlgorithm,
2551 plaintext: &[u8],
2552 aad: Option<&[u8]>,
2553 ) -> Result<CiphertextEnvelope, BackendError> {
2554 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2555 let aad = aad.unwrap_or(&[]);
2556 let mut ciphertext = Vec::new();
2557 ciphertext.push(u8::try_from(aad.len()).unwrap_or(u8::MAX));
2558 ciphertext.extend_from_slice(aad);
2559 ciphertext.extend_from_slice(plaintext);
2560 Ok(CiphertextEnvelope {
2561 alg: algorithm,
2562 key_version: u32::try_from(self.0.latest_version.load(Ordering::SeqCst))
2563 .unwrap_or(u32::MAX),
2564 nonce: Vec::new(),
2565 ciphertext,
2566 })
2567 }
2568
2569 async fn decrypt(
2570 &self,
2571 key_id: &str,
2572 envelope: &CiphertextEnvelope,
2573 aad: Option<&[u8]>,
2574 ) -> Result<Vec<u8>, BackendError> {
2575 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2576 let aad = aad.unwrap_or(&[]);
2577 let ct = &envelope.ciphertext;
2578 let aad_len = *ct.first().ok_or(BackendError::DecryptFailed)? as usize;
2579 let bound = ct.get(1..1 + aad_len).ok_or(BackendError::DecryptFailed)?;
2580 if bound != aad {
2582 return Err(BackendError::DecryptFailed);
2583 }
2584 Ok(ct
2585 .get(1 + aad_len..)
2586 .ok_or(BackendError::DecryptFailed)?
2587 .to_vec())
2588 }
2589
2590 async fn rotate(&self, key_id: &str) -> Result<u32, BackendError> {
2591 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2592 self.0.rotate_calls.fetch_add(1, Ordering::SeqCst);
2593 let v = self.0.latest_version.fetch_add(1, Ordering::SeqCst) + 1;
2594 Ok(u32::try_from(v).unwrap_or(u32::MAX))
2595 }
2596
2597 async fn kv_get(
2598 &self,
2599 key_id: &str,
2600 version: Option<u32>,
2601 ) -> Result<KvValue, BackendError> {
2602 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2603 let value = self.0.kv_lookup(key_id);
2607 let version = version.unwrap_or_else(|| {
2608 u32::try_from(self.0.latest_version.load(Ordering::SeqCst)).unwrap_or(u32::MAX)
2609 });
2610 Ok(KvValue { value, version })
2611 }
2612
2613 async fn kv_get_secret(
2614 &self,
2615 key_id: &str,
2616 _version: Option<u32>,
2617 ) -> Result<Zeroizing<Vec<u8>>, BackendError> {
2618 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2619 Ok(Zeroizing::new(self.0.kv_lookup(key_id)))
2620 }
2621
2622 async fn kv_put(&self, key_id: &str, value: &[u8]) -> Result<u32, BackendError> {
2623 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2624 self.0.kv_put_calls.fetch_add(1, Ordering::SeqCst);
2625 *self.0.last_kv_value.lock().unwrap() = Some(value.to_vec());
2626 self.0
2627 .kv_store
2628 .lock()
2629 .unwrap()
2630 .insert(key_id.to_string(), value.to_vec());
2631 self.0
2632 .kv_put_log
2633 .lock()
2634 .unwrap()
2635 .push((key_id.to_string(), value.to_vec()));
2636 let v = self.0.latest_version.fetch_add(1, Ordering::SeqCst) + 1;
2637 Ok(u32::try_from(v).unwrap_or(u32::MAX))
2638 }
2639
2640 async fn configure_versions(
2641 &self,
2642 key_id: &str,
2643 min_decryption_version: Option<u32>,
2644 min_available_version: Option<u32>,
2645 ) -> Result<(), BackendError> {
2646 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2647 *self.0.last_versions_config.lock().unwrap() =
2648 Some((min_decryption_version, min_available_version));
2649 Ok(())
2650 }
2651
2652 async fn issue_x509_svid(
2653 &self,
2654 key_id: &str,
2655 spiffe_id: &str,
2656 _ttl_seconds: u64,
2657 ) -> Result<X509Svid, BackendError> {
2658 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2659 *self.0.last_spiffe_id.lock().unwrap() = Some(spiffe_id.to_string());
2660 Ok(X509Svid {
2661 cert_chain_der: vec![vec![1, 2, 3]],
2662 leaf_private_key_der: zeroize::Zeroizing::new(vec![4, 5, 6]),
2663 bundle_der: vec![vec![7, 8, 9]],
2664 })
2665 }
2666
2667 async fn issue_x509_cert(
2668 &self,
2669 key_id: &str,
2670 request: &X509CertRequest,
2671 ) -> Result<X509Svid, BackendError> {
2672 *self.0.last_path.lock().unwrap() = Some(key_id.to_string());
2673 *self.0.last_spiffe_id.lock().unwrap() = Some(request.common_name.clone());
2675 Ok(X509Svid {
2676 cert_chain_der: vec![vec![0xCE, 0x27]],
2677 leaf_private_key_der: zeroize::Zeroizing::new(vec![0xDE, 0xAD]),
2678 bundle_der: vec![vec![0xCA, 0xFE]],
2679 })
2680 }
2681 }
2682
2683 const CATALOG: &str = r#"{
2693 "schemaVersion": 1,
2694 "backends": {
2695 "primary": {
2696 "kind": "vault", "addr": "https://127.0.0.1:8200",
2697 "engines": ["transit", "kv2"], "capabilities": [],
2698 "mintKeyTypes": ["ed25519", "ed25519-nkey", "rsa-2048", "ecdsa-p256", "ecdsa-p384", "ecdsa-p521"]
2699 },
2700 "secondary": {
2701 "kind": "vault", "addr": "https://127.0.0.1:8201",
2702 "engines": ["transit", "kv2"], "capabilities": [],
2703 "mintKeyTypes": ["ed25519", "ed25519-nkey", "rsa-2048", "ecdsa-p256"]
2704 }
2705 },
2706 "keys": {
2707 "asym.signer": {
2708 "class": "asymmetric", "keyType": "ed25519", "backend": "primary",
2709 "path": "signer", "writable": true, "missing": "error",
2710 "description": "a signing key"
2711 },
2712 "asym.nkey": {
2713 "class": "asymmetric", "keyType": "ed25519-nkey", "backend": "primary",
2714 "path": "nkey-signer", "writable": true, "missing": "error",
2715 "description": "a NATS nkey signing key"
2716 },
2717 "asym.rsa": {
2718 "class": "asymmetric", "keyType": "rsa-2048", "backend": "primary",
2719 "path": "rsa-signer", "writable": true, "missing": "error",
2720 "description": "an RSA signing key"
2721 },
2722 "asym.ecdsa": {
2723 "class": "asymmetric", "keyType": "ecdsa-p256", "backend": "primary",
2724 "path": "ecdsa-signer", "writable": true, "missing": "error",
2725 "description": "an ECDSA P-256 signing key"
2726 },
2727 "asym.ecdsa384": {
2728 "class": "asymmetric", "keyType": "ecdsa-p384", "backend": "primary",
2729 "path": "ecdsa384-signer", "writable": true, "missing": "error",
2730 "description": "an ECDSA P-384 signing key"
2731 },
2732 "asym.ecdsa521": {
2733 "class": "asymmetric", "keyType": "ecdsa-p521", "backend": "primary",
2734 "path": "ecdsa521-signer", "writable": true, "missing": "error",
2735 "description": "an ECDSA P-521 signing key"
2736 },
2737 "sym.box": {
2738 "class": "symmetric", "keyType": "aes-256-gcm", "backend": "secondary",
2739 "path": "box", "writable": true, "missing": "error",
2740 "description": "a symmetric key"
2741 },
2742 "web.value": {
2743 "class": "value", "backend": "primary", "engine": "kv2",
2744 "path": "secret/data/web/value", "writable": true, "missing": "error",
2745 "description": "an opaque value"
2746 },
2747 "gen.value": {
2748 "class": "value", "backend": "primary", "engine": "kv2",
2749 "path": "secret/data/gen/value", "writable": true, "missing": "generate",
2750 "generate": { "format": "ascii-printable", "bytes": 24 },
2751 "description": "a generate-able value (rotate regenerates)"
2752 },
2753 "tls.cert": {
2754 "class": "public", "backend": "primary", "engine": "kv2",
2755 "path": "secret/data/tls/cert", "writable": false, "missing": "generate",
2756 "generate": { "format": "self-signed-tls", "commonName": "example.test", "validity": "1h" },
2757 "description": "a generated self-signed cert"
2758 },
2759 "tls.key": {
2760 "class": "value", "backend": "primary", "engine": "kv2",
2761 "path": "secret/data/tls/key", "writable": true, "missing": "generate",
2762 "generate": { "format": "self-signed-tls-pair-of", "pairOf": "tls.cert" },
2763 "description": "the private key paired with tls.cert"
2764 },
2765 "web.cert": {
2766 "class": "public", "backend": "secondary", "engine": "kv2",
2767 "path": "secret/data/web/cert", "writable": false, "missing": "warn",
2768 "description": "a public cert"
2769 },
2770 "spiffe.issuer": {
2771 "class": "asymmetric", "keyType": "ed25519", "backend": "secondary", "engine": "pki",
2772 "path": "pki/issue/workload", "writable": false, "missing": "error",
2773 "description": "a pki issuer role"
2774 },
2775 "enroll.sealing": {
2776 "class": "sealing", "keyType": "x25519", "backend": "primary", "engine": "kv2",
2777 "path": "secret/data/enroll/x25519",
2778 "publicPath": "secret/data/enroll/x25519-public",
2779 "writable": true, "missing": "error",
2780 "description": "an x25519 enrollment sealing key"
2781 },
2782 "enroll.mlkem": {
2783 "class": "sealing", "keyType": "ml-kem-768", "backend": "primary", "engine": "kv2",
2784 "path": "secret/data/enroll/ml-kem-768",
2785 "publicPath": "secret/data/enroll/ml-kem-768-public",
2786 "labels": [
2787 "crypto_provider=local-software",
2788 "crypto_provider_version=1",
2789 "pqc_algorithm=ml-kem-768",
2790 "pqc_custody=software-encrypted",
2791 "crypto_provider_policy=local-software",
2792 "pqc_storage_key=pqc/storage/wrap"
2793 ],
2794 "writable": true, "missing": "error",
2795 "description": "an ML-KEM enrollment sealing key"
2796 },
2797 "kv2.signer": {
2798 "class": "asymmetric", "keyType": "ed25519", "backend": "primary", "engine": "kv2",
2799 "path": "secret/data/kv2/signer",
2800 "publicPath": "secret/data/kv2/signer-public",
2801 "writable": true, "missing": "error",
2802 "description": "a value-store Ed25519 materialize-to-sign key"
2803 }
2804 }
2805 }"#;
2806
2807 fn parse_catalog(json: &str) -> Catalog {
2808 serde_json::from_str(json).expect("catalog parses")
2809 }
2810
2811 fn fixture() -> (BackendManager, Arc<MockBackend>, Arc<MockBackend>) {
2814 let primary = MockBackend::new("primary");
2815 let secondary = MockBackend::new("secondary");
2816 let mut backends: BTreeMap<String, Box<dyn Backend>> = BTreeMap::new();
2817 backends.insert("primary".into(), Box::new(MockHandle(primary.clone())));
2818 backends.insert("secondary".into(), Box::new(MockHandle(secondary.clone())));
2819 let mgr = BackendManager::new(parse_catalog(CATALOG), backends)
2820 .expect("manager constructs cleanly");
2821 (mgr, primary, secondary)
2822 }
2823
2824 #[test]
2825 fn new_rejects_key_naming_absent_backend() {
2826 let mut backends: BTreeMap<String, Box<dyn Backend>> = BTreeMap::new();
2828 backends.insert(
2829 "primary".into(),
2830 Box::new(MockHandle(MockBackend::new("primary"))),
2831 );
2832 let err = BackendManager::new(parse_catalog(CATALOG), backends)
2834 .expect_err("missing backend must fail closed");
2835 match err {
2836 ManagerError::UnknownBackend { backend, .. } => assert_eq!(backend, "secondary"),
2837 other => panic!("expected UnknownBackend, got {other:?}"),
2838 }
2839 }
2840
2841 #[test]
2842 fn resolve_maps_key_to_correct_backend_and_path() {
2843 let (mgr, _p, _s) = fixture();
2844
2845 let routed = mgr.resolve("asym.signer").expect("resolves");
2846 assert_eq!(routed.backend.kind(), "primary");
2847 assert_eq!(routed.path(), "signer");
2848 assert_eq!(routed.class(), Class::Asymmetric);
2849 assert_eq!(routed.engine, Engine::Transit);
2850 assert_eq!(routed.key_type(), Some(KeyAlgorithm::Ed25519));
2851
2852 let routed = mgr.resolve("web.cert").expect("resolves");
2853 assert_eq!(routed.backend.kind(), "secondary");
2854 assert_eq!(routed.path(), "secret/data/web/cert");
2855 assert_eq!(routed.class(), Class::Public);
2856 assert_eq!(routed.engine, Engine::Kv2);
2857
2858 let routed = mgr.resolve("spiffe.issuer").expect("resolves");
2859 assert_eq!(routed.backend.kind(), "secondary");
2860 assert_eq!(routed.path(), "pki/issue/workload");
2861 assert_eq!(routed.class(), Class::Asymmetric);
2862 assert_eq!(routed.engine, Engine::Pki);
2863 }
2864
2865 #[test]
2866 fn resolve_infers_engine_when_catalog_omits_it() {
2867 let (mgr, _p, _s) = fixture();
2869 assert_eq!(mgr.resolve("asym.signer").unwrap().engine, Engine::Transit);
2870 assert_eq!(mgr.resolve("sym.box").unwrap().engine, Engine::Transit);
2871 }
2872
2873 #[test]
2874 fn resolve_unknown_key_errors() {
2875 let (mgr, _p, _s) = fixture();
2876 let err = mgr.resolve("does.not.exist").expect_err("unknown key");
2877 match err {
2878 ManagerError::UnknownKey(k) => assert_eq!(k, "does.not.exist"),
2879 other => panic!("expected UnknownKey, got {other:?}"),
2880 }
2881 }
2882
2883 #[tokio::test]
2884 async fn sign_routes_to_correct_backend_with_path() {
2885 let (mgr, primary, secondary) = fixture();
2886 let sig = mgr.sign("asym.signer", &[1, 2, 3]).await.expect("signs");
2887 assert_eq!(sig, vec![0xAB, 0xCD]);
2888 assert_eq!(primary.last_path().as_deref(), Some("signer"));
2891 assert_eq!(
2892 secondary.last_path(),
2893 None,
2894 "the other backend is untouched"
2895 );
2896 }
2897
2898 #[tokio::test]
2899 async fn nkey_sign_uses_raw_ed25519_transit_defaults() {
2900 let (mgr, primary, _secondary) = fixture();
2901 let sig = mgr.sign("asym.nkey", b"nonce").await.expect("signs nkey");
2902 assert_eq!(sig, vec![0xAB, 0xCD]);
2903 assert_eq!(primary.last_path().as_deref(), Some("nkey-signer"));
2904 assert_eq!(primary.last_sign_options(), Some(SignOptions::Default));
2905 }
2906
2907 #[tokio::test]
2908 async fn rsa_sign_and_verify_use_rs256_transit_options() {
2909 let (mgr, primary, _secondary) = fixture();
2910 let sig = mgr.sign("asym.rsa", b"jwt-input").await.expect("signs rsa");
2911 assert_eq!(sig, vec![0xAB, 0xCD, 0x52]);
2912 assert_eq!(primary.last_path().as_deref(), Some("rsa-signer"));
2913 assert_eq!(
2914 primary.last_sign_options(),
2915 Some(SignOptions::Rs256Pkcs1v15Sha256)
2916 );
2917
2918 let valid = mgr
2919 .verify("asym.rsa", b"jwt-input", &sig)
2920 .await
2921 .expect("verifies rsa");
2922 assert!(valid);
2923 assert_eq!(
2924 primary.last_verify_options(),
2925 Some(SignOptions::Rs256Pkcs1v15Sha256)
2926 );
2927 }
2928
2929 #[tokio::test]
2930 async fn ecdsa_sign_and_verify_use_es256_transit_options() {
2931 let (mgr, primary, _secondary) = fixture();
2932 let sig = mgr
2933 .sign("asym.ecdsa", b"jwt-input")
2934 .await
2935 .expect("signs ecdsa");
2936 assert_eq!(sig, vec![0xAB, 0xCD, 0x45]);
2937 assert_eq!(primary.last_path().as_deref(), Some("ecdsa-signer"));
2938 assert_eq!(primary.last_sign_options(), Some(SignOptions::Es256));
2939
2940 let valid = mgr
2941 .verify("asym.ecdsa", b"jwt-input", &sig)
2942 .await
2943 .expect("verifies ecdsa");
2944 assert!(valid);
2945 assert_eq!(primary.last_verify_options(), Some(SignOptions::Es256));
2946 }
2947
2948 #[tokio::test]
2949 async fn ecdsa_p384_sign_and_verify_use_es384_transit_options() {
2950 let (mgr, primary, _secondary) = fixture();
2951 let sig = mgr
2952 .sign("asym.ecdsa384", b"jwt-input")
2953 .await
2954 .expect("signs ecdsa p384");
2955 assert_eq!(sig, vec![0xAB, 0xCD, 0x46]);
2956 assert_eq!(primary.last_path().as_deref(), Some("ecdsa384-signer"));
2957 assert_eq!(primary.last_sign_options(), Some(SignOptions::Es384));
2958
2959 let valid = mgr
2960 .verify("asym.ecdsa384", b"jwt-input", &sig)
2961 .await
2962 .expect("verifies ecdsa p384");
2963 assert!(valid);
2964 assert_eq!(primary.last_verify_options(), Some(SignOptions::Es384));
2965 }
2966
2967 #[tokio::test]
2968 async fn ecdsa_p521_sign_and_verify_use_es512_transit_options() {
2969 let (mgr, primary, _secondary) = fixture();
2970 let sig = mgr
2971 .sign("asym.ecdsa521", b"jwt-input")
2972 .await
2973 .expect("signs ecdsa p521");
2974 assert_eq!(sig, vec![0xAB, 0xCD, 0x47]);
2975 assert_eq!(primary.last_path().as_deref(), Some("ecdsa521-signer"));
2976 assert_eq!(primary.last_sign_options(), Some(SignOptions::Es512));
2977
2978 let valid = mgr
2979 .verify("asym.ecdsa521", b"jwt-input", &sig)
2980 .await
2981 .expect("verifies ecdsa p521");
2982 assert!(valid);
2983 assert_eq!(primary.last_verify_options(), Some(SignOptions::Es512));
2984 }
2985
2986 #[tokio::test]
2987 async fn issue_x509_svid_routes_to_pki_issue_path() {
2988 let (mgr, _primary, secondary) = fixture();
2989 let svid = mgr
2990 .issue_x509_svid("spiffe.issuer", "spiffe://example.test/web", 300)
2991 .await
2992 .expect("issues x509 svid");
2993 assert_eq!(secondary.last_path().as_deref(), Some("pki/issue/workload"));
2994 assert_eq!(
2995 secondary.last_spiffe_id().as_deref(),
2996 Some("spiffe://example.test/web")
2997 );
2998 assert_eq!(svid.cert_chain_der, vec![vec![1, 2, 3]]);
2999 assert_eq!(&*svid.leaf_private_key_der, &[4, 5, 6]);
3000 assert_eq!(svid.bundle_der, vec![vec![7, 8, 9]]);
3001 }
3002
3003 #[tokio::test]
3004 async fn issue_x509_svid_rejects_non_pki_key() {
3005 let (mgr, _primary, _secondary) = fixture();
3006 let err = mgr
3007 .issue_x509_svid("asym.signer", "spiffe://example.test/web", 300)
3008 .await
3009 .expect_err("transit key is not a pki issuer");
3010 assert!(matches!(err, ManagerError::Unsupported("issue_x509_svid")));
3011 }
3012
3013 #[tokio::test]
3014 async fn issue_x509_cert_routes_to_pki_issue_path_with_sans() {
3015 let (mgr, _primary, secondary) = fixture();
3016 let request = X509CertRequest {
3017 common_name: "web.internal".into(),
3018 dns_sans: vec!["web.internal".into(), "alt.internal".into()],
3019 ip_sans: vec!["10.0.0.1".into()],
3020 ttl_seconds: 3600,
3021 };
3022 let issued = mgr
3023 .issue_x509_cert("spiffe.issuer", &request)
3024 .await
3025 .expect("issues x509 cert");
3026 assert_eq!(secondary.last_path().as_deref(), Some("pki/issue/workload"));
3027 assert_eq!(secondary.last_spiffe_id().as_deref(), Some("web.internal"));
3028 assert_eq!(issued.cert_chain_der, vec![vec![0xCE, 0x27]]);
3029 assert_eq!(&*issued.leaf_private_key_der, &[0xDE, 0xAD]);
3030 }
3031
3032 #[tokio::test]
3033 async fn issue_x509_cert_rejects_non_pki_key() {
3034 let (mgr, _primary, _secondary) = fixture();
3035 let request = X509CertRequest {
3036 common_name: "web.internal".into(),
3037 ..X509CertRequest::default()
3038 };
3039 let err = mgr
3040 .issue_x509_cert("asym.signer", &request)
3041 .await
3042 .expect_err("transit key is not a pki issuer");
3043 assert!(matches!(err, ManagerError::Unsupported("issue_x509_cert")));
3044 }
3045
3046 #[tokio::test]
3047 async fn new_key_routes_to_correct_backend() {
3048 let (mgr, primary, secondary) = fixture();
3049 let nk = mgr
3050 .new_key("asym.signer", KeyType::Ed25519)
3051 .await
3052 .expect("new_key");
3053 assert_eq!(nk.key_id, "primary-newkey");
3054 assert_eq!(primary.new_key_calls.load(Ordering::SeqCst), 1);
3055 assert_eq!(primary.last_new_key_type(), Some(KeyType::Ed25519));
3056 assert_eq!(secondary.new_key_calls.load(Ordering::SeqCst), 0);
3057
3058 mgr.new_key("asym.nkey", KeyType::Ed25519Nkey)
3059 .await
3060 .expect("new_key nkey");
3061 assert_eq!(primary.last_new_key_type(), Some(KeyType::Ed25519Nkey));
3062
3063 mgr.new_key("asym.rsa", KeyType::Rsa2048)
3064 .await
3065 .expect("new_key rsa");
3066 assert_eq!(primary.last_new_key_type(), Some(KeyType::Rsa2048));
3067
3068 mgr.new_key("asym.ecdsa", KeyType::EcdsaP256)
3069 .await
3070 .expect("new_key ecdsa");
3071 assert_eq!(primary.last_new_key_type(), Some(KeyType::EcdsaP256));
3072
3073 mgr.new_key("asym.ecdsa384", KeyType::EcdsaP384)
3074 .await
3075 .expect("new_key ecdsa p384");
3076 assert_eq!(primary.last_new_key_type(), Some(KeyType::EcdsaP384));
3077
3078 mgr.new_key("asym.ecdsa521", KeyType::EcdsaP521)
3079 .await
3080 .expect("new_key ecdsa p521");
3081 assert_eq!(primary.last_new_key_type(), Some(KeyType::EcdsaP521));
3082 }
3083
3084 #[tokio::test]
3085 async fn new_key_rejects_key_type_absent_from_static_backend_preset() {
3086 let catalog = CATALOG.replace(
3087 r#""mintKeyTypes": ["ed25519", "ed25519-nkey", "rsa-2048", "ecdsa-p256", "ecdsa-p384", "ecdsa-p521"]"#,
3088 r#""mintKeyTypes": ["ed25519"]"#,
3089 );
3090 let primary = MockBackend::new("primary");
3091 let secondary = MockBackend::new("secondary");
3092 let mut backends: BTreeMap<String, Box<dyn Backend>> = BTreeMap::new();
3093 backends.insert("primary".into(), Box::new(MockHandle(primary.clone())));
3094 backends.insert("secondary".into(), Box::new(MockHandle(secondary)));
3095 let mgr = BackendManager::new(parse_catalog(&catalog), backends).expect("manager builds");
3096
3097 let err = mgr
3098 .new_key("asym.signer", KeyType::Rsa2048)
3099 .await
3100 .expect_err("rsa absent from backend preset");
3101 assert!(matches!(
3102 err,
3103 ManagerError::UnsupportedKeyType {
3104 backend,
3105 op: "new_key",
3106 key_type: KeyType::Rsa2048
3107 } if backend == "primary"
3108 ));
3109 assert_eq!(primary.new_key_calls.load(Ordering::SeqCst), 0);
3110 }
3111
3112 #[tokio::test]
3113 async fn get_public_key_valid_on_public_class_returns_real_metadata() {
3114 let (mgr, _p, secondary) = fixture();
3115 let pk = mgr.get_public_key("web.cert").await.expect("public read");
3116 assert_eq!(pk.public_key, vec![9, 9, 9]);
3117 assert_eq!(pk.key_type, KeyType::Ed25519Nkey);
3119 assert_eq!(pk.version, 7);
3120 assert_eq!(
3121 secondary.last_path().as_deref(),
3122 Some("secret/data/web/cert")
3123 );
3124 }
3125
3126 #[tokio::test]
3127 async fn import_routes_to_backend_path_and_returns_only_public() {
3128 let (mgr, primary, _s) = fixture();
3129 let nk = mgr
3130 .import(
3131 "asym.signer",
3132 KeyType::Ed25519,
3133 &KeyMaterial::Ed25519Seed(vec![7; 32]),
3134 )
3135 .await
3136 .expect("import");
3137 assert_eq!(nk.public_key, vec![0xEE, 0xEE]);
3139 assert_eq!(primary.last_path().as_deref(), Some("signer"));
3141 }
3142
3143 #[tokio::test]
3144 async fn import_rejects_key_type_absent_from_static_backend_preset() {
3145 let catalog = CATALOG.replace(
3146 r#""mintKeyTypes": ["ed25519", "ed25519-nkey", "rsa-2048", "ecdsa-p256", "ecdsa-p384", "ecdsa-p521"]"#,
3147 r#""mintKeyTypes": ["ed25519"]"#,
3148 );
3149 let primary = MockBackend::new("primary");
3150 let secondary = MockBackend::new("secondary");
3151 let mut backends: BTreeMap<String, Box<dyn Backend>> = BTreeMap::new();
3152 backends.insert("primary".into(), Box::new(MockHandle(primary.clone())));
3153 backends.insert("secondary".into(), Box::new(MockHandle(secondary)));
3154 let mgr = BackendManager::new(parse_catalog(&catalog), backends).expect("manager builds");
3155
3156 let err = mgr
3157 .import(
3158 "asym.signer",
3159 KeyType::Rsa2048,
3160 &KeyMaterial::Pkcs8Der(vec![1, 2, 3]),
3161 )
3162 .await
3163 .expect_err("rsa absent from backend preset");
3164 assert!(matches!(
3165 err,
3166 ManagerError::UnsupportedKeyType {
3167 backend,
3168 op: "import",
3169 key_type: KeyType::Rsa2048
3170 } if backend == "primary"
3171 ));
3172 assert!(primary.last_path().is_none());
3173 }
3174
3175 #[tokio::test]
3176 async fn import_on_value_key_is_op_not_valid_for_class() {
3177 let (mgr, _p, _s) = fixture();
3178 let err = mgr
3179 .import(
3180 "web.value",
3181 KeyType::Ed25519,
3182 &KeyMaterial::Ed25519Seed(vec![0; 32]),
3183 )
3184 .await
3185 .expect_err("import on a value key must be rejected");
3186 assert!(matches!(err, ManagerError::OpNotValidForClass { .. }));
3187 }
3188
3189 #[tokio::test]
3190 async fn list_projects_catalog_value_free_and_respects_visibility() {
3191 let (mgr, _p, _s) = fixture();
3192 let entries = mgr
3194 .list(None, |name| name != "web.value")
3197 .await
3198 .expect("list");
3199 let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
3200 assert!(names.contains(&"asym.signer"));
3201 assert!(names.contains(&"sym.box"));
3202 assert!(names.contains(&"web.cert"));
3203 assert!(
3204 !names.contains(&"web.value"),
3205 "invisible key must be filtered"
3206 );
3207 let signer = entries.iter().find(|e| e.name == "asym.signer").unwrap();
3209 assert_eq!(signer.kind, CatalogKind::Signing);
3210 assert_eq!(signer.key_type, Some(KeyType::Ed25519));
3211 assert_eq!(signer.latest_version, 5);
3213 let boxx = entries.iter().find(|e| e.name == "sym.box").unwrap();
3214 assert_eq!(boxx.kind, CatalogKind::Encryption);
3215 let cert = entries.iter().find(|e| e.name == "web.cert").unwrap();
3216 assert_eq!(cert.kind, CatalogKind::Signing);
3217 }
3218
3219 #[tokio::test]
3220 async fn list_filters_by_prefix() {
3221 let (mgr, _p, _s) = fixture();
3222 let entries = mgr.list(Some("web."), |_| true).await.expect("list");
3223 let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
3224 assert_eq!(names, vec!["web.cert", "web.value"]);
3225 }
3226
3227 #[tokio::test]
3228 async fn sign_on_value_key_is_op_not_valid_for_class() {
3229 let (mgr, _p, _s) = fixture();
3230 let err = mgr
3231 .sign("web.value", &[1])
3232 .await
3233 .expect_err("sign on a value key must be rejected");
3234 match err {
3235 ManagerError::OpNotValidForClass { op, key, class } => {
3236 assert_eq!(op, "sign");
3237 assert_eq!(key, "web.value");
3238 assert_eq!(class, Class::Value);
3239 }
3240 other => panic!("expected OpNotValidForClass, got {other:?}"),
3241 }
3242 }
3243
3244 #[tokio::test]
3245 async fn new_key_on_value_key_is_op_not_valid_for_class() {
3246 let (mgr, _p, _s) = fixture();
3247 let err = mgr
3248 .new_key("web.value", KeyType::Ed25519)
3249 .await
3250 .expect_err("new_key on a value key must be rejected");
3251 assert!(matches!(err, ManagerError::OpNotValidForClass { .. }));
3252 }
3253
3254 #[tokio::test]
3255 async fn unbacked_op_still_resolves_first_unknown_key_wins() {
3256 let (mgr, _p, _s) = fixture();
3258 assert!(matches!(
3259 mgr.encrypt("nope", AeadAlgorithm::Aes256Gcm, &[1], None)
3260 .await,
3261 Err(ManagerError::UnknownKey(_))
3262 ));
3263 }
3264
3265 #[tokio::test]
3268 async fn set_writes_value_and_get_round_trips_it() {
3269 let (mgr, primary, _s) = fixture();
3270 let version = mgr.set("web.value", b"super-secret").await.expect("set");
3272 assert_eq!(primary.kv_put_calls.load(Ordering::SeqCst), 1);
3273 assert_eq!(version, 6);
3275 assert_eq!(
3277 primary.last_path().as_deref(),
3278 Some("secret/data/web/value")
3279 );
3280
3281 let kv = mgr.get("web.value", None).await.expect("get");
3283 assert_eq!(kv.value, b"super-secret");
3284 assert_eq!(kv.version, 6);
3285 }
3286
3287 #[tokio::test]
3288 async fn get_reads_a_specific_version() {
3289 let (mgr, _p, _s) = fixture();
3290 let kv = mgr.get("web.value", Some(3)).await.expect("get");
3292 assert_eq!(kv.version, 3);
3293 }
3294
3295 #[tokio::test]
3296 async fn get_valid_on_public_class_key() {
3297 let (mgr, _p, secondary) = fixture();
3298 let kv = mgr.get("web.cert", None).await.expect("get on public key");
3300 assert_eq!(kv.value, b"stored-value");
3301 assert_eq!(
3302 secondary.last_path().as_deref(),
3303 Some("secret/data/web/cert")
3304 );
3305 }
3306
3307 #[tokio::test]
3308 async fn get_on_asymmetric_key_is_op_not_valid_for_class() {
3309 let (mgr, primary, _s) = fixture();
3310 let err = mgr
3313 .get("asym.signer", None)
3314 .await
3315 .expect_err("get on a signing key must be rejected");
3316 match err {
3317 ManagerError::OpNotValidForClass { op, key, class } => {
3318 assert_eq!(op, "get");
3319 assert_eq!(key, "asym.signer");
3320 assert_eq!(class, Class::Asymmetric);
3321 }
3322 other => panic!("expected OpNotValidForClass, got {other:?}"),
3323 }
3324 assert_eq!(primary.last_path(), None);
3326 }
3327
3328 #[tokio::test]
3329 async fn set_on_public_key_is_op_not_valid_for_class() {
3330 let (mgr, _p, secondary) = fixture();
3331 let err = mgr
3333 .set("web.cert", b"x")
3334 .await
3335 .expect_err("set on a public key must be rejected");
3336 assert!(matches!(err, ManagerError::OpNotValidForClass { .. }));
3337 assert_eq!(secondary.last_path(), None);
3338 }
3339
3340 #[tokio::test]
3341 async fn set_on_asymmetric_key_is_op_not_valid_for_class() {
3342 let (mgr, _p, _s) = fixture();
3343 let err = mgr
3344 .set("asym.signer", b"x")
3345 .await
3346 .expect_err("set on a signing key must be rejected");
3347 assert!(matches!(err, ManagerError::OpNotValidForClass { .. }));
3348 }
3349
3350 #[tokio::test]
3351 async fn get_unknown_key_wins_over_class() {
3352 let (mgr, _p, _s) = fixture();
3354 assert!(matches!(
3355 mgr.get("does.not.exist", None).await,
3356 Err(ManagerError::UnknownKey(_))
3357 ));
3358 }
3359
3360 #[tokio::test]
3363 async fn encrypt_then_decrypt_round_trips_with_aad() {
3364 let (mgr, _p, secondary) = fixture();
3365 let pt = b"top secret payload";
3366 let aad = b"context-42";
3367 let env = mgr
3368 .encrypt("sym.box", AeadAlgorithm::Aes256Gcm, pt, Some(aad))
3369 .await
3370 .expect("encrypt");
3371 assert_eq!(env.alg, AeadAlgorithm::Aes256Gcm);
3374 assert!(env.nonce.is_empty());
3375 assert_eq!(secondary.last_path().as_deref(), Some("box"));
3376
3377 let recovered = mgr
3378 .decrypt("sym.box", &env, Some(aad))
3379 .await
3380 .expect("decrypt");
3381 assert_eq!(recovered, pt);
3382 }
3383
3384 #[tokio::test]
3385 async fn decrypt_with_wrong_aad_is_opaque_decrypt_failed() {
3386 let (mgr, _p, _s) = fixture();
3387 let env = mgr
3388 .encrypt("sym.box", AeadAlgorithm::Aes256Gcm, b"x", Some(b"good"))
3389 .await
3390 .expect("encrypt");
3391 let err = mgr
3392 .decrypt("sym.box", &env, Some(b"BAD"))
3393 .await
3394 .expect_err("aad mismatch must fail");
3395 assert!(matches!(
3396 err,
3397 ManagerError::Backend(BackendError::DecryptFailed)
3398 ));
3399 }
3400
3401 #[tokio::test]
3402 async fn encrypt_algorithm_must_match_catalog_key_type() {
3403 let (mgr, _p, _s) = fixture();
3404 let err = mgr
3406 .encrypt("sym.box", AeadAlgorithm::Chacha20Poly1305, b"x", None)
3407 .await
3408 .expect_err("algorithm mismatch must be rejected");
3409 assert!(matches!(err, ManagerError::AlgorithmMismatch { .. }));
3410 }
3411
3412 #[tokio::test]
3413 async fn encrypt_on_non_symmetric_key_is_op_not_valid_for_class() {
3414 let (mgr, _p, _s) = fixture();
3415 let err = mgr
3416 .encrypt("asym.signer", AeadAlgorithm::Aes256Gcm, b"x", None)
3417 .await
3418 .expect_err("encrypt on a signing key must be rejected");
3419 assert!(matches!(err, ManagerError::OpNotValidForClass { .. }));
3420 }
3421
3422 #[tokio::test]
3423 async fn decrypt_targets_the_envelope_key_version() {
3424 let (mgr, _p, _s) = fixture();
3428 let mut env = mgr
3429 .encrypt("sym.box", AeadAlgorithm::Aes256Gcm, b"data", None)
3430 .await
3431 .expect("encrypt");
3432 env.key_version = 2; let recovered = mgr.decrypt("sym.box", &env, None).await.expect("decrypt");
3434 assert_eq!(recovered, b"data");
3435 }
3436
3437 #[tokio::test]
3440 async fn rotate_crypto_key_bumps_version_and_sets_grace_floor() {
3441 let (mgr, _p, secondary) = fixture();
3442 let limits = BrokerLimits::default();
3445 let new_version = mgr.rotate("sym.box", limits).await.expect("rotate");
3446 assert_eq!(new_version, 6);
3447 assert_eq!(secondary.rotate_calls.load(Ordering::SeqCst), 1);
3448 assert_eq!(secondary.last_versions_config(), Some((Some(5), None)));
3449 }
3450
3451 #[tokio::test]
3452 async fn rotate_grace_zero_floors_at_latest() {
3453 let (mgr, primary, _s) = fixture();
3454 let limits = BrokerLimits {
3457 grace_versions: 0,
3458 ..BrokerLimits::default()
3459 };
3460 let new_version = mgr.rotate("asym.signer", limits).await.expect("rotate");
3461 assert_eq!(new_version, 6);
3462 assert_eq!(primary.last_versions_config(), Some((Some(6), None)));
3463 }
3464
3465 #[tokio::test]
3466 async fn rotate_value_key_with_recipe_regenerates_a_new_version() {
3467 let (mgr, primary, _s) = fixture();
3468 let new_version = mgr
3471 .rotate("gen.value", BrokerLimits::default())
3472 .await
3473 .expect("rotate");
3474 assert_eq!(primary.kv_put_calls.load(Ordering::SeqCst), 1);
3475 assert_eq!(new_version, 6);
3476 let value = primary.last_kv_value().expect("a value was written");
3478 assert_eq!(value.len(), 24);
3479 assert!(value.iter().all(|b| (b'!'..=b'~').contains(b)));
3480 }
3481
3482 #[tokio::test]
3483 async fn rotate_tls_pair_key_regenerates_matching_cert_side() {
3484 if !step_available() {
3485 return;
3486 }
3487 let (mgr, primary, _s) = fixture();
3488 let new_version = mgr
3489 .rotate("tls.key", BrokerLimits::default())
3490 .await
3491 .expect("rotate tls key");
3492 assert_eq!(new_version, 6);
3493 let writes = primary.kv_put_log();
3494 assert_eq!(writes.len(), 2);
3495 assert_eq!(writes[0].0, "secret/data/tls/key");
3496 assert_eq!(writes[1].0, "secret/data/tls/cert");
3497 assert!(writes[0].1.starts_with(b"-----BEGIN"));
3498 assert!(
3499 writes[0]
3500 .1
3501 .windows(b"PRIVATE KEY".len())
3502 .any(|w| w == b"PRIVATE KEY")
3503 );
3504 assert!(writes[1].1.starts_with(b"-----BEGIN CERTIFICATE-----"));
3505 }
3506
3507 #[test]
3508 fn age_x25519_generate_recipe_emits_parseable_identity() {
3509 let value = generate_value(&GenerateSpec::AgeX25519).expect("age identity generated");
3510 let identity = String::from_utf8(value).expect("age identity is utf8");
3511 assert!(identity.starts_with("AGE-SECRET-KEY-"));
3512 let parsed = identity.parse::<age::x25519::Identity>();
3513 assert!(parsed.is_ok(), "generated identity must parse");
3514 }
3515
3516 fn step_available() -> bool {
3517 Command::new("step")
3518 .arg("version")
3519 .output()
3520 .is_ok_and(|output| output.status.success())
3521 }
3522
3523 #[tokio::test]
3524 async fn rotate_value_key_without_recipe_is_invalid_request() {
3525 let (mgr, _p, _s) = fixture();
3526 let err = mgr
3529 .rotate("web.value", BrokerLimits::default())
3530 .await
3531 .expect_err("value rotate without recipe must fail");
3532 assert!(matches!(err, ManagerError::ValueRotateNeedsSet(_)));
3533 }
3534
3535 #[tokio::test]
3536 async fn sweep_retention_raises_min_available_version() {
3537 let (mgr, _p, secondary) = fixture();
3538 let limits = BrokerLimits {
3540 retain_versions: Some(2),
3541 ..BrokerLimits::default()
3542 };
3543 mgr.sweep_retention("sym.box", limits).await.expect("sweep");
3544 assert_eq!(secondary.last_versions_config(), Some((None, Some(3))));
3545 }
3546
3547 #[tokio::test]
3548 async fn sweep_all_retention_walks_crypto_keys() {
3549 let (mgr, primary, secondary) = fixture();
3550 let limits = BrokerLimits {
3551 retain_versions: Some(2),
3552 ..BrokerLimits::default()
3553 };
3554 mgr.sweep_all_retention(limits)
3555 .await
3556 .expect("catalog sweep");
3557 assert_eq!(primary.last_versions_config(), Some((None, Some(3))));
3558 assert_eq!(secondary.last_versions_config(), Some((None, Some(3))));
3559 }
3560
3561 #[tokio::test]
3562 async fn sweep_retention_disabled_is_a_noop() {
3563 let (mgr, _p, secondary) = fixture();
3564 mgr.sweep_retention("sym.box", BrokerLimits::default())
3566 .await
3567 .expect("sweep");
3568 assert_eq!(secondary.last_versions_config(), None);
3569 }
3570
3571 fn seed_sealing_private(primary: &Arc<MockBackend>) -> [u8; 32] {
3578 let private = Zeroizing::new([0x11u8; 32]);
3579 let public = x25519_seal::public_from_private(&private);
3580 primary.seed_kv("secret/data/enroll/x25519", private.to_vec());
3581 primary.seed_kv("secret/data/enroll/x25519-public", public.to_vec());
3582 public
3583 }
3584 fn seed_ml_kem_private(primary: &Arc<MockBackend>) -> [u8; ml_kem_envelope::SEED_LEN] {
3585 let seed = [0x42; ml_kem_envelope::SEED_LEN];
3586 primary.seed_kv(
3587 "secret/data/enroll/ml-kem-768",
3588 ml_kem_record_bytes("enroll.mlkem", &seed, 5),
3589 );
3590 primary.seed_kv("secret/data/enroll/ml-kem-768-public", vec![0x7A; 1184]);
3591 seed
3592 }
3593 fn ml_kem_record_bytes(
3594 key_id: &str,
3595 seed: &[u8; ml_kem_envelope::SEED_LEN],
3596 key_version: u32,
3597 ) -> Vec<u8> {
3598 let meta = SoftwareCustodyCatalog {
3601 key_id,
3602 algorithm: ml_kem_envelope::KemAlgorithm::MlKem768.token(),
3603 provider: "local-software",
3604 provider_version: "1",
3605 custody: "software-encrypted",
3606 storage_key: "pqc/storage/wrap",
3607 };
3608 let aad = meta.aad(key_version);
3609 let mut ciphertext = Vec::with_capacity(aad.len() + seed.len() + 1);
3610 ciphertext.push(u8::try_from(aad.len()).expect("test aad fits mock envelope"));
3611 ciphertext.extend_from_slice(&aad);
3612 ciphertext.extend_from_slice(seed);
3613 serde_json::json!({
3614 "schemaVersion": 1,
3615 "keyId": key_id,
3616 "keyVersion": key_version,
3617 "publicKey": encode_record_bytes(&[0x7A; 1184]),
3618 "algorithm": meta.algorithm,
3619 "provider": meta.provider,
3620 "providerVersion": meta.provider_version,
3621 "custody": meta.custody,
3622 "encryptedPrivateKey": {
3623 "wrappingKey": meta.storage_key,
3624 "algorithm": "aes-256-gcm",
3625 "keyVersion": key_version,
3626 "nonce": encode_record_bytes(&[]),
3627 "ciphertext": encode_record_bytes(&ciphertext),
3628 }
3629 })
3630 .to_string()
3631 .into_bytes()
3632 }
3633
3634 #[tokio::test]
3635 async fn sealing_key_rejects_get_and_set() {
3636 let (mgr, _p, _s) = fixture();
3639 let get_err = mgr
3640 .get("enroll.sealing", None)
3641 .await
3642 .expect_err("get must be denied on a sealing key");
3643 assert!(matches!(
3644 get_err,
3645 ManagerError::OpNotValidForClass {
3646 op: "get",
3647 class: Class::Sealing,
3648 ..
3649 }
3650 ));
3651 let set_err = mgr
3652 .set("enroll.sealing", b"anything")
3653 .await
3654 .expect_err("set must be denied on a sealing key");
3655 assert!(matches!(
3656 set_err,
3657 ManagerError::OpNotValidForClass {
3658 op: "set",
3659 class: Class::Sealing,
3660 ..
3661 }
3662 ));
3663 }
3664
3665 #[tokio::test]
3666 async fn sealing_key_rejects_sign_and_rotate() {
3667 let (mgr, _p, _s) = fixture();
3669 assert!(matches!(
3670 mgr.sign("enroll.sealing", b"m").await,
3671 Err(ManagerError::OpNotValidForClass { op: "sign", .. })
3672 ));
3673 assert!(matches!(
3674 mgr.rotate("enroll.sealing", BrokerLimits::default()).await,
3675 Err(ManagerError::OpNotValidForClass { op: "rotate", .. })
3676 ));
3677 }
3678
3679 #[tokio::test]
3680 async fn sealing_get_public_key_reads_out_of_band_public() {
3681 let (mgr, primary, _s) = fixture();
3684 let expected_pub = seed_sealing_private(&primary);
3685 let got = mgr
3686 .sealing_public_key("enroll.sealing")
3687 .await
3688 .expect("reads public");
3689 assert_eq!(got, expected_pub);
3690 assert_eq!(
3692 primary.last_path().as_deref(),
3693 Some("secret/data/enroll/x25519-public")
3694 );
3695 }
3696
3697 #[tokio::test]
3698 async fn sealing_public_ops_never_materialize_private() {
3699 let (mgr, primary, _s) = fixture();
3704 let private = Zeroizing::new([0x11u8; 32]);
3705 let public = x25519_seal::public_from_private(&private);
3706 primary.seed_kv("secret/data/enroll/x25519-public", public.to_vec());
3707 primary.seed_kv("secret/data/enroll/x25519", vec![0xFF; 7]);
3708
3709 let got = mgr
3710 .sealing_public_key("enroll.sealing")
3711 .await
3712 .expect("public read despite garbage private");
3713 assert_eq!(got, public);
3714 mgr.wrap_envelope("enroll.sealing", b"payload", b"ctx")
3717 .await
3718 .expect("wrap reads only the public");
3719 }
3720
3721 #[tokio::test]
3722 async fn sealing_wrap_then_unwrap_round_trips() {
3723 let (mgr, primary, _s) = fixture();
3726 seed_sealing_private(&primary);
3727 let plaintext = b"enrollment-payload";
3728 let aad = b"enroll-ctx";
3729 let env = mgr
3730 .wrap_envelope("enroll.sealing", plaintext, aad)
3731 .await
3732 .expect("wrap");
3733 let recovered = mgr
3734 .unwrap_envelope("enroll.sealing", &env, aad)
3735 .await
3736 .expect("unwrap");
3737 assert_eq!(recovered.as_slice(), plaintext);
3738 }
3739
3740 #[tokio::test]
3741 async fn kv_get_secret_returns_zeroizing_bytes_and_round_trips() {
3742 let backend = MockHandle(MockBackend::new("primary"));
3746 let stored = vec![0x11u8; 32];
3747 backend
3748 .kv_put("secret/data/enroll/x25519", &stored)
3749 .await
3750 .expect("put");
3751 let got: Zeroizing<Vec<u8>> = backend
3752 .kv_get_secret("secret/data/enroll/x25519", None)
3753 .await
3754 .expect("secret read");
3755 assert_eq!(got.as_slice(), stored.as_slice());
3756 }
3757
3758 #[tokio::test]
3759 async fn sealing_unwrap_opens_externally_sealed_payload() {
3760 let (mgr, primary, _s) = fixture();
3764 let recipient_pub = seed_sealing_private(&primary);
3765 let env = x25519_seal::seal(&recipient_pub, b"sealed-by-sender", b"ctx").expect("seal");
3766 let recovered = mgr
3767 .unwrap_envelope("enroll.sealing", &env, b"ctx")
3768 .await
3769 .expect("unwrap");
3770 assert_eq!(recovered.as_slice(), b"sealed-by-sender");
3771 }
3772
3773 fn ml_kem_parts(env: &ml_kem_envelope::MlKemEnvelope) -> MlKemEnvelopeParts<'_> {
3776 MlKemEnvelopeParts {
3777 encapsulated_key: &env.encapsulated_key,
3778 nonce: &env.nonce,
3779 ciphertext: &env.ciphertext,
3780 }
3781 }
3782 #[tokio::test]
3783 async fn ml_kem_provider_wrap_then_unwrap_round_trips() {
3784 for envelope_algorithm in [
3785 ProviderEnvelopeAlgorithm::Aes256Gcm,
3786 ProviderEnvelopeAlgorithm::ChaCha20Poly1305,
3787 ] {
3788 let (mgr, primary, _s) = fixture();
3789 seed_ml_kem_private(&primary);
3790 let (envelope, dispatch) = mgr
3791 .provider_wrap_envelope(
3792 "enroll.mlkem",
3793 ProviderKemAlgorithm::MlKem768,
3794 envelope_algorithm,
3795 b"top secret",
3796 b"ctx",
3797 ProviderGate {
3798 local_software_allowed: true,
3799 },
3800 )
3801 .await
3802 .expect("wrap");
3803 assert_eq!(envelope.kem_algorithm, ProviderKemAlgorithm::MlKem768);
3805 assert_eq!(envelope.key_version, 5);
3806 assert_eq!(dispatch.algorithm, "ml-kem-768");
3807 assert_eq!(dispatch.provider, CryptoProviderId::LocalSoftware);
3808
3809 let (recovered, _) = mgr
3810 .provider_unwrap_envelope(
3811 "enroll.mlkem",
3812 ProviderKemAlgorithm::MlKem768,
3813 envelope_algorithm,
3814 MlKemEnvelopeParts {
3815 encapsulated_key: &envelope.encapsulated_key,
3816 nonce: &envelope.nonce,
3817 ciphertext: &envelope.ciphertext,
3818 },
3819 b"ctx",
3820 ProviderGate {
3821 local_software_allowed: true,
3822 },
3823 )
3824 .await
3825 .expect("unwrap");
3826 assert_eq!(recovered.as_slice(), b"top secret");
3827 }
3828 }
3829 #[tokio::test]
3830 async fn ml_kem_provider_unwrap_opens_externally_sealed_payload() {
3831 let (mgr, primary, _s) = fixture();
3832 let seed = seed_ml_kem_private(&primary);
3833 let env = ml_kem_envelope::seal(
3836 &seed,
3837 ml_kem_envelope::KemAlgorithm::MlKem768,
3838 ml_kem_envelope::EnvelopeAlgorithm::ChaCha20Poly1305,
3839 b"sealed-by-sender",
3840 b"ctx",
3841 )
3842 .expect("seal");
3843 let (recovered, _) = mgr
3844 .provider_unwrap_envelope(
3845 "enroll.mlkem",
3846 ProviderKemAlgorithm::MlKem768,
3847 ProviderEnvelopeAlgorithm::ChaCha20Poly1305,
3848 ml_kem_parts(&env),
3849 b"ctx",
3850 ProviderGate {
3851 local_software_allowed: true,
3852 },
3853 )
3854 .await
3855 .expect("unwrap");
3856 assert_eq!(recovered.as_slice(), b"sealed-by-sender");
3857 }
3858
3859 fn ml_kem_record_with(public: &[u8], wrapped_seed: &[u8], key_version: u32) -> Vec<u8> {
3863 let meta = SoftwareCustodyCatalog {
3864 key_id: "enroll.mlkem",
3865 algorithm: ml_kem_envelope::KemAlgorithm::MlKem768.token(),
3866 provider: "local-software",
3867 provider_version: "1",
3868 custody: "software-encrypted",
3869 storage_key: "pqc/storage/wrap",
3870 };
3871 serde_json::json!({
3872 "schemaVersion": 1,
3873 "keyId": meta.key_id,
3874 "keyVersion": key_version,
3875 "publicKey": encode_record_bytes(public),
3876 "algorithm": meta.algorithm,
3877 "provider": meta.provider,
3878 "providerVersion": meta.provider_version,
3879 "custody": meta.custody,
3880 "encryptedPrivateKey": {
3881 "wrappingKey": meta.storage_key,
3882 "algorithm": "aes-256-gcm",
3883 "keyVersion": key_version,
3884 "nonce": encode_record_bytes(&[]),
3885 "ciphertext": encode_record_bytes(wrapped_seed),
3886 }
3887 })
3888 .to_string()
3889 .into_bytes()
3890 }
3891 #[tokio::test]
3892 async fn ml_kem_sealing_get_public_key_returns_encapsulation_key_without_seed() {
3893 let (mgr, primary, _s) = fixture();
3901 let encapsulation_key = vec![0xABu8; 1184];
3902 primary.seed_kv(
3903 "secret/data/enroll/ml-kem-768",
3904 ml_kem_record_with(&encapsulation_key, &[], 5),
3907 );
3908
3909 let got = mgr
3910 .get_public_key("enroll.mlkem")
3911 .await
3912 .expect("ML-KEM sealing public-encapsulation-key read");
3913 assert_eq!(got.public_key, encapsulation_key);
3914 assert_eq!(got.key_type, KeyType::MlKem768);
3915 assert_eq!(got.version, 1);
3917 assert_eq!(
3920 primary.last_path().as_deref(),
3921 Some("secret/data/enroll/ml-kem-768")
3922 );
3923 }
3924 #[tokio::test]
3925 async fn get_public_key_rejects_non_ml_kem_sealing_and_value_classes() {
3926 let (mgr, _p, _s) = fixture();
3930 assert!(matches!(
3931 mgr.get_public_key("enroll.sealing").await,
3932 Err(ManagerError::OpNotValidForClass {
3933 op: "get_public_key",
3934 class: Class::Sealing,
3935 ..
3936 })
3937 ));
3938 assert!(matches!(
3939 mgr.get_public_key("web.value").await,
3940 Err(ManagerError::OpNotValidForClass {
3941 op: "get_public_key",
3942 class: Class::Value,
3943 ..
3944 })
3945 ));
3946 }
3947 #[tokio::test]
3948 async fn ml_kem_provider_unwrap_tampered_ciphertext_fails_opaque() {
3949 let (mgr, primary, _s) = fixture();
3950 seed_ml_kem_private(&primary);
3951 let (mut envelope, _) = mgr
3952 .provider_wrap_envelope(
3953 "enroll.mlkem",
3954 ProviderKemAlgorithm::MlKem768,
3955 ProviderEnvelopeAlgorithm::Aes256Gcm,
3956 b"top secret",
3957 b"ctx",
3958 ProviderGate {
3959 local_software_allowed: true,
3960 },
3961 )
3962 .await
3963 .expect("wrap");
3964 if let Some(b) = envelope.ciphertext.first_mut() {
3965 *b ^= 0xFF;
3966 }
3967 let err = mgr
3968 .provider_unwrap_envelope(
3969 "enroll.mlkem",
3970 ProviderKemAlgorithm::MlKem768,
3971 ProviderEnvelopeAlgorithm::Aes256Gcm,
3972 MlKemEnvelopeParts {
3973 encapsulated_key: &envelope.encapsulated_key,
3974 nonce: &envelope.nonce,
3975 ciphertext: &envelope.ciphertext,
3976 },
3977 b"ctx",
3978 ProviderGate {
3979 local_software_allowed: true,
3980 },
3981 )
3982 .await
3983 .expect_err("tampered ciphertext must fail opaque");
3984 assert!(matches!(
3985 err,
3986 ManagerError::Provider(ProviderError::CryptoFailed {
3987 op: "unwrap_envelope",
3988 ..
3989 })
3990 ));
3991 }
3992 #[tokio::test]
3993 async fn ml_kem_provider_unwrap_wrong_aad_fails_opaque() {
3994 let (mgr, primary, _s) = fixture();
3995 seed_ml_kem_private(&primary);
3996 let (envelope, _) = mgr
3997 .provider_wrap_envelope(
3998 "enroll.mlkem",
3999 ProviderKemAlgorithm::MlKem768,
4000 ProviderEnvelopeAlgorithm::ChaCha20Poly1305,
4001 b"top secret",
4002 b"right",
4003 ProviderGate {
4004 local_software_allowed: true,
4005 },
4006 )
4007 .await
4008 .expect("wrap");
4009 let err = mgr
4010 .provider_unwrap_envelope(
4011 "enroll.mlkem",
4012 ProviderKemAlgorithm::MlKem768,
4013 ProviderEnvelopeAlgorithm::ChaCha20Poly1305,
4014 MlKemEnvelopeParts {
4015 encapsulated_key: &envelope.encapsulated_key,
4016 nonce: &envelope.nonce,
4017 ciphertext: &envelope.ciphertext,
4018 },
4019 b"wrong",
4020 ProviderGate {
4021 local_software_allowed: true,
4022 },
4023 )
4024 .await
4025 .expect_err("wrong aad must fail opaque");
4026 assert!(matches!(
4027 err,
4028 ManagerError::Provider(ProviderError::CryptoFailed { .. })
4029 ));
4030 }
4031 #[tokio::test]
4032 async fn ml_kem_software_custody_rejects_raw_seed_storage() {
4033 let (mgr, primary, _s) = fixture();
4034 let seed = [0x42; ml_kem_envelope::SEED_LEN];
4035 primary.seed_kv("secret/data/enroll/ml-kem-768", seed.to_vec());
4036 let env = ml_kem_envelope::seal(
4037 &seed,
4038 ml_kem_envelope::KemAlgorithm::MlKem768,
4039 ml_kem_envelope::EnvelopeAlgorithm::ChaCha20Poly1305,
4040 b"sealed-by-sender",
4041 b"ctx",
4042 )
4043 .expect("seal");
4044 let err = mgr
4045 .provider_unwrap_envelope(
4046 "enroll.mlkem",
4047 ProviderKemAlgorithm::MlKem768,
4048 ProviderEnvelopeAlgorithm::ChaCha20Poly1305,
4049 ml_kem_parts(&env),
4050 b"ctx",
4051 ProviderGate {
4052 local_software_allowed: true,
4053 },
4054 )
4055 .await
4056 .expect_err("raw private seed storage must be rejected");
4057 assert!(matches!(
4058 err,
4059 ManagerError::Provider(ProviderError::CryptoFailed { .. })
4060 ));
4061 assert_eq!(
4062 primary.last_path().as_deref(),
4063 Some("secret/data/enroll/ml-kem-768")
4064 );
4065 }
4066 #[tokio::test]
4067 async fn ml_kem_software_custody_rejects_record_metadata_mismatch() {
4068 let (mgr, primary, _s) = fixture();
4069 let seed = [0x42; ml_kem_envelope::SEED_LEN];
4070 primary.seed_kv(
4072 "secret/data/enroll/ml-kem-768",
4073 ml_kem_record_bytes("enroll.mlkem", &seed, 4),
4074 );
4075 let env = ml_kem_envelope::seal(
4076 &seed,
4077 ml_kem_envelope::KemAlgorithm::MlKem768,
4078 ml_kem_envelope::EnvelopeAlgorithm::ChaCha20Poly1305,
4079 b"sealed-by-sender",
4080 b"ctx",
4081 )
4082 .expect("seal");
4083 let err = mgr
4084 .provider_unwrap_envelope(
4085 "enroll.mlkem",
4086 ProviderKemAlgorithm::MlKem768,
4087 ProviderEnvelopeAlgorithm::ChaCha20Poly1305,
4088 ml_kem_parts(&env),
4089 b"ctx",
4090 ProviderGate {
4091 local_software_allowed: true,
4092 },
4093 )
4094 .await
4095 .expect_err("KV version mismatch must fail before decrypt");
4096 assert!(matches!(
4097 err,
4098 ManagerError::Provider(ProviderError::CryptoFailed { .. })
4099 ));
4100 assert_eq!(
4101 primary.last_path().as_deref(),
4102 Some("secret/data/enroll/ml-kem-768")
4103 );
4104 }
4105
4106 #[tokio::test]
4107 async fn ml_kem_unwrap_rejects_wrong_key_type() {
4108 let (mgr, _primary, _s) = fixture();
4109 let seed = [0x42; ml_kem_envelope::SEED_LEN];
4112 let env = ml_kem_envelope::seal(
4113 &seed,
4114 ml_kem_envelope::KemAlgorithm::MlKem768,
4115 ml_kem_envelope::EnvelopeAlgorithm::ChaCha20Poly1305,
4116 b"sealed-by-sender",
4117 b"ctx",
4118 )
4119 .expect("seal");
4120 let err = mgr
4123 .provider_unwrap_envelope(
4124 "enroll.sealing",
4125 ProviderKemAlgorithm::MlKem768,
4126 ProviderEnvelopeAlgorithm::ChaCha20Poly1305,
4127 ml_kem_parts(&env),
4128 b"ctx",
4129 ProviderGate {
4130 local_software_allowed: true,
4131 },
4132 )
4133 .await
4134 .expect_err("x25519 key cannot unwrap ML-KEM");
4135 assert!(matches!(err, ManagerError::KemAlgorithmMismatch { .. }));
4136 }
4137
4138 #[tokio::test]
4139 async fn ml_kem_wrap_rejects_wrong_kem_param_set() {
4140 let (mgr, _primary, _s) = fixture();
4141 let err = mgr
4144 .provider_wrap_envelope(
4145 "enroll.mlkem",
4146 ProviderKemAlgorithm::MlKem512,
4147 ProviderEnvelopeAlgorithm::ChaCha20Poly1305,
4148 b"top secret",
4149 b"ctx",
4150 ProviderGate {
4151 local_software_allowed: true,
4152 },
4153 )
4154 .await
4155 .expect_err("wrong KEM param set must be rejected");
4156 assert!(matches!(err, ManagerError::KemAlgorithmMismatch { .. }));
4157 }
4158
4159 #[tokio::test]
4160 async fn ml_kem_wrap_and_unwrap_without_grant_denied() {
4161 let (mgr, _primary, _s) = fixture();
4162 let deny = ProviderGate {
4163 local_software_allowed: false,
4164 };
4165 let wrap_err = mgr
4166 .provider_wrap_envelope(
4167 "enroll.mlkem",
4168 ProviderKemAlgorithm::MlKem768,
4169 ProviderEnvelopeAlgorithm::ChaCha20Poly1305,
4170 b"top secret",
4171 b"ctx",
4172 deny,
4173 )
4174 .await
4175 .expect_err("wrap denied without local-software grant");
4176 assert!(matches!(
4177 wrap_err,
4178 ManagerError::Provider(ProviderError::PolicyDenied { .. })
4179 ));
4180 let unwrap_err = mgr
4181 .provider_unwrap_envelope(
4182 "enroll.mlkem",
4183 ProviderKemAlgorithm::MlKem768,
4184 ProviderEnvelopeAlgorithm::ChaCha20Poly1305,
4185 MlKemEnvelopeParts {
4186 encapsulated_key: b"x",
4187 nonce: &[0u8; 12],
4188 ciphertext: b"y",
4189 },
4190 b"ctx",
4191 deny,
4192 )
4193 .await
4194 .expect_err("unwrap denied without local-software grant");
4195 assert!(matches!(
4196 unwrap_err,
4197 ManagerError::Provider(ProviderError::PolicyDenied { .. })
4198 ));
4199 }
4200
4201 #[tokio::test]
4202 async fn sealing_unwrap_wrong_aad_fails_opaque() {
4203 let (mgr, primary, _s) = fixture();
4204 let recipient_pub = seed_sealing_private(&primary);
4205 let env = x25519_seal::seal(&recipient_pub, b"x", b"right").expect("seal");
4206 let err = mgr
4207 .unwrap_envelope("enroll.sealing", &env, b"wrong")
4208 .await
4209 .expect_err("aad mismatch must fail");
4210 assert!(matches!(
4211 err,
4212 ManagerError::Sealing(SealingFailure::OpenFailed)
4213 ));
4214 }
4215
4216 #[tokio::test]
4217 async fn sealing_unwrap_tampered_ciphertext_fails_opaque() {
4218 let (mgr, primary, _s) = fixture();
4219 let recipient_pub = seed_sealing_private(&primary);
4220 let mut env = x25519_seal::seal(&recipient_pub, b"payload", b"aad").expect("seal");
4221 if let Some(b) = env.ciphertext.first_mut() {
4222 *b ^= 0xFF;
4223 }
4224 assert!(matches!(
4225 mgr.unwrap_envelope("enroll.sealing", &env, b"aad").await,
4226 Err(ManagerError::Sealing(SealingFailure::OpenFailed))
4227 ));
4228 }
4229
4230 #[tokio::test]
4231 async fn sealing_unwrap_malformed_private_fails() {
4232 let (mgr, primary, _s) = fixture();
4235 *primary.last_kv_value.lock().unwrap() = Some(vec![0u8; 16]);
4236 let env = SealedEnvelope {
4237 encapsulated_key: [9u8; 32],
4238 nonce: [0u8; 12],
4239 ciphertext: vec![1, 2, 3],
4240 };
4241 assert!(matches!(
4242 mgr.unwrap_envelope("enroll.sealing", &env, b"").await,
4243 Err(ManagerError::Sealing(SealingFailure::Malformed))
4244 ));
4245 }
4246
4247 #[tokio::test]
4248 async fn wrap_unwrap_reject_non_sealing_key() {
4249 let (mgr, _p, _s) = fixture();
4252 assert!(matches!(
4253 mgr.wrap_envelope("sym.box", b"x", b"").await,
4254 Err(ManagerError::OpNotValidForClass {
4255 op: "wrap_envelope",
4256 ..
4257 })
4258 ));
4259 let env = SealedEnvelope {
4260 encapsulated_key: [0u8; 32],
4261 nonce: [0u8; 12],
4262 ciphertext: vec![],
4263 };
4264 assert!(matches!(
4265 mgr.unwrap_envelope("sym.box", &env, b"").await,
4266 Err(ManagerError::OpNotValidForClass {
4267 op: "unwrap_envelope",
4268 ..
4269 })
4270 ));
4271 }
4272
4273 fn seed_signing_seed(primary: &Arc<MockBackend>) -> [u8; 32] {
4280 let seed = Zeroizing::new([0x11u8; 32]);
4281 let public = ed25519_sign::public_from_seed(&seed);
4282 primary.seed_kv("secret/data/kv2/signer", seed.to_vec());
4283 primary.seed_kv("secret/data/kv2/signer-public", public.to_vec());
4284 public
4285 }
4286
4287 #[tokio::test]
4288 async fn kv2_signer_resolves_as_asymmetric_kv2() {
4289 let (mgr, _p, _s) = fixture();
4290 let routed = mgr.resolve("kv2.signer").expect("resolves");
4291 assert_eq!(routed.class(), Class::Asymmetric);
4292 assert_eq!(routed.engine, Engine::Kv2);
4293 assert_eq!(routed.key_type(), Some(KeyAlgorithm::Ed25519));
4294 }
4295
4296 #[tokio::test]
4297 async fn kv2_signer_rejects_get_and_set() {
4298 let (mgr, _p, _s) = fixture();
4303 let get_err = mgr
4304 .get("kv2.signer", None)
4305 .await
4306 .expect_err("get must be denied on a value-store signing key");
4307 assert!(matches!(
4308 get_err,
4309 ManagerError::OpNotValidForClass {
4310 op: "get",
4311 class: Class::Asymmetric,
4312 ..
4313 }
4314 ));
4315 let set_err = mgr
4316 .set("kv2.signer", b"anything")
4317 .await
4318 .expect_err("set must be denied on a value-store signing key");
4319 assert!(matches!(
4320 set_err,
4321 ManagerError::OpNotValidForClass {
4322 op: "set",
4323 class: Class::Asymmetric,
4324 ..
4325 }
4326 ));
4327 }
4328
4329 #[tokio::test]
4330 async fn kv2_signer_rejects_rotate_and_import() {
4331 let (mgr, _p, _s) = fixture();
4336 assert!(matches!(
4337 mgr.rotate("kv2.signer", BrokerLimits::default()).await,
4338 Err(ManagerError::Unsupported(m)) if m.starts_with("rotate")
4339 ));
4340 assert!(matches!(
4341 mgr.import(
4342 "kv2.signer",
4343 KeyType::Ed25519,
4344 &KeyMaterial::Ed25519Seed(vec![0; 32]),
4345 )
4346 .await,
4347 Err(ManagerError::Unsupported(m)) if m.starts_with("import")
4348 ));
4349 }
4350
4351 #[tokio::test]
4352 async fn kv2_signer_materializes_and_signs_matching_in_proc() {
4353 let (mgr, primary, _s) = fixture();
4356 let public = seed_signing_seed(&primary);
4357 let message = b"sign me with a materialized seed";
4358
4359 let sig = mgr.sign("kv2.signer", message).await.expect("sign");
4360 assert_eq!(sig.len(), ed25519_sign::SIGNATURE_LEN);
4361 let expected = ed25519_sign::sign(&Zeroizing::new([0x11u8; 32]), message);
4363 assert_eq!(sig.as_slice(), expected.as_slice());
4364 assert_eq!(ed25519_sign::verify(&public, message, &sig), Ok(true));
4366 assert_eq!(
4369 primary.last_path().as_deref(),
4370 Some("secret/data/kv2/signer")
4371 );
4372 }
4373
4374 #[tokio::test]
4375 async fn kv2_signer_verify_round_trips_in_proc() {
4376 let (mgr, primary, _s) = fixture();
4377 seed_signing_seed(&primary);
4378 let message = b"verify me";
4379 let sig = mgr.sign("kv2.signer", message).await.expect("sign");
4380 assert!(
4381 mgr.verify("kv2.signer", message, &sig)
4382 .await
4383 .expect("verify")
4384 );
4385 assert!(
4387 !mgr.verify("kv2.signer", b"verify-tampered", &sig)
4388 .await
4389 .expect("verify")
4390 );
4391 }
4392
4393 #[tokio::test]
4394 async fn kv2_signer_get_public_key_reads_out_of_band_public() {
4395 let (mgr, primary, _s) = fixture();
4398 let expected_pub = seed_signing_seed(&primary);
4399 let pk = mgr.get_public_key("kv2.signer").await.expect("public");
4400 assert_eq!(pk.public_key.as_slice(), expected_pub.as_slice());
4401 assert_eq!(pk.key_type, KeyType::Ed25519);
4402 assert_eq!(
4404 primary.last_path().as_deref(),
4405 Some("secret/data/kv2/signer-public")
4406 );
4407 }
4408
4409 #[tokio::test]
4410 async fn kv2_signer_public_ops_never_materialize_seed() {
4411 let (mgr, primary, _s) = fixture();
4417 let seed = Zeroizing::new([0x11u8; 32]);
4418 let public = ed25519_sign::public_from_seed(&seed);
4419 primary.seed_kv("secret/data/kv2/signer-public", public.to_vec());
4420 primary.seed_kv("secret/data/kv2/signer", vec![0xFF; 7]);
4421
4422 let pk = mgr
4423 .get_public_key("kv2.signer")
4424 .await
4425 .expect("public read despite garbage seed");
4426 assert_eq!(pk.public_key.as_slice(), public.as_slice());
4427 let sig = ed25519_sign::sign(&seed, b"m");
4430 assert!(
4431 mgr.verify("kv2.signer", b"m", &sig)
4432 .await
4433 .expect("verify reads only the public")
4434 );
4435 }
4436
4437 #[tokio::test]
4438 async fn materialize_public_op_without_public_path_fails_closed() {
4439 const NO_PUB: &str = r#"{
4444 "schemaVersion": 1,
4445 "backends": { "primary": { "kind": "vault", "addr": "https://127.0.0.1:8200" } },
4446 "keys": {
4447 "enroll.sealing": {
4448 "class": "sealing", "keyType": "x25519", "backend": "primary", "engine": "kv2",
4449 "path": "secret/data/enroll/x25519", "writable": true, "missing": "error",
4450 "description": "a sealing key missing its publicPath"
4451 }
4452 }
4453 }"#;
4454 let primary = MockBackend::new("primary");
4455 let mut backends: BTreeMap<String, Box<dyn Backend>> = BTreeMap::new();
4456 backends.insert("primary".into(), Box::new(MockHandle(primary)));
4457 let mgr = BackendManager::new(parse_catalog(NO_PUB), backends).expect("constructs");
4458
4459 assert!(matches!(
4460 mgr.sealing_public_key("enroll.sealing").await,
4461 Err(ManagerError::MissingPublicPath(k)) if k == "enroll.sealing"
4462 ));
4463 assert!(matches!(
4464 mgr.wrap_envelope("enroll.sealing", b"x", b"a").await,
4465 Err(ManagerError::MissingPublicPath(_))
4466 ));
4467 }
4468
4469 #[tokio::test]
4470 async fn kv2_signer_malformed_seed_is_signing_failure_not_panic() {
4471 let (mgr, primary, _s) = fixture();
4474 *primary.last_kv_value.lock().unwrap() = Some(vec![0u8; 16]);
4475 assert!(matches!(
4476 mgr.sign("kv2.signer", b"m").await,
4477 Err(ManagerError::Signing(SigningFailure::Malformed))
4478 ));
4479 }
4480
4481 #[tokio::test]
4482 async fn kv2_signer_verify_rejects_wrong_length_signature_opaquely() {
4483 let (mgr, primary, _s) = fixture();
4486 seed_signing_seed(&primary);
4487 assert!(matches!(
4488 mgr.verify("kv2.signer", b"m", &[0u8; 10]).await,
4489 Err(ManagerError::Signing(SigningFailure::Malformed))
4490 ));
4491 }
4492
4493 #[tokio::test]
4494 async fn transit_signer_still_uses_backend_sign_in_place() {
4495 let (mgr, primary, _s) = fixture();
4498 let sig = mgr.sign("asym.signer", b"m").await.expect("sign");
4499 assert_eq!(sig, vec![0xAB, 0xCD]);
4502 assert_eq!(primary.last_path().as_deref(), Some("signer"));
4503 }
4504}
4505
4506#[cfg(test)]
4507mod pqc_dispatch_tests {
4508 use std::collections::{BTreeMap, HashMap};
4509 use std::sync::Mutex;
4510
4511 use async_trait::async_trait;
4512
4513 use super::{
4514 BackendManager, Catalog, CryptoProviderId, CustodyMode, KeyProviderDescriptor,
4515 ManagerError, ProviderError, ProviderGate, ProviderPolicy,
4516 };
4517 use crate::backend::{Backend, BackendError, KvValue, NativeAlgorithm, NewKey};
4518 use crate::core::crypto_provider::{ProviderAuditEvent, ProviderAuditOutcome};
4519 use crate::core::ml_dsa_sign::{self, MlDsaAlgorithm};
4520 use basil_proto::{AeadAlgorithm, CiphertextEnvelope, KeyType};
4521
4522 #[derive(Default)]
4527 struct PqcBackend {
4528 store: Mutex<HashMap<String, Vec<u8>>>,
4529 }
4530
4531 #[async_trait]
4532 impl Backend for PqcBackend {
4533 fn kind(&self) -> &'static str {
4534 "pqc-dispatch-test"
4535 }
4536
4537 async fn new_key(&self, _key_type: KeyType) -> Result<NewKey, BackendError> {
4538 Err(BackendError::Unsupported("new_key"))
4539 }
4540
4541 async fn public_key(&self, _key_id: &str) -> Result<Vec<u8>, BackendError> {
4542 Err(BackendError::Unsupported("public_key"))
4543 }
4544
4545 async fn sign(&self, _key_id: &str, _message: &[u8]) -> Result<Vec<u8>, BackendError> {
4546 Err(BackendError::Unsupported("sign"))
4547 }
4548
4549 async fn verify(
4550 &self,
4551 _key_id: &str,
4552 _message: &[u8],
4553 _signature: &[u8],
4554 ) -> Result<bool, BackendError> {
4555 Err(BackendError::Unsupported("verify"))
4556 }
4557
4558 async fn encrypt(
4559 &self,
4560 _key_id: &str,
4561 algorithm: AeadAlgorithm,
4562 plaintext: &[u8],
4563 aad: Option<&[u8]>,
4564 ) -> Result<CiphertextEnvelope, BackendError> {
4565 let aad = aad.unwrap_or(&[]);
4566 let mut ciphertext = vec![u8::try_from(aad.len()).unwrap_or(u8::MAX)];
4567 ciphertext.extend_from_slice(aad);
4568 ciphertext.extend_from_slice(plaintext);
4569 Ok(CiphertextEnvelope {
4570 alg: algorithm,
4571 key_version: 1,
4572 nonce: Vec::new(),
4573 ciphertext,
4574 })
4575 }
4576
4577 async fn decrypt(
4578 &self,
4579 _key_id: &str,
4580 envelope: &CiphertextEnvelope,
4581 aad: Option<&[u8]>,
4582 ) -> Result<Vec<u8>, BackendError> {
4583 let aad = aad.unwrap_or(&[]);
4584 let ct = &envelope.ciphertext;
4585 let aad_len = *ct.first().ok_or(BackendError::DecryptFailed)? as usize;
4586 let bound = ct.get(1..1 + aad_len).ok_or(BackendError::DecryptFailed)?;
4587 if bound != aad {
4588 return Err(BackendError::DecryptFailed);
4589 }
4590 Ok(ct
4591 .get(1 + aad_len..)
4592 .ok_or(BackendError::DecryptFailed)?
4593 .to_vec())
4594 }
4595
4596 async fn kv_put(&self, key_id: &str, value: &[u8]) -> Result<u32, BackendError> {
4597 self.store
4598 .lock()
4599 .map_err(|_| BackendError::Unsupported("kv_put"))?
4600 .insert(key_id.to_string(), value.to_vec());
4601 Ok(1)
4602 }
4603
4604 async fn kv_get(
4605 &self,
4606 key_id: &str,
4607 _version: Option<u32>,
4608 ) -> Result<KvValue, BackendError> {
4609 let value = self
4610 .store
4611 .lock()
4612 .map_err(|_| BackendError::Unsupported("kv_get"))?
4613 .get(key_id)
4614 .cloned();
4615 value
4616 .map(|value| KvValue { value, version: 1 })
4617 .ok_or(BackendError::Unsupported("kv_get"))
4618 }
4619 }
4620
4621 #[derive(Default)]
4630 struct NativePqcBackend {
4631 seeds: Mutex<HashMap<String, [u8; 32]>>,
4632 store: Mutex<HashMap<String, Vec<u8>>>,
4633 }
4634
4635 impl NativePqcBackend {
4636 const fn dsa(native: NativeAlgorithm) -> MlDsaAlgorithm {
4637 match native {
4638 NativeAlgorithm::MlDsa44 => MlDsaAlgorithm::MlDsa44,
4639 NativeAlgorithm::MlDsa65 => MlDsaAlgorithm::MlDsa65,
4640 NativeAlgorithm::MlDsa87 => MlDsaAlgorithm::MlDsa87,
4641 }
4642 }
4643
4644 fn seed_for(key_id: &str) -> [u8; 32] {
4647 let mut seed = [7u8; 32];
4648 for (slot, byte) in seed.iter_mut().zip(key_id.bytes()) {
4649 *slot = byte;
4650 }
4651 seed
4652 }
4653 }
4654
4655 #[async_trait]
4656 impl Backend for NativePqcBackend {
4657 fn kind(&self) -> &'static str {
4658 "pqc-native-test"
4659 }
4660
4661 async fn new_key(&self, _key_type: KeyType) -> Result<NewKey, BackendError> {
4662 Err(BackendError::Unsupported("new_key"))
4663 }
4664
4665 async fn public_key(&self, _key_id: &str) -> Result<Vec<u8>, BackendError> {
4666 Err(BackendError::Unsupported("public_key"))
4667 }
4668
4669 async fn sign(&self, _key_id: &str, _message: &[u8]) -> Result<Vec<u8>, BackendError> {
4670 Err(BackendError::Unsupported("sign"))
4671 }
4672
4673 async fn verify(
4674 &self,
4675 _key_id: &str,
4676 _message: &[u8],
4677 _signature: &[u8],
4678 ) -> Result<bool, BackendError> {
4679 Err(BackendError::Unsupported("verify"))
4680 }
4681
4682 fn supports_native_algorithm(&self, algorithm: NativeAlgorithm) -> bool {
4683 matches!(
4684 algorithm,
4685 NativeAlgorithm::MlDsa44 | NativeAlgorithm::MlDsa65 | NativeAlgorithm::MlDsa87
4686 )
4687 }
4688
4689 async fn create_named_pqc_key(
4690 &self,
4691 key_id: &str,
4692 algorithm: NativeAlgorithm,
4693 ) -> Result<NewKey, BackendError> {
4694 let seed = Self::seed_for(key_id);
4695 let public = ml_dsa_sign::public_from_seed(Self::dsa(algorithm), &seed)
4696 .map_err(|_| BackendError::Unsupported("create_named_pqc_key"))?;
4697 self.seeds
4698 .lock()
4699 .map_err(|_| BackendError::Unsupported("create_named_pqc_key"))?
4700 .insert(key_id.to_string(), seed);
4701 Ok(NewKey {
4702 key_id: key_id.to_string(),
4703 public_key: public,
4704 })
4705 }
4706
4707 async fn sign_pqc(
4708 &self,
4709 key_id: &str,
4710 message: &[u8],
4711 algorithm: NativeAlgorithm,
4712 ) -> Result<Vec<u8>, BackendError> {
4713 let seed = self
4714 .seeds
4715 .lock()
4716 .map_err(|_| BackendError::Unsupported("sign_pqc"))?
4717 .get(key_id)
4718 .copied()
4719 .ok_or_else(|| BackendError::KeyNotFound(key_id.to_string()))?;
4720 ml_dsa_sign::sign(Self::dsa(algorithm), &seed, message)
4721 .map_err(|_| BackendError::Unsupported("sign_pqc"))
4722 }
4723
4724 async fn verify_pqc(
4725 &self,
4726 key_id: &str,
4727 message: &[u8],
4728 signature: &[u8],
4729 algorithm: NativeAlgorithm,
4730 ) -> Result<bool, BackendError> {
4731 let seed = self
4732 .seeds
4733 .lock()
4734 .map_err(|_| BackendError::Unsupported("verify_pqc"))?
4735 .get(key_id)
4736 .copied()
4737 .ok_or_else(|| BackendError::KeyNotFound(key_id.to_string()))?;
4738 let public = ml_dsa_sign::public_from_seed(Self::dsa(algorithm), &seed)
4739 .map_err(|_| BackendError::Unsupported("verify_pqc"))?;
4740 ml_dsa_sign::verify(Self::dsa(algorithm), &public, message, signature)
4741 .map_err(|_| BackendError::Unsupported("verify_pqc"))
4742 }
4743
4744 async fn encrypt(
4745 &self,
4746 _key_id: &str,
4747 algorithm: AeadAlgorithm,
4748 plaintext: &[u8],
4749 aad: Option<&[u8]>,
4750 ) -> Result<CiphertextEnvelope, BackendError> {
4751 let aad = aad.unwrap_or(&[]);
4752 let mut ciphertext = vec![u8::try_from(aad.len()).unwrap_or(u8::MAX)];
4753 ciphertext.extend_from_slice(aad);
4754 ciphertext.extend_from_slice(plaintext);
4755 Ok(CiphertextEnvelope {
4756 alg: algorithm,
4757 key_version: 1,
4758 nonce: Vec::new(),
4759 ciphertext,
4760 })
4761 }
4762
4763 async fn decrypt(
4764 &self,
4765 _key_id: &str,
4766 envelope: &CiphertextEnvelope,
4767 aad: Option<&[u8]>,
4768 ) -> Result<Vec<u8>, BackendError> {
4769 let aad = aad.unwrap_or(&[]);
4770 let ct = &envelope.ciphertext;
4771 let aad_len = *ct.first().ok_or(BackendError::DecryptFailed)? as usize;
4772 let bound = ct.get(1..1 + aad_len).ok_or(BackendError::DecryptFailed)?;
4773 if bound != aad {
4774 return Err(BackendError::DecryptFailed);
4775 }
4776 Ok(ct
4777 .get(1 + aad_len..)
4778 .ok_or(BackendError::DecryptFailed)?
4779 .to_vec())
4780 }
4781
4782 async fn kv_put(&self, key_id: &str, value: &[u8]) -> Result<u32, BackendError> {
4783 self.store
4784 .lock()
4785 .map_err(|_| BackendError::Unsupported("kv_put"))?
4786 .insert(key_id.to_string(), value.to_vec());
4787 Ok(1)
4788 }
4789
4790 async fn kv_get(
4791 &self,
4792 key_id: &str,
4793 _version: Option<u32>,
4794 ) -> Result<KvValue, BackendError> {
4795 let value = self
4796 .store
4797 .lock()
4798 .map_err(|_| BackendError::Unsupported("kv_get"))?
4799 .get(key_id)
4800 .cloned();
4801 value
4802 .map(|value| KvValue { value, version: 1 })
4803 .ok_or(BackendError::Unsupported("kv_get"))
4804 }
4805 }
4806
4807 const PQC_CATALOG: &str = r#"{
4808 "schemaVersion": 1,
4809 "backends": { "primary": { "kind": "vault", "addr": "https://127.0.0.1:8200" } },
4810 "keys": {
4811 "pqc.signer44": {
4812 "class": "asymmetric", "keyType": "ml-dsa-44", "backend": "primary",
4813 "path": "secret/data/pqc/44", "writable": true, "missing": "error",
4814 "labels": ["crypto_provider=local-software", "crypto_provider_policy=local-software",
4815 "pqc_custody=software-encrypted", "pqc_storage_key=pqc/aead",
4816 "crypto_provider_version=1"],
4817 "description": "ml-dsa-44 software-custodied signer"
4818 },
4819 "pqc.signer65": {
4820 "class": "asymmetric", "keyType": "ml-dsa-65", "backend": "primary",
4821 "path": "secret/data/pqc/65", "writable": true, "missing": "error",
4822 "labels": ["crypto_provider=local-software", "crypto_provider_policy=local-software",
4823 "pqc_custody=software-encrypted", "pqc_storage_key=pqc/aead",
4824 "crypto_provider_version=1"],
4825 "description": "ml-dsa-65 software-custodied signer"
4826 },
4827 "pqc.signer87": {
4828 "class": "asymmetric", "keyType": "ml-dsa-87", "backend": "primary",
4829 "path": "secret/data/pqc/87", "writable": true, "missing": "error",
4830 "labels": ["crypto_provider=local-software", "crypto_provider_policy=local-software",
4831 "pqc_custody=software-encrypted", "pqc_storage_key=pqc/aead",
4832 "crypto_provider_version=1"],
4833 "description": "ml-dsa-87 software-custodied signer"
4834 },
4835 "pqc.backendreq": {
4836 "class": "asymmetric", "keyType": "ml-dsa-65", "backend": "primary",
4837 "path": "secret/data/pqc/breq", "writable": true, "missing": "error",
4838 "labels": ["crypto_provider_policy=backend-required", "pqc_custody=software-encrypted"],
4839 "description": "backend-required ml-dsa signer (no native provider)"
4840 },
4841 "pqc.preferred_native": {
4842 "class": "asymmetric", "keyType": "ml-dsa-65", "backend": "primary",
4843 "path": "pqc/native65", "writable": true, "missing": "error",
4844 "labels": ["crypto_provider_policy=backend-preferred", "crypto_provider_version=1"],
4845 "description": "backend-preferred ml-dsa signer (native when the backend supports it)"
4846 },
4847 "pqc.preferred_software": {
4848 "class": "asymmetric", "keyType": "ml-dsa-65", "backend": "primary",
4849 "path": "secret/data/pqc/pref-sw", "writable": true, "missing": "error",
4850 "labels": ["crypto_provider_policy=backend-preferred", "pqc_custody=software-encrypted",
4851 "pqc_storage_key=pqc/aead", "crypto_provider_version=1"],
4852 "description": "backend-preferred ml-dsa signer pinned to software custody"
4853 },
4854 "pqc.backendreq_native": {
4855 "class": "asymmetric", "keyType": "ml-dsa-65", "backend": "primary",
4856 "path": "pqc/breq-native", "writable": true, "missing": "error",
4857 "labels": ["crypto_provider_policy=backend-required", "crypto_provider_version=1"],
4858 "description": "backend-required ml-dsa signer served by a native backend"
4859 }
4860 }
4861 }"#;
4862
4863 fn manager() -> BackendManager {
4864 let catalog: Catalog = serde_json::from_str(PQC_CATALOG).expect("catalog parses");
4865 let mut backends: BTreeMap<String, Box<dyn Backend>> = BTreeMap::new();
4866 backends.insert("primary".into(), Box::new(PqcBackend::default()));
4867 BackendManager::new(catalog, backends).expect("manager builds")
4868 }
4869
4870 fn native_manager() -> BackendManager {
4873 let catalog: Catalog = serde_json::from_str(PQC_CATALOG).expect("catalog parses");
4874 let mut backends: BTreeMap<String, Box<dyn Backend>> = BTreeMap::new();
4875 backends.insert("primary".into(), Box::new(NativePqcBackend::default()));
4876 BackendManager::new(catalog, backends).expect("manager builds")
4877 }
4878
4879 const fn allowed() -> ProviderGate {
4880 ProviderGate {
4881 local_software_allowed: true,
4882 }
4883 }
4884
4885 #[tokio::test]
4886 async fn generate_sign_verify_round_trip_all_levels() {
4887 let cases = [
4888 ("pqc.signer44", MlDsaAlgorithm::MlDsa44, "ml-dsa-44"),
4889 ("pqc.signer65", MlDsaAlgorithm::MlDsa65, "ml-dsa-65"),
4890 ("pqc.signer87", MlDsaAlgorithm::MlDsa87, "ml-dsa-87"),
4891 ];
4892 for (key_id, dsa, token) in cases {
4893 let mgr = manager();
4894 let message = b"basil ml-dsa dispatch";
4895
4896 let (created, gen_dispatch) = mgr
4897 .provider_generate(key_id, allowed())
4898 .await
4899 .expect("generate");
4900 assert_eq!(gen_dispatch.provider, CryptoProviderId::LocalSoftware);
4901 assert_eq!(gen_dispatch.algorithm, token);
4902 assert!(!created.public_key.is_empty(), "{token} public");
4903
4904 let (signature, sign_dispatch) = mgr
4905 .provider_sign(key_id, message, allowed())
4906 .await
4907 .expect("sign");
4908 assert_eq!(sign_dispatch.provider, CryptoProviderId::LocalSoftware);
4909 assert_eq!(sign_dispatch.algorithm, token);
4910 assert_eq!(sign_dispatch.custody, CustodyMode::SoftwareEncrypted);
4911
4912 let (valid, _) = mgr
4913 .provider_verify(key_id, message, &signature, allowed())
4914 .await
4915 .expect("verify");
4916 assert!(valid, "{token} verifies through the broker");
4917
4918 assert!(
4920 ml_dsa_sign::verify(dsa, &created.public_key, message, &signature).expect("core"),
4921 "{token} verifies under returned public",
4922 );
4923
4924 let (bad, _) = mgr
4926 .provider_verify(key_id, b"tampered", &signature, allowed())
4927 .await
4928 .expect("verify");
4929 assert!(!bad, "{token} rejects a tampered message");
4930 }
4931 }
4932
4933 #[tokio::test]
4934 async fn backend_required_ml_dsa_fails_closed() {
4935 let mgr = manager();
4936 let err = mgr
4939 .provider_sign("pqc.backendreq", b"m", allowed())
4940 .await
4941 .expect_err("backend-required ml-dsa is unsupported");
4942 assert!(matches!(
4943 err,
4944 ManagerError::Provider(ProviderError::Unsupported { .. })
4945 ));
4946 let gen_err = mgr
4947 .provider_generate("pqc.backendreq", allowed())
4948 .await
4949 .expect_err("backend-required ml-dsa generate is unsupported");
4950 assert!(matches!(
4951 gen_err,
4952 ManagerError::Provider(ProviderError::Unsupported { .. })
4953 ));
4954 }
4955
4956 #[tokio::test]
4957 async fn local_software_without_policy_grant_is_denied() {
4958 let mgr = manager();
4959 let denied = ProviderGate {
4960 local_software_allowed: false,
4961 };
4962 for outcome in [
4963 mgr.provider_sign("pqc.signer65", b"m", denied).await,
4964 mgr.provider_generate("pqc.signer65", denied)
4965 .await
4966 .map(|(key, dispatch)| (key.public_key, dispatch)),
4967 ] {
4968 let err = outcome.expect_err("local software requires an explicit policy grant");
4969 assert!(matches!(
4970 err,
4971 ManagerError::Provider(ProviderError::PolicyDenied { .. })
4972 ));
4973 }
4974 }
4975
4976 #[tokio::test]
4977 async fn dispatch_feeds_a_secret_free_provider_audit_event() {
4978 let mgr = manager();
4979 mgr.provider_generate("pqc.signer87", allowed())
4980 .await
4981 .expect("generate");
4982 let (_signature, dispatch) = mgr
4983 .provider_sign("pqc.signer87", b"m", allowed())
4984 .await
4985 .expect("sign");
4986 let event = ProviderAuditEvent {
4989 op: "sign",
4990 key_id: "pqc.signer87",
4991 key_version: None,
4992 algorithm: dispatch.algorithm,
4993 provider: dispatch.provider,
4994 custody: dispatch.custody,
4995 caller_uid: 4242,
4996 outcome: ProviderAuditOutcome::Success,
4997 reason: "ok",
4998 };
4999 let value = event.to_json_value();
5000 assert_eq!(value["provider"], "local-software");
5001 assert_eq!(value["algorithm"], "ml-dsa-87");
5002 assert_eq!(value["op"], "sign");
5003 assert_eq!(value["caller_uid"], 4242);
5004 assert!(value.get("signature").is_none());
5005 assert!(value.get("private_key").is_none());
5006 }
5007
5008 #[tokio::test]
5009 async fn backend_preferred_routes_to_native_when_backend_supports_it() {
5010 let mgr = native_manager();
5014 let key_id = "pqc.preferred_native";
5015 let message = b"basil native ml-dsa";
5016
5017 let (created, gen_dispatch) = mgr
5018 .provider_generate(key_id, allowed())
5019 .await
5020 .expect("generate");
5021 assert_eq!(gen_dispatch.provider, CryptoProviderId::VaultTransit);
5022 assert_eq!(gen_dispatch.custody, CustodyMode::BackendNative);
5023 assert!(!created.public_key.is_empty(), "native public");
5024
5025 let (signature, sign_dispatch) = mgr
5026 .provider_sign(key_id, message, allowed())
5027 .await
5028 .expect("sign");
5029 assert_eq!(sign_dispatch.provider, CryptoProviderId::VaultTransit);
5030 assert_eq!(sign_dispatch.custody, CustodyMode::BackendNative);
5031
5032 let (valid, _) = mgr
5033 .provider_verify(key_id, message, &signature, allowed())
5034 .await
5035 .expect("verify");
5036 assert!(valid, "native signature verifies through the broker");
5037
5038 let (bad, _) = mgr
5039 .provider_verify(key_id, b"tampered", &signature, allowed())
5040 .await
5041 .expect("verify");
5042 assert!(!bad, "native verify rejects a tampered message");
5043 }
5044
5045 #[tokio::test]
5046 async fn backend_required_routes_to_native_when_backend_supports_it() {
5047 let mgr = native_manager();
5051 let key_id = "pqc.backendreq_native";
5052
5053 let (_created, gen_dispatch) = mgr
5054 .provider_generate(key_id, allowed())
5055 .await
5056 .expect("generate");
5057 assert_eq!(gen_dispatch.provider, CryptoProviderId::VaultTransit);
5058
5059 let (signature, sign_dispatch) = mgr
5060 .provider_sign(key_id, b"m", allowed())
5061 .await
5062 .expect("sign");
5063 assert_eq!(sign_dispatch.provider, CryptoProviderId::VaultTransit);
5064 let (valid, _) = mgr
5065 .provider_verify(key_id, b"m", &signature, allowed())
5066 .await
5067 .expect("verify");
5068 assert!(valid);
5069 }
5070
5071 #[tokio::test]
5072 async fn backend_preferred_falls_back_to_software_without_native_support() {
5073 let mgr = manager();
5076 let key_id = "pqc.preferred_software";
5077
5078 let (_created, gen_dispatch) = mgr
5079 .provider_generate(key_id, allowed())
5080 .await
5081 .expect("generate");
5082 assert_eq!(gen_dispatch.provider, CryptoProviderId::LocalSoftware);
5083 assert_eq!(gen_dispatch.custody, CustodyMode::SoftwareEncrypted);
5084
5085 let (signature, sign_dispatch) = mgr
5086 .provider_sign(key_id, b"m", allowed())
5087 .await
5088 .expect("sign");
5089 assert_eq!(sign_dispatch.provider, CryptoProviderId::LocalSoftware);
5090 let (valid, _) = mgr
5091 .provider_verify(key_id, b"m", &signature, allowed())
5092 .await
5093 .expect("verify");
5094 assert!(valid);
5095 }
5096
5097 #[tokio::test]
5098 async fn software_custodied_key_is_not_rerouted_by_native_probe() {
5099 let mgr = native_manager();
5104 let key_id = "pqc.preferred_software";
5105
5106 let (_created, gen_dispatch) = mgr
5107 .provider_generate(key_id, allowed())
5108 .await
5109 .expect("generate");
5110 assert_eq!(
5111 gen_dispatch.provider,
5112 CryptoProviderId::LocalSoftware,
5113 "software-custodied key stays local-software despite native support",
5114 );
5115
5116 let (_signature, sign_dispatch) = mgr
5117 .provider_sign(key_id, b"m", allowed())
5118 .await
5119 .expect("sign");
5120 assert_eq!(sign_dispatch.provider, CryptoProviderId::LocalSoftware);
5121 assert_eq!(sign_dispatch.custody, CustodyMode::SoftwareEncrypted);
5122 }
5123
5124 #[tokio::test]
5125 async fn describe_provider_surfaces_active_custody_and_migration_availability() {
5126 let native = native_manager();
5129
5130 let software = native
5131 .describe_provider("pqc.preferred_software")
5132 .expect("describe");
5133 let expected_software = KeyProviderDescriptor {
5134 policy: ProviderPolicy::BackendPreferred,
5135 provider: None,
5136 custody: Some(CustodyMode::SoftwareEncrypted),
5137 version: Some("1".to_string()),
5138 backend_native_available: true,
5139 };
5140 assert_eq!(
5141 software, expected_software,
5142 "an admin sees software custody AND that a native migration is available",
5143 );
5144
5145 let preferred = native
5146 .describe_provider("pqc.preferred_native")
5147 .expect("describe");
5148 assert_eq!(preferred.custody, None);
5149 assert!(preferred.backend_native_available);
5150
5151 let classical = manager();
5153 let no_native = classical
5154 .describe_provider("pqc.preferred_software")
5155 .expect("describe");
5156 assert!(!no_native.backend_native_available);
5157
5158 assert!(matches!(
5159 classical.describe_provider("does.not.exist"),
5160 Err(ManagerError::UnknownKey(_))
5161 ));
5162 }
5163}