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(
439 bytes: &[u8],
440 key_id: &str,
441 algorithm: &str,
442 provider_version: &str,
443 kv_version: u32,
444 storage_key: &str,
445 ) -> Result<LocalSoftwareMaterial, SoftwareCustodyRecordError> {
446 let record: Self =
447 serde_json::from_slice(bytes).map_err(|_| SoftwareCustodyRecordError::Malformed)?;
448 let catalog = SoftwareCustodyCatalog {
449 key_id,
450 algorithm,
451 provider: CryptoProviderId::LocalSoftware.token(),
452 provider_version,
453 custody: CustodyMode::SoftwareEncrypted.token(),
454 storage_key,
455 };
456 record.validate_metadata(&catalog, kv_version)?;
457 let ciphertext = record.encrypted_private_key()?;
458 let aad = record.aad(&catalog);
459 let public_key = decode_b64(&record.public_key)?;
460 Ok(LocalSoftwareMaterial {
461 ciphertext,
462 aad,
463 storage_key: storage_key.to_string(),
464 public_key,
465 key_version: kv_version,
466 })
467 }
468}
469
470#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
472pub(crate) enum SoftwareCustodyRecordError {
473 #[error("malformed software-custody key record")]
475 Malformed,
476 #[error("software-custody key record metadata mismatch: {0}")]
478 MetadataMismatch(&'static str),
479 #[error("unsupported software-custody storage AEAD")]
481 UnsupportedStorageAead,
482}
483fn parse_storage_aead(token: &str) -> Result<AeadAlgorithm, SoftwareCustodyRecordError> {
484 match token {
485 "aes-256-gcm" => Ok(AeadAlgorithm::Aes256Gcm),
486 "chacha20-poly1305" => Ok(AeadAlgorithm::Chacha20Poly1305),
487 _ => Err(SoftwareCustodyRecordError::UnsupportedStorageAead),
488 }
489}
490fn decode_b64(value: &str) -> Result<Vec<u8>, SoftwareCustodyRecordError> {
491 B64.decode(value)
492 .map_err(|_| SoftwareCustodyRecordError::Malformed)
493}
494
495#[cfg(test)]
497pub(crate) fn encode_record_bytes(bytes: &[u8]) -> String {
498 B64.encode(bytes)
499}
500
501#[derive(Debug, Clone, Copy, PartialEq, Eq)]
503pub enum ProviderAuditOutcome {
504 Allow,
506 Deny,
508 Success,
510 Failure,
512}
513
514impl ProviderAuditOutcome {
515 const fn token(self) -> &'static str {
516 match self {
517 Self::Allow => "allow",
518 Self::Deny => "deny",
519 Self::Success => "success",
520 Self::Failure => "failure",
521 }
522 }
523}
524
525#[derive(Debug, Clone, PartialEq, Eq)]
527pub struct ProviderAuditEvent<'a> {
528 pub op: &'static str,
530 pub key_id: &'a str,
532 pub key_version: Option<u32>,
534 pub algorithm: &'static str,
536 pub provider: CryptoProviderId,
538 pub custody: CustodyMode,
540 pub caller_uid: u32,
542 pub outcome: ProviderAuditOutcome,
544 pub reason: &'static str,
546}
547
548impl ProviderAuditEvent<'_> {
549 #[must_use]
552 pub fn to_json_value(&self) -> serde_json::Value {
553 json!({
554 "event": {
555 "kind": "basil.audit.provider_operation",
556 "version": 1,
557 },
558 "occurred_at": timestamp(),
559 "op": self.op,
560 "actor": {
561 "kind": "unix_uid",
562 "id": self.caller_uid.to_string(),
563 },
564 "target": {
565 "kind": "catalog_key",
566 "id": self.key_id,
567 "version": self.key_version,
568 },
569 "key_version": self.key_version,
570 "algorithm": self.algorithm,
571 "provider": self.provider.token(),
572 "custody": self.custody.token(),
573 "caller_uid": self.caller_uid,
574 "outcome": self.outcome.token(),
575 "reason": self.reason,
576 })
577 }
578}
579
580pub struct GenerateKey<'a> {
582 pub key_id: &'a str,
584 pub backend_path: &'a str,
587 pub algorithm: SignatureAlgorithm,
589 pub storage_key: Option<&'a str>,
593}
594
595pub struct GenerateSealingKey<'a> {
600 pub key_id: &'a str,
602 pub backend_path: &'a str,
604 pub algorithm: KemAlgorithm,
606 pub storage_key: Option<&'a str>,
609}
610
611pub struct ImportKey<'a> {
613 pub key_id: &'a str,
615 pub backend_path: &'a str,
617 pub algorithm: SignatureAlgorithm,
619 pub material: &'a KeyMaterial,
621}
622
623pub struct SignRequest<'a> {
625 pub key_id: &'a str,
627 pub backend_path: &'a str,
629 pub algorithm: SignatureAlgorithm,
631 pub message: &'a [u8],
633 pub storage_key: Option<&'a str>,
637}
638
639pub struct VerifyRequest<'a> {
641 pub key_id: &'a str,
643 pub backend_path: &'a str,
645 pub algorithm: SignatureAlgorithm,
647 pub message: &'a [u8],
649 pub signature: &'a [u8],
651 pub storage_key: Option<&'a str>,
655}
656
657pub struct EncapsulateRequest<'a> {
659 pub key_id: &'a str,
661 pub backend_path: &'a str,
663 pub algorithm: KemAlgorithm,
665 pub storage_key: Option<&'a str>,
669}
670
671pub struct DecapsulateRequest<'a> {
673 pub key_id: &'a str,
675 pub backend_path: &'a str,
677 pub algorithm: KemAlgorithm,
679 pub encapsulated_key: &'a [u8],
681 pub storage_key: Option<&'a str>,
685}
686
687pub struct Encapsulation {
689 pub encapsulated_key: Vec<u8>,
691 pub shared_secret: Zeroizing<Vec<u8>>,
693}
694
695pub struct WrapEnvelopeRequest<'a> {
697 pub key_id: &'a str,
699 pub backend_path: &'a str,
701 pub kem_algorithm: KemAlgorithm,
703 pub envelope_algorithm: EnvelopeAlgorithm,
705 pub plaintext: &'a [u8],
707 pub aad: Option<&'a [u8]>,
709 pub storage_key: Option<&'a str>,
713}
714
715pub struct UnwrapEnvelopeRequest<'a> {
717 pub key_id: &'a str,
719 pub backend_path: &'a str,
721 pub kem_algorithm: KemAlgorithm,
723 pub envelope_algorithm: EnvelopeAlgorithm,
725 pub encapsulated_key: &'a [u8],
727 pub nonce: &'a [u8],
729 pub ciphertext: &'a [u8],
731 pub aad: Option<&'a [u8]>,
733 pub storage_key: Option<&'a str>,
737}
738
739#[derive(Debug, Clone, PartialEq, Eq)]
741pub struct Envelope {
742 pub kem_algorithm: KemAlgorithm,
744 pub envelope_algorithm: EnvelopeAlgorithm,
746 pub key_version: u32,
748 pub encapsulated_key: Vec<u8>,
750 pub nonce: Vec<u8>,
752 pub ciphertext: Vec<u8>,
754}
755
756#[async_trait]
758pub trait CryptoProvider: Send + Sync {
759 fn provider_id(&self) -> CryptoProviderId;
761
762 async fn generate_key(&self, request: GenerateKey<'_>) -> Result<NewKey, ProviderError>;
764
765 async fn generate_sealing_key(
767 &self,
768 request: GenerateSealingKey<'_>,
769 ) -> Result<NewKey, ProviderError>;
770
771 async fn import_key(&self, request: ImportKey<'_>) -> Result<NewKey, ProviderError>;
773
774 async fn sign(&self, request: SignRequest<'_>) -> Result<Vec<u8>, ProviderError>;
776
777 async fn verify(&self, request: VerifyRequest<'_>) -> Result<bool, ProviderError>;
779
780 async fn encapsulate(
782 &self,
783 request: EncapsulateRequest<'_>,
784 ) -> Result<Encapsulation, ProviderError>;
785
786 async fn decapsulate(
788 &self,
789 request: DecapsulateRequest<'_>,
790 ) -> Result<Zeroizing<Vec<u8>>, ProviderError>;
791
792 async fn wrap_envelope(
794 &self,
795 request: WrapEnvelopeRequest<'_>,
796 ) -> Result<Envelope, ProviderError>;
797
798 async fn unwrap_envelope(
800 &self,
801 request: UnwrapEnvelopeRequest<'_>,
802 ) -> Result<Vec<u8>, ProviderError>;
803}
804
805pub struct BackendCryptoProvider<'a> {
807 backend: &'a dyn Backend,
808}
809
810impl<'a> BackendCryptoProvider<'a> {
811 #[must_use]
813 pub const fn new(backend: &'a dyn Backend) -> Self {
814 Self { backend }
815 }
816}
817
818#[async_trait]
819impl CryptoProvider for BackendCryptoProvider<'_> {
820 fn provider_id(&self) -> CryptoProviderId {
821 CryptoProviderId::VaultTransit
822 }
823
824 async fn generate_key(&self, request: GenerateKey<'_>) -> Result<NewKey, ProviderError> {
825 if let Some(native) = request.algorithm.native_algorithm() {
828 return self
829 .backend
830 .create_named_pqc_key(request.backend_path, native)
831 .await
832 .map_err(ProviderError::Backend);
833 }
834 let key_type = key_type_for_signature(request.algorithm).ok_or_else(|| {
835 unsupported(
836 self.provider_id(),
837 "generate_key",
838 signature_algorithm_name(request.algorithm),
839 )
840 })?;
841 self.backend
842 .create_named_key(request.backend_path, key_type)
843 .await
844 .map_err(ProviderError::Backend)
845 }
846
847 async fn generate_sealing_key(
848 &self,
849 request: GenerateSealingKey<'_>,
850 ) -> Result<NewKey, ProviderError> {
851 Err(unsupported(
854 self.provider_id(),
855 "generate_sealing_key",
856 kem_algorithm_name(request.algorithm),
857 ))
858 }
859
860 async fn import_key(&self, request: ImportKey<'_>) -> Result<NewKey, ProviderError> {
861 let key_type = key_type_for_signature(request.algorithm).ok_or_else(|| {
862 unsupported(
863 self.provider_id(),
864 "import_key",
865 signature_algorithm_name(request.algorithm),
866 )
867 })?;
868 self.backend
869 .import(request.backend_path, key_type, request.material)
870 .await
871 .map_err(ProviderError::Backend)
872 }
873
874 async fn sign(&self, request: SignRequest<'_>) -> Result<Vec<u8>, ProviderError> {
875 if let Some(native) = request.algorithm.native_algorithm() {
877 return self
878 .backend
879 .sign_pqc(request.backend_path, request.message, native)
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 "sign",
887 signature_algorithm_name(request.algorithm),
888 )
889 })?;
890 self.backend
891 .sign_with_options(request.backend_path, request.message, options)
892 .await
893 .map_err(ProviderError::Backend)
894 }
895
896 async fn verify(&self, request: VerifyRequest<'_>) -> Result<bool, ProviderError> {
897 if let Some(native) = request.algorithm.native_algorithm() {
899 return self
900 .backend
901 .verify_pqc(
902 request.backend_path,
903 request.message,
904 request.signature,
905 native,
906 )
907 .await
908 .map_err(ProviderError::Backend);
909 }
910 let options = sign_options(request.algorithm).ok_or_else(|| {
911 unsupported(
912 self.provider_id(),
913 "verify",
914 signature_algorithm_name(request.algorithm),
915 )
916 })?;
917 self.backend
918 .verify_with_options(
919 request.backend_path,
920 request.message,
921 request.signature,
922 options,
923 )
924 .await
925 .map_err(ProviderError::Backend)
926 }
927
928 async fn encapsulate(
929 &self,
930 request: EncapsulateRequest<'_>,
931 ) -> Result<Encapsulation, ProviderError> {
932 Err(unsupported(
933 self.provider_id(),
934 "encapsulate",
935 kem_algorithm_name(request.algorithm),
936 ))
937 }
938
939 async fn decapsulate(
940 &self,
941 request: DecapsulateRequest<'_>,
942 ) -> Result<Zeroizing<Vec<u8>>, ProviderError> {
943 Err(unsupported(
944 self.provider_id(),
945 "decapsulate",
946 kem_algorithm_name(request.algorithm),
947 ))
948 }
949
950 async fn wrap_envelope(
951 &self,
952 request: WrapEnvelopeRequest<'_>,
953 ) -> Result<Envelope, ProviderError> {
954 Err(unsupported(
955 self.provider_id(),
956 "wrap_envelope",
957 kem_algorithm_name(request.kem_algorithm),
958 ))
959 }
960
961 async fn unwrap_envelope(
962 &self,
963 request: UnwrapEnvelopeRequest<'_>,
964 ) -> Result<Vec<u8>, ProviderError> {
965 Err(unsupported(
966 self.provider_id(),
967 "unwrap_envelope",
968 kem_algorithm_name(request.kem_algorithm),
969 ))
970 }
971}
972
973pub struct LocalSoftwareProvider<'a> {
989 backend: &'a dyn Backend,
990 provider_version: &'static str,
991}
992impl<'a> LocalSoftwareProvider<'a> {
993 pub const PROVIDER_VERSION: &'static str = "1";
997
998 #[must_use]
1000 pub const fn new(backend: &'a dyn Backend) -> Self {
1001 Self {
1002 backend,
1003 provider_version: Self::PROVIDER_VERSION,
1004 }
1005 }
1006
1007 async fn material(
1014 &self,
1015 key_id: &str,
1016 backend_path: &str,
1017 storage_key: Option<&str>,
1018 op: &'static str,
1019 algorithm: &'static str,
1020 ) -> Result<LocalSoftwareMaterial, ProviderError> {
1021 let storage_key = storage_key.ok_or_else(|| {
1022 crypto_failed(
1023 CryptoProviderId::LocalSoftware,
1024 op,
1025 algorithm,
1026 "software-custody key is missing its storage AEAD key",
1027 )
1028 })?;
1029 let kv = self.backend.kv_get(backend_path, None).await?;
1030 SoftwareCustodyKeyRecord::parse_for_local_software(
1031 &kv.value,
1032 key_id,
1033 algorithm,
1034 self.provider_version,
1035 kv.version,
1036 storage_key,
1037 )
1038 .map_err(|_| {
1039 crypto_failed(
1040 CryptoProviderId::LocalSoftware,
1041 op,
1042 algorithm,
1043 "malformed software-custody record",
1044 )
1045 })
1046 }
1047
1048 async fn materialize_seed(
1052 &self,
1053 material: &LocalSoftwareMaterial,
1054 ) -> Result<Zeroizing<Vec<u8>>, ProviderError> {
1055 let plaintext = self
1056 .backend
1057 .decrypt(
1058 &material.storage_key,
1059 &material.ciphertext,
1060 Some(&material.aad),
1061 )
1062 .await?;
1063 Ok(Zeroizing::new(plaintext))
1064 }
1065
1066 pub(crate) async fn public_key(
1071 &self,
1072 key_id: &str,
1073 backend_path: &str,
1074 storage_key: Option<&str>,
1075 algorithm: &'static str,
1076 ) -> Result<Vec<u8>, ProviderError> {
1077 let material = self
1078 .material(key_id, backend_path, storage_key, "get_public_key", algorithm)
1079 .await?;
1080 Ok(material.public_key)
1081 }
1082
1083 async fn seal_and_store(
1089 &self,
1090 params: SealParams<'_>,
1091 seed: &[u8],
1092 public_key: Vec<u8>,
1093 ) -> Result<NewKey, ProviderError> {
1094 const RECORD_VERSION: u32 = 1;
1097
1098 let custody = SoftwareCustodyCatalog {
1099 key_id: params.key_id,
1100 algorithm: params.algorithm_token,
1101 provider: CryptoProviderId::LocalSoftware.token(),
1102 provider_version: self.provider_version,
1103 custody: CustodyMode::SoftwareEncrypted.token(),
1104 storage_key: params.storage_key,
1105 };
1106 let aad = custody.aad(RECORD_VERSION);
1107 let envelope = self
1108 .backend
1109 .encrypt(
1110 params.storage_key,
1111 AeadAlgorithm::Aes256Gcm,
1112 seed,
1113 Some(&aad),
1114 )
1115 .await?;
1116 let record = SoftwareCustodyKeyRecord {
1117 schema_version: SoftwareCustodyKeyRecord::SCHEMA_VERSION,
1118 key_id: params.key_id.to_string(),
1119 key_version: RECORD_VERSION,
1120 public_key: B64.encode(&public_key),
1121 algorithm: params.algorithm_token.to_string(),
1122 provider: CryptoProviderId::LocalSoftware.token().to_string(),
1123 provider_version: self.provider_version.to_string(),
1124 custody: CustodyMode::SoftwareEncrypted.token().to_string(),
1125 encrypted_private_key: EncryptedPrivateKey {
1126 wrapping_key: params.storage_key.to_string(),
1127 algorithm: aead_token(envelope.alg).to_string(),
1128 key_version: envelope.key_version,
1129 nonce: B64.encode(&envelope.nonce),
1130 ciphertext: B64.encode(&envelope.ciphertext),
1131 },
1132 };
1133 let bytes = serde_json::to_vec(&record).map_err(|_| {
1134 crypto_failed(
1135 CryptoProviderId::LocalSoftware,
1136 params.op,
1137 params.algorithm_token,
1138 "software-custody record serialization failed",
1139 )
1140 })?;
1141 self.backend.kv_put(params.backend_path, &bytes).await?;
1142 Ok(NewKey {
1143 key_id: params.key_id.to_string(),
1144 public_key,
1145 })
1146 }
1147}
1148
1149struct SealParams<'a> {
1152 op: &'static str,
1154 key_id: &'a str,
1156 backend_path: &'a str,
1158 algorithm_token: &'static str,
1160 storage_key: &'a str,
1162}
1163#[async_trait]
1164impl CryptoProvider for LocalSoftwareProvider<'_> {
1165 fn provider_id(&self) -> CryptoProviderId {
1166 CryptoProviderId::LocalSoftware
1167 }
1168
1169 async fn generate_key(&self, request: GenerateKey<'_>) -> Result<NewKey, ProviderError> {
1170 use rand::RngCore as _;
1171 use rand::rngs::OsRng;
1172
1173 let algorithm = ml_dsa_algorithm(request.algorithm).ok_or_else(|| {
1174 unsupported(
1175 CryptoProviderId::LocalSoftware,
1176 "generate_key",
1177 signature_algorithm_name(request.algorithm),
1178 )
1179 })?;
1180 let algorithm_token = algorithm.token();
1181 let storage_key = request.storage_key.ok_or_else(|| {
1182 crypto_failed(
1183 CryptoProviderId::LocalSoftware,
1184 "generate_key",
1185 algorithm_token,
1186 "software-custody key is missing its storage AEAD key",
1187 )
1188 })?;
1189
1190 let mut seed = Zeroizing::new([0u8; ml_dsa_sign::SEED_LEN]);
1193 OsRng.fill_bytes(seed.as_mut_slice());
1194 let public_key =
1195 ml_dsa_sign::public_from_seed(algorithm, seed.as_slice()).map_err(|_| {
1196 crypto_failed(
1197 CryptoProviderId::LocalSoftware,
1198 "generate_key",
1199 algorithm_token,
1200 "ml-dsa key generation failed",
1201 )
1202 })?;
1203
1204 self.seal_and_store(
1205 SealParams {
1206 op: "generate_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 generate_sealing_key(
1219 &self,
1220 request: GenerateSealingKey<'_>,
1221 ) -> Result<NewKey, ProviderError> {
1222 use rand::RngCore as _;
1223 use rand::rngs::OsRng;
1224
1225 let kem = request.algorithm;
1226 let algorithm_token = kem.token();
1227 let storage_key = request.storage_key.ok_or_else(|| {
1228 crypto_failed(
1229 CryptoProviderId::LocalSoftware,
1230 "generate_sealing_key",
1231 algorithm_token,
1232 "software-custody key is missing its storage AEAD key",
1233 )
1234 })?;
1235
1236 let mut seed = Zeroizing::new([0u8; ml_kem_envelope::SEED_LEN]);
1240 OsRng.fill_bytes(seed.as_mut_slice());
1241 let public_key = ml_kem_envelope::public_from_seed(seed.as_slice(), kem_to_core(kem))
1242 .map_err(|_| {
1243 crypto_failed(
1244 CryptoProviderId::LocalSoftware,
1245 "generate_sealing_key",
1246 algorithm_token,
1247 "ml-kem key generation failed",
1248 )
1249 })?;
1250
1251 self.seal_and_store(
1252 SealParams {
1253 op: "generate_sealing_key",
1254 key_id: request.key_id,
1255 backend_path: request.backend_path,
1256 algorithm_token,
1257 storage_key,
1258 },
1259 seed.as_slice(),
1260 public_key,
1261 )
1262 .await
1263 }
1264
1265 async fn import_key(&self, request: ImportKey<'_>) -> Result<NewKey, ProviderError> {
1266 Err(unsupported(
1267 CryptoProviderId::LocalSoftware,
1268 "import_key",
1269 signature_algorithm_name(request.algorithm),
1270 ))
1271 }
1272
1273 async fn sign(&self, request: SignRequest<'_>) -> Result<Vec<u8>, ProviderError> {
1274 let algorithm = ml_dsa_algorithm(request.algorithm).ok_or_else(|| {
1275 unsupported(
1276 CryptoProviderId::LocalSoftware,
1277 "sign",
1278 signature_algorithm_name(request.algorithm),
1279 )
1280 })?;
1281 let material = self
1282 .material(
1283 request.key_id,
1284 request.backend_path,
1285 request.storage_key,
1286 "sign",
1287 algorithm.token(),
1288 )
1289 .await?;
1290 let seed = self.materialize_seed(&material).await?;
1291 ml_dsa_sign::sign(algorithm, &seed, request.message).map_err(|_| {
1292 crypto_failed(
1293 CryptoProviderId::LocalSoftware,
1294 "sign",
1295 algorithm.token(),
1296 "ml-dsa signing failed",
1297 )
1298 })
1299 }
1300
1301 async fn verify(&self, request: VerifyRequest<'_>) -> Result<bool, ProviderError> {
1302 let algorithm = ml_dsa_algorithm(request.algorithm).ok_or_else(|| {
1303 unsupported(
1304 CryptoProviderId::LocalSoftware,
1305 "verify",
1306 signature_algorithm_name(request.algorithm),
1307 )
1308 })?;
1309 let material = self
1311 .material(
1312 request.key_id,
1313 request.backend_path,
1314 request.storage_key,
1315 "verify",
1316 algorithm.token(),
1317 )
1318 .await?;
1319 ml_dsa_sign::verify(
1320 algorithm,
1321 &material.public_key,
1322 request.message,
1323 request.signature,
1324 )
1325 .map_err(|_| {
1326 crypto_failed(
1327 CryptoProviderId::LocalSoftware,
1328 "verify",
1329 algorithm.token(),
1330 "malformed verification input",
1331 )
1332 })
1333 }
1334
1335 async fn encapsulate(
1336 &self,
1337 request: EncapsulateRequest<'_>,
1338 ) -> Result<Encapsulation, ProviderError> {
1339 let material = self
1340 .material(
1341 request.key_id,
1342 request.backend_path,
1343 request.storage_key,
1344 "encapsulate",
1345 kem_algorithm_name(request.algorithm),
1346 )
1347 .await?;
1348 let seed = self.materialize_seed(&material).await?;
1349 let (encapsulated_key, shared_secret) =
1350 ml_kem_envelope::encapsulate(&seed, kem_to_core(request.algorithm)).map_err(|_| {
1351 crypto_failed(
1352 CryptoProviderId::LocalSoftware,
1353 "encapsulate",
1354 kem_algorithm_name(request.algorithm),
1355 "ml-kem encapsulation failed",
1356 )
1357 })?;
1358 Ok(Encapsulation {
1359 encapsulated_key,
1360 shared_secret,
1361 })
1362 }
1363
1364 async fn decapsulate(
1365 &self,
1366 request: DecapsulateRequest<'_>,
1367 ) -> Result<Zeroizing<Vec<u8>>, ProviderError> {
1368 let material = self
1369 .material(
1370 request.key_id,
1371 request.backend_path,
1372 request.storage_key,
1373 "decapsulate",
1374 kem_algorithm_name(request.algorithm),
1375 )
1376 .await?;
1377 let seed = self.materialize_seed(&material).await?;
1378 ml_kem_envelope::decapsulate(
1379 &seed,
1380 kem_to_core(request.algorithm),
1381 request.encapsulated_key,
1382 )
1383 .map_err(|_| {
1384 crypto_failed(
1385 CryptoProviderId::LocalSoftware,
1386 "decapsulate",
1387 kem_algorithm_name(request.algorithm),
1388 "ml-kem decapsulation failed",
1389 )
1390 })
1391 }
1392
1393 async fn wrap_envelope(
1394 &self,
1395 request: WrapEnvelopeRequest<'_>,
1396 ) -> Result<Envelope, ProviderError> {
1397 let material = self
1398 .material(
1399 request.key_id,
1400 request.backend_path,
1401 request.storage_key,
1402 "wrap_envelope",
1403 kem_algorithm_name(request.kem_algorithm),
1404 )
1405 .await?;
1406 let seed = self.materialize_seed(&material).await?;
1407 let sealed = ml_kem_envelope::seal(
1408 &seed,
1409 kem_to_core(request.kem_algorithm),
1410 envelope_to_core(request.envelope_algorithm),
1411 request.plaintext,
1412 request.aad.unwrap_or_default(),
1413 )
1414 .map_err(|_| {
1415 crypto_failed(
1416 CryptoProviderId::LocalSoftware,
1417 "wrap_envelope",
1418 kem_algorithm_name(request.kem_algorithm),
1419 "ml-kem envelope seal failed",
1420 )
1421 })?;
1422 Ok(Envelope {
1423 kem_algorithm: request.kem_algorithm,
1424 envelope_algorithm: request.envelope_algorithm,
1425 key_version: material.key_version,
1426 encapsulated_key: sealed.encapsulated_key,
1427 nonce: sealed.nonce.to_vec(),
1428 ciphertext: sealed.ciphertext,
1429 })
1430 }
1431
1432 async fn unwrap_envelope(
1433 &self,
1434 request: UnwrapEnvelopeRequest<'_>,
1435 ) -> Result<Vec<u8>, ProviderError> {
1436 let material = self
1437 .material(
1438 request.key_id,
1439 request.backend_path,
1440 request.storage_key,
1441 "unwrap_envelope",
1442 kem_algorithm_name(request.kem_algorithm),
1443 )
1444 .await?;
1445 let seed = self.materialize_seed(&material).await?;
1446 let envelope = ml_kem_envelope::envelope_from_parts(
1447 kem_to_core(request.kem_algorithm),
1448 envelope_to_core(request.envelope_algorithm),
1449 request.encapsulated_key,
1450 request.nonce,
1451 request.ciphertext,
1452 )
1453 .map_err(|_| {
1454 crypto_failed(
1455 CryptoProviderId::LocalSoftware,
1456 "unwrap_envelope",
1457 kem_algorithm_name(request.kem_algorithm),
1458 "malformed ml-kem envelope",
1459 )
1460 })?;
1461 let plaintext = ml_kem_envelope::open(&seed, &envelope, request.aad.unwrap_or_default())
1462 .map_err(|_| {
1463 crypto_failed(
1464 CryptoProviderId::LocalSoftware,
1465 "unwrap_envelope",
1466 kem_algorithm_name(request.kem_algorithm),
1467 "ml-kem envelope open failed",
1468 )
1469 })?;
1470 Ok(plaintext.to_vec())
1471 }
1472}
1473const fn ml_dsa_algorithm(algorithm: SignatureAlgorithm) -> Option<ml_dsa_sign::MlDsaAlgorithm> {
1474 match algorithm {
1475 SignatureAlgorithm::MlDsa44 => Some(ml_dsa_sign::MlDsaAlgorithm::MlDsa44),
1476 SignatureAlgorithm::MlDsa65 => Some(ml_dsa_sign::MlDsaAlgorithm::MlDsa65),
1477 SignatureAlgorithm::MlDsa87 => Some(ml_dsa_sign::MlDsaAlgorithm::MlDsa87),
1478 SignatureAlgorithm::Ed25519
1479 | SignatureAlgorithm::Ed25519Nkey
1480 | SignatureAlgorithm::Rs256
1481 | SignatureAlgorithm::Es256 => None,
1482 }
1483}
1484const fn kem_to_core(algorithm: KemAlgorithm) -> ml_kem_envelope::KemAlgorithm {
1485 match algorithm {
1486 KemAlgorithm::MlKem512 => ml_kem_envelope::KemAlgorithm::MlKem512,
1487 KemAlgorithm::MlKem768 => ml_kem_envelope::KemAlgorithm::MlKem768,
1488 KemAlgorithm::MlKem1024 => ml_kem_envelope::KemAlgorithm::MlKem1024,
1489 }
1490}
1491const fn envelope_to_core(algorithm: EnvelopeAlgorithm) -> ml_kem_envelope::EnvelopeAlgorithm {
1492 match algorithm {
1493 EnvelopeAlgorithm::Aes256Gcm => ml_kem_envelope::EnvelopeAlgorithm::Aes256Gcm,
1494 EnvelopeAlgorithm::ChaCha20Poly1305 => ml_kem_envelope::EnvelopeAlgorithm::ChaCha20Poly1305,
1495 }
1496}
1497
1498pub fn select_provider(
1503 metadata: ProviderMetadata,
1504 backend_native_supported: bool,
1505 local_software_allowed: bool,
1506 op: &'static str,
1507) -> Result<CryptoProviderId, ProviderError> {
1508 match metadata.policy {
1509 ProviderPolicy::BackendRequired => select_backend_required(backend_native_supported, op),
1510 ProviderPolicy::BackendPreferred => select_backend_preferred(
1511 metadata.custody,
1512 backend_native_supported,
1513 local_software_allowed,
1514 op,
1515 ),
1516 ProviderPolicy::LocalSoftware => {
1517 select_local_software(metadata.custody, local_software_allowed, op)
1518 }
1519 }
1520}
1521
1522const fn select_backend_required(
1523 backend_native_supported: bool,
1524 op: &'static str,
1525) -> Result<CryptoProviderId, ProviderError> {
1526 if backend_native_supported {
1527 return Ok(CryptoProviderId::VaultTransit);
1528 }
1529 Err(unsupported(
1530 CryptoProviderId::VaultTransit,
1531 op,
1532 "backend-native",
1533 ))
1534}
1535
1536fn select_backend_preferred(
1537 custody: Option<CustodyMode>,
1538 backend_native_supported: bool,
1539 local_software_allowed: bool,
1540 op: &'static str,
1541) -> Result<CryptoProviderId, ProviderError> {
1542 if custody == Some(CustodyMode::SoftwareEncrypted) {
1550 require_local_software_allowed(local_software_allowed, op)?;
1551 return Ok(CryptoProviderId::LocalSoftware);
1552 }
1553 if backend_native_supported {
1554 return Ok(CryptoProviderId::VaultTransit);
1555 }
1556 require_local_software_allowed(local_software_allowed, op)?;
1557 require_software_custody(
1558 custody,
1559 op,
1560 "local software fallback requires software-encrypted custody",
1561 )?;
1562 Ok(CryptoProviderId::LocalSoftware)
1563}
1564
1565fn select_local_software(
1566 custody: Option<CustodyMode>,
1567 local_software_allowed: bool,
1568 op: &'static str,
1569) -> Result<CryptoProviderId, ProviderError> {
1570 require_local_software_allowed(local_software_allowed, op)?;
1571 require_software_custody(
1572 custody,
1573 op,
1574 "local software provider requires software-encrypted custody",
1575 )?;
1576 Ok(CryptoProviderId::LocalSoftware)
1577}
1578
1579const fn require_local_software_allowed(
1580 local_software_allowed: bool,
1581 op: &'static str,
1582) -> Result<(), ProviderError> {
1583 if local_software_allowed {
1584 return Ok(());
1585 }
1586 Err(ProviderError::PolicyDenied {
1587 op,
1588 reason: "local software custody requires caller policy grant",
1589 })
1590}
1591
1592fn require_software_custody(
1593 custody: Option<CustodyMode>,
1594 op: &'static str,
1595 reason: &'static str,
1596) -> Result<(), ProviderError> {
1597 if custody == Some(CustodyMode::SoftwareEncrypted) {
1598 return Ok(());
1599 }
1600 Err(ProviderError::PolicyDenied { op, reason })
1601}
1602
1603const fn key_type_for_signature(algorithm: SignatureAlgorithm) -> Option<KeyType> {
1604 match algorithm {
1605 SignatureAlgorithm::Ed25519 => Some(KeyType::Ed25519),
1606 SignatureAlgorithm::Ed25519Nkey => Some(KeyType::Ed25519Nkey),
1607 SignatureAlgorithm::Rs256 => Some(KeyType::Rsa2048),
1608 SignatureAlgorithm::Es256 => Some(KeyType::EcdsaP256),
1609 SignatureAlgorithm::MlDsa44 | SignatureAlgorithm::MlDsa65 | SignatureAlgorithm::MlDsa87 => {
1610 None
1611 }
1612 }
1613}
1614
1615const fn sign_options(algorithm: SignatureAlgorithm) -> Option<SignOptions> {
1616 match algorithm {
1617 SignatureAlgorithm::Ed25519 | SignatureAlgorithm::Ed25519Nkey => Some(SignOptions::Default),
1618 SignatureAlgorithm::Rs256 => Some(SignOptions::Rs256Pkcs1v15Sha256),
1619 SignatureAlgorithm::Es256 => Some(SignOptions::Es256),
1620 SignatureAlgorithm::MlDsa44 | SignatureAlgorithm::MlDsa65 | SignatureAlgorithm::MlDsa87 => {
1621 None
1622 }
1623 }
1624}
1625
1626const fn signature_algorithm_name(algorithm: SignatureAlgorithm) -> &'static str {
1627 match algorithm {
1628 SignatureAlgorithm::Ed25519 => "ed25519",
1629 SignatureAlgorithm::Ed25519Nkey => "ed25519-nkey",
1630 SignatureAlgorithm::Rs256 => "rs256",
1631 SignatureAlgorithm::Es256 => "es256",
1632 SignatureAlgorithm::MlDsa44 => "ml-dsa-44",
1633 SignatureAlgorithm::MlDsa65 => "ml-dsa-65",
1634 SignatureAlgorithm::MlDsa87 => "ml-dsa-87",
1635 }
1636}
1637
1638const fn kem_algorithm_name(algorithm: KemAlgorithm) -> &'static str {
1639 algorithm.token()
1640}
1641
1642const fn unsupported(
1643 provider: CryptoProviderId,
1644 op: &'static str,
1645 algorithm: &'static str,
1646) -> ProviderError {
1647 ProviderError::Unsupported {
1648 provider,
1649 op,
1650 algorithm,
1651 }
1652}
1653const fn crypto_failed(
1654 provider: CryptoProviderId,
1655 op: &'static str,
1656 algorithm: &'static str,
1657 reason: &'static str,
1658) -> ProviderError {
1659 ProviderError::CryptoFailed {
1660 provider,
1661 op,
1662 algorithm,
1663 reason,
1664 }
1665}
1666
1667const fn aead_token(alg: AeadAlgorithm) -> &'static str {
1670 match alg {
1671 AeadAlgorithm::Aes256Gcm => "aes-256-gcm",
1672 AeadAlgorithm::Chacha20Poly1305 => "chacha20-poly1305",
1673 }
1674}
1675
1676#[cfg(test)]
1677mod tests {
1678 use async_trait::async_trait;
1679
1680 use super::*;
1681 use crate::backend::{BackendError, NewKey};
1682 use crate::catalog::schema::Labels;
1683
1684 struct RecordingBackend;
1685
1686 #[async_trait]
1687 impl Backend for RecordingBackend {
1688 fn kind(&self) -> &'static str {
1689 "recording"
1690 }
1691
1692 async fn new_key(&self, key_type: KeyType) -> Result<NewKey, BackendError> {
1693 let _ = key_type;
1694 Err(BackendError::Unsupported("new_key"))
1695 }
1696
1697 async fn create_named_key(
1698 &self,
1699 key_id: &str,
1700 key_type: KeyType,
1701 ) -> Result<NewKey, BackendError> {
1702 Ok(NewKey {
1703 key_id: format!("{key_id}:{key_type}"),
1704 public_key: vec![1, 2, 3],
1705 })
1706 }
1707
1708 async fn public_key(&self, key_id: &str) -> Result<Vec<u8>, BackendError> {
1709 let _ = key_id;
1710 Ok(vec![1, 2, 3])
1711 }
1712
1713 async fn sign(&self, key_id: &str, message: &[u8]) -> Result<Vec<u8>, BackendError> {
1714 Ok([key_id.as_bytes(), message].concat())
1715 }
1716
1717 async fn sign_with_options(
1718 &self,
1719 key_id: &str,
1720 message: &[u8],
1721 options: SignOptions,
1722 ) -> Result<Vec<u8>, BackendError> {
1723 let mut out = self.sign(key_id, message).await?;
1724 if options == SignOptions::Rs256Pkcs1v15Sha256 {
1727 out.extend_from_slice(b":rs256");
1728 } else if options == SignOptions::Es256 {
1729 out.extend_from_slice(b":es256");
1730 }
1731 Ok(out)
1732 }
1733
1734 async fn verify(
1735 &self,
1736 key_id: &str,
1737 message: &[u8],
1738 signature: &[u8],
1739 ) -> Result<bool, BackendError> {
1740 Ok(signature == [key_id.as_bytes(), message].concat())
1743 }
1744 }
1745
1746 fn key_entry_with_labels(labels: &[&str]) -> KeyEntry {
1747 KeyEntry {
1748 class: crate::catalog::schema::Class::Asymmetric,
1749 key_type: Some(crate::catalog::schema::KeyAlgorithm::Ed25519),
1750 backend: "bao".to_string(),
1751 engine: None,
1752 path: "issuer".to_string(),
1753 public_path: None,
1754 writable: true,
1755 missing: crate::catalog::schema::MissingPolicy::Error,
1756 generate: None,
1757 sealing_pin: None,
1758 labels: Labels(labels.iter().map(ToString::to_string).collect()),
1759 description: "issuer".to_string(),
1760 }
1761 }
1762
1763 #[test]
1764 fn provider_metadata_defaults_to_backend_required() {
1765 let entry = key_entry_with_labels(&[]);
1766 assert_eq!(
1767 ProviderMetadata::from_key(&entry),
1768 ProviderMetadata {
1769 provider: None,
1770 policy: ProviderPolicy::BackendRequired,
1771 custody: None
1772 }
1773 );
1774 }
1775
1776 #[test]
1777 fn provider_metadata_parses_reserved_labels() {
1778 let entry = key_entry_with_labels(&[
1779 "crypto_provider=local-software",
1780 "crypto_provider_policy=backend-preferred",
1781 "pqc_custody=software-encrypted",
1782 ]);
1783 assert_eq!(
1784 ProviderMetadata::from_key(&entry),
1785 ProviderMetadata {
1786 provider: Some(CryptoProviderId::LocalSoftware),
1787 policy: ProviderPolicy::BackendPreferred,
1788 custody: Some(CustodyMode::SoftwareEncrypted)
1789 }
1790 );
1791 }
1792
1793 #[test]
1794 fn provider_selection_honors_backend_required() {
1795 let selected = select_provider(
1796 ProviderMetadata {
1797 provider: None,
1798 policy: ProviderPolicy::BackendRequired,
1799 custody: None,
1800 },
1801 true,
1802 false,
1803 "sign",
1804 )
1805 .expect("backend-native selected");
1806 assert_eq!(selected, CryptoProviderId::VaultTransit);
1807
1808 assert!(matches!(
1809 select_provider(
1810 ProviderMetadata {
1811 provider: None,
1812 policy: ProviderPolicy::BackendRequired,
1813 custody: None,
1814 },
1815 false,
1816 false,
1817 "sign",
1818 ),
1819 Err(ProviderError::Unsupported { .. })
1820 ));
1821 }
1822
1823 #[test]
1824 fn provider_selection_honors_local_software_custody_gate() {
1825 let denied = select_provider(
1826 ProviderMetadata {
1827 provider: None,
1828 policy: ProviderPolicy::BackendPreferred,
1829 custody: Some(CustodyMode::BackendNative),
1830 },
1831 false,
1832 true,
1833 "sign",
1834 )
1835 .expect_err("backend fallback needs software custody");
1836 assert!(matches!(denied, ProviderError::PolicyDenied { .. }));
1837
1838 let denied = select_provider(
1839 ProviderMetadata {
1840 provider: None,
1841 policy: ProviderPolicy::BackendPreferred,
1842 custody: Some(CustodyMode::SoftwareEncrypted),
1843 },
1844 false,
1845 false,
1846 "sign",
1847 )
1848 .expect_err("local software needs caller policy");
1849 assert!(matches!(
1850 denied,
1851 ProviderError::PolicyDenied {
1852 reason: "local software custody requires caller policy grant",
1853 ..
1854 }
1855 ));
1856
1857 let selected = select_provider(
1858 ProviderMetadata {
1859 provider: None,
1860 policy: ProviderPolicy::BackendPreferred,
1861 custody: Some(CustodyMode::SoftwareEncrypted),
1862 },
1863 false,
1864 true,
1865 "sign",
1866 )
1867 .expect("local fallback selected");
1868 assert_eq!(selected, CryptoProviderId::LocalSoftware);
1869 }
1870
1871 #[test]
1872 fn backend_preferred_software_custody_pins_despite_native_support() {
1873 let pinned = select_provider(
1878 ProviderMetadata {
1879 provider: None,
1880 policy: ProviderPolicy::BackendPreferred,
1881 custody: Some(CustodyMode::SoftwareEncrypted),
1882 },
1883 true,
1884 true,
1885 "sign",
1886 )
1887 .expect("software-custodied key stays local-software");
1888 assert_eq!(pinned, CryptoProviderId::LocalSoftware);
1889
1890 let native = select_provider(
1893 ProviderMetadata {
1894 provider: None,
1895 policy: ProviderPolicy::BackendPreferred,
1896 custody: None,
1897 },
1898 true,
1899 true,
1900 "sign",
1901 )
1902 .expect("no-custody preferred key routes to native");
1903 assert_eq!(native, CryptoProviderId::VaultTransit);
1904
1905 let denied = select_provider(
1908 ProviderMetadata {
1909 provider: None,
1910 policy: ProviderPolicy::BackendPreferred,
1911 custody: Some(CustodyMode::SoftwareEncrypted),
1912 },
1913 true,
1914 false,
1915 "sign",
1916 )
1917 .expect_err("local software still needs the caller grant");
1918 assert!(matches!(denied, ProviderError::PolicyDenied { .. }));
1919 }
1920
1921 #[test]
1922 fn provider_selection_honors_local_software_policy_mode() {
1923 let selected = select_provider(
1924 ProviderMetadata {
1925 provider: Some(CryptoProviderId::LocalSoftware),
1926 policy: ProviderPolicy::LocalSoftware,
1927 custody: Some(CustodyMode::SoftwareEncrypted),
1928 },
1929 true,
1930 true,
1931 "decapsulate",
1932 )
1933 .expect("explicit local software selected");
1934 assert_eq!(selected, CryptoProviderId::LocalSoftware);
1935
1936 let denied = select_provider(
1937 ProviderMetadata {
1938 provider: Some(CryptoProviderId::LocalSoftware),
1939 policy: ProviderPolicy::LocalSoftware,
1940 custody: Some(CustodyMode::SoftwareEncrypted),
1941 },
1942 true,
1943 false,
1944 "decapsulate",
1945 )
1946 .expect_err("caller grant required");
1947 assert!(matches!(
1948 denied,
1949 ProviderError::PolicyDenied {
1950 reason: "local software custody requires caller policy grant",
1951 ..
1952 }
1953 ));
1954 }
1955
1956 #[test]
1957 fn provider_audit_event_is_secret_free() {
1958 let event = ProviderAuditEvent {
1959 op: "decapsulate",
1960 key_id: "pqc.kem",
1961 key_version: Some(7),
1962 algorithm: "ml-kem-768",
1963 provider: CryptoProviderId::LocalSoftware,
1964 custody: CustodyMode::SoftwareEncrypted,
1965 caller_uid: 9100,
1966 outcome: ProviderAuditOutcome::Success,
1967 reason: "ok",
1968 };
1969 let value = event.to_json_value();
1970 assert_eq!(value["event"]["kind"], "basil.audit.provider_operation");
1971 assert_eq!(value["event"]["version"], 1);
1972 assert_eq!(value["actor"]["kind"], "unix_uid");
1973 assert_eq!(value["actor"]["id"], "9100");
1974 assert_eq!(value["target"]["kind"], "catalog_key");
1975 assert_eq!(value["target"]["id"], "pqc.kem");
1976 assert_eq!(value["target"]["version"], 7);
1977 assert!(value["occurred_at"].as_str().is_some());
1978 assert_eq!(value["op"], "decapsulate");
1979 assert_eq!(value["key_version"], 7);
1980 assert_eq!(value["algorithm"], "ml-kem-768");
1981 assert_eq!(value["provider"], "local-software");
1982 assert_eq!(value["custody"], "software-encrypted");
1983 assert_eq!(value["caller_uid"], 9100);
1984 assert_eq!(value["outcome"], "success");
1985 assert!(value.get("private_key").is_none());
1986 assert!(value.get("plaintext").is_none());
1987 assert!(value.get("ciphertext").is_none());
1988 assert!(value.get("signature").is_none());
1989 assert!(value.get("shared_secret").is_none());
1990 }
1991
1992 #[tokio::test]
1993 async fn backend_provider_delegates_legacy_signing() {
1994 let backend = RecordingBackend;
1995 let provider = BackendCryptoProvider::new(&backend);
1996 let signature = provider
1997 .sign(SignRequest {
1998 key_id: "catalog.issuer",
1999 backend_path: "issuer",
2000 algorithm: SignatureAlgorithm::Rs256,
2001 message: b"digest",
2002 storage_key: None,
2003 })
2004 .await
2005 .expect("signs");
2006 assert_eq!(signature, b"issuerdigest:rs256");
2007 }
2008
2009 #[tokio::test]
2010 async fn backend_provider_rejects_pqc_without_native_support() {
2011 let backend = RecordingBackend;
2016 let provider = BackendCryptoProvider::new(&backend);
2017 assert!(!backend.supports_native_algorithm(NativeAlgorithm::MlDsa65));
2018 let err = provider
2019 .sign(SignRequest {
2020 key_id: "catalog.issuer",
2021 backend_path: "issuer",
2022 algorithm: SignatureAlgorithm::MlDsa65,
2023 message: b"digest",
2024 storage_key: None,
2025 })
2026 .await
2027 .expect_err("ML-DSA unsupported");
2028 assert!(matches!(
2029 err,
2030 ProviderError::Backend(BackendError::Unsupported("sign_pqc"))
2031 ));
2032 }
2033}
2034
2035#[cfg(test)]
2036mod pqc_provider_tests {
2037 use std::collections::HashMap;
2038
2039 use async_trait::async_trait;
2040 use basil_proto::{CiphertextEnvelope, KeyType};
2041
2042 use super::*;
2043 use crate::backend::{Backend, BackendError, KvValue, NewKey, SignOptions};
2044 use crate::core::{ml_dsa_sign, ml_kem_envelope};
2045
2046 const STORAGE_KEY: &str = "pqc-storage-aead";
2047 const KEY_ID: &str = "pqc.example";
2048 const PATH: &str = "kv/pqc/example";
2049
2050 struct CustodyBackend {
2054 records: HashMap<String, Vec<u8>>,
2055 secrets: HashMap<String, Vec<u8>>,
2056 }
2057
2058 impl CustodyBackend {
2059 fn single(record: Vec<u8>, seed: &[u8]) -> Self {
2060 Self {
2061 records: HashMap::from([(PATH.to_string(), record)]),
2062 secrets: HashMap::from([(STORAGE_KEY.to_string(), seed.to_vec())]),
2063 }
2064 }
2065 }
2066
2067 #[async_trait]
2068 impl Backend for CustodyBackend {
2069 fn kind(&self) -> &'static str {
2070 "custody-test"
2071 }
2072
2073 async fn new_key(&self, _key_type: KeyType) -> Result<NewKey, BackendError> {
2074 Err(BackendError::Unsupported("new_key"))
2075 }
2076
2077 async fn create_named_key(
2078 &self,
2079 _key_id: &str,
2080 _key_type: KeyType,
2081 ) -> Result<NewKey, BackendError> {
2082 Err(BackendError::Unsupported("create_named_key"))
2083 }
2084
2085 async fn public_key(&self, _key_id: &str) -> Result<Vec<u8>, BackendError> {
2086 Err(BackendError::Unsupported("public_key"))
2087 }
2088
2089 async fn sign(&self, _key_id: &str, _message: &[u8]) -> Result<Vec<u8>, BackendError> {
2090 Err(BackendError::Unsupported("sign"))
2091 }
2092
2093 async fn sign_with_options(
2094 &self,
2095 _key_id: &str,
2096 _message: &[u8],
2097 _options: SignOptions,
2098 ) -> Result<Vec<u8>, BackendError> {
2099 Err(BackendError::Unsupported("sign_with_options"))
2100 }
2101
2102 async fn verify(
2103 &self,
2104 _key_id: &str,
2105 _message: &[u8],
2106 _signature: &[u8],
2107 ) -> Result<bool, BackendError> {
2108 Err(BackendError::Unsupported("verify"))
2109 }
2110
2111 async fn kv_get(
2112 &self,
2113 key_id: &str,
2114 _version: Option<u32>,
2115 ) -> Result<KvValue, BackendError> {
2116 let value = self
2117 .records
2118 .get(key_id)
2119 .cloned()
2120 .ok_or(BackendError::Unsupported("kv_get"))?;
2121 Ok(KvValue { value, version: 1 })
2122 }
2123
2124 async fn decrypt(
2125 &self,
2126 key_id: &str,
2127 _envelope: &CiphertextEnvelope,
2128 _aad: Option<&[u8]>,
2129 ) -> Result<Vec<u8>, BackendError> {
2130 self.secrets
2131 .get(key_id)
2132 .cloned()
2133 .ok_or(BackendError::Unsupported("decrypt"))
2134 }
2135 }
2136
2137 fn custody_record(algorithm: &str, public_key: &[u8], provider_version: &str) -> Vec<u8> {
2139 let record = SoftwareCustodyKeyRecord {
2140 schema_version: SoftwareCustodyKeyRecord::SCHEMA_VERSION,
2141 key_id: KEY_ID.to_string(),
2142 key_version: 1,
2143 public_key: encode_record_bytes(public_key),
2144 algorithm: algorithm.to_string(),
2145 provider: CryptoProviderId::LocalSoftware.token().to_string(),
2146 provider_version: provider_version.to_string(),
2147 custody: CustodyMode::SoftwareEncrypted.token().to_string(),
2148 encrypted_private_key: EncryptedPrivateKey {
2149 wrapping_key: STORAGE_KEY.to_string(),
2150 algorithm: "aes-256-gcm".to_string(),
2151 key_version: 1,
2152 nonce: encode_record_bytes(&[0u8; 12]),
2153 ciphertext: encode_record_bytes(&[0u8; 16]),
2154 },
2155 };
2156 serde_json::to_vec(&record).expect("serialize record")
2157 }
2158
2159 const DSA_LEVELS: [(SignatureAlgorithm, ml_dsa_sign::MlDsaAlgorithm); 3] = [
2160 (
2161 SignatureAlgorithm::MlDsa44,
2162 ml_dsa_sign::MlDsaAlgorithm::MlDsa44,
2163 ),
2164 (
2165 SignatureAlgorithm::MlDsa65,
2166 ml_dsa_sign::MlDsaAlgorithm::MlDsa65,
2167 ),
2168 (
2169 SignatureAlgorithm::MlDsa87,
2170 ml_dsa_sign::MlDsaAlgorithm::MlDsa87,
2171 ),
2172 ];
2173
2174 const KEM_LEVELS: [KemAlgorithm; 3] = [
2175 KemAlgorithm::MlKem512,
2176 KemAlgorithm::MlKem768,
2177 KemAlgorithm::MlKem1024,
2178 ];
2179
2180 #[tokio::test]
2181 async fn signs_and_verifies_every_ml_dsa_level() {
2182 for (sig_algorithm, dsa_algorithm) in DSA_LEVELS {
2183 let seed = [0x11u8; ml_dsa_sign::SEED_LEN];
2184 let public = ml_dsa_sign::public_from_seed(dsa_algorithm, &seed).expect("public");
2185 let record = custody_record(
2186 dsa_algorithm.token(),
2187 &public,
2188 LocalSoftwareProvider::PROVIDER_VERSION,
2189 );
2190 let backend = CustodyBackend::single(record, &seed);
2191 let provider = LocalSoftwareProvider::new(&backend);
2192
2193 let signature = provider
2194 .sign(SignRequest {
2195 key_id: KEY_ID,
2196 backend_path: PATH,
2197 algorithm: sig_algorithm,
2198 message: b"basil pqc payload",
2199 storage_key: Some(STORAGE_KEY),
2200 })
2201 .await
2202 .expect("sign");
2203 assert!(
2204 provider
2205 .verify(VerifyRequest {
2206 key_id: KEY_ID,
2207 backend_path: PATH,
2208 algorithm: sig_algorithm,
2209 message: b"basil pqc payload",
2210 signature: &signature,
2211 storage_key: Some(STORAGE_KEY),
2212 })
2213 .await
2214 .expect("verify"),
2215 "{} verifies",
2216 dsa_algorithm.token()
2217 );
2218 assert!(
2219 !provider
2220 .verify(VerifyRequest {
2221 key_id: KEY_ID,
2222 backend_path: PATH,
2223 algorithm: sig_algorithm,
2224 message: b"tampered payload",
2225 signature: &signature,
2226 storage_key: Some(STORAGE_KEY),
2227 })
2228 .await
2229 .expect("verify"),
2230 "{} rejects wrong message",
2231 dsa_algorithm.token()
2232 );
2233 }
2234 }
2235
2236 #[tokio::test]
2237 async fn encapsulate_decapsulate_every_ml_kem_level() {
2238 for kem in KEM_LEVELS {
2239 let seed = [0x42u8; ml_kem_envelope::SEED_LEN];
2240 let record = custody_record(
2241 kem_algorithm_name(kem),
2242 &[7u8; 32],
2243 LocalSoftwareProvider::PROVIDER_VERSION,
2244 );
2245 let backend = CustodyBackend::single(record, &seed);
2246 let provider = LocalSoftwareProvider::new(&backend);
2247
2248 let encapsulation = provider
2249 .encapsulate(EncapsulateRequest {
2250 key_id: KEY_ID,
2251 backend_path: PATH,
2252 algorithm: kem,
2253 storage_key: Some(STORAGE_KEY),
2254 })
2255 .await
2256 .expect("encapsulate");
2257 let shared = provider
2258 .decapsulate(DecapsulateRequest {
2259 key_id: KEY_ID,
2260 backend_path: PATH,
2261 algorithm: kem,
2262 encapsulated_key: &encapsulation.encapsulated_key,
2263 storage_key: Some(STORAGE_KEY),
2264 })
2265 .await
2266 .expect("decapsulate");
2267 assert_eq!(
2268 encapsulation.shared_secret.as_slice(),
2269 shared.as_slice(),
2270 "{} shared secret matches",
2271 kem_algorithm_name(kem)
2272 );
2273 }
2274 }
2275
2276 #[tokio::test]
2277 async fn wrap_unwrap_envelope_every_ml_kem_level() {
2278 for kem in KEM_LEVELS {
2279 for envelope_algorithm in [
2280 EnvelopeAlgorithm::Aes256Gcm,
2281 EnvelopeAlgorithm::ChaCha20Poly1305,
2282 ] {
2283 let seed = [0x42u8; ml_kem_envelope::SEED_LEN];
2284 let record = custody_record(
2285 kem_algorithm_name(kem),
2286 &[7u8; 32],
2287 LocalSoftwareProvider::PROVIDER_VERSION,
2288 );
2289 let backend = CustodyBackend::single(record, &seed);
2290 let provider = LocalSoftwareProvider::new(&backend);
2291
2292 let envelope = provider
2293 .wrap_envelope(WrapEnvelopeRequest {
2294 key_id: KEY_ID,
2295 backend_path: PATH,
2296 kem_algorithm: kem,
2297 envelope_algorithm,
2298 plaintext: b"top secret",
2299 aad: Some(b"context"),
2300 storage_key: Some(STORAGE_KEY),
2301 })
2302 .await
2303 .expect("wrap");
2304 assert_eq!(envelope.key_version, 1);
2305 let plaintext = provider
2306 .unwrap_envelope(UnwrapEnvelopeRequest {
2307 key_id: KEY_ID,
2308 backend_path: PATH,
2309 kem_algorithm: kem,
2310 envelope_algorithm,
2311 encapsulated_key: &envelope.encapsulated_key,
2312 nonce: &envelope.nonce,
2313 ciphertext: &envelope.ciphertext,
2314 aad: Some(b"context"),
2315 storage_key: Some(STORAGE_KEY),
2316 })
2317 .await
2318 .expect("unwrap");
2319 assert_eq!(plaintext, b"top secret");
2320
2321 let wrong_aad = provider
2322 .unwrap_envelope(UnwrapEnvelopeRequest {
2323 key_id: KEY_ID,
2324 backend_path: PATH,
2325 kem_algorithm: kem,
2326 envelope_algorithm,
2327 encapsulated_key: &envelope.encapsulated_key,
2328 nonce: &envelope.nonce,
2329 ciphertext: &envelope.ciphertext,
2330 aad: Some(b"wrong"),
2331 storage_key: Some(STORAGE_KEY),
2332 })
2333 .await
2334 .expect_err("wrong aad fails");
2335 assert!(matches!(
2336 wrong_aad,
2337 ProviderError::CryptoFailed {
2338 op: "unwrap_envelope",
2339 ..
2340 }
2341 ));
2342 }
2343 }
2344 }
2345
2346 #[tokio::test]
2347 async fn rejects_non_pqc_signature_algorithm() {
2348 let backend = CustodyBackend::single(Vec::new(), &[]);
2349 let provider = LocalSoftwareProvider::new(&backend);
2350 let err = provider
2351 .sign(SignRequest {
2352 key_id: KEY_ID,
2353 backend_path: PATH,
2354 algorithm: SignatureAlgorithm::Ed25519,
2355 message: b"m",
2356 storage_key: Some(STORAGE_KEY),
2357 })
2358 .await
2359 .expect_err("non-pqc rejected");
2360 assert!(matches!(
2361 err,
2362 ProviderError::Unsupported {
2363 provider: CryptoProviderId::LocalSoftware,
2364 op: "sign",
2365 algorithm: "ed25519"
2366 }
2367 ));
2368 }
2369
2370 #[tokio::test]
2371 async fn malformed_record_fails_closed() {
2372 let backend = CustodyBackend::single(b"not a record".to_vec(), &[0u8; 32]);
2373 let provider = LocalSoftwareProvider::new(&backend);
2374 let err = provider
2375 .sign(SignRequest {
2376 key_id: KEY_ID,
2377 backend_path: PATH,
2378 algorithm: SignatureAlgorithm::MlDsa44,
2379 message: b"m",
2380 storage_key: Some(STORAGE_KEY),
2381 })
2382 .await
2383 .expect_err("malformed record");
2384 assert!(matches!(
2385 err,
2386 ProviderError::CryptoFailed {
2387 provider: CryptoProviderId::LocalSoftware,
2388 op: "sign",
2389 algorithm: "ml-dsa-44",
2390 reason: "malformed software-custody record"
2391 }
2392 ));
2393 }
2394
2395 #[tokio::test]
2396 async fn wrong_provider_version_record_fails_closed() {
2397 let seed = [0x11u8; ml_dsa_sign::SEED_LEN];
2398 let public =
2399 ml_dsa_sign::public_from_seed(ml_dsa_sign::MlDsaAlgorithm::MlDsa65, &seed).expect("pk");
2400 let record = custody_record("ml-dsa-65", &public, "99");
2403 let backend = CustodyBackend::single(record, &seed);
2404 let provider = LocalSoftwareProvider::new(&backend);
2405 let err = provider
2406 .sign(SignRequest {
2407 key_id: KEY_ID,
2408 backend_path: PATH,
2409 algorithm: SignatureAlgorithm::MlDsa65,
2410 message: b"m",
2411 storage_key: Some(STORAGE_KEY),
2412 })
2413 .await
2414 .expect_err("version mismatch");
2415 assert!(matches!(err, ProviderError::CryptoFailed { .. }));
2416 }
2417
2418 #[tokio::test]
2419 async fn record_wrapping_key_must_match_the_catalog_storage_key() {
2420 let seed = [0x11u8; ml_dsa_sign::SEED_LEN];
2421 let public =
2422 ml_dsa_sign::public_from_seed(ml_dsa_sign::MlDsaAlgorithm::MlDsa65, &seed).expect("pk");
2423 let record = custody_record("ml-dsa-65", &public, LocalSoftwareProvider::PROVIDER_VERSION);
2424 let backend = CustodyBackend::single(record, &seed);
2425 let provider = LocalSoftwareProvider::new(&backend);
2426 let err = provider
2430 .sign(SignRequest {
2431 key_id: KEY_ID,
2432 backend_path: PATH,
2433 algorithm: SignatureAlgorithm::MlDsa65,
2434 message: b"m",
2435 storage_key: Some("other-storage-aead"),
2436 })
2437 .await
2438 .expect_err("wrapping-key mismatch");
2439 assert!(matches!(
2440 err,
2441 ProviderError::CryptoFailed {
2442 op: "sign",
2443 reason: "malformed software-custody record",
2444 ..
2445 }
2446 ));
2447 }
2448
2449 #[tokio::test]
2450 async fn use_path_without_a_catalog_storage_key_fails_closed() {
2451 let seed = [0x11u8; ml_dsa_sign::SEED_LEN];
2452 let public =
2453 ml_dsa_sign::public_from_seed(ml_dsa_sign::MlDsaAlgorithm::MlDsa65, &seed).expect("pk");
2454 let record = custody_record("ml-dsa-65", &public, LocalSoftwareProvider::PROVIDER_VERSION);
2455 let backend = CustodyBackend::single(record, &seed);
2456 let provider = LocalSoftwareProvider::new(&backend);
2457 let err = provider
2461 .sign(SignRequest {
2462 key_id: KEY_ID,
2463 backend_path: PATH,
2464 algorithm: SignatureAlgorithm::MlDsa65,
2465 message: b"m",
2466 storage_key: None,
2467 })
2468 .await
2469 .expect_err("missing storage key");
2470 assert!(matches!(
2471 err,
2472 ProviderError::CryptoFailed {
2473 op: "sign",
2474 reason: "software-custody key is missing its storage AEAD key",
2475 ..
2476 }
2477 ));
2478 }
2479
2480 #[tokio::test]
2481 async fn generate_without_storage_key_and_import_fail_closed() {
2482 let backend = CustodyBackend::single(Vec::new(), &[]);
2483 let provider = LocalSoftwareProvider::new(&backend);
2484 let material = basil_proto::KeyMaterial::Ed25519Seed(vec![0u8; 32]);
2485 assert!(matches!(
2488 provider
2489 .generate_key(GenerateKey {
2490 key_id: KEY_ID,
2491 backend_path: PATH,
2492 algorithm: SignatureAlgorithm::MlDsa87,
2493 storage_key: None,
2494 })
2495 .await,
2496 Err(ProviderError::CryptoFailed {
2497 op: "generate_key",
2498 ..
2499 })
2500 ));
2501 assert!(matches!(
2503 provider
2504 .import_key(ImportKey {
2505 key_id: KEY_ID,
2506 backend_path: PATH,
2507 algorithm: SignatureAlgorithm::MlDsa87,
2508 material: &material,
2509 })
2510 .await,
2511 Err(ProviderError::Unsupported {
2512 op: "import_key",
2513 ..
2514 })
2515 ));
2516 }
2517
2518 struct RoundTripBackend {
2524 records: std::sync::Mutex<HashMap<String, Vec<u8>>>,
2525 }
2526
2527 impl RoundTripBackend {
2528 fn new() -> Self {
2529 Self {
2530 records: std::sync::Mutex::new(HashMap::new()),
2531 }
2532 }
2533 }
2534
2535 #[async_trait]
2536 impl Backend for RoundTripBackend {
2537 fn kind(&self) -> &'static str {
2538 "round-trip-custody-test"
2539 }
2540
2541 async fn new_key(&self, _key_type: KeyType) -> Result<NewKey, BackendError> {
2542 Err(BackendError::Unsupported("new_key"))
2543 }
2544
2545 async fn public_key(&self, _key_id: &str) -> Result<Vec<u8>, BackendError> {
2546 Err(BackendError::Unsupported("public_key"))
2547 }
2548
2549 async fn sign(&self, _key_id: &str, _message: &[u8]) -> Result<Vec<u8>, BackendError> {
2550 Err(BackendError::Unsupported("sign"))
2551 }
2552
2553 async fn verify(
2554 &self,
2555 _key_id: &str,
2556 _message: &[u8],
2557 _signature: &[u8],
2558 ) -> Result<bool, BackendError> {
2559 Err(BackendError::Unsupported("verify"))
2560 }
2561
2562 async fn encrypt(
2563 &self,
2564 _key_id: &str,
2565 algorithm: basil_proto::AeadAlgorithm,
2566 plaintext: &[u8],
2567 _aad: Option<&[u8]>,
2568 ) -> Result<CiphertextEnvelope, BackendError> {
2569 Ok(CiphertextEnvelope {
2570 alg: algorithm,
2571 key_version: 1,
2572 nonce: vec![0u8; 12],
2573 ciphertext: plaintext.to_vec(),
2574 })
2575 }
2576
2577 async fn decrypt(
2578 &self,
2579 _key_id: &str,
2580 envelope: &CiphertextEnvelope,
2581 _aad: Option<&[u8]>,
2582 ) -> Result<Vec<u8>, BackendError> {
2583 Ok(envelope.ciphertext.clone())
2584 }
2585
2586 async fn kv_put(&self, key_id: &str, value: &[u8]) -> Result<u32, BackendError> {
2587 self.records
2588 .lock()
2589 .map_err(|_| BackendError::Unsupported("kv_put"))?
2590 .insert(key_id.to_string(), value.to_vec());
2591 Ok(1)
2592 }
2593
2594 async fn kv_get(
2595 &self,
2596 key_id: &str,
2597 _version: Option<u32>,
2598 ) -> Result<KvValue, BackendError> {
2599 let value = self
2600 .records
2601 .lock()
2602 .map_err(|_| BackendError::Unsupported("kv_get"))?
2603 .get(key_id)
2604 .cloned();
2605 value
2606 .map(|value| KvValue { value, version: 1 })
2607 .ok_or(BackendError::Unsupported("kv_get"))
2608 }
2609 }
2610
2611 #[tokio::test]
2612 async fn generate_then_sign_verify_round_trip_every_level() {
2613 for (sig_algorithm, dsa_algorithm) in DSA_LEVELS {
2614 let backend = RoundTripBackend::new();
2615 let provider = LocalSoftwareProvider::new(&backend);
2616 let created = provider
2617 .generate_key(GenerateKey {
2618 key_id: KEY_ID,
2619 backend_path: PATH,
2620 algorithm: sig_algorithm,
2621 storage_key: Some(STORAGE_KEY),
2622 })
2623 .await
2624 .expect("generate");
2625 assert!(
2627 !created.public_key.is_empty(),
2628 "{} public",
2629 dsa_algorithm.token()
2630 );
2631
2632 let signature = provider
2633 .sign(SignRequest {
2634 key_id: KEY_ID,
2635 backend_path: PATH,
2636 algorithm: sig_algorithm,
2637 message: b"provisioned payload",
2638 storage_key: Some(STORAGE_KEY),
2639 })
2640 .await
2641 .expect("sign");
2642 assert!(
2643 provider
2644 .verify(VerifyRequest {
2645 key_id: KEY_ID,
2646 backend_path: PATH,
2647 algorithm: sig_algorithm,
2648 message: b"provisioned payload",
2649 signature: &signature,
2650 storage_key: Some(STORAGE_KEY),
2651 })
2652 .await
2653 .expect("verify"),
2654 "{} round trip verifies",
2655 dsa_algorithm.token()
2656 );
2657 assert!(
2658 ml_dsa_sign::verify(
2659 dsa_algorithm,
2660 &created.public_key,
2661 b"provisioned payload",
2662 &signature,
2663 )
2664 .expect("core verify"),
2665 "{} verifies under returned public",
2666 dsa_algorithm.token()
2667 );
2668 }
2669 }
2670}