1use super::{ml_dsa_sign, ml_kem_envelope};
12use crate::audit::timestamp;
13use crate::backend::{Backend, BackendError, NativeAlgorithm, NewKey, SignOptions};
14use crate::catalog::schema::KeyEntry;
15use async_trait::async_trait;
16use base64::Engine;
17use base64::engine::general_purpose::STANDARD as B64;
18use basil_proto::{AeadAlgorithm, CiphertextEnvelope};
19use basil_proto::{KeyMaterial, KeyType};
20use serde::{Deserialize, Serialize};
21use serde_json::json;
22use zeroize::Zeroizing;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum SignatureAlgorithm {
27 Ed25519,
29 Ed25519Nkey,
31 Rs256,
33 Es256,
35 MlDsa44,
37 MlDsa65,
39 MlDsa87,
41}
42
43impl SignatureAlgorithm {
44 #[must_use]
47 pub const fn is_ml_dsa(self) -> bool {
48 matches!(self, Self::MlDsa44 | Self::MlDsa65 | Self::MlDsa87)
49 }
50
51 #[must_use]
53 pub const fn token(self) -> &'static str {
54 signature_algorithm_name(self)
55 }
56
57 #[must_use]
62 pub const fn native_algorithm(self) -> Option<NativeAlgorithm> {
63 match self {
64 Self::MlDsa44 => Some(NativeAlgorithm::MlDsa44),
65 Self::MlDsa65 => Some(NativeAlgorithm::MlDsa65),
66 Self::MlDsa87 => Some(NativeAlgorithm::MlDsa87),
67 Self::Ed25519 | Self::Ed25519Nkey | Self::Rs256 | Self::Es256 => None,
68 }
69 }
70}
71
72#[must_use]
81pub const fn ml_dsa_signature_algorithm(
82 key_type: crate::catalog::KeyAlgorithm,
83) -> Option<SignatureAlgorithm> {
84 use crate::catalog::KeyAlgorithm;
85 match key_type {
86 KeyAlgorithm::MlDsa44 => Some(SignatureAlgorithm::MlDsa44),
87 KeyAlgorithm::MlDsa65 => Some(SignatureAlgorithm::MlDsa65),
88 KeyAlgorithm::MlDsa87 => Some(SignatureAlgorithm::MlDsa87),
89 KeyAlgorithm::Ed25519
90 | KeyAlgorithm::Ed25519Nkey
91 | KeyAlgorithm::Rsa2048
92 | KeyAlgorithm::EcdsaP256
93 | KeyAlgorithm::EcdsaP384
94 | KeyAlgorithm::EcdsaP521
95 | KeyAlgorithm::Aes256Gcm
96 | KeyAlgorithm::ChaCha20Poly1305
97 | KeyAlgorithm::X25519
98 | KeyAlgorithm::MlKem512
99 | KeyAlgorithm::MlKem768
100 | KeyAlgorithm::MlKem1024 => None,
101 }
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub enum KemAlgorithm {
107 MlKem512,
109 MlKem768,
111 MlKem1024,
113}
114
115impl KemAlgorithm {
116 #[must_use]
119 pub const fn token(self) -> &'static str {
120 match self {
121 Self::MlKem512 => "ml-kem-512",
122 Self::MlKem768 => "ml-kem-768",
123 Self::MlKem1024 => "ml-kem-1024",
124 }
125 }
126
127 #[must_use]
133 pub const fn native_algorithm(self) -> Option<NativeAlgorithm> {
134 match self {
135 Self::MlKem512 | Self::MlKem768 | Self::MlKem1024 => None,
136 }
137 }
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142pub enum EnvelopeAlgorithm {
143 Aes256Gcm,
145 ChaCha20Poly1305,
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub enum CryptoProviderId {
152 VaultTransit,
154 LocalSoftware,
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160pub enum ProviderPolicy {
161 BackendRequired,
163 BackendPreferred,
165 LocalSoftware,
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub enum CustodyMode {
172 BackendNative,
174 SoftwareEncrypted,
176}
177
178impl CustodyMode {
179 const fn token(self) -> &'static str {
180 match self {
181 Self::BackendNative => "backend-native",
182 Self::SoftwareEncrypted => "software-encrypted",
183 }
184 }
185}
186
187impl CryptoProviderId {
188 const fn token(self) -> &'static str {
189 match self {
190 Self::VaultTransit => "vault-transit",
191 Self::LocalSoftware => "local-software",
192 }
193 }
194
195 #[must_use]
199 pub const fn custody_mode(self) -> CustodyMode {
200 match self {
201 Self::VaultTransit => CustodyMode::BackendNative,
202 Self::LocalSoftware => CustodyMode::SoftwareEncrypted,
203 }
204 }
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq)]
209pub struct ProviderMetadata {
210 pub provider: Option<CryptoProviderId>,
212 pub policy: ProviderPolicy,
214 pub custody: Option<CustodyMode>,
216}
217
218impl ProviderMetadata {
219 #[must_use]
221 pub fn from_key(entry: &KeyEntry) -> Self {
222 let provider = match entry.labels.get("crypto_provider") {
223 Some("vault-transit") => Some(CryptoProviderId::VaultTransit),
224 Some("local-software") => Some(CryptoProviderId::LocalSoftware),
225 _ => None,
226 };
227 let policy = match entry.labels.get("crypto_provider_policy") {
228 Some("backend-preferred") => ProviderPolicy::BackendPreferred,
229 Some("local-software") => ProviderPolicy::LocalSoftware,
230 _ => ProviderPolicy::BackendRequired,
231 };
232 let custody = match entry.labels.get("pqc_custody") {
233 Some("backend-native") => Some(CustodyMode::BackendNative),
234 Some("software-encrypted") => Some(CustodyMode::SoftwareEncrypted),
235 _ => None,
236 };
237 Self {
238 provider,
239 policy,
240 custody,
241 }
242 }
243}
244
245#[derive(Debug, thiserror::Error)]
247pub enum ProviderError {
248 #[error("provider {provider:?} does not support {op} for {algorithm}")]
250 Unsupported {
251 provider: CryptoProviderId,
253 op: &'static str,
255 algorithm: &'static str,
257 },
258
259 #[error("provider policy denied {op}: {reason}")]
261 PolicyDenied {
262 op: &'static str,
264 reason: &'static str,
266 },
267
268 #[error("provider {provider:?} {op} failed for {algorithm}: {reason}")]
273 CryptoFailed {
274 provider: CryptoProviderId,
276 op: &'static str,
278 algorithm: &'static str,
280 reason: &'static str,
282 },
283
284 #[error(transparent)]
286 Backend(#[from] BackendError),
287}
288
289#[derive(Debug, Clone, Copy, PartialEq, Eq)]
291pub(crate) struct SoftwareCustodyCatalog<'a> {
292 pub key_id: &'a str,
294 pub algorithm: &'a str,
296 pub provider: &'a str,
298 pub provider_version: &'a str,
300 pub custody: &'a str,
302 pub storage_key: &'a str,
304}
305impl SoftwareCustodyCatalog<'_> {
306 pub(crate) fn aad(&self, key_version: u32) -> Vec<u8> {
307 format!(
308 "basil:pqc-software-custody:v1\nkey_id={}\nkey_version={key_version}\nalgorithm={}\nprovider={}\nprovider_version={}\ncustody={}\nstorage_key={}\n",
309 self.key_id,
310 self.algorithm,
311 self.provider,
312 self.provider_version,
313 self.custody,
314 self.storage_key,
315 )
316 .into_bytes()
317 }
318}
319
320#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
322#[serde(rename_all = "camelCase", deny_unknown_fields)]
323pub(crate) struct SoftwareCustodyKeyRecord {
324 schema_version: u32,
325 key_id: String,
326 key_version: u32,
327 public_key: String,
328 algorithm: String,
329 provider: String,
330 provider_version: String,
331 custody: String,
332 encrypted_private_key: EncryptedPrivateKey,
333}
334#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
335#[serde(rename_all = "camelCase", deny_unknown_fields)]
336struct EncryptedPrivateKey {
337 wrapping_key: String,
338 algorithm: String,
339 key_version: u32,
340 nonce: String,
341 ciphertext: String,
342}
343impl SoftwareCustodyKeyRecord {
344 const SCHEMA_VERSION: u32 = 1;
345
346 #[must_use]
348 pub(crate) fn aad(&self, catalog: &SoftwareCustodyCatalog<'_>) -> Vec<u8> {
349 catalog.aad(self.key_version)
350 }
351
352 pub(crate) fn encrypted_private_key(
354 &self,
355 ) -> Result<CiphertextEnvelope, SoftwareCustodyRecordError> {
356 Ok(CiphertextEnvelope {
357 alg: parse_storage_aead(&self.encrypted_private_key.algorithm)?,
358 key_version: self.encrypted_private_key.key_version,
359 nonce: decode_b64(&self.encrypted_private_key.nonce)?,
360 ciphertext: decode_b64(&self.encrypted_private_key.ciphertext)?,
361 })
362 }
363
364 fn validate_metadata(
365 &self,
366 catalog: &SoftwareCustodyCatalog<'_>,
367 kv_version: u32,
368 ) -> Result<(), SoftwareCustodyRecordError> {
369 if self.schema_version != Self::SCHEMA_VERSION {
370 return Err(SoftwareCustodyRecordError::MetadataMismatch(
371 "schema_version",
372 ));
373 }
374 if self.key_id != catalog.key_id {
375 return Err(SoftwareCustodyRecordError::MetadataMismatch("key_id"));
376 }
377 if self.key_version != kv_version {
378 return Err(SoftwareCustodyRecordError::MetadataMismatch("key_version"));
379 }
380 if self.algorithm != catalog.algorithm {
381 return Err(SoftwareCustodyRecordError::MetadataMismatch("algorithm"));
382 }
383 if self.provider != catalog.provider {
384 return Err(SoftwareCustodyRecordError::MetadataMismatch("provider"));
385 }
386 if self.provider_version != catalog.provider_version {
387 return Err(SoftwareCustodyRecordError::MetadataMismatch(
388 "provider_version",
389 ));
390 }
391 if self.custody != catalog.custody {
392 return Err(SoftwareCustodyRecordError::MetadataMismatch("custody"));
393 }
394 if self.public_key.is_empty() || decode_b64(&self.public_key).is_err() {
395 return Err(SoftwareCustodyRecordError::Malformed);
396 }
397 if self.encrypted_private_key.wrapping_key != catalog.storage_key {
398 return Err(SoftwareCustodyRecordError::MetadataMismatch("wrapping_key"));
399 }
400 if self.encrypted_private_key.key_version == 0 {
401 return Err(SoftwareCustodyRecordError::MetadataMismatch(
402 "encrypted_private_key.key_version",
403 ));
404 }
405 Ok(())
406 }
407}
408
409pub(crate) struct LocalSoftwareMaterial {
413 pub(crate) ciphertext: CiphertextEnvelope,
415 pub(crate) aad: Vec<u8>,
417 pub(crate) storage_key: String,
419 pub(crate) public_key: Vec<u8>,
421 pub(crate) key_version: u32,
423}
424impl SoftwareCustodyKeyRecord {
425 pub(crate) fn parse_for_local_software(
437 bytes: &[u8],
438 key_id: &str,
439 algorithm: &str,
440 provider_version: &str,
441 kv_version: u32,
442 ) -> Result<LocalSoftwareMaterial, SoftwareCustodyRecordError> {
443 let record: Self =
444 serde_json::from_slice(bytes).map_err(|_| SoftwareCustodyRecordError::Malformed)?;
445 let catalog = SoftwareCustodyCatalog {
446 key_id,
447 algorithm,
448 provider: CryptoProviderId::LocalSoftware.token(),
449 provider_version,
450 custody: CustodyMode::SoftwareEncrypted.token(),
451 storage_key: &record.encrypted_private_key.wrapping_key,
452 };
453 record.validate_metadata(&catalog, kv_version)?;
454 let ciphertext = record.encrypted_private_key()?;
455 let aad = record.aad(&catalog);
456 let public_key = decode_b64(&record.public_key)?;
457 Ok(LocalSoftwareMaterial {
458 ciphertext,
459 aad,
460 storage_key: record.encrypted_private_key.wrapping_key.clone(),
461 public_key,
462 key_version: kv_version,
463 })
464 }
465}
466
467#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
469pub(crate) enum SoftwareCustodyRecordError {
470 #[error("malformed software-custody key record")]
472 Malformed,
473 #[error("software-custody key record metadata mismatch: {0}")]
475 MetadataMismatch(&'static str),
476 #[error("unsupported software-custody storage AEAD")]
478 UnsupportedStorageAead,
479}
480fn parse_storage_aead(token: &str) -> Result<AeadAlgorithm, SoftwareCustodyRecordError> {
481 match token {
482 "aes-256-gcm" => Ok(AeadAlgorithm::Aes256Gcm),
483 "chacha20-poly1305" => Ok(AeadAlgorithm::Chacha20Poly1305),
484 _ => Err(SoftwareCustodyRecordError::UnsupportedStorageAead),
485 }
486}
487fn decode_b64(value: &str) -> Result<Vec<u8>, SoftwareCustodyRecordError> {
488 B64.decode(value)
489 .map_err(|_| SoftwareCustodyRecordError::Malformed)
490}
491
492#[cfg(test)]
494pub(crate) fn encode_record_bytes(bytes: &[u8]) -> String {
495 B64.encode(bytes)
496}
497
498#[derive(Debug, Clone, Copy, PartialEq, Eq)]
500pub enum ProviderAuditOutcome {
501 Allow,
503 Deny,
505 Success,
507 Failure,
509}
510
511impl ProviderAuditOutcome {
512 const fn token(self) -> &'static str {
513 match self {
514 Self::Allow => "allow",
515 Self::Deny => "deny",
516 Self::Success => "success",
517 Self::Failure => "failure",
518 }
519 }
520}
521
522#[derive(Debug, Clone, PartialEq, Eq)]
524pub struct ProviderAuditEvent<'a> {
525 pub op: &'static str,
527 pub key_id: &'a str,
529 pub key_version: Option<u32>,
531 pub algorithm: &'static str,
533 pub provider: CryptoProviderId,
535 pub custody: CustodyMode,
537 pub caller_uid: u32,
539 pub outcome: ProviderAuditOutcome,
541 pub reason: &'static str,
543}
544
545impl ProviderAuditEvent<'_> {
546 #[must_use]
549 pub fn to_json_value(&self) -> serde_json::Value {
550 json!({
551 "event": {
552 "kind": "basil.audit.provider_operation",
553 "version": 1,
554 },
555 "occurred_at": timestamp(),
556 "op": self.op,
557 "actor": {
558 "kind": "unix_uid",
559 "id": self.caller_uid.to_string(),
560 },
561 "target": {
562 "kind": "catalog_key",
563 "id": self.key_id,
564 "version": self.key_version,
565 },
566 "key_version": self.key_version,
567 "algorithm": self.algorithm,
568 "provider": self.provider.token(),
569 "custody": self.custody.token(),
570 "caller_uid": self.caller_uid,
571 "outcome": self.outcome.token(),
572 "reason": self.reason,
573 })
574 }
575}
576
577pub struct GenerateKey<'a> {
579 pub key_id: &'a str,
581 pub backend_path: &'a str,
584 pub algorithm: SignatureAlgorithm,
586 pub storage_key: Option<&'a str>,
590}
591
592pub struct GenerateSealingKey<'a> {
597 pub key_id: &'a str,
599 pub backend_path: &'a str,
601 pub algorithm: KemAlgorithm,
603 pub storage_key: Option<&'a str>,
606}
607
608pub struct ImportKey<'a> {
610 pub key_id: &'a str,
612 pub backend_path: &'a str,
614 pub algorithm: SignatureAlgorithm,
616 pub material: &'a KeyMaterial,
618}
619
620pub struct SignRequest<'a> {
622 pub key_id: &'a str,
624 pub backend_path: &'a str,
626 pub algorithm: SignatureAlgorithm,
628 pub message: &'a [u8],
630}
631
632pub struct VerifyRequest<'a> {
634 pub key_id: &'a str,
636 pub backend_path: &'a str,
638 pub algorithm: SignatureAlgorithm,
640 pub message: &'a [u8],
642 pub signature: &'a [u8],
644}
645
646pub struct EncapsulateRequest<'a> {
648 pub key_id: &'a str,
650 pub backend_path: &'a str,
652 pub algorithm: KemAlgorithm,
654}
655
656pub struct DecapsulateRequest<'a> {
658 pub key_id: &'a str,
660 pub backend_path: &'a str,
662 pub algorithm: KemAlgorithm,
664 pub encapsulated_key: &'a [u8],
666}
667
668pub struct Encapsulation {
670 pub encapsulated_key: Vec<u8>,
672 pub shared_secret: Zeroizing<Vec<u8>>,
674}
675
676pub struct WrapEnvelopeRequest<'a> {
678 pub key_id: &'a str,
680 pub backend_path: &'a str,
682 pub kem_algorithm: KemAlgorithm,
684 pub envelope_algorithm: EnvelopeAlgorithm,
686 pub plaintext: &'a [u8],
688 pub aad: Option<&'a [u8]>,
690}
691
692pub struct UnwrapEnvelopeRequest<'a> {
694 pub key_id: &'a str,
696 pub backend_path: &'a str,
698 pub kem_algorithm: KemAlgorithm,
700 pub envelope_algorithm: EnvelopeAlgorithm,
702 pub encapsulated_key: &'a [u8],
704 pub nonce: &'a [u8],
706 pub ciphertext: &'a [u8],
708 pub aad: Option<&'a [u8]>,
710}
711
712#[derive(Debug, Clone, PartialEq, Eq)]
714pub struct Envelope {
715 pub kem_algorithm: KemAlgorithm,
717 pub envelope_algorithm: EnvelopeAlgorithm,
719 pub key_version: u32,
721 pub encapsulated_key: Vec<u8>,
723 pub nonce: Vec<u8>,
725 pub ciphertext: Vec<u8>,
727}
728
729#[async_trait]
731pub trait CryptoProvider: Send + Sync {
732 fn provider_id(&self) -> CryptoProviderId;
734
735 async fn generate_key(&self, request: GenerateKey<'_>) -> Result<NewKey, ProviderError>;
737
738 async fn generate_sealing_key(
740 &self,
741 request: GenerateSealingKey<'_>,
742 ) -> Result<NewKey, ProviderError>;
743
744 async fn import_key(&self, request: ImportKey<'_>) -> Result<NewKey, ProviderError>;
746
747 async fn sign(&self, request: SignRequest<'_>) -> Result<Vec<u8>, ProviderError>;
749
750 async fn verify(&self, request: VerifyRequest<'_>) -> Result<bool, ProviderError>;
752
753 async fn encapsulate(
755 &self,
756 request: EncapsulateRequest<'_>,
757 ) -> Result<Encapsulation, ProviderError>;
758
759 async fn decapsulate(
761 &self,
762 request: DecapsulateRequest<'_>,
763 ) -> Result<Zeroizing<Vec<u8>>, ProviderError>;
764
765 async fn wrap_envelope(
767 &self,
768 request: WrapEnvelopeRequest<'_>,
769 ) -> Result<Envelope, ProviderError>;
770
771 async fn unwrap_envelope(
773 &self,
774 request: UnwrapEnvelopeRequest<'_>,
775 ) -> Result<Vec<u8>, ProviderError>;
776}
777
778pub struct BackendCryptoProvider<'a> {
780 backend: &'a dyn Backend,
781}
782
783impl<'a> BackendCryptoProvider<'a> {
784 #[must_use]
786 pub const fn new(backend: &'a dyn Backend) -> Self {
787 Self { backend }
788 }
789}
790
791#[async_trait]
792impl CryptoProvider for BackendCryptoProvider<'_> {
793 fn provider_id(&self) -> CryptoProviderId {
794 CryptoProviderId::VaultTransit
795 }
796
797 async fn generate_key(&self, request: GenerateKey<'_>) -> Result<NewKey, ProviderError> {
798 if let Some(native) = request.algorithm.native_algorithm() {
801 return self
802 .backend
803 .create_named_pqc_key(request.backend_path, native)
804 .await
805 .map_err(ProviderError::Backend);
806 }
807 let key_type = key_type_for_signature(request.algorithm).ok_or_else(|| {
808 unsupported(
809 self.provider_id(),
810 "generate_key",
811 signature_algorithm_name(request.algorithm),
812 )
813 })?;
814 self.backend
815 .create_named_key(request.backend_path, key_type)
816 .await
817 .map_err(ProviderError::Backend)
818 }
819
820 async fn generate_sealing_key(
821 &self,
822 request: GenerateSealingKey<'_>,
823 ) -> Result<NewKey, ProviderError> {
824 Err(unsupported(
827 self.provider_id(),
828 "generate_sealing_key",
829 kem_algorithm_name(request.algorithm),
830 ))
831 }
832
833 async fn import_key(&self, request: ImportKey<'_>) -> Result<NewKey, ProviderError> {
834 let key_type = key_type_for_signature(request.algorithm).ok_or_else(|| {
835 unsupported(
836 self.provider_id(),
837 "import_key",
838 signature_algorithm_name(request.algorithm),
839 )
840 })?;
841 self.backend
842 .import(request.backend_path, key_type, request.material)
843 .await
844 .map_err(ProviderError::Backend)
845 }
846
847 async fn sign(&self, request: SignRequest<'_>) -> Result<Vec<u8>, ProviderError> {
848 if let Some(native) = request.algorithm.native_algorithm() {
850 return self
851 .backend
852 .sign_pqc(request.backend_path, request.message, native)
853 .await
854 .map_err(ProviderError::Backend);
855 }
856 let options = sign_options(request.algorithm).ok_or_else(|| {
857 unsupported(
858 self.provider_id(),
859 "sign",
860 signature_algorithm_name(request.algorithm),
861 )
862 })?;
863 self.backend
864 .sign_with_options(request.backend_path, request.message, options)
865 .await
866 .map_err(ProviderError::Backend)
867 }
868
869 async fn verify(&self, request: VerifyRequest<'_>) -> Result<bool, ProviderError> {
870 if let Some(native) = request.algorithm.native_algorithm() {
872 return self
873 .backend
874 .verify_pqc(
875 request.backend_path,
876 request.message,
877 request.signature,
878 native,
879 )
880 .await
881 .map_err(ProviderError::Backend);
882 }
883 let options = sign_options(request.algorithm).ok_or_else(|| {
884 unsupported(
885 self.provider_id(),
886 "verify",
887 signature_algorithm_name(request.algorithm),
888 )
889 })?;
890 self.backend
891 .verify_with_options(
892 request.backend_path,
893 request.message,
894 request.signature,
895 options,
896 )
897 .await
898 .map_err(ProviderError::Backend)
899 }
900
901 async fn encapsulate(
902 &self,
903 request: EncapsulateRequest<'_>,
904 ) -> Result<Encapsulation, ProviderError> {
905 Err(unsupported(
906 self.provider_id(),
907 "encapsulate",
908 kem_algorithm_name(request.algorithm),
909 ))
910 }
911
912 async fn decapsulate(
913 &self,
914 request: DecapsulateRequest<'_>,
915 ) -> Result<Zeroizing<Vec<u8>>, ProviderError> {
916 Err(unsupported(
917 self.provider_id(),
918 "decapsulate",
919 kem_algorithm_name(request.algorithm),
920 ))
921 }
922
923 async fn wrap_envelope(
924 &self,
925 request: WrapEnvelopeRequest<'_>,
926 ) -> Result<Envelope, ProviderError> {
927 Err(unsupported(
928 self.provider_id(),
929 "wrap_envelope",
930 kem_algorithm_name(request.kem_algorithm),
931 ))
932 }
933
934 async fn unwrap_envelope(
935 &self,
936 request: UnwrapEnvelopeRequest<'_>,
937 ) -> Result<Vec<u8>, ProviderError> {
938 Err(unsupported(
939 self.provider_id(),
940 "unwrap_envelope",
941 kem_algorithm_name(request.kem_algorithm),
942 ))
943 }
944}
945
946pub struct LocalSoftwareProvider<'a> {
962 backend: &'a dyn Backend,
963 provider_version: &'static str,
964}
965impl<'a> LocalSoftwareProvider<'a> {
966 pub const PROVIDER_VERSION: &'static str = "1";
970
971 #[must_use]
973 pub const fn new(backend: &'a dyn Backend) -> Self {
974 Self {
975 backend,
976 provider_version: Self::PROVIDER_VERSION,
977 }
978 }
979
980 async fn material(
982 &self,
983 key_id: &str,
984 backend_path: &str,
985 op: &'static str,
986 algorithm: &'static str,
987 ) -> Result<LocalSoftwareMaterial, ProviderError> {
988 let kv = self.backend.kv_get(backend_path, None).await?;
989 SoftwareCustodyKeyRecord::parse_for_local_software(
990 &kv.value,
991 key_id,
992 algorithm,
993 self.provider_version,
994 kv.version,
995 )
996 .map_err(|_| {
997 crypto_failed(
998 CryptoProviderId::LocalSoftware,
999 op,
1000 algorithm,
1001 "malformed software-custody record",
1002 )
1003 })
1004 }
1005
1006 async fn materialize_seed(
1010 &self,
1011 material: &LocalSoftwareMaterial,
1012 ) -> Result<Zeroizing<Vec<u8>>, ProviderError> {
1013 let plaintext = self
1014 .backend
1015 .decrypt(
1016 &material.storage_key,
1017 &material.ciphertext,
1018 Some(&material.aad),
1019 )
1020 .await?;
1021 Ok(Zeroizing::new(plaintext))
1022 }
1023
1024 pub(crate) async fn public_key(
1029 &self,
1030 key_id: &str,
1031 backend_path: &str,
1032 algorithm: &'static str,
1033 ) -> Result<Vec<u8>, ProviderError> {
1034 let material = self
1035 .material(key_id, backend_path, "get_public_key", algorithm)
1036 .await?;
1037 Ok(material.public_key)
1038 }
1039
1040 async fn seal_and_store(
1046 &self,
1047 params: SealParams<'_>,
1048 seed: &[u8],
1049 public_key: Vec<u8>,
1050 ) -> Result<NewKey, ProviderError> {
1051 const RECORD_VERSION: u32 = 1;
1054
1055 let custody = SoftwareCustodyCatalog {
1056 key_id: params.key_id,
1057 algorithm: params.algorithm_token,
1058 provider: CryptoProviderId::LocalSoftware.token(),
1059 provider_version: self.provider_version,
1060 custody: CustodyMode::SoftwareEncrypted.token(),
1061 storage_key: params.storage_key,
1062 };
1063 let aad = custody.aad(RECORD_VERSION);
1064 let envelope = self
1065 .backend
1066 .encrypt(
1067 params.storage_key,
1068 AeadAlgorithm::Aes256Gcm,
1069 seed,
1070 Some(&aad),
1071 )
1072 .await?;
1073 let record = SoftwareCustodyKeyRecord {
1074 schema_version: SoftwareCustodyKeyRecord::SCHEMA_VERSION,
1075 key_id: params.key_id.to_string(),
1076 key_version: RECORD_VERSION,
1077 public_key: B64.encode(&public_key),
1078 algorithm: params.algorithm_token.to_string(),
1079 provider: CryptoProviderId::LocalSoftware.token().to_string(),
1080 provider_version: self.provider_version.to_string(),
1081 custody: CustodyMode::SoftwareEncrypted.token().to_string(),
1082 encrypted_private_key: EncryptedPrivateKey {
1083 wrapping_key: params.storage_key.to_string(),
1084 algorithm: aead_token(envelope.alg).to_string(),
1085 key_version: envelope.key_version,
1086 nonce: B64.encode(&envelope.nonce),
1087 ciphertext: B64.encode(&envelope.ciphertext),
1088 },
1089 };
1090 let bytes = serde_json::to_vec(&record).map_err(|_| {
1091 crypto_failed(
1092 CryptoProviderId::LocalSoftware,
1093 params.op,
1094 params.algorithm_token,
1095 "software-custody record serialization failed",
1096 )
1097 })?;
1098 self.backend.kv_put(params.backend_path, &bytes).await?;
1099 Ok(NewKey {
1100 key_id: params.key_id.to_string(),
1101 public_key,
1102 })
1103 }
1104}
1105
1106struct SealParams<'a> {
1109 op: &'static str,
1111 key_id: &'a str,
1113 backend_path: &'a str,
1115 algorithm_token: &'static str,
1117 storage_key: &'a str,
1119}
1120#[async_trait]
1121impl CryptoProvider for LocalSoftwareProvider<'_> {
1122 fn provider_id(&self) -> CryptoProviderId {
1123 CryptoProviderId::LocalSoftware
1124 }
1125
1126 async fn generate_key(&self, request: GenerateKey<'_>) -> Result<NewKey, ProviderError> {
1127 use rand::RngCore as _;
1128 use rand::rngs::OsRng;
1129
1130 let algorithm = ml_dsa_algorithm(request.algorithm).ok_or_else(|| {
1131 unsupported(
1132 CryptoProviderId::LocalSoftware,
1133 "generate_key",
1134 signature_algorithm_name(request.algorithm),
1135 )
1136 })?;
1137 let algorithm_token = algorithm.token();
1138 let storage_key = request.storage_key.ok_or_else(|| {
1139 crypto_failed(
1140 CryptoProviderId::LocalSoftware,
1141 "generate_key",
1142 algorithm_token,
1143 "software-custody key is missing its storage AEAD key",
1144 )
1145 })?;
1146
1147 let mut seed = Zeroizing::new([0u8; ml_dsa_sign::SEED_LEN]);
1150 OsRng.fill_bytes(seed.as_mut_slice());
1151 let public_key =
1152 ml_dsa_sign::public_from_seed(algorithm, seed.as_slice()).map_err(|_| {
1153 crypto_failed(
1154 CryptoProviderId::LocalSoftware,
1155 "generate_key",
1156 algorithm_token,
1157 "ml-dsa key generation failed",
1158 )
1159 })?;
1160
1161 self.seal_and_store(
1162 SealParams {
1163 op: "generate_key",
1164 key_id: request.key_id,
1165 backend_path: request.backend_path,
1166 algorithm_token,
1167 storage_key,
1168 },
1169 seed.as_slice(),
1170 public_key,
1171 )
1172 .await
1173 }
1174
1175 async fn generate_sealing_key(
1176 &self,
1177 request: GenerateSealingKey<'_>,
1178 ) -> Result<NewKey, ProviderError> {
1179 use rand::RngCore as _;
1180 use rand::rngs::OsRng;
1181
1182 let kem = request.algorithm;
1183 let algorithm_token = kem.token();
1184 let storage_key = request.storage_key.ok_or_else(|| {
1185 crypto_failed(
1186 CryptoProviderId::LocalSoftware,
1187 "generate_sealing_key",
1188 algorithm_token,
1189 "software-custody key is missing its storage AEAD key",
1190 )
1191 })?;
1192
1193 let mut seed = Zeroizing::new([0u8; ml_kem_envelope::SEED_LEN]);
1197 OsRng.fill_bytes(seed.as_mut_slice());
1198 let public_key = ml_kem_envelope::public_from_seed(seed.as_slice(), kem_to_core(kem))
1199 .map_err(|_| {
1200 crypto_failed(
1201 CryptoProviderId::LocalSoftware,
1202 "generate_sealing_key",
1203 algorithm_token,
1204 "ml-kem key generation failed",
1205 )
1206 })?;
1207
1208 self.seal_and_store(
1209 SealParams {
1210 op: "generate_sealing_key",
1211 key_id: request.key_id,
1212 backend_path: request.backend_path,
1213 algorithm_token,
1214 storage_key,
1215 },
1216 seed.as_slice(),
1217 public_key,
1218 )
1219 .await
1220 }
1221
1222 async fn import_key(&self, request: ImportKey<'_>) -> Result<NewKey, ProviderError> {
1223 Err(unsupported(
1224 CryptoProviderId::LocalSoftware,
1225 "import_key",
1226 signature_algorithm_name(request.algorithm),
1227 ))
1228 }
1229
1230 async fn sign(&self, request: SignRequest<'_>) -> Result<Vec<u8>, ProviderError> {
1231 let algorithm = ml_dsa_algorithm(request.algorithm).ok_or_else(|| {
1232 unsupported(
1233 CryptoProviderId::LocalSoftware,
1234 "sign",
1235 signature_algorithm_name(request.algorithm),
1236 )
1237 })?;
1238 let material = self
1239 .material(
1240 request.key_id,
1241 request.backend_path,
1242 "sign",
1243 algorithm.token(),
1244 )
1245 .await?;
1246 let seed = self.materialize_seed(&material).await?;
1247 ml_dsa_sign::sign(algorithm, &seed, request.message).map_err(|_| {
1248 crypto_failed(
1249 CryptoProviderId::LocalSoftware,
1250 "sign",
1251 algorithm.token(),
1252 "ml-dsa signing failed",
1253 )
1254 })
1255 }
1256
1257 async fn verify(&self, request: VerifyRequest<'_>) -> Result<bool, ProviderError> {
1258 let algorithm = ml_dsa_algorithm(request.algorithm).ok_or_else(|| {
1259 unsupported(
1260 CryptoProviderId::LocalSoftware,
1261 "verify",
1262 signature_algorithm_name(request.algorithm),
1263 )
1264 })?;
1265 let material = self
1267 .material(
1268 request.key_id,
1269 request.backend_path,
1270 "verify",
1271 algorithm.token(),
1272 )
1273 .await?;
1274 ml_dsa_sign::verify(
1275 algorithm,
1276 &material.public_key,
1277 request.message,
1278 request.signature,
1279 )
1280 .map_err(|_| {
1281 crypto_failed(
1282 CryptoProviderId::LocalSoftware,
1283 "verify",
1284 algorithm.token(),
1285 "malformed verification input",
1286 )
1287 })
1288 }
1289
1290 async fn encapsulate(
1291 &self,
1292 request: EncapsulateRequest<'_>,
1293 ) -> Result<Encapsulation, ProviderError> {
1294 let material = self
1295 .material(
1296 request.key_id,
1297 request.backend_path,
1298 "encapsulate",
1299 kem_algorithm_name(request.algorithm),
1300 )
1301 .await?;
1302 let seed = self.materialize_seed(&material).await?;
1303 let (encapsulated_key, shared_secret) =
1304 ml_kem_envelope::encapsulate(&seed, kem_to_core(request.algorithm)).map_err(|_| {
1305 crypto_failed(
1306 CryptoProviderId::LocalSoftware,
1307 "encapsulate",
1308 kem_algorithm_name(request.algorithm),
1309 "ml-kem encapsulation failed",
1310 )
1311 })?;
1312 Ok(Encapsulation {
1313 encapsulated_key,
1314 shared_secret,
1315 })
1316 }
1317
1318 async fn decapsulate(
1319 &self,
1320 request: DecapsulateRequest<'_>,
1321 ) -> Result<Zeroizing<Vec<u8>>, ProviderError> {
1322 let material = self
1323 .material(
1324 request.key_id,
1325 request.backend_path,
1326 "decapsulate",
1327 kem_algorithm_name(request.algorithm),
1328 )
1329 .await?;
1330 let seed = self.materialize_seed(&material).await?;
1331 ml_kem_envelope::decapsulate(
1332 &seed,
1333 kem_to_core(request.algorithm),
1334 request.encapsulated_key,
1335 )
1336 .map_err(|_| {
1337 crypto_failed(
1338 CryptoProviderId::LocalSoftware,
1339 "decapsulate",
1340 kem_algorithm_name(request.algorithm),
1341 "ml-kem decapsulation failed",
1342 )
1343 })
1344 }
1345
1346 async fn wrap_envelope(
1347 &self,
1348 request: WrapEnvelopeRequest<'_>,
1349 ) -> Result<Envelope, ProviderError> {
1350 let material = self
1351 .material(
1352 request.key_id,
1353 request.backend_path,
1354 "wrap_envelope",
1355 kem_algorithm_name(request.kem_algorithm),
1356 )
1357 .await?;
1358 let seed = self.materialize_seed(&material).await?;
1359 let sealed = ml_kem_envelope::seal(
1360 &seed,
1361 kem_to_core(request.kem_algorithm),
1362 envelope_to_core(request.envelope_algorithm),
1363 request.plaintext,
1364 request.aad.unwrap_or_default(),
1365 )
1366 .map_err(|_| {
1367 crypto_failed(
1368 CryptoProviderId::LocalSoftware,
1369 "wrap_envelope",
1370 kem_algorithm_name(request.kem_algorithm),
1371 "ml-kem envelope seal failed",
1372 )
1373 })?;
1374 Ok(Envelope {
1375 kem_algorithm: request.kem_algorithm,
1376 envelope_algorithm: request.envelope_algorithm,
1377 key_version: material.key_version,
1378 encapsulated_key: sealed.encapsulated_key,
1379 nonce: sealed.nonce.to_vec(),
1380 ciphertext: sealed.ciphertext,
1381 })
1382 }
1383
1384 async fn unwrap_envelope(
1385 &self,
1386 request: UnwrapEnvelopeRequest<'_>,
1387 ) -> Result<Vec<u8>, ProviderError> {
1388 let material = self
1389 .material(
1390 request.key_id,
1391 request.backend_path,
1392 "unwrap_envelope",
1393 kem_algorithm_name(request.kem_algorithm),
1394 )
1395 .await?;
1396 let seed = self.materialize_seed(&material).await?;
1397 let envelope = ml_kem_envelope::envelope_from_parts(
1398 kem_to_core(request.kem_algorithm),
1399 envelope_to_core(request.envelope_algorithm),
1400 request.encapsulated_key,
1401 request.nonce,
1402 request.ciphertext,
1403 )
1404 .map_err(|_| {
1405 crypto_failed(
1406 CryptoProviderId::LocalSoftware,
1407 "unwrap_envelope",
1408 kem_algorithm_name(request.kem_algorithm),
1409 "malformed ml-kem envelope",
1410 )
1411 })?;
1412 let plaintext = ml_kem_envelope::open(&seed, &envelope, request.aad.unwrap_or_default())
1413 .map_err(|_| {
1414 crypto_failed(
1415 CryptoProviderId::LocalSoftware,
1416 "unwrap_envelope",
1417 kem_algorithm_name(request.kem_algorithm),
1418 "ml-kem envelope open failed",
1419 )
1420 })?;
1421 Ok(plaintext.to_vec())
1422 }
1423}
1424const fn ml_dsa_algorithm(algorithm: SignatureAlgorithm) -> Option<ml_dsa_sign::MlDsaAlgorithm> {
1425 match algorithm {
1426 SignatureAlgorithm::MlDsa44 => Some(ml_dsa_sign::MlDsaAlgorithm::MlDsa44),
1427 SignatureAlgorithm::MlDsa65 => Some(ml_dsa_sign::MlDsaAlgorithm::MlDsa65),
1428 SignatureAlgorithm::MlDsa87 => Some(ml_dsa_sign::MlDsaAlgorithm::MlDsa87),
1429 SignatureAlgorithm::Ed25519
1430 | SignatureAlgorithm::Ed25519Nkey
1431 | SignatureAlgorithm::Rs256
1432 | SignatureAlgorithm::Es256 => None,
1433 }
1434}
1435const fn kem_to_core(algorithm: KemAlgorithm) -> ml_kem_envelope::KemAlgorithm {
1436 match algorithm {
1437 KemAlgorithm::MlKem512 => ml_kem_envelope::KemAlgorithm::MlKem512,
1438 KemAlgorithm::MlKem768 => ml_kem_envelope::KemAlgorithm::MlKem768,
1439 KemAlgorithm::MlKem1024 => ml_kem_envelope::KemAlgorithm::MlKem1024,
1440 }
1441}
1442const fn envelope_to_core(algorithm: EnvelopeAlgorithm) -> ml_kem_envelope::EnvelopeAlgorithm {
1443 match algorithm {
1444 EnvelopeAlgorithm::Aes256Gcm => ml_kem_envelope::EnvelopeAlgorithm::Aes256Gcm,
1445 EnvelopeAlgorithm::ChaCha20Poly1305 => ml_kem_envelope::EnvelopeAlgorithm::ChaCha20Poly1305,
1446 }
1447}
1448
1449pub fn select_provider(
1454 metadata: ProviderMetadata,
1455 backend_native_supported: bool,
1456 local_software_allowed: bool,
1457 op: &'static str,
1458) -> Result<CryptoProviderId, ProviderError> {
1459 match metadata.policy {
1460 ProviderPolicy::BackendRequired => select_backend_required(backend_native_supported, op),
1461 ProviderPolicy::BackendPreferred => select_backend_preferred(
1462 metadata.custody,
1463 backend_native_supported,
1464 local_software_allowed,
1465 op,
1466 ),
1467 ProviderPolicy::LocalSoftware => {
1468 select_local_software(metadata.custody, local_software_allowed, op)
1469 }
1470 }
1471}
1472
1473const fn select_backend_required(
1474 backend_native_supported: bool,
1475 op: &'static str,
1476) -> Result<CryptoProviderId, ProviderError> {
1477 if backend_native_supported {
1478 return Ok(CryptoProviderId::VaultTransit);
1479 }
1480 Err(unsupported(
1481 CryptoProviderId::VaultTransit,
1482 op,
1483 "backend-native",
1484 ))
1485}
1486
1487fn select_backend_preferred(
1488 custody: Option<CustodyMode>,
1489 backend_native_supported: bool,
1490 local_software_allowed: bool,
1491 op: &'static str,
1492) -> Result<CryptoProviderId, ProviderError> {
1493 if custody == Some(CustodyMode::SoftwareEncrypted) {
1501 require_local_software_allowed(local_software_allowed, op)?;
1502 return Ok(CryptoProviderId::LocalSoftware);
1503 }
1504 if backend_native_supported {
1505 return Ok(CryptoProviderId::VaultTransit);
1506 }
1507 require_local_software_allowed(local_software_allowed, op)?;
1508 require_software_custody(
1509 custody,
1510 op,
1511 "local software fallback requires software-encrypted custody",
1512 )?;
1513 Ok(CryptoProviderId::LocalSoftware)
1514}
1515
1516fn select_local_software(
1517 custody: Option<CustodyMode>,
1518 local_software_allowed: bool,
1519 op: &'static str,
1520) -> Result<CryptoProviderId, ProviderError> {
1521 require_local_software_allowed(local_software_allowed, op)?;
1522 require_software_custody(
1523 custody,
1524 op,
1525 "local software provider requires software-encrypted custody",
1526 )?;
1527 Ok(CryptoProviderId::LocalSoftware)
1528}
1529
1530const fn require_local_software_allowed(
1531 local_software_allowed: bool,
1532 op: &'static str,
1533) -> Result<(), ProviderError> {
1534 if local_software_allowed {
1535 return Ok(());
1536 }
1537 Err(ProviderError::PolicyDenied {
1538 op,
1539 reason: "local software custody requires caller policy grant",
1540 })
1541}
1542
1543fn require_software_custody(
1544 custody: Option<CustodyMode>,
1545 op: &'static str,
1546 reason: &'static str,
1547) -> Result<(), ProviderError> {
1548 if custody == Some(CustodyMode::SoftwareEncrypted) {
1549 return Ok(());
1550 }
1551 Err(ProviderError::PolicyDenied { op, reason })
1552}
1553
1554const fn key_type_for_signature(algorithm: SignatureAlgorithm) -> Option<KeyType> {
1555 match algorithm {
1556 SignatureAlgorithm::Ed25519 => Some(KeyType::Ed25519),
1557 SignatureAlgorithm::Ed25519Nkey => Some(KeyType::Ed25519Nkey),
1558 SignatureAlgorithm::Rs256 => Some(KeyType::Rsa2048),
1559 SignatureAlgorithm::Es256 => Some(KeyType::EcdsaP256),
1560 SignatureAlgorithm::MlDsa44 | SignatureAlgorithm::MlDsa65 | SignatureAlgorithm::MlDsa87 => {
1561 None
1562 }
1563 }
1564}
1565
1566const fn sign_options(algorithm: SignatureAlgorithm) -> Option<SignOptions> {
1567 match algorithm {
1568 SignatureAlgorithm::Ed25519 | SignatureAlgorithm::Ed25519Nkey => Some(SignOptions::Default),
1569 SignatureAlgorithm::Rs256 => Some(SignOptions::Rs256Pkcs1v15Sha256),
1570 SignatureAlgorithm::Es256 => Some(SignOptions::Es256),
1571 SignatureAlgorithm::MlDsa44 | SignatureAlgorithm::MlDsa65 | SignatureAlgorithm::MlDsa87 => {
1572 None
1573 }
1574 }
1575}
1576
1577const fn signature_algorithm_name(algorithm: SignatureAlgorithm) -> &'static str {
1578 match algorithm {
1579 SignatureAlgorithm::Ed25519 => "ed25519",
1580 SignatureAlgorithm::Ed25519Nkey => "ed25519-nkey",
1581 SignatureAlgorithm::Rs256 => "rs256",
1582 SignatureAlgorithm::Es256 => "es256",
1583 SignatureAlgorithm::MlDsa44 => "ml-dsa-44",
1584 SignatureAlgorithm::MlDsa65 => "ml-dsa-65",
1585 SignatureAlgorithm::MlDsa87 => "ml-dsa-87",
1586 }
1587}
1588
1589const fn kem_algorithm_name(algorithm: KemAlgorithm) -> &'static str {
1590 algorithm.token()
1591}
1592
1593const fn unsupported(
1594 provider: CryptoProviderId,
1595 op: &'static str,
1596 algorithm: &'static str,
1597) -> ProviderError {
1598 ProviderError::Unsupported {
1599 provider,
1600 op,
1601 algorithm,
1602 }
1603}
1604const fn crypto_failed(
1605 provider: CryptoProviderId,
1606 op: &'static str,
1607 algorithm: &'static str,
1608 reason: &'static str,
1609) -> ProviderError {
1610 ProviderError::CryptoFailed {
1611 provider,
1612 op,
1613 algorithm,
1614 reason,
1615 }
1616}
1617
1618const fn aead_token(alg: AeadAlgorithm) -> &'static str {
1621 match alg {
1622 AeadAlgorithm::Aes256Gcm => "aes-256-gcm",
1623 AeadAlgorithm::Chacha20Poly1305 => "chacha20-poly1305",
1624 }
1625}
1626
1627#[cfg(test)]
1628mod tests {
1629 use async_trait::async_trait;
1630
1631 use super::*;
1632 use crate::backend::{BackendError, NewKey};
1633 use crate::catalog::schema::Labels;
1634
1635 struct RecordingBackend;
1636
1637 #[async_trait]
1638 impl Backend for RecordingBackend {
1639 fn kind(&self) -> &'static str {
1640 "recording"
1641 }
1642
1643 async fn new_key(&self, key_type: KeyType) -> Result<NewKey, BackendError> {
1644 let _ = key_type;
1645 Err(BackendError::Unsupported("new_key"))
1646 }
1647
1648 async fn create_named_key(
1649 &self,
1650 key_id: &str,
1651 key_type: KeyType,
1652 ) -> Result<NewKey, BackendError> {
1653 Ok(NewKey {
1654 key_id: format!("{key_id}:{key_type}"),
1655 public_key: vec![1, 2, 3],
1656 })
1657 }
1658
1659 async fn public_key(&self, key_id: &str) -> Result<Vec<u8>, BackendError> {
1660 let _ = key_id;
1661 Ok(vec![1, 2, 3])
1662 }
1663
1664 async fn sign(&self, key_id: &str, message: &[u8]) -> Result<Vec<u8>, BackendError> {
1665 Ok([key_id.as_bytes(), message].concat())
1666 }
1667
1668 async fn sign_with_options(
1669 &self,
1670 key_id: &str,
1671 message: &[u8],
1672 options: SignOptions,
1673 ) -> Result<Vec<u8>, BackendError> {
1674 let mut out = self.sign(key_id, message).await?;
1675 if options == SignOptions::Rs256Pkcs1v15Sha256 {
1678 out.extend_from_slice(b":rs256");
1679 } else if options == SignOptions::Es256 {
1680 out.extend_from_slice(b":es256");
1681 }
1682 Ok(out)
1683 }
1684
1685 async fn verify(
1686 &self,
1687 key_id: &str,
1688 message: &[u8],
1689 signature: &[u8],
1690 ) -> Result<bool, BackendError> {
1691 Ok(signature == [key_id.as_bytes(), message].concat())
1694 }
1695 }
1696
1697 fn key_entry_with_labels(labels: &[&str]) -> KeyEntry {
1698 KeyEntry {
1699 class: crate::catalog::schema::Class::Asymmetric,
1700 key_type: Some(crate::catalog::schema::KeyAlgorithm::Ed25519),
1701 backend: "bao".to_string(),
1702 engine: None,
1703 path: "issuer".to_string(),
1704 public_path: None,
1705 writable: true,
1706 missing: crate::catalog::schema::MissingPolicy::Error,
1707 generate: None,
1708 sealing_pin: None,
1709 labels: Labels(labels.iter().map(ToString::to_string).collect()),
1710 description: "issuer".to_string(),
1711 }
1712 }
1713
1714 #[test]
1715 fn provider_metadata_defaults_to_backend_required() {
1716 let entry = key_entry_with_labels(&[]);
1717 assert_eq!(
1718 ProviderMetadata::from_key(&entry),
1719 ProviderMetadata {
1720 provider: None,
1721 policy: ProviderPolicy::BackendRequired,
1722 custody: None
1723 }
1724 );
1725 }
1726
1727 #[test]
1728 fn provider_metadata_parses_reserved_labels() {
1729 let entry = key_entry_with_labels(&[
1730 "crypto_provider=local-software",
1731 "crypto_provider_policy=backend-preferred",
1732 "pqc_custody=software-encrypted",
1733 ]);
1734 assert_eq!(
1735 ProviderMetadata::from_key(&entry),
1736 ProviderMetadata {
1737 provider: Some(CryptoProviderId::LocalSoftware),
1738 policy: ProviderPolicy::BackendPreferred,
1739 custody: Some(CustodyMode::SoftwareEncrypted)
1740 }
1741 );
1742 }
1743
1744 #[test]
1745 fn provider_selection_honors_backend_required() {
1746 let selected = select_provider(
1747 ProviderMetadata {
1748 provider: None,
1749 policy: ProviderPolicy::BackendRequired,
1750 custody: None,
1751 },
1752 true,
1753 false,
1754 "sign",
1755 )
1756 .expect("backend-native selected");
1757 assert_eq!(selected, CryptoProviderId::VaultTransit);
1758
1759 assert!(matches!(
1760 select_provider(
1761 ProviderMetadata {
1762 provider: None,
1763 policy: ProviderPolicy::BackendRequired,
1764 custody: None,
1765 },
1766 false,
1767 false,
1768 "sign",
1769 ),
1770 Err(ProviderError::Unsupported { .. })
1771 ));
1772 }
1773
1774 #[test]
1775 fn provider_selection_honors_local_software_custody_gate() {
1776 let denied = select_provider(
1777 ProviderMetadata {
1778 provider: None,
1779 policy: ProviderPolicy::BackendPreferred,
1780 custody: Some(CustodyMode::BackendNative),
1781 },
1782 false,
1783 true,
1784 "sign",
1785 )
1786 .expect_err("backend fallback needs software custody");
1787 assert!(matches!(denied, ProviderError::PolicyDenied { .. }));
1788
1789 let denied = select_provider(
1790 ProviderMetadata {
1791 provider: None,
1792 policy: ProviderPolicy::BackendPreferred,
1793 custody: Some(CustodyMode::SoftwareEncrypted),
1794 },
1795 false,
1796 false,
1797 "sign",
1798 )
1799 .expect_err("local software needs caller policy");
1800 assert!(matches!(
1801 denied,
1802 ProviderError::PolicyDenied {
1803 reason: "local software custody requires caller policy grant",
1804 ..
1805 }
1806 ));
1807
1808 let selected = select_provider(
1809 ProviderMetadata {
1810 provider: None,
1811 policy: ProviderPolicy::BackendPreferred,
1812 custody: Some(CustodyMode::SoftwareEncrypted),
1813 },
1814 false,
1815 true,
1816 "sign",
1817 )
1818 .expect("local fallback selected");
1819 assert_eq!(selected, CryptoProviderId::LocalSoftware);
1820 }
1821
1822 #[test]
1823 fn backend_preferred_software_custody_pins_despite_native_support() {
1824 let pinned = select_provider(
1829 ProviderMetadata {
1830 provider: None,
1831 policy: ProviderPolicy::BackendPreferred,
1832 custody: Some(CustodyMode::SoftwareEncrypted),
1833 },
1834 true,
1835 true,
1836 "sign",
1837 )
1838 .expect("software-custodied key stays local-software");
1839 assert_eq!(pinned, CryptoProviderId::LocalSoftware);
1840
1841 let native = select_provider(
1844 ProviderMetadata {
1845 provider: None,
1846 policy: ProviderPolicy::BackendPreferred,
1847 custody: None,
1848 },
1849 true,
1850 true,
1851 "sign",
1852 )
1853 .expect("no-custody preferred key routes to native");
1854 assert_eq!(native, CryptoProviderId::VaultTransit);
1855
1856 let denied = select_provider(
1859 ProviderMetadata {
1860 provider: None,
1861 policy: ProviderPolicy::BackendPreferred,
1862 custody: Some(CustodyMode::SoftwareEncrypted),
1863 },
1864 true,
1865 false,
1866 "sign",
1867 )
1868 .expect_err("local software still needs the caller grant");
1869 assert!(matches!(denied, ProviderError::PolicyDenied { .. }));
1870 }
1871
1872 #[test]
1873 fn provider_selection_honors_local_software_policy_mode() {
1874 let selected = select_provider(
1875 ProviderMetadata {
1876 provider: Some(CryptoProviderId::LocalSoftware),
1877 policy: ProviderPolicy::LocalSoftware,
1878 custody: Some(CustodyMode::SoftwareEncrypted),
1879 },
1880 true,
1881 true,
1882 "decapsulate",
1883 )
1884 .expect("explicit local software selected");
1885 assert_eq!(selected, CryptoProviderId::LocalSoftware);
1886
1887 let denied = select_provider(
1888 ProviderMetadata {
1889 provider: Some(CryptoProviderId::LocalSoftware),
1890 policy: ProviderPolicy::LocalSoftware,
1891 custody: Some(CustodyMode::SoftwareEncrypted),
1892 },
1893 true,
1894 false,
1895 "decapsulate",
1896 )
1897 .expect_err("caller grant required");
1898 assert!(matches!(
1899 denied,
1900 ProviderError::PolicyDenied {
1901 reason: "local software custody requires caller policy grant",
1902 ..
1903 }
1904 ));
1905 }
1906
1907 #[test]
1908 fn provider_audit_event_is_secret_free() {
1909 let event = ProviderAuditEvent {
1910 op: "decapsulate",
1911 key_id: "pqc.kem",
1912 key_version: Some(7),
1913 algorithm: "ml-kem-768",
1914 provider: CryptoProviderId::LocalSoftware,
1915 custody: CustodyMode::SoftwareEncrypted,
1916 caller_uid: 9100,
1917 outcome: ProviderAuditOutcome::Success,
1918 reason: "ok",
1919 };
1920 let value = event.to_json_value();
1921 assert_eq!(value["event"]["kind"], "basil.audit.provider_operation");
1922 assert_eq!(value["event"]["version"], 1);
1923 assert_eq!(value["actor"]["kind"], "unix_uid");
1924 assert_eq!(value["actor"]["id"], "9100");
1925 assert_eq!(value["target"]["kind"], "catalog_key");
1926 assert_eq!(value["target"]["id"], "pqc.kem");
1927 assert_eq!(value["target"]["version"], 7);
1928 assert!(value["occurred_at"].as_str().is_some());
1929 assert_eq!(value["op"], "decapsulate");
1930 assert_eq!(value["key_version"], 7);
1931 assert_eq!(value["algorithm"], "ml-kem-768");
1932 assert_eq!(value["provider"], "local-software");
1933 assert_eq!(value["custody"], "software-encrypted");
1934 assert_eq!(value["caller_uid"], 9100);
1935 assert_eq!(value["outcome"], "success");
1936 assert!(value.get("private_key").is_none());
1937 assert!(value.get("plaintext").is_none());
1938 assert!(value.get("ciphertext").is_none());
1939 assert!(value.get("signature").is_none());
1940 assert!(value.get("shared_secret").is_none());
1941 }
1942
1943 #[tokio::test]
1944 async fn backend_provider_delegates_legacy_signing() {
1945 let backend = RecordingBackend;
1946 let provider = BackendCryptoProvider::new(&backend);
1947 let signature = provider
1948 .sign(SignRequest {
1949 key_id: "catalog.issuer",
1950 backend_path: "issuer",
1951 algorithm: SignatureAlgorithm::Rs256,
1952 message: b"digest",
1953 })
1954 .await
1955 .expect("signs");
1956 assert_eq!(signature, b"issuerdigest:rs256");
1957 }
1958
1959 #[tokio::test]
1960 async fn backend_provider_rejects_pqc_without_native_support() {
1961 let backend = RecordingBackend;
1966 let provider = BackendCryptoProvider::new(&backend);
1967 assert!(!backend.supports_native_algorithm(NativeAlgorithm::MlDsa65));
1968 let err = provider
1969 .sign(SignRequest {
1970 key_id: "catalog.issuer",
1971 backend_path: "issuer",
1972 algorithm: SignatureAlgorithm::MlDsa65,
1973 message: b"digest",
1974 })
1975 .await
1976 .expect_err("ML-DSA unsupported");
1977 assert!(matches!(
1978 err,
1979 ProviderError::Backend(BackendError::Unsupported("sign_pqc"))
1980 ));
1981 }
1982}
1983
1984#[cfg(test)]
1985mod pqc_provider_tests {
1986 use std::collections::HashMap;
1987
1988 use async_trait::async_trait;
1989 use basil_proto::{CiphertextEnvelope, KeyType};
1990
1991 use super::*;
1992 use crate::backend::{Backend, BackendError, KvValue, NewKey, SignOptions};
1993 use crate::core::{ml_dsa_sign, ml_kem_envelope};
1994
1995 const STORAGE_KEY: &str = "pqc-storage-aead";
1996 const KEY_ID: &str = "pqc.example";
1997 const PATH: &str = "kv/pqc/example";
1998
1999 struct CustodyBackend {
2003 records: HashMap<String, Vec<u8>>,
2004 secrets: HashMap<String, Vec<u8>>,
2005 }
2006
2007 impl CustodyBackend {
2008 fn single(record: Vec<u8>, seed: &[u8]) -> Self {
2009 Self {
2010 records: HashMap::from([(PATH.to_string(), record)]),
2011 secrets: HashMap::from([(STORAGE_KEY.to_string(), seed.to_vec())]),
2012 }
2013 }
2014 }
2015
2016 #[async_trait]
2017 impl Backend for CustodyBackend {
2018 fn kind(&self) -> &'static str {
2019 "custody-test"
2020 }
2021
2022 async fn new_key(&self, _key_type: KeyType) -> Result<NewKey, BackendError> {
2023 Err(BackendError::Unsupported("new_key"))
2024 }
2025
2026 async fn create_named_key(
2027 &self,
2028 _key_id: &str,
2029 _key_type: KeyType,
2030 ) -> Result<NewKey, BackendError> {
2031 Err(BackendError::Unsupported("create_named_key"))
2032 }
2033
2034 async fn public_key(&self, _key_id: &str) -> Result<Vec<u8>, BackendError> {
2035 Err(BackendError::Unsupported("public_key"))
2036 }
2037
2038 async fn sign(&self, _key_id: &str, _message: &[u8]) -> Result<Vec<u8>, BackendError> {
2039 Err(BackendError::Unsupported("sign"))
2040 }
2041
2042 async fn sign_with_options(
2043 &self,
2044 _key_id: &str,
2045 _message: &[u8],
2046 _options: SignOptions,
2047 ) -> Result<Vec<u8>, BackendError> {
2048 Err(BackendError::Unsupported("sign_with_options"))
2049 }
2050
2051 async fn verify(
2052 &self,
2053 _key_id: &str,
2054 _message: &[u8],
2055 _signature: &[u8],
2056 ) -> Result<bool, BackendError> {
2057 Err(BackendError::Unsupported("verify"))
2058 }
2059
2060 async fn kv_get(
2061 &self,
2062 key_id: &str,
2063 _version: Option<u32>,
2064 ) -> Result<KvValue, BackendError> {
2065 let value = self
2066 .records
2067 .get(key_id)
2068 .cloned()
2069 .ok_or(BackendError::Unsupported("kv_get"))?;
2070 Ok(KvValue { value, version: 1 })
2071 }
2072
2073 async fn decrypt(
2074 &self,
2075 key_id: &str,
2076 _envelope: &CiphertextEnvelope,
2077 _aad: Option<&[u8]>,
2078 ) -> Result<Vec<u8>, BackendError> {
2079 self.secrets
2080 .get(key_id)
2081 .cloned()
2082 .ok_or(BackendError::Unsupported("decrypt"))
2083 }
2084 }
2085
2086 fn custody_record(algorithm: &str, public_key: &[u8], provider_version: &str) -> Vec<u8> {
2088 let record = SoftwareCustodyKeyRecord {
2089 schema_version: SoftwareCustodyKeyRecord::SCHEMA_VERSION,
2090 key_id: KEY_ID.to_string(),
2091 key_version: 1,
2092 public_key: encode_record_bytes(public_key),
2093 algorithm: algorithm.to_string(),
2094 provider: CryptoProviderId::LocalSoftware.token().to_string(),
2095 provider_version: provider_version.to_string(),
2096 custody: CustodyMode::SoftwareEncrypted.token().to_string(),
2097 encrypted_private_key: EncryptedPrivateKey {
2098 wrapping_key: STORAGE_KEY.to_string(),
2099 algorithm: "aes-256-gcm".to_string(),
2100 key_version: 1,
2101 nonce: encode_record_bytes(&[0u8; 12]),
2102 ciphertext: encode_record_bytes(&[0u8; 16]),
2103 },
2104 };
2105 serde_json::to_vec(&record).expect("serialize record")
2106 }
2107
2108 const DSA_LEVELS: [(SignatureAlgorithm, ml_dsa_sign::MlDsaAlgorithm); 3] = [
2109 (
2110 SignatureAlgorithm::MlDsa44,
2111 ml_dsa_sign::MlDsaAlgorithm::MlDsa44,
2112 ),
2113 (
2114 SignatureAlgorithm::MlDsa65,
2115 ml_dsa_sign::MlDsaAlgorithm::MlDsa65,
2116 ),
2117 (
2118 SignatureAlgorithm::MlDsa87,
2119 ml_dsa_sign::MlDsaAlgorithm::MlDsa87,
2120 ),
2121 ];
2122
2123 const KEM_LEVELS: [KemAlgorithm; 3] = [
2124 KemAlgorithm::MlKem512,
2125 KemAlgorithm::MlKem768,
2126 KemAlgorithm::MlKem1024,
2127 ];
2128
2129 #[tokio::test]
2130 async fn signs_and_verifies_every_ml_dsa_level() {
2131 for (sig_algorithm, dsa_algorithm) in DSA_LEVELS {
2132 let seed = [0x11u8; ml_dsa_sign::SEED_LEN];
2133 let public = ml_dsa_sign::public_from_seed(dsa_algorithm, &seed).expect("public");
2134 let record = custody_record(
2135 dsa_algorithm.token(),
2136 &public,
2137 LocalSoftwareProvider::PROVIDER_VERSION,
2138 );
2139 let backend = CustodyBackend::single(record, &seed);
2140 let provider = LocalSoftwareProvider::new(&backend);
2141
2142 let signature = provider
2143 .sign(SignRequest {
2144 key_id: KEY_ID,
2145 backend_path: PATH,
2146 algorithm: sig_algorithm,
2147 message: b"basil pqc payload",
2148 })
2149 .await
2150 .expect("sign");
2151 assert!(
2152 provider
2153 .verify(VerifyRequest {
2154 key_id: KEY_ID,
2155 backend_path: PATH,
2156 algorithm: sig_algorithm,
2157 message: b"basil pqc payload",
2158 signature: &signature,
2159 })
2160 .await
2161 .expect("verify"),
2162 "{} verifies",
2163 dsa_algorithm.token()
2164 );
2165 assert!(
2166 !provider
2167 .verify(VerifyRequest {
2168 key_id: KEY_ID,
2169 backend_path: PATH,
2170 algorithm: sig_algorithm,
2171 message: b"tampered payload",
2172 signature: &signature,
2173 })
2174 .await
2175 .expect("verify"),
2176 "{} rejects wrong message",
2177 dsa_algorithm.token()
2178 );
2179 }
2180 }
2181
2182 #[tokio::test]
2183 async fn encapsulate_decapsulate_every_ml_kem_level() {
2184 for kem in KEM_LEVELS {
2185 let seed = [0x42u8; ml_kem_envelope::SEED_LEN];
2186 let record = custody_record(
2187 kem_algorithm_name(kem),
2188 &[7u8; 32],
2189 LocalSoftwareProvider::PROVIDER_VERSION,
2190 );
2191 let backend = CustodyBackend::single(record, &seed);
2192 let provider = LocalSoftwareProvider::new(&backend);
2193
2194 let encapsulation = provider
2195 .encapsulate(EncapsulateRequest {
2196 key_id: KEY_ID,
2197 backend_path: PATH,
2198 algorithm: kem,
2199 })
2200 .await
2201 .expect("encapsulate");
2202 let shared = provider
2203 .decapsulate(DecapsulateRequest {
2204 key_id: KEY_ID,
2205 backend_path: PATH,
2206 algorithm: kem,
2207 encapsulated_key: &encapsulation.encapsulated_key,
2208 })
2209 .await
2210 .expect("decapsulate");
2211 assert_eq!(
2212 encapsulation.shared_secret.as_slice(),
2213 shared.as_slice(),
2214 "{} shared secret matches",
2215 kem_algorithm_name(kem)
2216 );
2217 }
2218 }
2219
2220 #[tokio::test]
2221 async fn wrap_unwrap_envelope_every_ml_kem_level() {
2222 for kem in KEM_LEVELS {
2223 for envelope_algorithm in [
2224 EnvelopeAlgorithm::Aes256Gcm,
2225 EnvelopeAlgorithm::ChaCha20Poly1305,
2226 ] {
2227 let seed = [0x42u8; ml_kem_envelope::SEED_LEN];
2228 let record = custody_record(
2229 kem_algorithm_name(kem),
2230 &[7u8; 32],
2231 LocalSoftwareProvider::PROVIDER_VERSION,
2232 );
2233 let backend = CustodyBackend::single(record, &seed);
2234 let provider = LocalSoftwareProvider::new(&backend);
2235
2236 let envelope = provider
2237 .wrap_envelope(WrapEnvelopeRequest {
2238 key_id: KEY_ID,
2239 backend_path: PATH,
2240 kem_algorithm: kem,
2241 envelope_algorithm,
2242 plaintext: b"top secret",
2243 aad: Some(b"context"),
2244 })
2245 .await
2246 .expect("wrap");
2247 assert_eq!(envelope.key_version, 1);
2248 let plaintext = provider
2249 .unwrap_envelope(UnwrapEnvelopeRequest {
2250 key_id: KEY_ID,
2251 backend_path: PATH,
2252 kem_algorithm: kem,
2253 envelope_algorithm,
2254 encapsulated_key: &envelope.encapsulated_key,
2255 nonce: &envelope.nonce,
2256 ciphertext: &envelope.ciphertext,
2257 aad: Some(b"context"),
2258 })
2259 .await
2260 .expect("unwrap");
2261 assert_eq!(plaintext, b"top secret");
2262
2263 let wrong_aad = provider
2264 .unwrap_envelope(UnwrapEnvelopeRequest {
2265 key_id: KEY_ID,
2266 backend_path: PATH,
2267 kem_algorithm: kem,
2268 envelope_algorithm,
2269 encapsulated_key: &envelope.encapsulated_key,
2270 nonce: &envelope.nonce,
2271 ciphertext: &envelope.ciphertext,
2272 aad: Some(b"wrong"),
2273 })
2274 .await
2275 .expect_err("wrong aad fails");
2276 assert!(matches!(
2277 wrong_aad,
2278 ProviderError::CryptoFailed {
2279 op: "unwrap_envelope",
2280 ..
2281 }
2282 ));
2283 }
2284 }
2285 }
2286
2287 #[tokio::test]
2288 async fn rejects_non_pqc_signature_algorithm() {
2289 let backend = CustodyBackend::single(Vec::new(), &[]);
2290 let provider = LocalSoftwareProvider::new(&backend);
2291 let err = provider
2292 .sign(SignRequest {
2293 key_id: KEY_ID,
2294 backend_path: PATH,
2295 algorithm: SignatureAlgorithm::Ed25519,
2296 message: b"m",
2297 })
2298 .await
2299 .expect_err("non-pqc rejected");
2300 assert!(matches!(
2301 err,
2302 ProviderError::Unsupported {
2303 provider: CryptoProviderId::LocalSoftware,
2304 op: "sign",
2305 algorithm: "ed25519"
2306 }
2307 ));
2308 }
2309
2310 #[tokio::test]
2311 async fn malformed_record_fails_closed() {
2312 let backend = CustodyBackend::single(b"not a record".to_vec(), &[0u8; 32]);
2313 let provider = LocalSoftwareProvider::new(&backend);
2314 let err = provider
2315 .sign(SignRequest {
2316 key_id: KEY_ID,
2317 backend_path: PATH,
2318 algorithm: SignatureAlgorithm::MlDsa44,
2319 message: b"m",
2320 })
2321 .await
2322 .expect_err("malformed record");
2323 assert!(matches!(
2324 err,
2325 ProviderError::CryptoFailed {
2326 provider: CryptoProviderId::LocalSoftware,
2327 op: "sign",
2328 algorithm: "ml-dsa-44",
2329 reason: "malformed software-custody record"
2330 }
2331 ));
2332 }
2333
2334 #[tokio::test]
2335 async fn wrong_provider_version_record_fails_closed() {
2336 let seed = [0x11u8; ml_dsa_sign::SEED_LEN];
2337 let public =
2338 ml_dsa_sign::public_from_seed(ml_dsa_sign::MlDsaAlgorithm::MlDsa65, &seed).expect("pk");
2339 let record = custody_record("ml-dsa-65", &public, "99");
2342 let backend = CustodyBackend::single(record, &seed);
2343 let provider = LocalSoftwareProvider::new(&backend);
2344 let err = provider
2345 .sign(SignRequest {
2346 key_id: KEY_ID,
2347 backend_path: PATH,
2348 algorithm: SignatureAlgorithm::MlDsa65,
2349 message: b"m",
2350 })
2351 .await
2352 .expect_err("version mismatch");
2353 assert!(matches!(err, ProviderError::CryptoFailed { .. }));
2354 }
2355
2356 #[tokio::test]
2357 async fn generate_without_storage_key_and_import_fail_closed() {
2358 let backend = CustodyBackend::single(Vec::new(), &[]);
2359 let provider = LocalSoftwareProvider::new(&backend);
2360 let material = basil_proto::KeyMaterial::Ed25519Seed(vec![0u8; 32]);
2361 assert!(matches!(
2364 provider
2365 .generate_key(GenerateKey {
2366 key_id: KEY_ID,
2367 backend_path: PATH,
2368 algorithm: SignatureAlgorithm::MlDsa87,
2369 storage_key: None,
2370 })
2371 .await,
2372 Err(ProviderError::CryptoFailed {
2373 op: "generate_key",
2374 ..
2375 })
2376 ));
2377 assert!(matches!(
2379 provider
2380 .import_key(ImportKey {
2381 key_id: KEY_ID,
2382 backend_path: PATH,
2383 algorithm: SignatureAlgorithm::MlDsa87,
2384 material: &material,
2385 })
2386 .await,
2387 Err(ProviderError::Unsupported {
2388 op: "import_key",
2389 ..
2390 })
2391 ));
2392 }
2393
2394 struct RoundTripBackend {
2400 records: std::sync::Mutex<HashMap<String, Vec<u8>>>,
2401 }
2402
2403 impl RoundTripBackend {
2404 fn new() -> Self {
2405 Self {
2406 records: std::sync::Mutex::new(HashMap::new()),
2407 }
2408 }
2409 }
2410
2411 #[async_trait]
2412 impl Backend for RoundTripBackend {
2413 fn kind(&self) -> &'static str {
2414 "round-trip-custody-test"
2415 }
2416
2417 async fn new_key(&self, _key_type: KeyType) -> Result<NewKey, BackendError> {
2418 Err(BackendError::Unsupported("new_key"))
2419 }
2420
2421 async fn public_key(&self, _key_id: &str) -> Result<Vec<u8>, BackendError> {
2422 Err(BackendError::Unsupported("public_key"))
2423 }
2424
2425 async fn sign(&self, _key_id: &str, _message: &[u8]) -> Result<Vec<u8>, BackendError> {
2426 Err(BackendError::Unsupported("sign"))
2427 }
2428
2429 async fn verify(
2430 &self,
2431 _key_id: &str,
2432 _message: &[u8],
2433 _signature: &[u8],
2434 ) -> Result<bool, BackendError> {
2435 Err(BackendError::Unsupported("verify"))
2436 }
2437
2438 async fn encrypt(
2439 &self,
2440 _key_id: &str,
2441 algorithm: basil_proto::AeadAlgorithm,
2442 plaintext: &[u8],
2443 _aad: Option<&[u8]>,
2444 ) -> Result<CiphertextEnvelope, BackendError> {
2445 Ok(CiphertextEnvelope {
2446 alg: algorithm,
2447 key_version: 1,
2448 nonce: vec![0u8; 12],
2449 ciphertext: plaintext.to_vec(),
2450 })
2451 }
2452
2453 async fn decrypt(
2454 &self,
2455 _key_id: &str,
2456 envelope: &CiphertextEnvelope,
2457 _aad: Option<&[u8]>,
2458 ) -> Result<Vec<u8>, BackendError> {
2459 Ok(envelope.ciphertext.clone())
2460 }
2461
2462 async fn kv_put(&self, key_id: &str, value: &[u8]) -> Result<u32, BackendError> {
2463 self.records
2464 .lock()
2465 .map_err(|_| BackendError::Unsupported("kv_put"))?
2466 .insert(key_id.to_string(), value.to_vec());
2467 Ok(1)
2468 }
2469
2470 async fn kv_get(
2471 &self,
2472 key_id: &str,
2473 _version: Option<u32>,
2474 ) -> Result<KvValue, BackendError> {
2475 let value = self
2476 .records
2477 .lock()
2478 .map_err(|_| BackendError::Unsupported("kv_get"))?
2479 .get(key_id)
2480 .cloned();
2481 value
2482 .map(|value| KvValue { value, version: 1 })
2483 .ok_or(BackendError::Unsupported("kv_get"))
2484 }
2485 }
2486
2487 #[tokio::test]
2488 async fn generate_then_sign_verify_round_trip_every_level() {
2489 for (sig_algorithm, dsa_algorithm) in DSA_LEVELS {
2490 let backend = RoundTripBackend::new();
2491 let provider = LocalSoftwareProvider::new(&backend);
2492 let created = provider
2493 .generate_key(GenerateKey {
2494 key_id: KEY_ID,
2495 backend_path: PATH,
2496 algorithm: sig_algorithm,
2497 storage_key: Some(STORAGE_KEY),
2498 })
2499 .await
2500 .expect("generate");
2501 assert!(
2503 !created.public_key.is_empty(),
2504 "{} public",
2505 dsa_algorithm.token()
2506 );
2507
2508 let signature = provider
2509 .sign(SignRequest {
2510 key_id: KEY_ID,
2511 backend_path: PATH,
2512 algorithm: sig_algorithm,
2513 message: b"provisioned payload",
2514 })
2515 .await
2516 .expect("sign");
2517 assert!(
2518 provider
2519 .verify(VerifyRequest {
2520 key_id: KEY_ID,
2521 backend_path: PATH,
2522 algorithm: sig_algorithm,
2523 message: b"provisioned payload",
2524 signature: &signature,
2525 })
2526 .await
2527 .expect("verify"),
2528 "{} round trip verifies",
2529 dsa_algorithm.token()
2530 );
2531 assert!(
2532 ml_dsa_sign::verify(
2533 dsa_algorithm,
2534 &created.public_key,
2535 b"provisioned payload",
2536 &signature,
2537 )
2538 .expect("core verify"),
2539 "{} verifies under returned public",
2540 dsa_algorithm.token()
2541 );
2542 }
2543 }
2544}