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(
1079 key_id,
1080 backend_path,
1081 storage_key,
1082 "get_public_key",
1083 algorithm,
1084 )
1085 .await?;
1086 Ok(material.public_key)
1087 }
1088
1089 async fn seal_and_store(
1095 &self,
1096 params: SealParams<'_>,
1097 seed: &[u8],
1098 public_key: Vec<u8>,
1099 ) -> Result<NewKey, ProviderError> {
1100 const RECORD_VERSION: u32 = 1;
1103
1104 let custody = SoftwareCustodyCatalog {
1105 key_id: params.key_id,
1106 algorithm: params.algorithm_token,
1107 provider: CryptoProviderId::LocalSoftware.token(),
1108 provider_version: self.provider_version,
1109 custody: CustodyMode::SoftwareEncrypted.token(),
1110 storage_key: params.storage_key,
1111 };
1112 let aad = custody.aad(RECORD_VERSION);
1113 let envelope = self
1114 .backend
1115 .encrypt(
1116 params.storage_key,
1117 AeadAlgorithm::Aes256Gcm,
1118 seed,
1119 Some(&aad),
1120 )
1121 .await?;
1122 let record = SoftwareCustodyKeyRecord {
1123 schema_version: SoftwareCustodyKeyRecord::SCHEMA_VERSION,
1124 key_id: params.key_id.to_string(),
1125 key_version: RECORD_VERSION,
1126 public_key: B64.encode(&public_key),
1127 algorithm: params.algorithm_token.to_string(),
1128 provider: CryptoProviderId::LocalSoftware.token().to_string(),
1129 provider_version: self.provider_version.to_string(),
1130 custody: CustodyMode::SoftwareEncrypted.token().to_string(),
1131 encrypted_private_key: EncryptedPrivateKey {
1132 wrapping_key: params.storage_key.to_string(),
1133 algorithm: aead_token(envelope.alg).to_string(),
1134 key_version: envelope.key_version,
1135 nonce: B64.encode(&envelope.nonce),
1136 ciphertext: B64.encode(&envelope.ciphertext),
1137 },
1138 };
1139 let bytes = serde_json::to_vec(&record).map_err(|_| {
1140 crypto_failed(
1141 CryptoProviderId::LocalSoftware,
1142 params.op,
1143 params.algorithm_token,
1144 "software-custody record serialization failed",
1145 )
1146 })?;
1147 self.backend.kv_put(params.backend_path, &bytes).await?;
1148 Ok(NewKey {
1149 key_id: params.key_id.to_string(),
1150 public_key,
1151 })
1152 }
1153}
1154
1155struct SealParams<'a> {
1158 op: &'static str,
1160 key_id: &'a str,
1162 backend_path: &'a str,
1164 algorithm_token: &'static str,
1166 storage_key: &'a str,
1168}
1169#[async_trait]
1170impl CryptoProvider for LocalSoftwareProvider<'_> {
1171 fn provider_id(&self) -> CryptoProviderId {
1172 CryptoProviderId::LocalSoftware
1173 }
1174
1175 async fn generate_key(&self, request: GenerateKey<'_>) -> Result<NewKey, ProviderError> {
1176 use rand::RngCore as _;
1177 use rand::rngs::OsRng;
1178
1179 let algorithm = ml_dsa_algorithm(request.algorithm).ok_or_else(|| {
1180 unsupported(
1181 CryptoProviderId::LocalSoftware,
1182 "generate_key",
1183 signature_algorithm_name(request.algorithm),
1184 )
1185 })?;
1186 let algorithm_token = algorithm.token();
1187 let storage_key = request.storage_key.ok_or_else(|| {
1188 crypto_failed(
1189 CryptoProviderId::LocalSoftware,
1190 "generate_key",
1191 algorithm_token,
1192 "software-custody key is missing its storage AEAD key",
1193 )
1194 })?;
1195
1196 let mut seed = Zeroizing::new([0u8; ml_dsa_sign::SEED_LEN]);
1199 OsRng.fill_bytes(seed.as_mut_slice());
1200 let public_key =
1201 ml_dsa_sign::public_from_seed(algorithm, seed.as_slice()).map_err(|_| {
1202 crypto_failed(
1203 CryptoProviderId::LocalSoftware,
1204 "generate_key",
1205 algorithm_token,
1206 "ml-dsa key generation failed",
1207 )
1208 })?;
1209
1210 self.seal_and_store(
1211 SealParams {
1212 op: "generate_key",
1213 key_id: request.key_id,
1214 backend_path: request.backend_path,
1215 algorithm_token,
1216 storage_key,
1217 },
1218 seed.as_slice(),
1219 public_key,
1220 )
1221 .await
1222 }
1223
1224 async fn generate_sealing_key(
1225 &self,
1226 request: GenerateSealingKey<'_>,
1227 ) -> Result<NewKey, ProviderError> {
1228 use rand::RngCore as _;
1229 use rand::rngs::OsRng;
1230
1231 let kem = request.algorithm;
1232 let algorithm_token = kem.token();
1233 let storage_key = request.storage_key.ok_or_else(|| {
1234 crypto_failed(
1235 CryptoProviderId::LocalSoftware,
1236 "generate_sealing_key",
1237 algorithm_token,
1238 "software-custody key is missing its storage AEAD key",
1239 )
1240 })?;
1241
1242 let mut seed = Zeroizing::new([0u8; ml_kem_envelope::SEED_LEN]);
1246 OsRng.fill_bytes(seed.as_mut_slice());
1247 let public_key = ml_kem_envelope::public_from_seed(seed.as_slice(), kem_to_core(kem))
1248 .map_err(|_| {
1249 crypto_failed(
1250 CryptoProviderId::LocalSoftware,
1251 "generate_sealing_key",
1252 algorithm_token,
1253 "ml-kem key generation failed",
1254 )
1255 })?;
1256
1257 self.seal_and_store(
1258 SealParams {
1259 op: "generate_sealing_key",
1260 key_id: request.key_id,
1261 backend_path: request.backend_path,
1262 algorithm_token,
1263 storage_key,
1264 },
1265 seed.as_slice(),
1266 public_key,
1267 )
1268 .await
1269 }
1270
1271 async fn import_key(&self, request: ImportKey<'_>) -> Result<NewKey, ProviderError> {
1272 Err(unsupported(
1273 CryptoProviderId::LocalSoftware,
1274 "import_key",
1275 signature_algorithm_name(request.algorithm),
1276 ))
1277 }
1278
1279 async fn sign(&self, request: SignRequest<'_>) -> Result<Vec<u8>, ProviderError> {
1280 let algorithm = ml_dsa_algorithm(request.algorithm).ok_or_else(|| {
1281 unsupported(
1282 CryptoProviderId::LocalSoftware,
1283 "sign",
1284 signature_algorithm_name(request.algorithm),
1285 )
1286 })?;
1287 let material = self
1288 .material(
1289 request.key_id,
1290 request.backend_path,
1291 request.storage_key,
1292 "sign",
1293 algorithm.token(),
1294 )
1295 .await?;
1296 let seed = self.materialize_seed(&material).await?;
1297 ml_dsa_sign::sign(algorithm, &seed, request.message).map_err(|_| {
1298 crypto_failed(
1299 CryptoProviderId::LocalSoftware,
1300 "sign",
1301 algorithm.token(),
1302 "ml-dsa signing failed",
1303 )
1304 })
1305 }
1306
1307 async fn verify(&self, request: VerifyRequest<'_>) -> Result<bool, ProviderError> {
1308 let algorithm = ml_dsa_algorithm(request.algorithm).ok_or_else(|| {
1309 unsupported(
1310 CryptoProviderId::LocalSoftware,
1311 "verify",
1312 signature_algorithm_name(request.algorithm),
1313 )
1314 })?;
1315 let material = self
1317 .material(
1318 request.key_id,
1319 request.backend_path,
1320 request.storage_key,
1321 "verify",
1322 algorithm.token(),
1323 )
1324 .await?;
1325 ml_dsa_sign::verify(
1326 algorithm,
1327 &material.public_key,
1328 request.message,
1329 request.signature,
1330 )
1331 .map_err(|_| {
1332 crypto_failed(
1333 CryptoProviderId::LocalSoftware,
1334 "verify",
1335 algorithm.token(),
1336 "malformed verification input",
1337 )
1338 })
1339 }
1340
1341 async fn encapsulate(
1342 &self,
1343 request: EncapsulateRequest<'_>,
1344 ) -> Result<Encapsulation, ProviderError> {
1345 let material = self
1346 .material(
1347 request.key_id,
1348 request.backend_path,
1349 request.storage_key,
1350 "encapsulate",
1351 kem_algorithm_name(request.algorithm),
1352 )
1353 .await?;
1354 let seed = self.materialize_seed(&material).await?;
1355 let (encapsulated_key, shared_secret) =
1356 ml_kem_envelope::encapsulate(&seed, kem_to_core(request.algorithm)).map_err(|_| {
1357 crypto_failed(
1358 CryptoProviderId::LocalSoftware,
1359 "encapsulate",
1360 kem_algorithm_name(request.algorithm),
1361 "ml-kem encapsulation failed",
1362 )
1363 })?;
1364 Ok(Encapsulation {
1365 encapsulated_key,
1366 shared_secret,
1367 })
1368 }
1369
1370 async fn decapsulate(
1371 &self,
1372 request: DecapsulateRequest<'_>,
1373 ) -> Result<Zeroizing<Vec<u8>>, ProviderError> {
1374 let material = self
1375 .material(
1376 request.key_id,
1377 request.backend_path,
1378 request.storage_key,
1379 "decapsulate",
1380 kem_algorithm_name(request.algorithm),
1381 )
1382 .await?;
1383 let seed = self.materialize_seed(&material).await?;
1384 ml_kem_envelope::decapsulate(
1385 &seed,
1386 kem_to_core(request.algorithm),
1387 request.encapsulated_key,
1388 )
1389 .map_err(|_| {
1390 crypto_failed(
1391 CryptoProviderId::LocalSoftware,
1392 "decapsulate",
1393 kem_algorithm_name(request.algorithm),
1394 "ml-kem decapsulation failed",
1395 )
1396 })
1397 }
1398
1399 async fn wrap_envelope(
1400 &self,
1401 request: WrapEnvelopeRequest<'_>,
1402 ) -> Result<Envelope, ProviderError> {
1403 let material = self
1404 .material(
1405 request.key_id,
1406 request.backend_path,
1407 request.storage_key,
1408 "wrap_envelope",
1409 kem_algorithm_name(request.kem_algorithm),
1410 )
1411 .await?;
1412 let seed = self.materialize_seed(&material).await?;
1413 let sealed = ml_kem_envelope::seal(
1414 &seed,
1415 kem_to_core(request.kem_algorithm),
1416 envelope_to_core(request.envelope_algorithm),
1417 request.plaintext,
1418 request.aad.unwrap_or_default(),
1419 )
1420 .map_err(|_| {
1421 crypto_failed(
1422 CryptoProviderId::LocalSoftware,
1423 "wrap_envelope",
1424 kem_algorithm_name(request.kem_algorithm),
1425 "ml-kem envelope seal failed",
1426 )
1427 })?;
1428 Ok(Envelope {
1429 kem_algorithm: request.kem_algorithm,
1430 envelope_algorithm: request.envelope_algorithm,
1431 key_version: material.key_version,
1432 encapsulated_key: sealed.encapsulated_key,
1433 nonce: sealed.nonce.to_vec(),
1434 ciphertext: sealed.ciphertext,
1435 })
1436 }
1437
1438 async fn unwrap_envelope(
1439 &self,
1440 request: UnwrapEnvelopeRequest<'_>,
1441 ) -> Result<Vec<u8>, ProviderError> {
1442 let material = self
1443 .material(
1444 request.key_id,
1445 request.backend_path,
1446 request.storage_key,
1447 "unwrap_envelope",
1448 kem_algorithm_name(request.kem_algorithm),
1449 )
1450 .await?;
1451 let seed = self.materialize_seed(&material).await?;
1452 let envelope = ml_kem_envelope::envelope_from_parts(
1453 kem_to_core(request.kem_algorithm),
1454 envelope_to_core(request.envelope_algorithm),
1455 request.encapsulated_key,
1456 request.nonce,
1457 request.ciphertext,
1458 )
1459 .map_err(|_| {
1460 crypto_failed(
1461 CryptoProviderId::LocalSoftware,
1462 "unwrap_envelope",
1463 kem_algorithm_name(request.kem_algorithm),
1464 "malformed ml-kem envelope",
1465 )
1466 })?;
1467 let plaintext = ml_kem_envelope::open(&seed, &envelope, request.aad.unwrap_or_default())
1468 .map_err(|_| {
1469 crypto_failed(
1470 CryptoProviderId::LocalSoftware,
1471 "unwrap_envelope",
1472 kem_algorithm_name(request.kem_algorithm),
1473 "ml-kem envelope open failed",
1474 )
1475 })?;
1476 Ok(plaintext.to_vec())
1477 }
1478}
1479const fn ml_dsa_algorithm(algorithm: SignatureAlgorithm) -> Option<ml_dsa_sign::MlDsaAlgorithm> {
1480 match algorithm {
1481 SignatureAlgorithm::MlDsa44 => Some(ml_dsa_sign::MlDsaAlgorithm::MlDsa44),
1482 SignatureAlgorithm::MlDsa65 => Some(ml_dsa_sign::MlDsaAlgorithm::MlDsa65),
1483 SignatureAlgorithm::MlDsa87 => Some(ml_dsa_sign::MlDsaAlgorithm::MlDsa87),
1484 SignatureAlgorithm::Ed25519
1485 | SignatureAlgorithm::Ed25519Nkey
1486 | SignatureAlgorithm::Rs256
1487 | SignatureAlgorithm::Es256 => None,
1488 }
1489}
1490const fn kem_to_core(algorithm: KemAlgorithm) -> ml_kem_envelope::KemAlgorithm {
1491 match algorithm {
1492 KemAlgorithm::MlKem512 => ml_kem_envelope::KemAlgorithm::MlKem512,
1493 KemAlgorithm::MlKem768 => ml_kem_envelope::KemAlgorithm::MlKem768,
1494 KemAlgorithm::MlKem1024 => ml_kem_envelope::KemAlgorithm::MlKem1024,
1495 }
1496}
1497const fn envelope_to_core(algorithm: EnvelopeAlgorithm) -> ml_kem_envelope::EnvelopeAlgorithm {
1498 match algorithm {
1499 EnvelopeAlgorithm::Aes256Gcm => ml_kem_envelope::EnvelopeAlgorithm::Aes256Gcm,
1500 EnvelopeAlgorithm::ChaCha20Poly1305 => ml_kem_envelope::EnvelopeAlgorithm::ChaCha20Poly1305,
1501 }
1502}
1503
1504pub fn select_provider(
1509 metadata: ProviderMetadata,
1510 backend_native_supported: bool,
1511 local_software_allowed: bool,
1512 op: &'static str,
1513) -> Result<CryptoProviderId, ProviderError> {
1514 match metadata.policy {
1515 ProviderPolicy::BackendRequired => select_backend_required(backend_native_supported, op),
1516 ProviderPolicy::BackendPreferred => select_backend_preferred(
1517 metadata.custody,
1518 backend_native_supported,
1519 local_software_allowed,
1520 op,
1521 ),
1522 ProviderPolicy::LocalSoftware => {
1523 select_local_software(metadata.custody, local_software_allowed, op)
1524 }
1525 }
1526}
1527
1528const fn select_backend_required(
1529 backend_native_supported: bool,
1530 op: &'static str,
1531) -> Result<CryptoProviderId, ProviderError> {
1532 if backend_native_supported {
1533 return Ok(CryptoProviderId::VaultTransit);
1534 }
1535 Err(unsupported(
1536 CryptoProviderId::VaultTransit,
1537 op,
1538 "backend-native",
1539 ))
1540}
1541
1542fn select_backend_preferred(
1543 custody: Option<CustodyMode>,
1544 backend_native_supported: bool,
1545 local_software_allowed: bool,
1546 op: &'static str,
1547) -> Result<CryptoProviderId, ProviderError> {
1548 if custody == Some(CustodyMode::SoftwareEncrypted) {
1556 require_local_software_allowed(local_software_allowed, op)?;
1557 return Ok(CryptoProviderId::LocalSoftware);
1558 }
1559 if backend_native_supported {
1560 return Ok(CryptoProviderId::VaultTransit);
1561 }
1562 require_local_software_allowed(local_software_allowed, op)?;
1563 require_software_custody(
1564 custody,
1565 op,
1566 "local software fallback requires software-encrypted custody",
1567 )?;
1568 Ok(CryptoProviderId::LocalSoftware)
1569}
1570
1571fn select_local_software(
1572 custody: Option<CustodyMode>,
1573 local_software_allowed: bool,
1574 op: &'static str,
1575) -> Result<CryptoProviderId, ProviderError> {
1576 require_local_software_allowed(local_software_allowed, op)?;
1577 require_software_custody(
1578 custody,
1579 op,
1580 "local software provider requires software-encrypted custody",
1581 )?;
1582 Ok(CryptoProviderId::LocalSoftware)
1583}
1584
1585const fn require_local_software_allowed(
1586 local_software_allowed: bool,
1587 op: &'static str,
1588) -> Result<(), ProviderError> {
1589 if local_software_allowed {
1590 return Ok(());
1591 }
1592 Err(ProviderError::PolicyDenied {
1593 op,
1594 reason: "local software custody requires caller policy grant",
1595 })
1596}
1597
1598fn require_software_custody(
1599 custody: Option<CustodyMode>,
1600 op: &'static str,
1601 reason: &'static str,
1602) -> Result<(), ProviderError> {
1603 if custody == Some(CustodyMode::SoftwareEncrypted) {
1604 return Ok(());
1605 }
1606 Err(ProviderError::PolicyDenied { op, reason })
1607}
1608
1609const fn key_type_for_signature(algorithm: SignatureAlgorithm) -> Option<KeyType> {
1610 match algorithm {
1611 SignatureAlgorithm::Ed25519 => Some(KeyType::Ed25519),
1612 SignatureAlgorithm::Ed25519Nkey => Some(KeyType::Ed25519Nkey),
1613 SignatureAlgorithm::Rs256 => Some(KeyType::Rsa2048),
1614 SignatureAlgorithm::Es256 => Some(KeyType::EcdsaP256),
1615 SignatureAlgorithm::MlDsa44 | SignatureAlgorithm::MlDsa65 | SignatureAlgorithm::MlDsa87 => {
1616 None
1617 }
1618 }
1619}
1620
1621const fn sign_options(algorithm: SignatureAlgorithm) -> Option<SignOptions> {
1622 match algorithm {
1623 SignatureAlgorithm::Ed25519 | SignatureAlgorithm::Ed25519Nkey => Some(SignOptions::Default),
1624 SignatureAlgorithm::Rs256 => Some(SignOptions::Rs256Pkcs1v15Sha256),
1625 SignatureAlgorithm::Es256 => Some(SignOptions::Es256),
1626 SignatureAlgorithm::MlDsa44 | SignatureAlgorithm::MlDsa65 | SignatureAlgorithm::MlDsa87 => {
1627 None
1628 }
1629 }
1630}
1631
1632const fn signature_algorithm_name(algorithm: SignatureAlgorithm) -> &'static str {
1633 match algorithm {
1634 SignatureAlgorithm::Ed25519 => "ed25519",
1635 SignatureAlgorithm::Ed25519Nkey => "ed25519-nkey",
1636 SignatureAlgorithm::Rs256 => "rs256",
1637 SignatureAlgorithm::Es256 => "es256",
1638 SignatureAlgorithm::MlDsa44 => "ml-dsa-44",
1639 SignatureAlgorithm::MlDsa65 => "ml-dsa-65",
1640 SignatureAlgorithm::MlDsa87 => "ml-dsa-87",
1641 }
1642}
1643
1644const fn kem_algorithm_name(algorithm: KemAlgorithm) -> &'static str {
1645 algorithm.token()
1646}
1647
1648const fn unsupported(
1649 provider: CryptoProviderId,
1650 op: &'static str,
1651 algorithm: &'static str,
1652) -> ProviderError {
1653 ProviderError::Unsupported {
1654 provider,
1655 op,
1656 algorithm,
1657 }
1658}
1659const fn crypto_failed(
1660 provider: CryptoProviderId,
1661 op: &'static str,
1662 algorithm: &'static str,
1663 reason: &'static str,
1664) -> ProviderError {
1665 ProviderError::CryptoFailed {
1666 provider,
1667 op,
1668 algorithm,
1669 reason,
1670 }
1671}
1672
1673const fn aead_token(alg: AeadAlgorithm) -> &'static str {
1676 match alg {
1677 AeadAlgorithm::Aes256Gcm => "aes-256-gcm",
1678 AeadAlgorithm::Chacha20Poly1305 => "chacha20-poly1305",
1679 }
1680}
1681
1682#[cfg(test)]
1683mod tests {
1684 use async_trait::async_trait;
1685
1686 use super::*;
1687 use crate::backend::{BackendError, NewKey};
1688 use crate::catalog::schema::Labels;
1689
1690 struct RecordingBackend;
1691
1692 #[async_trait]
1693 impl Backend for RecordingBackend {
1694 fn kind(&self) -> &'static str {
1695 "recording"
1696 }
1697
1698 async fn new_key(&self, key_type: KeyType) -> Result<NewKey, BackendError> {
1699 let _ = key_type;
1700 Err(BackendError::Unsupported("new_key"))
1701 }
1702
1703 async fn create_named_key(
1704 &self,
1705 key_id: &str,
1706 key_type: KeyType,
1707 ) -> Result<NewKey, BackendError> {
1708 Ok(NewKey {
1709 key_id: format!("{key_id}:{key_type}"),
1710 public_key: vec![1, 2, 3],
1711 })
1712 }
1713
1714 async fn public_key(&self, key_id: &str) -> Result<Vec<u8>, BackendError> {
1715 let _ = key_id;
1716 Ok(vec![1, 2, 3])
1717 }
1718
1719 async fn sign(&self, key_id: &str, message: &[u8]) -> Result<Vec<u8>, BackendError> {
1720 Ok([key_id.as_bytes(), message].concat())
1721 }
1722
1723 async fn sign_with_options(
1724 &self,
1725 key_id: &str,
1726 message: &[u8],
1727 options: SignOptions,
1728 ) -> Result<Vec<u8>, BackendError> {
1729 let mut out = self.sign(key_id, message).await?;
1730 if options == SignOptions::Rs256Pkcs1v15Sha256 {
1733 out.extend_from_slice(b":rs256");
1734 } else if options == SignOptions::Es256 {
1735 out.extend_from_slice(b":es256");
1736 }
1737 Ok(out)
1738 }
1739
1740 async fn verify(
1741 &self,
1742 key_id: &str,
1743 message: &[u8],
1744 signature: &[u8],
1745 ) -> Result<bool, BackendError> {
1746 Ok(signature == [key_id.as_bytes(), message].concat())
1749 }
1750 }
1751
1752 fn key_entry_with_labels(labels: &[&str]) -> KeyEntry {
1753 KeyEntry {
1754 class: crate::catalog::schema::Class::Asymmetric,
1755 key_type: Some(crate::catalog::schema::KeyAlgorithm::Ed25519),
1756 backend: "bao".to_string(),
1757 engine: None,
1758 path: "issuer".to_string(),
1759 public_path: None,
1760 writable: true,
1761 missing: crate::catalog::schema::MissingPolicy::Error,
1762 generate: None,
1763 sealing_pin: None,
1764 labels: Labels(labels.iter().map(ToString::to_string).collect()),
1765 description: "issuer".to_string(),
1766 }
1767 }
1768
1769 #[test]
1770 fn provider_metadata_defaults_to_backend_required() {
1771 let entry = key_entry_with_labels(&[]);
1772 assert_eq!(
1773 ProviderMetadata::from_key(&entry),
1774 ProviderMetadata {
1775 provider: None,
1776 policy: ProviderPolicy::BackendRequired,
1777 custody: None
1778 }
1779 );
1780 }
1781
1782 #[test]
1783 fn provider_metadata_parses_reserved_labels() {
1784 let entry = key_entry_with_labels(&[
1785 "crypto_provider=local-software",
1786 "crypto_provider_policy=backend-preferred",
1787 "pqc_custody=software-encrypted",
1788 ]);
1789 assert_eq!(
1790 ProviderMetadata::from_key(&entry),
1791 ProviderMetadata {
1792 provider: Some(CryptoProviderId::LocalSoftware),
1793 policy: ProviderPolicy::BackendPreferred,
1794 custody: Some(CustodyMode::SoftwareEncrypted)
1795 }
1796 );
1797 }
1798
1799 #[test]
1800 fn provider_selection_honors_backend_required() {
1801 let selected = select_provider(
1802 ProviderMetadata {
1803 provider: None,
1804 policy: ProviderPolicy::BackendRequired,
1805 custody: None,
1806 },
1807 true,
1808 false,
1809 "sign",
1810 )
1811 .expect("backend-native selected");
1812 assert_eq!(selected, CryptoProviderId::VaultTransit);
1813
1814 assert!(matches!(
1815 select_provider(
1816 ProviderMetadata {
1817 provider: None,
1818 policy: ProviderPolicy::BackendRequired,
1819 custody: None,
1820 },
1821 false,
1822 false,
1823 "sign",
1824 ),
1825 Err(ProviderError::Unsupported { .. })
1826 ));
1827 }
1828
1829 #[test]
1830 fn provider_selection_honors_local_software_custody_gate() {
1831 let denied = select_provider(
1832 ProviderMetadata {
1833 provider: None,
1834 policy: ProviderPolicy::BackendPreferred,
1835 custody: Some(CustodyMode::BackendNative),
1836 },
1837 false,
1838 true,
1839 "sign",
1840 )
1841 .expect_err("backend fallback needs software custody");
1842 assert!(matches!(denied, ProviderError::PolicyDenied { .. }));
1843
1844 let denied = select_provider(
1845 ProviderMetadata {
1846 provider: None,
1847 policy: ProviderPolicy::BackendPreferred,
1848 custody: Some(CustodyMode::SoftwareEncrypted),
1849 },
1850 false,
1851 false,
1852 "sign",
1853 )
1854 .expect_err("local software needs caller policy");
1855 assert!(matches!(
1856 denied,
1857 ProviderError::PolicyDenied {
1858 reason: "local software custody requires caller policy grant",
1859 ..
1860 }
1861 ));
1862
1863 let selected = select_provider(
1864 ProviderMetadata {
1865 provider: None,
1866 policy: ProviderPolicy::BackendPreferred,
1867 custody: Some(CustodyMode::SoftwareEncrypted),
1868 },
1869 false,
1870 true,
1871 "sign",
1872 )
1873 .expect("local fallback selected");
1874 assert_eq!(selected, CryptoProviderId::LocalSoftware);
1875 }
1876
1877 #[test]
1878 fn backend_preferred_software_custody_pins_despite_native_support() {
1879 let pinned = select_provider(
1884 ProviderMetadata {
1885 provider: None,
1886 policy: ProviderPolicy::BackendPreferred,
1887 custody: Some(CustodyMode::SoftwareEncrypted),
1888 },
1889 true,
1890 true,
1891 "sign",
1892 )
1893 .expect("software-custodied key stays local-software");
1894 assert_eq!(pinned, CryptoProviderId::LocalSoftware);
1895
1896 let native = select_provider(
1899 ProviderMetadata {
1900 provider: None,
1901 policy: ProviderPolicy::BackendPreferred,
1902 custody: None,
1903 },
1904 true,
1905 true,
1906 "sign",
1907 )
1908 .expect("no-custody preferred key routes to native");
1909 assert_eq!(native, CryptoProviderId::VaultTransit);
1910
1911 let denied = select_provider(
1914 ProviderMetadata {
1915 provider: None,
1916 policy: ProviderPolicy::BackendPreferred,
1917 custody: Some(CustodyMode::SoftwareEncrypted),
1918 },
1919 true,
1920 false,
1921 "sign",
1922 )
1923 .expect_err("local software still needs the caller grant");
1924 assert!(matches!(denied, ProviderError::PolicyDenied { .. }));
1925 }
1926
1927 #[test]
1928 fn provider_selection_honors_local_software_policy_mode() {
1929 let selected = select_provider(
1930 ProviderMetadata {
1931 provider: Some(CryptoProviderId::LocalSoftware),
1932 policy: ProviderPolicy::LocalSoftware,
1933 custody: Some(CustodyMode::SoftwareEncrypted),
1934 },
1935 true,
1936 true,
1937 "decapsulate",
1938 )
1939 .expect("explicit local software selected");
1940 assert_eq!(selected, CryptoProviderId::LocalSoftware);
1941
1942 let denied = select_provider(
1943 ProviderMetadata {
1944 provider: Some(CryptoProviderId::LocalSoftware),
1945 policy: ProviderPolicy::LocalSoftware,
1946 custody: Some(CustodyMode::SoftwareEncrypted),
1947 },
1948 true,
1949 false,
1950 "decapsulate",
1951 )
1952 .expect_err("caller grant required");
1953 assert!(matches!(
1954 denied,
1955 ProviderError::PolicyDenied {
1956 reason: "local software custody requires caller policy grant",
1957 ..
1958 }
1959 ));
1960 }
1961
1962 #[test]
1963 fn provider_audit_event_is_secret_free() {
1964 let event = ProviderAuditEvent {
1965 op: "decapsulate",
1966 key_id: "pqc.kem",
1967 key_version: Some(7),
1968 algorithm: "ml-kem-768",
1969 provider: CryptoProviderId::LocalSoftware,
1970 custody: CustodyMode::SoftwareEncrypted,
1971 caller_uid: 9100,
1972 outcome: ProviderAuditOutcome::Success,
1973 reason: "ok",
1974 };
1975 let value = event.to_json_value();
1976 assert_eq!(value["event"]["kind"], "basil.audit.provider_operation");
1977 assert_eq!(value["event"]["version"], 1);
1978 assert_eq!(value["actor"]["kind"], "unix_uid");
1979 assert_eq!(value["actor"]["id"], "9100");
1980 assert_eq!(value["target"]["kind"], "catalog_key");
1981 assert_eq!(value["target"]["id"], "pqc.kem");
1982 assert_eq!(value["target"]["version"], 7);
1983 assert!(value["occurred_at"].as_str().is_some());
1984 assert_eq!(value["op"], "decapsulate");
1985 assert_eq!(value["key_version"], 7);
1986 assert_eq!(value["algorithm"], "ml-kem-768");
1987 assert_eq!(value["provider"], "local-software");
1988 assert_eq!(value["custody"], "software-encrypted");
1989 assert_eq!(value["caller_uid"], 9100);
1990 assert_eq!(value["outcome"], "success");
1991 assert!(value.get("private_key").is_none());
1992 assert!(value.get("plaintext").is_none());
1993 assert!(value.get("ciphertext").is_none());
1994 assert!(value.get("signature").is_none());
1995 assert!(value.get("shared_secret").is_none());
1996 }
1997
1998 #[tokio::test]
1999 async fn backend_provider_delegates_legacy_signing() {
2000 let backend = RecordingBackend;
2001 let provider = BackendCryptoProvider::new(&backend);
2002 let signature = provider
2003 .sign(SignRequest {
2004 key_id: "catalog.issuer",
2005 backend_path: "issuer",
2006 algorithm: SignatureAlgorithm::Rs256,
2007 message: b"digest",
2008 storage_key: None,
2009 })
2010 .await
2011 .expect("signs");
2012 assert_eq!(signature, b"issuerdigest:rs256");
2013 }
2014
2015 #[tokio::test]
2016 async fn backend_provider_rejects_pqc_without_native_support() {
2017 let backend = RecordingBackend;
2022 let provider = BackendCryptoProvider::new(&backend);
2023 assert!(!backend.supports_native_algorithm(NativeAlgorithm::MlDsa65));
2024 let err = provider
2025 .sign(SignRequest {
2026 key_id: "catalog.issuer",
2027 backend_path: "issuer",
2028 algorithm: SignatureAlgorithm::MlDsa65,
2029 message: b"digest",
2030 storage_key: None,
2031 })
2032 .await
2033 .expect_err("ML-DSA unsupported");
2034 assert!(matches!(
2035 err,
2036 ProviderError::Backend(BackendError::Unsupported("sign_pqc"))
2037 ));
2038 }
2039}
2040
2041#[cfg(test)]
2042mod pqc_provider_tests {
2043 use std::collections::HashMap;
2044
2045 use async_trait::async_trait;
2046 use basil_proto::{CiphertextEnvelope, KeyType};
2047
2048 use super::*;
2049 use crate::backend::{Backend, BackendError, KvValue, NewKey, SignOptions};
2050 use crate::core::{ml_dsa_sign, ml_kem_envelope};
2051
2052 const STORAGE_KEY: &str = "pqc-storage-aead";
2053 const KEY_ID: &str = "pqc.example";
2054 const PATH: &str = "kv/pqc/example";
2055
2056 struct CustodyBackend {
2060 records: HashMap<String, Vec<u8>>,
2061 secrets: HashMap<String, Vec<u8>>,
2062 }
2063
2064 impl CustodyBackend {
2065 fn single(record: Vec<u8>, seed: &[u8]) -> Self {
2066 Self {
2067 records: HashMap::from([(PATH.to_string(), record)]),
2068 secrets: HashMap::from([(STORAGE_KEY.to_string(), seed.to_vec())]),
2069 }
2070 }
2071 }
2072
2073 #[async_trait]
2074 impl Backend for CustodyBackend {
2075 fn kind(&self) -> &'static str {
2076 "custody-test"
2077 }
2078
2079 async fn new_key(&self, _key_type: KeyType) -> Result<NewKey, BackendError> {
2080 Err(BackendError::Unsupported("new_key"))
2081 }
2082
2083 async fn create_named_key(
2084 &self,
2085 _key_id: &str,
2086 _key_type: KeyType,
2087 ) -> Result<NewKey, BackendError> {
2088 Err(BackendError::Unsupported("create_named_key"))
2089 }
2090
2091 async fn public_key(&self, _key_id: &str) -> Result<Vec<u8>, BackendError> {
2092 Err(BackendError::Unsupported("public_key"))
2093 }
2094
2095 async fn sign(&self, _key_id: &str, _message: &[u8]) -> Result<Vec<u8>, BackendError> {
2096 Err(BackendError::Unsupported("sign"))
2097 }
2098
2099 async fn sign_with_options(
2100 &self,
2101 _key_id: &str,
2102 _message: &[u8],
2103 _options: SignOptions,
2104 ) -> Result<Vec<u8>, BackendError> {
2105 Err(BackendError::Unsupported("sign_with_options"))
2106 }
2107
2108 async fn verify(
2109 &self,
2110 _key_id: &str,
2111 _message: &[u8],
2112 _signature: &[u8],
2113 ) -> Result<bool, BackendError> {
2114 Err(BackendError::Unsupported("verify"))
2115 }
2116
2117 async fn kv_get(
2118 &self,
2119 key_id: &str,
2120 _version: Option<u32>,
2121 ) -> Result<KvValue, BackendError> {
2122 let value = self
2123 .records
2124 .get(key_id)
2125 .cloned()
2126 .ok_or(BackendError::Unsupported("kv_get"))?;
2127 Ok(KvValue { value, version: 1 })
2128 }
2129
2130 async fn decrypt(
2131 &self,
2132 key_id: &str,
2133 _envelope: &CiphertextEnvelope,
2134 _aad: Option<&[u8]>,
2135 ) -> Result<Vec<u8>, BackendError> {
2136 self.secrets
2137 .get(key_id)
2138 .cloned()
2139 .ok_or(BackendError::Unsupported("decrypt"))
2140 }
2141 }
2142
2143 fn custody_record(algorithm: &str, public_key: &[u8], provider_version: &str) -> Vec<u8> {
2145 let record = SoftwareCustodyKeyRecord {
2146 schema_version: SoftwareCustodyKeyRecord::SCHEMA_VERSION,
2147 key_id: KEY_ID.to_string(),
2148 key_version: 1,
2149 public_key: encode_record_bytes(public_key),
2150 algorithm: algorithm.to_string(),
2151 provider: CryptoProviderId::LocalSoftware.token().to_string(),
2152 provider_version: provider_version.to_string(),
2153 custody: CustodyMode::SoftwareEncrypted.token().to_string(),
2154 encrypted_private_key: EncryptedPrivateKey {
2155 wrapping_key: STORAGE_KEY.to_string(),
2156 algorithm: "aes-256-gcm".to_string(),
2157 key_version: 1,
2158 nonce: encode_record_bytes(&[0u8; 12]),
2159 ciphertext: encode_record_bytes(&[0u8; 16]),
2160 },
2161 };
2162 serde_json::to_vec(&record).expect("serialize record")
2163 }
2164
2165 const DSA_LEVELS: [(SignatureAlgorithm, ml_dsa_sign::MlDsaAlgorithm); 3] = [
2166 (
2167 SignatureAlgorithm::MlDsa44,
2168 ml_dsa_sign::MlDsaAlgorithm::MlDsa44,
2169 ),
2170 (
2171 SignatureAlgorithm::MlDsa65,
2172 ml_dsa_sign::MlDsaAlgorithm::MlDsa65,
2173 ),
2174 (
2175 SignatureAlgorithm::MlDsa87,
2176 ml_dsa_sign::MlDsaAlgorithm::MlDsa87,
2177 ),
2178 ];
2179
2180 const KEM_LEVELS: [KemAlgorithm; 3] = [
2181 KemAlgorithm::MlKem512,
2182 KemAlgorithm::MlKem768,
2183 KemAlgorithm::MlKem1024,
2184 ];
2185
2186 #[tokio::test]
2187 async fn signs_and_verifies_every_ml_dsa_level() {
2188 for (sig_algorithm, dsa_algorithm) in DSA_LEVELS {
2189 let seed = [0x11u8; ml_dsa_sign::SEED_LEN];
2190 let public = ml_dsa_sign::public_from_seed(dsa_algorithm, &seed).expect("public");
2191 let record = custody_record(
2192 dsa_algorithm.token(),
2193 &public,
2194 LocalSoftwareProvider::PROVIDER_VERSION,
2195 );
2196 let backend = CustodyBackend::single(record, &seed);
2197 let provider = LocalSoftwareProvider::new(&backend);
2198
2199 let signature = provider
2200 .sign(SignRequest {
2201 key_id: KEY_ID,
2202 backend_path: PATH,
2203 algorithm: sig_algorithm,
2204 message: b"basil pqc payload",
2205 storage_key: Some(STORAGE_KEY),
2206 })
2207 .await
2208 .expect("sign");
2209 assert!(
2210 provider
2211 .verify(VerifyRequest {
2212 key_id: KEY_ID,
2213 backend_path: PATH,
2214 algorithm: sig_algorithm,
2215 message: b"basil pqc payload",
2216 signature: &signature,
2217 storage_key: Some(STORAGE_KEY),
2218 })
2219 .await
2220 .expect("verify"),
2221 "{} verifies",
2222 dsa_algorithm.token()
2223 );
2224 assert!(
2225 !provider
2226 .verify(VerifyRequest {
2227 key_id: KEY_ID,
2228 backend_path: PATH,
2229 algorithm: sig_algorithm,
2230 message: b"tampered payload",
2231 signature: &signature,
2232 storage_key: Some(STORAGE_KEY),
2233 })
2234 .await
2235 .expect("verify"),
2236 "{} rejects wrong message",
2237 dsa_algorithm.token()
2238 );
2239 }
2240 }
2241
2242 #[tokio::test]
2243 async fn encapsulate_decapsulate_every_ml_kem_level() {
2244 for kem in KEM_LEVELS {
2245 let seed = [0x42u8; ml_kem_envelope::SEED_LEN];
2246 let record = custody_record(
2247 kem_algorithm_name(kem),
2248 &[7u8; 32],
2249 LocalSoftwareProvider::PROVIDER_VERSION,
2250 );
2251 let backend = CustodyBackend::single(record, &seed);
2252 let provider = LocalSoftwareProvider::new(&backend);
2253
2254 let encapsulation = provider
2255 .encapsulate(EncapsulateRequest {
2256 key_id: KEY_ID,
2257 backend_path: PATH,
2258 algorithm: kem,
2259 storage_key: Some(STORAGE_KEY),
2260 })
2261 .await
2262 .expect("encapsulate");
2263 let shared = provider
2264 .decapsulate(DecapsulateRequest {
2265 key_id: KEY_ID,
2266 backend_path: PATH,
2267 algorithm: kem,
2268 encapsulated_key: &encapsulation.encapsulated_key,
2269 storage_key: Some(STORAGE_KEY),
2270 })
2271 .await
2272 .expect("decapsulate");
2273 assert_eq!(
2274 encapsulation.shared_secret.as_slice(),
2275 shared.as_slice(),
2276 "{} shared secret matches",
2277 kem_algorithm_name(kem)
2278 );
2279 }
2280 }
2281
2282 #[tokio::test]
2283 async fn wrap_unwrap_envelope_every_ml_kem_level() {
2284 for kem in KEM_LEVELS {
2285 for envelope_algorithm in [
2286 EnvelopeAlgorithm::Aes256Gcm,
2287 EnvelopeAlgorithm::ChaCha20Poly1305,
2288 ] {
2289 let seed = [0x42u8; ml_kem_envelope::SEED_LEN];
2290 let record = custody_record(
2291 kem_algorithm_name(kem),
2292 &[7u8; 32],
2293 LocalSoftwareProvider::PROVIDER_VERSION,
2294 );
2295 let backend = CustodyBackend::single(record, &seed);
2296 let provider = LocalSoftwareProvider::new(&backend);
2297
2298 let envelope = provider
2299 .wrap_envelope(WrapEnvelopeRequest {
2300 key_id: KEY_ID,
2301 backend_path: PATH,
2302 kem_algorithm: kem,
2303 envelope_algorithm,
2304 plaintext: b"top secret",
2305 aad: Some(b"context"),
2306 storage_key: Some(STORAGE_KEY),
2307 })
2308 .await
2309 .expect("wrap");
2310 assert_eq!(envelope.key_version, 1);
2311 let plaintext = provider
2312 .unwrap_envelope(UnwrapEnvelopeRequest {
2313 key_id: KEY_ID,
2314 backend_path: PATH,
2315 kem_algorithm: kem,
2316 envelope_algorithm,
2317 encapsulated_key: &envelope.encapsulated_key,
2318 nonce: &envelope.nonce,
2319 ciphertext: &envelope.ciphertext,
2320 aad: Some(b"context"),
2321 storage_key: Some(STORAGE_KEY),
2322 })
2323 .await
2324 .expect("unwrap");
2325 assert_eq!(plaintext, b"top secret");
2326
2327 let wrong_aad = provider
2328 .unwrap_envelope(UnwrapEnvelopeRequest {
2329 key_id: KEY_ID,
2330 backend_path: PATH,
2331 kem_algorithm: kem,
2332 envelope_algorithm,
2333 encapsulated_key: &envelope.encapsulated_key,
2334 nonce: &envelope.nonce,
2335 ciphertext: &envelope.ciphertext,
2336 aad: Some(b"wrong"),
2337 storage_key: Some(STORAGE_KEY),
2338 })
2339 .await
2340 .expect_err("wrong aad fails");
2341 assert!(matches!(
2342 wrong_aad,
2343 ProviderError::CryptoFailed {
2344 op: "unwrap_envelope",
2345 ..
2346 }
2347 ));
2348 }
2349 }
2350 }
2351
2352 #[tokio::test]
2353 async fn rejects_non_pqc_signature_algorithm() {
2354 let backend = CustodyBackend::single(Vec::new(), &[]);
2355 let provider = LocalSoftwareProvider::new(&backend);
2356 let err = provider
2357 .sign(SignRequest {
2358 key_id: KEY_ID,
2359 backend_path: PATH,
2360 algorithm: SignatureAlgorithm::Ed25519,
2361 message: b"m",
2362 storage_key: Some(STORAGE_KEY),
2363 })
2364 .await
2365 .expect_err("non-pqc rejected");
2366 assert!(matches!(
2367 err,
2368 ProviderError::Unsupported {
2369 provider: CryptoProviderId::LocalSoftware,
2370 op: "sign",
2371 algorithm: "ed25519"
2372 }
2373 ));
2374 }
2375
2376 #[tokio::test]
2377 async fn malformed_record_fails_closed() {
2378 let backend = CustodyBackend::single(b"not a record".to_vec(), &[0u8; 32]);
2379 let provider = LocalSoftwareProvider::new(&backend);
2380 let err = provider
2381 .sign(SignRequest {
2382 key_id: KEY_ID,
2383 backend_path: PATH,
2384 algorithm: SignatureAlgorithm::MlDsa44,
2385 message: b"m",
2386 storage_key: Some(STORAGE_KEY),
2387 })
2388 .await
2389 .expect_err("malformed record");
2390 assert!(matches!(
2391 err,
2392 ProviderError::CryptoFailed {
2393 provider: CryptoProviderId::LocalSoftware,
2394 op: "sign",
2395 algorithm: "ml-dsa-44",
2396 reason: "malformed software-custody record"
2397 }
2398 ));
2399 }
2400
2401 #[tokio::test]
2402 async fn wrong_provider_version_record_fails_closed() {
2403 let seed = [0x11u8; ml_dsa_sign::SEED_LEN];
2404 let public =
2405 ml_dsa_sign::public_from_seed(ml_dsa_sign::MlDsaAlgorithm::MlDsa65, &seed).expect("pk");
2406 let record = custody_record("ml-dsa-65", &public, "99");
2409 let backend = CustodyBackend::single(record, &seed);
2410 let provider = LocalSoftwareProvider::new(&backend);
2411 let err = provider
2412 .sign(SignRequest {
2413 key_id: KEY_ID,
2414 backend_path: PATH,
2415 algorithm: SignatureAlgorithm::MlDsa65,
2416 message: b"m",
2417 storage_key: Some(STORAGE_KEY),
2418 })
2419 .await
2420 .expect_err("version mismatch");
2421 assert!(matches!(err, ProviderError::CryptoFailed { .. }));
2422 }
2423
2424 #[tokio::test]
2425 async fn record_wrapping_key_must_match_the_catalog_storage_key() {
2426 let seed = [0x11u8; ml_dsa_sign::SEED_LEN];
2427 let public =
2428 ml_dsa_sign::public_from_seed(ml_dsa_sign::MlDsaAlgorithm::MlDsa65, &seed).expect("pk");
2429 let record = custody_record(
2430 "ml-dsa-65",
2431 &public,
2432 LocalSoftwareProvider::PROVIDER_VERSION,
2433 );
2434 let backend = CustodyBackend::single(record, &seed);
2435 let provider = LocalSoftwareProvider::new(&backend);
2436 let err = provider
2440 .sign(SignRequest {
2441 key_id: KEY_ID,
2442 backend_path: PATH,
2443 algorithm: SignatureAlgorithm::MlDsa65,
2444 message: b"m",
2445 storage_key: Some("other-storage-aead"),
2446 })
2447 .await
2448 .expect_err("wrapping-key mismatch");
2449 assert!(matches!(
2450 err,
2451 ProviderError::CryptoFailed {
2452 op: "sign",
2453 reason: "malformed software-custody record",
2454 ..
2455 }
2456 ));
2457 }
2458
2459 #[tokio::test]
2460 async fn use_path_without_a_catalog_storage_key_fails_closed() {
2461 let seed = [0x11u8; ml_dsa_sign::SEED_LEN];
2462 let public =
2463 ml_dsa_sign::public_from_seed(ml_dsa_sign::MlDsaAlgorithm::MlDsa65, &seed).expect("pk");
2464 let record = custody_record(
2465 "ml-dsa-65",
2466 &public,
2467 LocalSoftwareProvider::PROVIDER_VERSION,
2468 );
2469 let backend = CustodyBackend::single(record, &seed);
2470 let provider = LocalSoftwareProvider::new(&backend);
2471 let err = provider
2475 .sign(SignRequest {
2476 key_id: KEY_ID,
2477 backend_path: PATH,
2478 algorithm: SignatureAlgorithm::MlDsa65,
2479 message: b"m",
2480 storage_key: None,
2481 })
2482 .await
2483 .expect_err("missing storage key");
2484 assert!(matches!(
2485 err,
2486 ProviderError::CryptoFailed {
2487 op: "sign",
2488 reason: "software-custody key is missing its storage AEAD key",
2489 ..
2490 }
2491 ));
2492 }
2493
2494 #[tokio::test]
2495 async fn generate_without_storage_key_and_import_fail_closed() {
2496 let backend = CustodyBackend::single(Vec::new(), &[]);
2497 let provider = LocalSoftwareProvider::new(&backend);
2498 let material = basil_proto::KeyMaterial::Ed25519Seed(vec![0u8; 32]);
2499 assert!(matches!(
2502 provider
2503 .generate_key(GenerateKey {
2504 key_id: KEY_ID,
2505 backend_path: PATH,
2506 algorithm: SignatureAlgorithm::MlDsa87,
2507 storage_key: None,
2508 })
2509 .await,
2510 Err(ProviderError::CryptoFailed {
2511 op: "generate_key",
2512 ..
2513 })
2514 ));
2515 assert!(matches!(
2517 provider
2518 .import_key(ImportKey {
2519 key_id: KEY_ID,
2520 backend_path: PATH,
2521 algorithm: SignatureAlgorithm::MlDsa87,
2522 material: &material,
2523 })
2524 .await,
2525 Err(ProviderError::Unsupported {
2526 op: "import_key",
2527 ..
2528 })
2529 ));
2530 }
2531
2532 struct RoundTripBackend {
2538 records: std::sync::Mutex<HashMap<String, Vec<u8>>>,
2539 }
2540
2541 impl RoundTripBackend {
2542 fn new() -> Self {
2543 Self {
2544 records: std::sync::Mutex::new(HashMap::new()),
2545 }
2546 }
2547 }
2548
2549 #[async_trait]
2550 impl Backend for RoundTripBackend {
2551 fn kind(&self) -> &'static str {
2552 "round-trip-custody-test"
2553 }
2554
2555 async fn new_key(&self, _key_type: KeyType) -> Result<NewKey, BackendError> {
2556 Err(BackendError::Unsupported("new_key"))
2557 }
2558
2559 async fn public_key(&self, _key_id: &str) -> Result<Vec<u8>, BackendError> {
2560 Err(BackendError::Unsupported("public_key"))
2561 }
2562
2563 async fn sign(&self, _key_id: &str, _message: &[u8]) -> Result<Vec<u8>, BackendError> {
2564 Err(BackendError::Unsupported("sign"))
2565 }
2566
2567 async fn verify(
2568 &self,
2569 _key_id: &str,
2570 _message: &[u8],
2571 _signature: &[u8],
2572 ) -> Result<bool, BackendError> {
2573 Err(BackendError::Unsupported("verify"))
2574 }
2575
2576 async fn encrypt(
2577 &self,
2578 _key_id: &str,
2579 algorithm: basil_proto::AeadAlgorithm,
2580 plaintext: &[u8],
2581 _aad: Option<&[u8]>,
2582 ) -> Result<CiphertextEnvelope, BackendError> {
2583 Ok(CiphertextEnvelope {
2584 alg: algorithm,
2585 key_version: 1,
2586 nonce: vec![0u8; 12],
2587 ciphertext: plaintext.to_vec(),
2588 })
2589 }
2590
2591 async fn decrypt(
2592 &self,
2593 _key_id: &str,
2594 envelope: &CiphertextEnvelope,
2595 _aad: Option<&[u8]>,
2596 ) -> Result<Vec<u8>, BackendError> {
2597 Ok(envelope.ciphertext.clone())
2598 }
2599
2600 async fn kv_put(&self, key_id: &str, value: &[u8]) -> Result<u32, BackendError> {
2601 self.records
2602 .lock()
2603 .map_err(|_| BackendError::Unsupported("kv_put"))?
2604 .insert(key_id.to_string(), value.to_vec());
2605 Ok(1)
2606 }
2607
2608 async fn kv_get(
2609 &self,
2610 key_id: &str,
2611 _version: Option<u32>,
2612 ) -> Result<KvValue, BackendError> {
2613 let value = self
2614 .records
2615 .lock()
2616 .map_err(|_| BackendError::Unsupported("kv_get"))?
2617 .get(key_id)
2618 .cloned();
2619 value
2620 .map(|value| KvValue { value, version: 1 })
2621 .ok_or(BackendError::Unsupported("kv_get"))
2622 }
2623 }
2624
2625 #[tokio::test]
2626 async fn generate_then_sign_verify_round_trip_every_level() {
2627 for (sig_algorithm, dsa_algorithm) in DSA_LEVELS {
2628 let backend = RoundTripBackend::new();
2629 let provider = LocalSoftwareProvider::new(&backend);
2630 let created = provider
2631 .generate_key(GenerateKey {
2632 key_id: KEY_ID,
2633 backend_path: PATH,
2634 algorithm: sig_algorithm,
2635 storage_key: Some(STORAGE_KEY),
2636 })
2637 .await
2638 .expect("generate");
2639 assert!(
2641 !created.public_key.is_empty(),
2642 "{} public",
2643 dsa_algorithm.token()
2644 );
2645
2646 let signature = provider
2647 .sign(SignRequest {
2648 key_id: KEY_ID,
2649 backend_path: PATH,
2650 algorithm: sig_algorithm,
2651 message: b"provisioned payload",
2652 storage_key: Some(STORAGE_KEY),
2653 })
2654 .await
2655 .expect("sign");
2656 assert!(
2657 provider
2658 .verify(VerifyRequest {
2659 key_id: KEY_ID,
2660 backend_path: PATH,
2661 algorithm: sig_algorithm,
2662 message: b"provisioned payload",
2663 signature: &signature,
2664 storage_key: Some(STORAGE_KEY),
2665 })
2666 .await
2667 .expect("verify"),
2668 "{} round trip verifies",
2669 dsa_algorithm.token()
2670 );
2671 assert!(
2672 ml_dsa_sign::verify(
2673 dsa_algorithm,
2674 &created.public_key,
2675 b"provisioned payload",
2676 &signature,
2677 )
2678 .expect("core verify"),
2679 "{} verifies under returned public",
2680 dsa_algorithm.token()
2681 );
2682 }
2683 }
2684}