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