Skip to main content

xml_sec/
key_manager.rs

1//! Caller-owned, provider-neutral key inventory and xmlsec key-store import.
2
3use std::collections::HashSet;
4
5#[cfg(test)]
6use crate::xmldsig::DsaSigningKey;
7#[cfg(test)]
8use base64::Engine as _;
9use crypto_bigint::{
10    BoxedUint,
11    modular::{BoxedMontyForm, BoxedMontyParams},
12};
13use der::Decode as _;
14use dsa::{
15    Components as DsaComponents, SigningKey as NativeDsaSigningKey,
16    VerifyingKey as DsaVerifyingKey, pkcs8::EncodePrivateKey as _,
17};
18mod pkcs12_import;
19use crate::rsa_encoding::RsaPrivateKeyEncoding as _;
20#[cfg(feature = "xmlenc")]
21use crate::rsa_encoding::RsaPublicKeyEncoding as _;
22use pkcs8::{DecodePrivateKey as _, EncryptedPrivateKeyInfoRef, PrivateKeyInfoRef};
23use pkcs12_import::Limits as Pkcs12Limits;
24use rsa::RsaPrivateKey;
25#[cfg(feature = "xmlenc")]
26use rsa::RsaPublicKey;
27use x509_parser::prelude::{FromDer as _, X509Certificate};
28use zeroize::Zeroizing;
29
30#[cfg(feature = "xmlenc")]
31use crate::xmldsig::parse::X509PublicKeyInfo;
32use crate::{
33    XmlBackend, XmlDomNode as Node,
34    document::{
35        DocumentParseSettings, XmlParseWorkBudget, parse_borrowed_with_settings_and_budget,
36    },
37    policy::ResourcePolicy,
38    xmldsig::keys::InspectedKeyCandidateBudget,
39    xmldsig::parse::XMLDSIG11_NS,
40    xmldsig::{
41        DefaultKeyResolver, DsigError, HmacSigningKey, HmacVerificationKey, KeyInfo, KeyInfoSource,
42        KeyResolver, KeyResolverConfig, KeyValueInfo, SignatureAlgorithm, SigningKey, VerifyingKey,
43        X509DataInfo, validate_signing_key,
44    },
45};
46
47const XMLSEC_NS: &str = "http://www.aleksey.com/xmlsec/2002";
48const XMLDSIG_NS: &str = "http://www.w3.org/2000/09/xmldsig#";
49
50enum PublicDerFormat {
51    KeyOrCertificate,
52    SubjectPublicKeyInfo,
53}
54
55fn check_selected_public_material(
56    info: &KeyInfo,
57    resources: &ResourcePolicy,
58) -> Result<usize, DsigError> {
59    let mut total = 0_usize;
60    for source in &info.sources {
61        let mut charge = |length: usize| -> Result<(), DsigError> {
62            if length > resources.max_external_resource_bytes {
63                return Err(crate::policy::PolicyViolation::ResourceLimit {
64                    resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
65                    maximum: resources.max_external_resource_bytes,
66                    actual: length,
67                }
68                .into());
69            }
70            total = total.checked_add(length).ok_or({
71                crate::policy::PolicyViolation::ResourceLimit {
72                    resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
73                    maximum: resources.max_external_resource_total_bytes,
74                    actual: usize::MAX,
75                }
76            })?;
77            if total > resources.max_external_resource_total_bytes {
78                return Err(crate::policy::PolicyViolation::ResourceLimit {
79                    resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
80                    maximum: resources.max_external_resource_total_bytes,
81                    actual: total,
82                }
83                .into());
84            }
85            Ok(())
86        };
87        match source {
88            KeyInfoSource::KeyValue(value) => {
89                // One selected key is one resource, irrespective of how many
90                // XML fields encode it. Bound the complete borrowed payload
91                // before resolution materializes its SPKI.
92                let lengths = match value {
93                    KeyValueInfo::Rsa { modulus, exponent } => {
94                        [modulus.len(), exponent.len(), 0, 0, 0, 0]
95                    }
96                    KeyValueInfo::Dsa { p, q, g, y } => [
97                        p.as_ref().map_or(0, Vec::len),
98                        q.as_ref().map_or(0, Vec::len),
99                        g.as_ref().map_or(0, Vec::len),
100                        y.len(),
101                        0,
102                        0,
103                    ],
104                    KeyValueInfo::Dh {
105                        p,
106                        q,
107                        generator,
108                        public,
109                        seed,
110                        pgen_counter,
111                    } => [
112                        p.as_ref().map_or(0, Vec::len),
113                        q.as_ref().map_or(0, Vec::len),
114                        generator.as_ref().map_or(0, Vec::len),
115                        public.len(),
116                        seed.as_ref().map_or(0, Vec::len),
117                        pgen_counter.as_ref().map_or(0, Vec::len),
118                    ],
119                    KeyValueInfo::Ec {
120                        curve_oid,
121                        public_key,
122                    } => [curve_oid.len(), public_key.len(), 0, 0, 0, 0],
123                    KeyValueInfo::InvalidEcKeyValue | KeyValueInfo::Unsupported { .. } => continue,
124                };
125                let length = lengths.into_iter().try_fold(0_usize, |sum, length| {
126                    sum.checked_add(length)
127                        .ok_or(crate::policy::PolicyViolation::ResourceLimit {
128                            resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
129                            maximum: resources.max_external_resource_bytes,
130                            actual: usize::MAX,
131                        })
132                })?;
133                charge(length)?;
134            }
135            KeyInfoSource::DerEncodedKeyValue(bytes) => charge(bytes.len())?,
136            KeyInfoSource::X509Data(data) => {
137                for certificate in &data.certificates {
138                    charge(certificate.len())?;
139                }
140                for crl in &data.crls {
141                    charge(crl.len())?;
142                }
143            }
144            _ => {}
145        }
146    }
147    Ok(total)
148}
149
150/// A named secret imported from an xmlsec key store.
151pub struct StoredSymmetricKey {
152    /// Opaque caller-supplied lookup name.
153    pub name: String,
154    /// XML Security symmetric-key family.
155    pub kind: SymmetricKeyKind,
156    /// Secret bytes, zeroized when the inventory is dropped.
157    pub bytes: Zeroizing<Vec<u8>>,
158    /// Allowed operations.
159    pub usages: KeyUsages,
160}
161
162#[derive(Clone, Copy, Debug, PartialEq, Eq)]
163/// The secret-key family declared by an xmlsec key store.
164pub enum SymmetricKeyKind {
165    /// HMAC signing and verification key.
166    Hmac,
167    /// AES content-encryption key.
168    Aes,
169    /// Three-key TDEA key, available with `legacy-algorithms`.
170    Des,
171}
172
173impl SymmetricKeyKind {
174    /// Whether the compiled mechanism supports this content-key family.
175    pub fn is_encryption_key(self) -> bool {
176        self == Self::Aes || (cfg!(feature = "legacy-algorithms") && self == Self::Des)
177    }
178
179    #[cfg(feature = "xmlenc")]
180    fn accepts_algorithm(self, algorithm: crate::xmlenc::DataEncryptionAlgorithm) -> bool {
181        #[cfg(feature = "legacy-algorithms")]
182        if algorithm == crate::xmlenc::DataEncryptionAlgorithm::TripleDesCbc {
183            return self == Self::Des;
184        }
185        #[cfg(not(feature = "legacy-algorithms"))]
186        let _ = algorithm;
187        self == Self::Aes
188    }
189}
190
191fn des_key_usages(bytes: &[u8]) -> Result<KeyUsages, KeyStoreError> {
192    if cfg!(feature = "legacy-algorithms") && bytes.len() == 24 {
193        Ok(KeyUsages::ENCRYPT.union(KeyUsages::DECRYPT))
194    } else {
195        Err(KeyStoreError::Selection(
196            "unsupported DES key length or mechanism",
197        ))
198    }
199}
200
201#[derive(Default)]
202/// A caller-owned inventory of imported XML Security key material.
203pub struct KeyInventory {
204    /// Total XML entries inspected, including unsupported algorithms.
205    entry_count: usize,
206    /// Supported symmetric keys.
207    symmetric_keys: Vec<StoredSymmetricKey>,
208    /// Supported public keys.
209    public_keys: Vec<StoredPublicKey>,
210    /// Private PKCS#8 keys imported from caller-owned byte sources.
211    private_keys: Vec<StoredPrivateKey>,
212    /// Untrusted certificates available for key lookup or path construction.
213    lookup_certificates: Vec<Vec<u8>>,
214    /// Explicit caller-trusted certificate anchors.
215    trusted_certificates: Vec<Vec<u8>>,
216    /// Caller-supplied DER certificate revocation lists.
217    crls: Vec<Vec<u8>>,
218    material_bytes: usize,
219    kdf_work: usize,
220}
221
222/// Import session for multiple XML key stores under one operation snapshot.
223/// Failed attempts retain their inspection and parser-work charges. Successful
224/// stores are moved into the inventory, never cloned.
225pub struct XmlKeyStoreImporter<'a, P: crate::document::XmlDocumentPolicy> {
226    policy: &'a P,
227    backend: XmlBackend,
228    parse_work: XmlParseWorkBudget,
229    inspected: usize,
230    inventory: KeyInventory,
231    live_material_bytes: usize,
232}
233
234impl<'a, P: crate::document::XmlDocumentPolicy> XmlKeyStoreImporter<'a, P> {
235    /// Bind this session to the caller's immutable policy and XML backend.
236    pub fn new(policy: &'a P, backend: XmlBackend) -> Result<Self, KeyStoreError> {
237        Self::with_live_material(policy, backend, 0)
238    }
239
240    /// Bind import accounting to material already retained by the operation.
241    /// This is usage, not a second policy limit; it remains live until finish.
242    pub fn with_live_material(
243        policy: &'a P,
244        backend: XmlBackend,
245        live_material_bytes: usize,
246    ) -> Result<Self, KeyStoreError> {
247        ensure_resource_policy(policy.resource_policy())?;
248        if live_material_bytes > policy.resource_policy().max_external_resource_total_bytes {
249            return Err(import_resource_limit(
250                crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
251                policy.resource_policy().max_external_resource_total_bytes,
252            ));
253        }
254        Ok(Self {
255            policy,
256            backend,
257            parse_work: XmlParseWorkBudget::from_resources(policy.resource_policy()),
258            inspected: 0,
259            inventory: KeyInventory::default(),
260            live_material_bytes,
261        })
262    }
263
264    /// Import a file without resetting work budgets. Failure leaves stored keys
265    /// unchanged, but does not refund work already performed.
266    pub fn import(&mut self, bytes: &[u8]) -> Result<(), KeyStoreError> {
267        let store = KeyInventory::from_xml_bytes_with_budget(
268            bytes,
269            self.policy,
270            self.backend,
271            &self.parse_work,
272            &mut self.inspected,
273            self.inventory
274                .material_bytes
275                .checked_add(self.live_material_bytes)
276                .ok_or_else(|| {
277                    import_resource_limit(
278                        crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
279                        self.policy
280                            .resource_policy()
281                            .max_external_resource_total_bytes,
282                    )
283                })?,
284        )?;
285        self.inventory.extend(store, self.policy.resource_policy())
286    }
287
288    /// Finish the import session and transfer ownership of the complete inventory.
289    #[must_use]
290    pub fn finish(self) -> KeyInventory {
291        self.inventory
292    }
293}
294
295/// Candidate inspections shared by named signing lookups in one operation.
296#[derive(Default)]
297pub struct SigningLookupBudget {
298    inspected: usize,
299}
300
301/// Operations for which a caller may authorize a key.
302#[derive(Clone, Copy, Debug, PartialEq, Eq)]
303pub enum KeyUsage {
304    /// XMLDSig signing.
305    Sign,
306    /// XMLDSig verification.
307    Verify,
308    /// XMLEnc encryption or key wrapping.
309    Encrypt,
310    /// XMLEnc decryption or key unwrapping.
311    Decrypt,
312}
313
314/// Explicit, immutable allowed-use set for one imported key.
315#[derive(Clone, Copy, Debug, PartialEq, Eq)]
316pub struct KeyUsages(u8);
317
318impl KeyUsages {
319    /// A key usable only for signing.
320    pub const SIGN: Self = Self(1);
321    /// A key usable only for verification.
322    pub const VERIFY: Self = Self(2);
323    /// A key usable only for encryption.
324    pub const ENCRYPT: Self = Self(4);
325    /// A key usable only for decryption.
326    pub const DECRYPT: Self = Self(8);
327
328    /// Combine disjoint permissions explicitly.
329    #[must_use]
330    pub const fn union(self, other: Self) -> Self {
331        Self(self.0 | other.0)
332    }
333
334    /// Test one requested operation.
335    #[must_use]
336    pub const fn allows(self, usage: KeyUsage) -> bool {
337        let bit = match usage {
338            KeyUsage::Sign => Self::SIGN.0,
339            KeyUsage::Verify => Self::VERIFY.0,
340            KeyUsage::Encrypt => Self::ENCRYPT.0,
341            KeyUsage::Decrypt => Self::DECRYPT.0,
342        };
343        self.0 & bit != 0
344    }
345}
346
347/// Private key encoded as provider-neutral PKCS#8 DER.
348pub struct StoredPrivateKey {
349    /// Opaque caller-assigned name.
350    pub name: String,
351    /// Private key bytes; zeroized on drop.
352    pub pkcs8_der: Zeroizing<Vec<u8>>,
353    /// Allowed operations.
354    pub usages: KeyUsages,
355    /// Associated certificates, leaf first when a matching leaf exists.
356    pub certificate_chain: Vec<Vec<u8>>,
357    has_matching_leaf: bool,
358}
359
360impl StoredPrivateKey {
361    /// Import an explicitly selected recipient into the operation's crypto engine.
362    pub fn decapsulation_key(
363        &self,
364        provider: &dyn crate::provider::CryptoProvider,
365        algorithm: crate::provider::KeyEncapsulationAlgorithm,
366        policy: &crate::policy::KeyEstablishmentPolicy,
367        resources: &ResourcePolicy,
368    ) -> Result<std::sync::Arc<dyn crate::provider::KeyDecapsulationKey>, KeyStoreError> {
369        policy.validate()?;
370        policy.check_encapsulation(algorithm)?;
371        if !self.usages.allows(KeyUsage::Decrypt) {
372            return Err(KeyStoreError::Selection(
373                "key is not authorized for decapsulation",
374            ));
375        }
376        check_operation_material_size(self.pkcs8_der.len(), resources)?;
377        provider
378            .import_decapsulation_key(algorithm, &self.pkcs8_der)
379            .map_err(KeyStoreError::Provider)
380    }
381
382    /// Return the chain only when its first certificate matches this private key.
383    #[must_use]
384    pub fn matching_certificate_chain(&self) -> Option<&[Vec<u8>]> {
385        self.has_matching_leaf.then_some(&self.certificate_chain)
386    }
387}
388
389/// A named public key imported from an xmlsec key store.
390pub struct StoredPublicKey {
391    /// Opaque caller-supplied lookup name.
392    pub name: String,
393    /// Parsed XMLDSig key material.
394    pub key_info: KeyInfo,
395    /// Allowed operations.
396    pub usages: KeyUsages,
397}
398
399impl StoredPublicKey {
400    /// Import one named ML-KEM recipient; document hints never grant permission.
401    pub fn encapsulation_key(
402        &self,
403        provider: &dyn crate::provider::CryptoProvider,
404        algorithm: crate::provider::KeyEncapsulationAlgorithm,
405        policy: &crate::policy::KeyEstablishmentPolicy,
406        resources: &ResourcePolicy,
407    ) -> Result<std::sync::Arc<dyn crate::provider::KeyEncapsulationKey>, KeyStoreError> {
408        policy.validate()?;
409        policy.check_encapsulation(algorithm)?;
410        if !self.usages.allows(KeyUsage::Encrypt) {
411            return Err(KeyStoreError::Selection(
412                "key is not authorized for encapsulation",
413            ));
414        }
415        let der = self
416            .key_info
417            .sources
418            .iter()
419            .find_map(|source| match source {
420                KeyInfoSource::DerEncodedKeyValue(der) => Some(der.as_slice()),
421                _ => None,
422            })
423            .ok_or(KeyStoreError::Selection(
424                "recipient requires an ML-KEM SPKI",
425            ))?;
426        check_operation_material_size(der.len(), resources)?;
427        provider
428            .import_encapsulation_key(algorithm, der)
429            .map_err(KeyStoreError::Provider)
430    }
431
432    /// Decode this already-selected RSA recipient without searching the inventory again.
433    #[cfg(feature = "xmlenc")]
434    pub fn rsa_encryption_key(
435        &self,
436        policy: &crate::policy::EncryptionPolicy,
437    ) -> Result<RsaPublicKey, KeyStoreError> {
438        policy.validate()?;
439        if !self.usages.allows(KeyUsage::Encrypt) {
440            return Err(KeyStoreError::Selection(
441                "key is not authorized for encryption",
442            ));
443        }
444        for source in &self.key_info.sources {
445            return match source {
446                KeyInfoSource::KeyValue(KeyValueInfo::Rsa { modulus, exponent }) => {
447                    rsa_recipient_from_components(modulus, exponent, policy)
448                }
449                KeyInfoSource::DerEncodedKeyValue(der) => {
450                    check_encryption_material_size(der.len(), &policy.resources)?;
451                    let (rest, spki) = x509_parser::x509::SubjectPublicKeyInfo::from_der(der)
452                        .map_err(|_| KeyStoreError::Selection("invalid RSA encryption key"))?;
453                    if !rest.is_empty() {
454                        return Err(KeyStoreError::Selection("invalid RSA encryption key"));
455                    }
456                    let x509_parser::public_key::PublicKey::RSA(raw) = spki
457                        .parsed()
458                        .map_err(|_| KeyStoreError::Selection("invalid RSA encryption key"))?
459                    else {
460                        return Err(KeyStoreError::Selection(
461                            "named key is not an RSA public key",
462                        ));
463                    };
464                    rsa_recipient_preflight(raw.modulus, raw.exponent, policy)?;
465                    RsaPublicKey::from_public_key_der(der)
466                        .map_err(|_| KeyStoreError::Selection("invalid RSA encryption key"))
467                }
468                KeyInfoSource::X509Data(data) if data.parsed_certificates.len() == 1 => {
469                    if let Some(certificate) = data.certificates.first() {
470                        check_encryption_material_size(certificate.len(), &policy.resources)?;
471                    }
472                    match &data.parsed_certificates[0].public_key {
473                        X509PublicKeyInfo::Rsa { modulus, exponent } => {
474                            rsa_recipient_from_components(modulus, exponent, policy)
475                        }
476                        _ => Err(KeyStoreError::Selection("certificate does not contain RSA")),
477                    }
478                }
479                _ => continue,
480            };
481        }
482        Err(KeyStoreError::Selection(
483            "named key is not an RSA public key",
484        ))
485    }
486}
487
488/// Policy-aware verification adapter over a caller-owned inventory.
489pub struct InventoryVerificationResolver<'a> {
490    inventory: &'a KeyInventory,
491}
492
493impl<'a> KeyResolver for InventoryVerificationResolver<'a> {
494    fn resolve<'k>(
495        &'k self,
496        key_info: Option<&KeyInfo>,
497        algorithm: SignatureAlgorithm,
498    ) -> Result<Option<Box<dyn VerifyingKey + 'k>>, DsigError> {
499        self.resolve_with_policy_and_provider(
500            key_info,
501            algorithm,
502            &crate::policy::VerificationPolicy::default(),
503            crate::provider::default_provider(),
504        )
505    }
506
507    fn resolve_with_policy<'k>(
508        &'k self,
509        key_info: Option<&KeyInfo>,
510        algorithm: SignatureAlgorithm,
511        policy: &crate::policy::VerificationPolicy,
512    ) -> Result<Option<Box<dyn VerifyingKey + 'k>>, DsigError> {
513        self.resolve_with_policy_and_provider(
514            key_info,
515            algorithm,
516            policy,
517            crate::provider::default_provider(),
518        )
519    }
520
521    fn resolve_with_policy_and_provider<'k>(
522        &'k self,
523        key_info: Option<&KeyInfo>,
524        algorithm: SignatureAlgorithm,
525        policy: &crate::policy::VerificationPolicy,
526        provider: &dyn crate::provider::CryptoProvider,
527    ) -> Result<Option<Box<dyn VerifyingKey + 'k>>, DsigError> {
528        self.resolve_for_verification(key_info, algorithm, policy, provider)
529            .map(|key| key.map(crate::xmldsig::ResolvedVerificationKey::into_key))
530    }
531
532    fn resolve_for_verification<'k>(
533        &'k self,
534        key_info: Option<&KeyInfo>,
535        algorithm: SignatureAlgorithm,
536        policy: &crate::policy::VerificationPolicy,
537        provider: &dyn crate::provider::CryptoProvider,
538    ) -> Result<Option<crate::xmldsig::ResolvedVerificationKey<'k>>, DsigError> {
539        policy.validate()?;
540        if let Some(info) = key_info {
541            crate::xmldsig::keys::validate_key_info_source_permissions(info, policy.key_sources)?;
542        }
543        let mut candidate: Option<&StoredPublicKey> = None;
544        let mut secret_candidate: Option<&StoredSymmetricKey> = None;
545        let mut inspected_candidates = InspectedKeyCandidateBudget::new(policy);
546        for name in key_info
547            .into_iter()
548            .flat_map(|info| &info.sources)
549            .filter_map(|source| match source {
550                KeyInfoSource::KeyName(name) => Some(name.as_str()),
551                _ => None,
552            })
553        {
554            if self.inventory.symmetric_keys.is_empty() && self.inventory.public_keys.is_empty() {
555                inspected_candidates.charge()?;
556            }
557            let mut symmetric_match = None;
558            for entry in &self.inventory.symmetric_keys {
559                inspected_candidates.charge()?;
560                if entry.name == name {
561                    symmetric_match = Some(entry);
562                    break;
563                }
564            }
565            if let Some(found) = symmetric_match {
566                if !found.usages.allows(KeyUsage::Verify) || found.kind != SymmetricKeyKind::Hmac {
567                    return Err(DsigError::InvalidStructure {
568                        reason: "named key is not authorized for verification",
569                    });
570                }
571                if candidate.is_some()
572                    || secret_candidate.is_some_and(|previous| !core::ptr::eq(previous, found))
573                {
574                    return Err(DsigError::InvalidStructure {
575                        reason: "more than one named verification key matches",
576                    });
577                }
578                secret_candidate = Some(found);
579                continue;
580            }
581            let mut public_match = None;
582            for entry in &self.inventory.public_keys {
583                inspected_candidates.charge()?;
584                if entry.name == name {
585                    public_match = Some(entry);
586                    break;
587                }
588            }
589            if let Some(found) = public_match {
590                if !found.usages.allows(KeyUsage::Verify) {
591                    return Err(DsigError::InvalidStructure {
592                        reason: "named key is not authorized for verification",
593                    });
594                }
595                if secret_candidate.is_some()
596                    || candidate.is_some_and(|previous| !core::ptr::eq(previous, found))
597                {
598                    return Err(DsigError::InvalidStructure {
599                        reason: "more than one named verification key matches",
600                    });
601                }
602                candidate = Some(found);
603            }
604        }
605        if let Some(candidate) = secret_candidate {
606            if algorithm.hmac_output_bits().is_none() {
607                return Err(DsigError::InvalidStructure {
608                    reason: "named HMAC key is incompatible with signature method",
609                });
610            }
611            for (resource, maximum) in [
612                (
613                    crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
614                    policy.resources.max_external_resource_bytes,
615                ),
616                (
617                    crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
618                    policy.resources.max_external_resource_total_bytes,
619                ),
620            ] {
621                if candidate.bytes.len() > maximum {
622                    return Err(crate::policy::PolicyViolation::ResourceLimit {
623                        resource,
624                        maximum,
625                        actual: candidate.bytes.len(),
626                    }
627                    .into());
628                }
629            }
630            let key = HmacVerificationKey::new(candidate.bytes.to_vec()).map_err(|_| {
631                DsigError::InvalidStructure {
632                    reason: "invalid named HMAC key",
633                }
634            })?;
635            return Ok(Some(
636                crate::xmldsig::ResolvedVerificationKey::CallerTrusted(Box::new(key)),
637            ));
638        }
639        let selected_material_bytes = candidate
640            .map(|candidate| check_selected_public_material(&candidate.key_info, &policy.resources))
641            .transpose()?
642            .unwrap_or(0);
643        let selected_info = candidate.map_or(key_info, |entry| Some(&entry.key_info));
644        let configured_x509_index = selected_info.and_then(|info| {
645            info.sources.iter().position(|source| match source {
646                KeyInfoSource::X509Data(data) if data.certificate_chain.is_empty() => {
647                    crate::xmldsig::parse::x509_data_has_lookup_identifiers(data)
648                }
649                KeyInfoSource::X509Data(_) => {
650                    policy.key_trust.verify_x509_chains
651                        || !self.inventory.trusted_certificates.is_empty()
652                }
653                _ => false,
654            })
655        });
656        // Try only sources preceding the first configured-X.509 use without
657        // inspecting or copying inventory certificates that may never be used.
658        let mut prefix_error = None;
659        if let Some(info) = selected_info
660            && let Some(first_x509) = configured_x509_index
661            && first_x509 != 0
662        {
663            let prefix_resolver = DefaultKeyResolver::new(KeyResolverConfig::default());
664            let outcome = prefix_resolver.resolve_sources_with_candidate_budget(
665                info,
666                algorithm,
667                policy,
668                provider,
669                &mut inspected_candidates,
670                if candidate.is_some() {
671                    crate::xmldsig::keys::ResolutionScope::TrustedPrefix(first_x509)
672                } else {
673                    crate::xmldsig::keys::ResolutionScope::DocumentPrefix(first_x509)
674                },
675            )?;
676            if let Some(key) = outcome.key {
677                return Ok(Some(key));
678            }
679            prefix_error = outcome.deferred_error;
680        }
681        let fallback = if configured_x509_index.is_some() {
682            let certificates = self
683                .inventory
684                .lookup_certificates
685                .iter()
686                .chain(&self.inventory.trusted_certificates);
687            // Selecting a trusted named key substitutes key material, not
688            // document revocation evidence. Retain CRLs without importing any
689            // document certificate into the trusted candidate's chain.
690            let document_crls = key_info
691                .filter(|_| {
692                    candidate.is_some()
693                        && policy.key_trust.check_crls
694                        && policy.key_trust.verify_x509_chains
695                })
696                .into_iter()
697                .flat_map(|info| &info.sources)
698                .filter_map(|source| match source {
699                    KeyInfoSource::X509Data(data) => Some(data.crls.as_slice()),
700                    _ => None,
701                })
702                .flatten();
703            let crls = self
704                .inventory
705                .crls
706                .iter()
707                .chain(document_crls)
708                .filter(|_| policy.key_trust.check_crls && policy.key_trust.verify_x509_chains);
709            let mut total = selected_material_bytes;
710            for material in certificates.chain(crls.clone()) {
711                if material.len() > policy.resources.max_external_resource_bytes {
712                    return Err(crate::policy::PolicyViolation::ResourceLimit {
713                        resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
714                        maximum: policy.resources.max_external_resource_bytes,
715                        actual: material.len(),
716                    }
717                    .into());
718                }
719                // The owned resolver config and its borrowed inventory/document
720                // input coexist. Reserve both payloads before any config clone;
721                // this is live-memory policy, not a certificate syntax rule.
722                for _ in 0..2 {
723                    debug_assert!(total <= policy.resources.max_external_resource_total_bytes);
724                    if material.len() > policy.resources.max_external_resource_total_bytes - total {
725                        return Err(crate::policy::PolicyViolation::ResourceLimit {
726                            resource:
727                                crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
728                            maximum: policy.resources.max_external_resource_total_bytes,
729                            actual: total.saturating_add(material.len()),
730                        }
731                        .into());
732                    }
733                    total += material.len();
734                }
735            }
736            DefaultKeyResolver::new(KeyResolverConfig {
737                lookup_certs: self.inventory.lookup_certificates.clone(),
738                trusted_certs: self.inventory.trusted_certificates.clone(),
739                crls: crls.cloned().collect(),
740                ..KeyResolverConfig::default()
741            })
742        } else {
743            DefaultKeyResolver::new(KeyResolverConfig::default())
744        };
745        if let Some(first_x509) = configured_x509_index
746            && let Some(info) = selected_info
747        {
748            // Resume after the inspected prefix: one budget counts actual
749            // work, not a replay caused by attaching configured certificates.
750            let mut outcome = fallback.resolve_sources_with_candidate_budget(
751                info,
752                algorithm,
753                policy,
754                provider,
755                &mut inspected_candidates,
756                if candidate.is_some() {
757                    crate::xmldsig::keys::ResolutionScope::TrustedSuffix(first_x509)
758                } else {
759                    crate::xmldsig::keys::ResolutionScope::DocumentSuffix(first_x509)
760                },
761            )?;
762            // Terminal suffix failures have already propagated. On a complete
763            // miss, keep the first deferred error in original source order.
764            if outcome.key.is_none()
765                && let Some(error) = prefix_error
766            {
767                outcome.deferred_error = Some(error);
768            }
769            return outcome.finish();
770        }
771        if let Some(candidate) = candidate {
772            return fallback.resolve_trusted_material_with_candidate_budget(
773                &candidate.key_info,
774                algorithm,
775                policy,
776                provider,
777                &mut inspected_candidates,
778            );
779        }
780        fallback.resolve_with_candidate_budget(
781            key_info,
782            algorithm,
783            policy,
784            provider,
785            &mut inspected_candidates,
786        )
787    }
788
789    fn consumes_document_key_info(&self) -> bool {
790        true
791    }
792}
793
794enum ParsedMaterial {
795    Symmetric(SymmetricKeyKind, Zeroizing<Vec<u8>>),
796    Public(KeyValueInfo),
797    PublicDer(Vec<u8>),
798    Dsa(KeyValueInfo, Option<Zeroizing<Vec<u8>>>),
799    Unsupported,
800}
801
802type ParsedDsaKey = (KeyValueInfo, Option<Zeroizing<Vec<u8>>>);
803
804#[cfg(feature = "xmlenc")]
805struct InventoryDirectSymmetric {
806    bytes: Zeroizing<Vec<u8>>,
807    kind: SymmetricKeyKind,
808}
809
810#[cfg(feature = "xmlenc")]
811impl crate::xmlenc::DecryptionKeyResolver for InventoryDirectSymmetric {
812    fn resolve_key(
813        &self,
814        _provider: &dyn crate::provider::CryptoProvider,
815        algorithm: crate::xmlenc::DataEncryptionAlgorithm,
816        encrypted_key: Option<&crate::xmlenc::EncryptedKey>,
817    ) -> Result<Vec<u8>, crate::xmlenc::XmlEncError> {
818        if encrypted_key.is_some() || !self.kind.accepts_algorithm(algorithm) {
819            return Err(crate::xmlenc::XmlEncError::KeyNotFound);
820        }
821        crate::xmlenc::validate_key_len(algorithm, &self.bytes)?;
822        Ok(self.bytes.to_vec())
823    }
824
825    fn resolve_key_candidates(
826        &self,
827        provider: &dyn crate::provider::CryptoProvider,
828        algorithm: crate::xmlenc::DataEncryptionAlgorithm,
829        encrypted_key: Option<&crate::xmlenc::EncryptedKey>,
830        budget: &mut crate::xmlenc::KeyCandidateBudget,
831    ) -> Result<Vec<Vec<u8>>, crate::xmlenc::XmlEncError> {
832        // This inventory entry is a content key, not a transport key.
833        // Ineligible recipient paths neither copy it nor consume candidates.
834        if encrypted_key.is_some() {
835            return Err(crate::xmlenc::XmlEncError::KeyNotFound);
836        }
837        budget.consume(1)?;
838        self.resolve_key(provider, algorithm, None)
839            .map(|key| vec![key])
840    }
841
842    fn resolve_key_candidates_with_policy(
843        &self,
844        provider: &dyn crate::provider::CryptoProvider,
845        algorithm: crate::xmlenc::DataEncryptionAlgorithm,
846        encrypted_key: Option<&crate::xmlenc::EncryptedKey>,
847        policy: &crate::policy::DecryptionPolicy,
848        budget: &mut crate::xmlenc::KeyCandidateBudget,
849    ) -> Result<Vec<Vec<u8>>, crate::xmlenc::XmlEncError> {
850        policy.validate()?;
851        if encrypted_key.is_some() {
852            return Err(crate::xmlenc::XmlEncError::KeyNotFound);
853        }
854        check_selected_material_size(self.bytes.len(), &policy.resources).map_err(|error| {
855            match error {
856                KeyStoreError::Policy(violation) => crate::xmlenc::XmlEncError::Policy(violation),
857                other => crate::xmlenc::XmlEncError::InvalidStructure(other.to_string()),
858            }
859        })?;
860        self.resolve_key_candidates(provider, algorithm, None, budget)
861    }
862}
863
864#[derive(Debug, thiserror::Error)]
865/// Key-store import errors that never include secret material.
866pub enum KeyStoreError {
867    /// The selected cryptographic engine failed; do not retry another engine.
868    #[error(transparent)]
869    Provider(#[from] crate::provider::ProviderError),
870    /// The XML structure or key material was invalid.
871    #[error("invalid xmlsec keys.xml: {0}")]
872    Invalid(String),
873    /// The named key is missing, duplicated, or incompatible with the requested operation.
874    #[error("key inventory selection failed: {0}")]
875    Selection(&'static str),
876    /// The active operation policy rejected the key or its resources.
877    #[error("key inventory policy violation: {0}")]
878    Policy(#[from] crate::policy::PolicyViolation),
879    /// A protected container could not be decoded with the supplied password.
880    #[error("protected key container could not be decoded")]
881    ProtectedContainer,
882}
883
884impl KeyInventory {
885    /// Work reserved before password derivation, including unsuccessful imports.
886    /// An operation importing through temporary inventories must carry this
887    /// usage forward rather than resetting its remaining KDF allowance.
888    #[must_use]
889    pub fn key_import_kdf_work(&self) -> usize {
890        self.kdf_work
891    }
892
893    fn retained_material_bytes(&self) -> Result<usize, KeyStoreError> {
894        let mut total = 0_usize;
895        let mut add = |length: usize| -> Result<(), KeyStoreError> {
896            total = total
897                .checked_add(length)
898                .ok_or(KeyStoreError::Selection("key material size overflow"))?;
899            Ok(())
900        };
901        for key in &self.symmetric_keys {
902            add(key.name.len())?;
903            add(key.bytes.len())?;
904        }
905        for key in &self.private_keys {
906            add(key.name.len())?;
907            add(key.pkcs8_der.len())?;
908            for certificate in &key.certificate_chain {
909                add(certificate.len())?;
910            }
911        }
912        for key in &self.public_keys {
913            add(key.name.len())?;
914            for source in &key.key_info.sources {
915                match source {
916                    KeyInfoSource::KeyName(name) => add(name.len())?,
917                    KeyInfoSource::KeyValue(KeyValueInfo::Dsa { p, q, g, y }) => {
918                        add(p.as_ref().map_or(0, Vec::len))?;
919                        add(q.as_ref().map_or(0, Vec::len))?;
920                        add(g.as_ref().map_or(0, Vec::len))?;
921                        add(y.len())?;
922                    }
923                    KeyInfoSource::KeyValue(KeyValueInfo::Rsa { modulus, exponent }) => {
924                        add(modulus.len())?;
925                        add(exponent.len())?;
926                    }
927                    KeyInfoSource::KeyValue(KeyValueInfo::Ec {
928                        curve_oid,
929                        public_key,
930                    }) => {
931                        add(curve_oid.len())?;
932                        add(public_key.len())?;
933                    }
934                    KeyInfoSource::DerEncodedKeyValue(bytes) => add(bytes.len())?,
935                    _ => {}
936                }
937            }
938        }
939        for certificate in self
940            .lookup_certificates
941            .iter()
942            .chain(&self.trusted_certificates)
943        {
944            add(certificate.len())?;
945        }
946        for crl in &self.crls {
947            add(crl.len())?;
948        }
949        Ok(total)
950    }
951
952    /// Number of imported candidates, including unsupported XML entries.
953    #[must_use]
954    pub fn entry_count(&self) -> usize {
955        self.entry_count
956    }
957
958    /// Imported symmetric keys, without mutable access to inventory bounds.
959    #[must_use]
960    pub fn symmetric_keys(&self) -> &[StoredSymmetricKey] {
961        &self.symmetric_keys
962    }
963
964    /// Imported public keys, without mutable access to inventory bounds.
965    #[must_use]
966    pub fn public_keys(&self) -> &[StoredPublicKey] {
967        &self.public_keys
968    }
969
970    /// Imported private keys, without mutable access to inventory bounds.
971    #[must_use]
972    pub fn private_keys(&self) -> &[StoredPrivateKey] {
973        &self.private_keys
974    }
975
976    /// Combine two caller-owned imports after checking aggregate bytes,
977    /// candidates, KDF work, and cross-store name collisions before mutation.
978    pub fn extend(
979        &mut self,
980        mut other: Self,
981        resources: &ResourcePolicy,
982    ) -> Result<(), KeyStoreError> {
983        ensure_resource_policy(resources)?;
984        let work = self
985            .kdf_work
986            .checked_add(other.kdf_work)
987            .filter(|work| *work <= resources.max_key_import_kdf_work)
988            .ok_or_else(|| {
989                import_resource_limit(
990                    crate::policy::resource_name::KEY_IMPORT_KDF_WORK,
991                    resources.max_key_import_kdf_work,
992                )
993            })?;
994        let candidates = self
995            .entry_count
996            .checked_add(other.entry_count)
997            .ok_or(KeyStoreError::Selection("key candidate count overflow"))?;
998        if candidates > resources.max_key_candidates {
999            return Err(import_resource_limit(
1000                crate::policy::resource_name::KEY_CANDIDATES,
1001                resources.max_key_candidates,
1002            ));
1003        }
1004        let bytes = self
1005            .material_bytes
1006            .checked_add(other.material_bytes)
1007            .ok_or(KeyStoreError::Selection("key material size overflow"))?;
1008        if bytes > resources.max_external_resource_total_bytes {
1009            return Err(import_resource_limit(
1010                crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
1011                resources.max_external_resource_total_bytes,
1012            ));
1013        }
1014        let mut names = HashSet::new();
1015        for name in other
1016            .symmetric_keys
1017            .iter()
1018            .map(|entry| entry.name.as_str())
1019            .chain(other.public_keys.iter().map(|entry| entry.name.as_str()))
1020            .chain(other.private_keys.iter().map(|entry| entry.name.as_str()))
1021        {
1022            names.insert(name);
1023        }
1024        if names.iter().any(|name| {
1025            self.symmetric_keys.iter().any(|entry| entry.name == *name)
1026                || self.public_keys.iter().any(|entry| entry.name == *name)
1027                || self.private_keys.iter().any(|entry| entry.name == *name)
1028        }) {
1029            return Err(KeyStoreError::Selection("duplicate key name"));
1030        }
1031        self.entry_count = candidates;
1032        self.material_bytes = bytes;
1033        self.kdf_work = work;
1034        self.symmetric_keys.append(&mut other.symmetric_keys);
1035        self.public_keys.append(&mut other.public_keys);
1036        self.private_keys.append(&mut other.private_keys);
1037        self.lookup_certificates
1038            .append(&mut other.lookup_certificates);
1039        self.trusted_certificates
1040            .append(&mut other.trusted_certificates);
1041        self.crls.append(&mut other.crls);
1042        Ok(())
1043    }
1044
1045    /// Select an authorized named RSA recipient from XMLDSig RSAKeyValue or
1046    /// DER SubjectPublicKeyInfo without introducing an implicit key source.
1047    #[cfg(feature = "xmlenc")]
1048    pub fn rsa_encryption_key(
1049        &self,
1050        name: &str,
1051        policy: &crate::policy::EncryptionPolicy,
1052    ) -> Result<RsaPublicKey, KeyStoreError> {
1053        policy.validate()?;
1054        let entry = find_named_entry(
1055            &self.public_keys,
1056            name,
1057            &policy.resources,
1058            &mut 0,
1059            |entry| &entry.name,
1060        )?
1061        .ok_or(KeyStoreError::Selection("named encryption key not found"))?;
1062        entry.rsa_encryption_key(policy)
1063    }
1064
1065    /// Select a named signer under the operation's immutable policy.
1066    pub fn signing_key(
1067        &self,
1068        name: &str,
1069        algorithm: SignatureAlgorithm,
1070        policy: &crate::policy::SigningPolicy,
1071    ) -> Result<Box<dyn SigningKey>, KeyStoreError> {
1072        self.signing_key_with_budget(name, algorithm, policy, &mut SigningLookupBudget::default())
1073    }
1074
1075    /// Select a named signer while sharing lookup work across caller retries.
1076    pub fn signing_key_with_budget(
1077        &self,
1078        name: &str,
1079        algorithm: SignatureAlgorithm,
1080        policy: &crate::policy::SigningPolicy,
1081        budget: &mut SigningLookupBudget,
1082    ) -> Result<Box<dyn SigningKey>, KeyStoreError> {
1083        self.signing_key_with_provider_and_budget(
1084            name,
1085            algorithm,
1086            policy,
1087            crate::provider::default_provider(),
1088            budget,
1089        )
1090    }
1091
1092    /// Select an authorized key and import it into the explicitly chosen engine.
1093    pub fn signing_key_with_provider(
1094        &self,
1095        name: &str,
1096        algorithm: SignatureAlgorithm,
1097        policy: &crate::policy::SigningPolicy,
1098        provider: &dyn crate::provider::CryptoProvider,
1099    ) -> Result<Box<dyn SigningKey>, KeyStoreError> {
1100        self.signing_key_with_provider_and_budget(
1101            name,
1102            algorithm,
1103            policy,
1104            provider,
1105            &mut SigningLookupBudget::default(),
1106        )
1107    }
1108
1109    /// Provider-aware selection sharing the operation's retry budget.
1110    pub fn signing_key_with_provider_and_budget(
1111        &self,
1112        name: &str,
1113        algorithm: SignatureAlgorithm,
1114        policy: &crate::policy::SigningPolicy,
1115        provider: &dyn crate::provider::CryptoProvider,
1116        budget: &mut SigningLookupBudget,
1117    ) -> Result<Box<dyn SigningKey>, KeyStoreError> {
1118        policy.validate()?;
1119        policy.check_signature_algorithm(algorithm)?;
1120        provider
1121            .require_capability(crate::provider::ProviderCapability::Sign(algorithm))
1122            .map_err(|_| KeyStoreError::Selection("provider does not support signing method"))?;
1123        if algorithm.hmac_output_bits().is_some() {
1124            let entry = find_named_entry(
1125                &self.symmetric_keys,
1126                name,
1127                &policy.resources,
1128                &mut budget.inspected,
1129                |entry| &entry.name,
1130            )?
1131            .ok_or(KeyStoreError::Selection("named signing key not found"))?;
1132            if entry.kind != SymmetricKeyKind::Hmac || !entry.usages.allows(KeyUsage::Sign) {
1133                return Err(KeyStoreError::Selection(
1134                    "key is not authorized for signing",
1135                ));
1136            }
1137            check_operation_material_size(entry.bytes.len(), &policy.resources)?;
1138            let key = HmacSigningKey::new(entry.bytes.to_vec())
1139                .map_err(|_| KeyStoreError::Selection("invalid HMAC key"))?;
1140            validate_signing_key(&key, algorithm, policy).map_err(signing_policy_error)?;
1141            return Ok(Box::new(key));
1142        }
1143        let entry = find_named_entry(
1144            &self.private_keys,
1145            name,
1146            &policy.resources,
1147            &mut budget.inspected,
1148            |entry| &entry.name,
1149        )?
1150        .ok_or(KeyStoreError::Selection("named signing key not found"))?;
1151        if !entry.usages.allows(KeyUsage::Sign) {
1152            return Err(KeyStoreError::Selection(
1153                "key is not authorized for signing",
1154            ));
1155        }
1156        let der = entry.pkcs8_der.as_slice();
1157        check_operation_material_size(der.len(), &policy.resources)?;
1158        if let Ok(info) = PrivateKeyInfoRef::try_from(der)
1159            && info.algorithm.oid == dsa::OID
1160        {
1161            preflight_dsa_pkcs8_components(&info)?;
1162        }
1163        let key = provider
1164            .import_signing_key(algorithm, der)
1165            .map_err(|_| KeyStoreError::Selection("incompatible signing key for provider"))?;
1166        validate_signing_key(key.as_ref(), algorithm, policy).map_err(signing_policy_error)?;
1167        Ok(key)
1168    }
1169
1170    /// Select a named direct symmetric, RSA transport, or ML-KEM recipient key.
1171    #[cfg(feature = "xmlenc")]
1172    pub fn decryption_resolver(
1173        &self,
1174        name: &str,
1175        policy: &crate::policy::DecryptionPolicy,
1176    ) -> Result<Box<dyn crate::xmlenc::DecryptionKeyResolver>, KeyStoreError> {
1177        self.decryption_resolver_with_provider(name, policy, crate::provider::default_provider())
1178    }
1179
1180    /// Select an authorized recovery key owned by the chosen native engine.
1181    #[cfg(feature = "xmlenc")]
1182    pub fn decryption_resolver_with_provider(
1183        &self,
1184        name: &str,
1185        policy: &crate::policy::DecryptionPolicy,
1186        provider: &dyn crate::provider::CryptoProvider,
1187    ) -> Result<Box<dyn crate::xmlenc::DecryptionKeyResolver>, KeyStoreError> {
1188        policy.validate()?;
1189        let mut visited = 0;
1190        if let Some(entry) = find_named_entry(
1191            &self.symmetric_keys,
1192            name,
1193            &policy.resources,
1194            &mut visited,
1195            |entry| &entry.name,
1196        )? {
1197            if !entry.kind.is_encryption_key() || !entry.usages.allows(KeyUsage::Decrypt) {
1198                return Err(KeyStoreError::Selection(
1199                    "key is not authorized for decryption",
1200                ));
1201            }
1202            check_selected_material_size(entry.bytes.len(), &policy.resources)?;
1203            return Ok(Box::new(InventoryDirectSymmetric {
1204                bytes: Zeroizing::new(entry.bytes.to_vec()),
1205                kind: entry.kind,
1206            }));
1207        }
1208        let entry = find_named_entry(
1209            &self.private_keys,
1210            name,
1211            &policy.resources,
1212            &mut visited,
1213            |entry| &entry.name,
1214        )?
1215        .ok_or(KeyStoreError::Selection("named decryption key not found"))?;
1216        if !entry.usages.allows(KeyUsage::Decrypt) {
1217            return Err(KeyStoreError::Selection(
1218                "key is not authorized for decryption",
1219            ));
1220        }
1221        check_selected_material_size(entry.pkcs8_der.len(), &policy.resources)?;
1222        // Borrow public PKCS#1 components before bigint decoding: import
1223        // permission is not an exemption from the decryption snapshot.
1224        let info = PrivateKeyInfoRef::try_from(entry.pkcs8_der.as_slice())
1225            .map_err(|_| KeyStoreError::Selection("incompatible RSA decryption key"))?;
1226        if let Some(algorithm) =
1227            crate::provider::KeyEncapsulationAlgorithm::from_oid(info.algorithm.oid)
1228        {
1229            let key = entry.decapsulation_key(
1230                provider,
1231                algorithm,
1232                &policy.key_establishment,
1233                &policy.resources,
1234            )?;
1235            return Ok(Box::new(
1236                crate::xmlenc::EncapsulationDecryptor::provider_key(key),
1237            ));
1238        }
1239        let components = pkcs1::RsaPrivateKeyRef::from_der(info.private_key.as_bytes())
1240            .map_err(|_| KeyStoreError::Selection("incompatible RSA decryption key"))?;
1241        policy.rsa_keys.validate_components(
1242            "decryption",
1243            components.modulus.as_bytes(),
1244            components.public_exponent.as_bytes(),
1245        )?;
1246        let key = provider
1247            .import_recovery_key(&entry.pkcs8_der)
1248            .map_err(|_| KeyStoreError::Selection("incompatible RSA decryption key"))?;
1249        Ok(Box::new(crate::xmlenc::PrivateKeyDecryptor::provider_key(
1250            key,
1251        )))
1252    }
1253    /// Build a resolver from this inventory and one immutable key-store snapshot.
1254    /// Trust anchors are copied once, not on each candidate lookup.
1255    #[must_use]
1256    pub fn verification_resolver(&self) -> InventoryVerificationResolver<'_> {
1257        InventoryVerificationResolver { inventory: self }
1258    }
1259    /// Register raw symmetric bytes under a unique name.
1260    pub fn add_symmetric(
1261        &mut self,
1262        name: String,
1263        kind: SymmetricKeyKind,
1264        bytes: Vec<u8>,
1265        usages: KeyUsages,
1266        resources: &ResourcePolicy,
1267    ) -> Result<(), KeyStoreError> {
1268        self.check_new_name(&name, resources)?;
1269        if bytes.len() > resources.max_external_resource_bytes {
1270            return Err(import_resource_limit(
1271                crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
1272                resources.max_external_resource_bytes,
1273            ));
1274        }
1275        if bytes.is_empty()
1276            || (kind == SymmetricKeyKind::Aes && !matches!(bytes.len(), 16 | 24 | 32))
1277        {
1278            return Err(KeyStoreError::Selection("invalid symmetric key length"));
1279        }
1280        let permitted = match kind {
1281            SymmetricKeyKind::Hmac => KeyUsages::SIGN.union(KeyUsages::VERIFY),
1282            SymmetricKeyKind::Aes => KeyUsages::ENCRYPT.union(KeyUsages::DECRYPT),
1283            SymmetricKeyKind::Des => des_key_usages(&bytes)?,
1284        };
1285        if usages.0 == 0 || usages.0 & !permitted.0 != 0 {
1286            return Err(KeyStoreError::Selection(
1287                "symmetric key usage is incompatible",
1288            ));
1289        }
1290        self.reserve_material(named_material_length(&name, bytes.len(), 1)?, resources)?;
1291        self.symmetric_keys.push(StoredSymmetricKey {
1292            name,
1293            kind,
1294            bytes: Zeroizing::new(bytes),
1295            usages,
1296        });
1297        self.entry_count += 1;
1298        Ok(())
1299    }
1300
1301    /// Register DER SubjectPublicKeyInfo or a complete X.509 certificate.
1302    /// A certificate is a lookup candidate, never an implicit trust anchor.
1303    pub fn add_public_der(
1304        &mut self,
1305        name: String,
1306        der: Vec<u8>,
1307        resources: &ResourcePolicy,
1308    ) -> Result<(), KeyStoreError> {
1309        self.add_public_der_inner(name, der, None, resources)
1310    }
1311
1312    /// Register public DER with explicit verification/encryption permissions.
1313    pub fn add_public_der_with_usages(
1314        &mut self,
1315        name: String,
1316        der: Vec<u8>,
1317        usages: KeyUsages,
1318        resources: &ResourcePolicy,
1319    ) -> Result<(), KeyStoreError> {
1320        self.add_public_der_inner(name, der, Some(usages), resources)
1321    }
1322
1323    fn add_public_der_inner(
1324        &mut self,
1325        name: String,
1326        der: Vec<u8>,
1327        usages: Option<KeyUsages>,
1328        resources: &ResourcePolicy,
1329    ) -> Result<(), KeyStoreError> {
1330        self.add_public_der_payload(
1331            name,
1332            der,
1333            usages,
1334            resources,
1335            PublicDerFormat::KeyOrCertificate,
1336        )
1337    }
1338
1339    fn add_public_der_payload(
1340        &mut self,
1341        name: String,
1342        der: Vec<u8>,
1343        usages: Option<KeyUsages>,
1344        resources: &ResourcePolicy,
1345        format: PublicDerFormat,
1346    ) -> Result<(), KeyStoreError> {
1347        self.check_new_name(&name, resources)?;
1348        let permitted = KeyUsages::VERIFY.union(KeyUsages::ENCRYPT);
1349        if usages.is_some_and(|usages| usages.0 == 0 || usages.0 & !permitted.0 != 0) {
1350            return Err(KeyStoreError::Selection("public key usage is incompatible"));
1351        }
1352        if der.len() > resources.max_external_resource_bytes {
1353            return Err(import_resource_limit(
1354                crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
1355                resources.max_external_resource_bytes,
1356            ));
1357        }
1358        self.check_material_capacity(named_material_length(&name, der.len(), 2)?, resources)?;
1359        #[cfg(feature = "experimental-pq")]
1360        if let Ok(spki) = pkcs8::SubjectPublicKeyInfoRef::from_der(&der)
1361            && crate::provider::KeyEncapsulationAlgorithm::from_oid(spki.algorithm.oid).is_some()
1362        {
1363            crate::provider::RustCryptoMlKemPublicKey::from_spki_der(&der)
1364                .map_err(|_| KeyStoreError::Selection("invalid ML-KEM public key"))?;
1365            let usages = usages.unwrap_or(KeyUsages::ENCRYPT);
1366            if usages != KeyUsages::ENCRYPT {
1367                return Err(KeyStoreError::Selection(
1368                    "ML-KEM public keys establish keys, not signatures",
1369                ));
1370            }
1371            self.reserve_material(named_material_length(&name, der.len(), 2)?, resources)?;
1372            self.public_keys.push(StoredPublicKey {
1373                key_info: KeyInfo {
1374                    sources: vec![
1375                        KeyInfoSource::KeyName(name.clone()),
1376                        KeyInfoSource::DerEncodedKeyValue(der),
1377                    ],
1378                    ..KeyInfo::default()
1379                },
1380                name,
1381                usages,
1382            });
1383            self.entry_count += 1;
1384            return Ok(());
1385        }
1386        let material_len = der.len();
1387        let mut key_info = KeyInfo::default();
1388        key_info.sources.push(KeyInfoSource::KeyName(name.clone()));
1389        let is_rsa = if let Ok((rest, spki)) =
1390            x509_parser::x509::SubjectPublicKeyInfo::from_der(&der)
1391            && rest.is_empty()
1392            && spki.raw == der
1393        {
1394            let is_rsa = crate::xmldsig::keys::supported_parsed_spki_is_rsa(&spki, &der)
1395                .map_err(|_| KeyStoreError::Selection("unsupported public key algorithm"))?;
1396            key_info
1397                .sources
1398                .push(KeyInfoSource::DerEncodedKeyValue(der));
1399            is_rsa
1400        } else {
1401            if matches!(format, PublicDerFormat::SubjectPublicKeyInfo) {
1402                // RFC 7468 section 13 identifies PUBLIC KEY as SPKI. Section 2
1403                // permits reinterpretation, but this label-dispatched API does
1404                // not: certificate fallback belongs only to generic DER import.
1405                // https://www.rfc-editor.org/rfc/rfc7468#section-13
1406                // https://www.rfc-editor.org/rfc/rfc7468#section-2
1407                return Err(KeyStoreError::Selection("invalid PUBLIC KEY payload"));
1408            }
1409            let (rest, certificate) = X509Certificate::from_der(&der)
1410                .map_err(|_| KeyStoreError::Selection("invalid public key or X.509 certificate"))?;
1411            if !rest.is_empty() {
1412                return Err(KeyStoreError::Selection("invalid X.509 certificate"));
1413            }
1414            let parsed = crate::xmldsig::parse::parse_x509_certificate(&der)
1415                .map_err(|_| KeyStoreError::Selection("unsupported X.509 certificate key"))?;
1416            let is_rsa = crate::xmldsig::keys::supported_parsed_spki_is_rsa(
1417                certificate.public_key(),
1418                certificate.public_key().raw,
1419            )
1420            .map_err(|_| KeyStoreError::Selection("unsupported X.509 certificate key"))?;
1421            key_info.sources.push(KeyInfoSource::X509Data(X509DataInfo {
1422                certificates: vec![der],
1423                parsed_certificates: vec![parsed],
1424                certificate_chain: vec![0],
1425                ..X509DataInfo::default()
1426            }));
1427            is_rsa
1428        };
1429        let usages = usages.unwrap_or(if is_rsa { permitted } else { KeyUsages::VERIFY });
1430        if usages.allows(KeyUsage::Encrypt) && !is_rsa {
1431            return Err(KeyStoreError::Selection(
1432                "only RSA public keys can be used for encryption",
1433            ));
1434        }
1435        self.reserve_material(named_material_length(&name, material_len, 2)?, resources)?;
1436        self.public_keys.push(StoredPublicKey {
1437            name,
1438            key_info,
1439            usages,
1440        });
1441        self.entry_count += 1;
1442        Ok(())
1443    }
1444
1445    /// Import one PEM-encoded public key. RFC 7468 labels select SPKI or
1446    /// PKCS#1; extra text and multiple armor blocks are rejected.
1447    /// Aggregate capacity includes retained inventory, the live encoded input,
1448    /// decoded DER, and any SPKI normalization output before allocation.
1449    pub fn add_public_pem(
1450        &mut self,
1451        name: String,
1452        bytes: &[u8],
1453        resources: &ResourcePolicy,
1454    ) -> Result<(), KeyStoreError> {
1455        self.add_public_pem_inner(name, bytes, None, resources)
1456    }
1457
1458    /// Import one PEM public key with explicit verification/encryption permissions.
1459    pub fn add_public_pem_with_usages(
1460        &mut self,
1461        name: String,
1462        bytes: &[u8],
1463        usages: KeyUsages,
1464        resources: &ResourcePolicy,
1465    ) -> Result<(), KeyStoreError> {
1466        self.add_public_pem_inner(name, bytes, Some(usages), resources)
1467    }
1468
1469    fn add_public_pem_inner(
1470        &mut self,
1471        name: String,
1472        bytes: &[u8],
1473        usages: Option<KeyUsages>,
1474        resources: &ResourcePolicy,
1475    ) -> Result<(), KeyStoreError> {
1476        ensure_resource_policy(resources)?;
1477        self.check_new_name(&name, resources)?;
1478        self.check_material_capacity(named_material_length(&name, bytes.len(), 2)?, resources)?;
1479        let block = BorrowedPem::parse(bytes, resources.max_external_resource_bytes)?;
1480        if !matches!(block.label, "PUBLIC KEY" | "RSA PUBLIC KEY") {
1481            return Err(KeyStoreError::Selection("unsupported public PEM label"));
1482        }
1483        self.check_material_capacity(
1484            named_material_length(
1485                &name,
1486                bytes
1487                    .len()
1488                    .checked_add(block.decoded_len)
1489                    .ok_or(KeyStoreError::Selection("key material size overflow"))?,
1490                2,
1491            )?,
1492            resources,
1493        )?;
1494        let decoded = block.decode()?;
1495        let der = match block.label {
1496            "PUBLIC KEY" => decoded,
1497            "RSA PUBLIC KEY" => {
1498                use der::Encode as _;
1499                let components = pkcs1::RsaPublicKeyRef::from_der(&decoded)
1500                    .map_err(|_| KeyStoreError::Selection("invalid RSA public key"))?;
1501                crate::xmldsig::keys::bounded_rsa_public_components(
1502                    components.modulus.as_bytes(),
1503                    components.public_exponent.as_bytes(),
1504                )
1505                .map_err(|_| KeyStoreError::Selection("RSA public key exceeds safety limit"))?;
1506                // Wrap borrowed PKCS#1 bytes directly; the final SPKI importer
1507                // performs native RSA validation once, without an intermediate key.
1508                let spki = pkcs8::SubjectPublicKeyInfoRef {
1509                    algorithm: pkcs1::ALGORITHM_ID,
1510                    subject_public_key: der::asn1::BitStringRef::new(0, &decoded)
1511                        .map_err(|_| KeyStoreError::Selection("invalid RSA public key"))?,
1512                };
1513                let encoded_len = usize::try_from(
1514                    spki.encoded_len()
1515                        .map_err(|_| KeyStoreError::Selection("invalid RSA public key"))?,
1516                )
1517                .map_err(|_| KeyStoreError::Selection("invalid RSA public key"))?;
1518                if encoded_len > resources.max_external_resource_bytes {
1519                    return Err(import_resource_limit(
1520                        crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
1521                        resources.max_external_resource_bytes,
1522                    ));
1523                }
1524                self.check_material_capacity(
1525                    named_material_length(
1526                        &name,
1527                        bytes
1528                            .len()
1529                            .checked_add(decoded.capacity())
1530                            .and_then(|total| total.checked_add(encoded_len))
1531                            .ok_or(KeyStoreError::Selection("key material size overflow"))?,
1532                        2,
1533                    )?,
1534                    resources,
1535                )?;
1536                spki.to_der()
1537                    .map_err(|_| KeyStoreError::Selection("invalid RSA public key"))?
1538            }
1539            _ => return Err(KeyStoreError::Selection("unsupported public PEM label")),
1540        };
1541        let previous_total = self.material_bytes;
1542        let charged_total = self.check_material_capacity(
1543            named_material_length(&name, bytes.len().max(der.len()), 2)?,
1544            resources,
1545        )?;
1546        self.add_public_der_payload(
1547            name,
1548            der,
1549            usages,
1550            resources,
1551            PublicDerFormat::SubjectPublicKeyInfo,
1552        )?;
1553        debug_assert!(self.material_bytes >= previous_total);
1554        self.material_bytes = charged_total;
1555        Ok(())
1556    }
1557
1558    /// Import a DER private key as PKCS#8 (plain or encrypted) or RSA PKCS#1.
1559    /// Passwords are consulted only for a structurally encrypted container;
1560    /// a wrong password never retries a plaintext decoder.
1561    pub fn add_private_der(
1562        &mut self,
1563        name: String,
1564        bytes: &[u8],
1565        password: Option<&[u8]>,
1566        usages: KeyUsages,
1567        resources: &ResourcePolicy,
1568    ) -> Result<(), KeyStoreError> {
1569        self.add_private_der_inner(name, bytes, password, usages, resources, 0)
1570    }
1571
1572    fn add_private_der_inner(
1573        &mut self,
1574        name: String,
1575        bytes: &[u8],
1576        password: Option<&[u8]>,
1577        usages: KeyUsages,
1578        resources: &ResourcePolicy,
1579        live_encoded_bytes: usize,
1580    ) -> Result<(), KeyStoreError> {
1581        self.check_new_name(&name, resources)?;
1582        if bytes.len() > resources.max_external_resource_bytes {
1583            return Err(import_resource_limit(
1584                crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
1585                resources.max_external_resource_bytes,
1586            ));
1587        }
1588        let retained_with_input =
1589            self.check_material_capacity(named_material_length(&name, bytes.len(), 1)?, resources)?;
1590        validate_private_key_usages(usages)?;
1591        // Borrowed input and decoded PEM remain live while an owned output is
1592        // created. Preflight that peak, not only the final inventory footprint.
1593        let check_plain_output = |output: usize| {
1594            if output > resources.max_external_resource_bytes {
1595                return Err(import_resource_limit(
1596                    crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
1597                    resources.max_external_resource_bytes,
1598                ));
1599            }
1600            let live = bytes
1601                .len()
1602                .checked_add(live_encoded_bytes)
1603                .and_then(|live| live.checked_add(output))
1604                .ok_or_else(|| {
1605                    import_resource_limit(
1606                        crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
1607                        resources.max_external_resource_total_bytes,
1608                    )
1609                })?;
1610            self.check_material_capacity(named_material_length(&name, live, 1)?, resources)
1611        };
1612        let der = if PrivateKeyInfoRef::try_from(bytes).is_ok() {
1613            check_plain_output(bytes.len())?;
1614            Zeroizing::new(bytes.to_vec())
1615        } else if let Ok(encrypted) = EncryptedPrivateKeyInfoRef::try_from(bytes) {
1616            // PEM decoding does not end the caller's encoded buffer lifetime.
1617            // Its bytes coexist with DER and the KDF workspace, not replace DER.
1618            let kdf_live_bytes = retained_with_input
1619                .checked_add(live_encoded_bytes)
1620                .ok_or_else(|| {
1621                    import_resource_limit(
1622                        crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
1623                        resources.max_external_resource_total_bytes,
1624                    )
1625                })?;
1626            let remaining = self.remaining_kdf_resources(resources)?;
1627            enforce_pkcs8_kdf_policy(&encrypted, &remaining, kdf_live_bytes)?;
1628            let password = password.ok_or(KeyStoreError::ProtectedContainer)?;
1629            let work =
1630                enforce_pkcs8_password_policy(&encrypted, password, &remaining, kdf_live_bytes)?;
1631            self.kdf_work += work;
1632            // Decrypt in the one preflighted, zeroizing output allocation;
1633            // SecretDocument followed by to_vec would retain two plaintext copies.
1634            let mut plain = Zeroizing::new(encrypted.encrypted_data.as_bytes().to_vec());
1635            let plaintext_len = encrypted
1636                .encryption_algorithm
1637                .decrypt_in_place(password, &mut plain)
1638                .map_err(|_| KeyStoreError::ProtectedContainer)?;
1639            let plaintext_len = plaintext_len.len();
1640            plain.truncate(plaintext_len);
1641            PrivateKeyInfoRef::try_from(plain.as_slice())
1642                .map_err(|_| KeyStoreError::ProtectedContainer)?;
1643            plain
1644        } else {
1645            use der::Encode as _;
1646            preflight_rsa_pkcs1_components(bytes)?;
1647            // RFC 8017 A.1.2 / RFC 5958 section 2: wrap the original PKCS#1
1648            // octets directly, avoiding bigint re-encoding and two owned DERs.
1649            // https://www.rfc-editor.org/rfc/rfc5958#section-2
1650            let normalized = PrivateKeyInfoRef::new(
1651                pkcs1::ALGORITHM_ID,
1652                der::asn1::OctetStringRef::new(bytes)
1653                    .map_err(|_| KeyStoreError::Selection("invalid RSA private key"))?,
1654            );
1655            let size = usize::try_from(
1656                normalized
1657                    .encoded_len()
1658                    .map_err(|_| KeyStoreError::Selection("invalid RSA private key"))?,
1659            )
1660            .map_err(|_| KeyStoreError::Selection("key material size overflow"))?;
1661            check_plain_output(size)?;
1662            let normalized = normalized
1663                .to_der()
1664                .map_err(|_| KeyStoreError::Selection("invalid RSA private key"))?;
1665            Zeroizing::new(normalized)
1666        };
1667        if der.len() > resources.max_external_resource_bytes {
1668            return Err(import_resource_limit(
1669                crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
1670                resources.max_external_resource_bytes,
1671            ));
1672        }
1673        let identity = PrivateKeyIdentity::decode(&der)?;
1674        if usages.allows(KeyUsage::Decrypt) && !identity.can_decrypt() {
1675            return Err(KeyStoreError::Selection(
1676                "private key cannot recover encryption keys",
1677            ));
1678        }
1679        identity.validate_usages(usages)?;
1680        drop(identity);
1681        self.reserve_material(
1682            named_material_length(&name, bytes.len().max(der.len()), 1)?,
1683            resources,
1684        )?;
1685        self.private_keys.push(StoredPrivateKey {
1686            name,
1687            pkcs8_der: der,
1688            usages,
1689            certificate_chain: Vec::new(),
1690            has_matching_leaf: false,
1691        });
1692        self.entry_count += 1;
1693        Ok(())
1694    }
1695
1696    /// Import private DER using a caller-owned password callback only when
1697    /// the input is an encrypted PKCS#8 container.
1698    pub fn add_private_der_with_password_callback<F>(
1699        &mut self,
1700        name: String,
1701        bytes: &[u8],
1702        password: F,
1703        usages: KeyUsages,
1704        resources: &ResourcePolicy,
1705    ) -> Result<(), KeyStoreError>
1706    where
1707        F: FnOnce() -> Option<Zeroizing<Vec<u8>>>,
1708    {
1709        self.check_new_name(&name, resources)?;
1710        validate_private_key_usages(usages)?;
1711        if bytes.len() > resources.max_external_resource_bytes {
1712            return Err(import_resource_limit(
1713                crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
1714                resources.max_external_resource_bytes,
1715            ));
1716        }
1717        let retained_with_input =
1718            self.check_material_capacity(named_material_length(&name, bytes.len(), 1)?, resources)?;
1719        let encrypted = EncryptedPrivateKeyInfoRef::try_from(bytes).ok();
1720        let secret = if let Some(encrypted) = &encrypted {
1721            let remaining = self.remaining_kdf_resources(resources)?;
1722            // Secret acquisition is observable work: use the same remaining
1723            // allowance as direct import before invoking the caller.
1724            enforce_pkcs8_kdf_policy(encrypted, &remaining, retained_with_input)?;
1725            Some(password().ok_or(KeyStoreError::ProtectedContainer)?)
1726        } else {
1727            None
1728        };
1729        if let Some(secret) = &secret {
1730            // The callback owns capacity, not only initialized password bytes.
1731            self.check_material_capacity(
1732                named_material_length(
1733                    &name,
1734                    bytes.len().checked_add(secret.capacity()).ok_or_else(|| {
1735                        import_resource_limit(
1736                            crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
1737                            resources.max_external_resource_total_bytes,
1738                        )
1739                    })?,
1740                    1,
1741                )?,
1742                resources,
1743            )?;
1744            let encrypted = encrypted
1745                .as_ref()
1746                .ok_or(KeyStoreError::ProtectedContainer)?;
1747            enforce_pkcs8_password_policy(
1748                encrypted,
1749                secret,
1750                &self.remaining_kdf_resources(resources)?,
1751                retained_with_input + secret.capacity() - secret.len(),
1752            )?;
1753        }
1754        self.add_private_der(
1755            name,
1756            bytes,
1757            secret.as_deref().map(Vec::as_slice),
1758            usages,
1759            resources,
1760        )
1761    }
1762
1763    /// Import one PEM private key, including encrypted PKCS#8. Traditional
1764    /// OpenSSL PEM encryption is handled at the CLI compatibility boundary.
1765    pub fn add_private_pem(
1766        &mut self,
1767        name: String,
1768        bytes: &[u8],
1769        password: Option<&[u8]>,
1770        usages: KeyUsages,
1771        resources: &ResourcePolicy,
1772    ) -> Result<(), KeyStoreError> {
1773        ensure_resource_policy(resources)?;
1774        self.check_new_name(&name, resources)?;
1775        self.check_material_capacity(named_material_length(&name, bytes.len(), 1)?, resources)?;
1776        let block = BorrowedPem::parse(bytes, resources.max_external_resource_bytes)?;
1777        validate_private_key_usages(usages)?;
1778        self.check_material_capacity(
1779            named_material_length(
1780                &name,
1781                bytes
1782                    .len()
1783                    .checked_add(block.decoded_len)
1784                    .ok_or(KeyStoreError::Selection("key material size overflow"))?,
1785                1,
1786            )?,
1787            resources,
1788        )?;
1789        enum Payload {
1790            Plain,
1791            Encrypted,
1792            Rsa,
1793        }
1794        let payload = match block.label {
1795            "PRIVATE KEY" => Payload::Plain,
1796            "ENCRYPTED PRIVATE KEY" => Payload::Encrypted,
1797            "RSA PRIVATE KEY" => Payload::Rsa,
1798            _ => return Err(KeyStoreError::Selection("unsupported private PEM label")),
1799        };
1800        let mut der = Zeroizing::new(vec![0; block.decoded_len]);
1801        block.decode_into(&mut der)?;
1802        // RFC 7468 sections 10/11 define distinct PKCS#8 labels. Section 2
1803        // permits reinterpretation, but our protected-key contract forbids it:
1804        // https://www.rfc-editor.org/rfc/rfc7468#section-2
1805        match payload {
1806            Payload::Encrypted => {
1807                EncryptedPrivateKeyInfoRef::try_from(der.as_slice())
1808                    .map_err(|_| KeyStoreError::ProtectedContainer)?;
1809            }
1810            Payload::Plain => {
1811                PrivateKeyInfoRef::try_from(der.as_slice())
1812                    .map_err(|_| KeyStoreError::Selection("invalid PRIVATE KEY payload"))?;
1813            }
1814            Payload::Rsa => preflight_rsa_pkcs1_components(&der)?,
1815        }
1816        let previous_total = self.material_bytes;
1817        let name_len = name.len();
1818        self.add_private_der_inner(name, &der, password, usages, resources, bytes.len())?;
1819        let retained_len = self.material_bytes - previous_total;
1820        self.material_bytes = previous_total + name_len + bytes.len().max(retained_len - name_len);
1821        Ok(())
1822    }
1823
1824    /// Import a bounded PKCS#12 bundle from caller-owned bytes. Signing keys
1825    /// may sign; RSA keys may also decrypt; ML-KEM keys only decapsulate.
1826    /// A bundle with more than one private
1827    /// key is rejected rather than assigning names from iteration order.
1828    pub fn add_pkcs12(
1829        &mut self,
1830        name: String,
1831        bytes: &[u8],
1832        password: &str,
1833        resources: &ResourcePolicy,
1834    ) -> Result<(), KeyStoreError> {
1835        self.add_pkcs12_inner(
1836            name,
1837            bytes,
1838            password,
1839            KeyUsages::SIGN.union(KeyUsages::DECRYPT),
1840            resources,
1841            true,
1842        )
1843    }
1844
1845    /// Import PKCS#12 using a caller-owned password callback. Its result is
1846    /// zeroized after decoding and callback failure exposes no diagnostic.
1847    pub fn add_pkcs12_with_password_callback<F>(
1848        &mut self,
1849        name: String,
1850        bytes: &[u8],
1851        password: F,
1852        usages: KeyUsages,
1853        resources: &ResourcePolicy,
1854    ) -> Result<(), KeyStoreError>
1855    where
1856        F: FnOnce() -> Option<Zeroizing<String>>,
1857    {
1858        let limits = self.pkcs12_import_limits(&name, bytes, usages, resources)?;
1859        let prepared = pkcs12_import::prepare_with_work(bytes, &limits, self.kdf_work)?;
1860        let secret = password().ok_or(KeyStoreError::ProtectedContainer)?;
1861        let contents =
1862            prepared.decrypt_with_work(&secret, secret.capacity(), &mut self.kdf_work)?;
1863        self.add_pkcs12_contents(name, bytes.len(), contents, usages, resources, false)
1864    }
1865
1866    /// Import a PKCS#12 bundle with explicit signing/decryption permissions.
1867    pub fn add_pkcs12_with_usages(
1868        &mut self,
1869        name: String,
1870        bytes: &[u8],
1871        password: &str,
1872        usages: KeyUsages,
1873        resources: &ResourcePolicy,
1874    ) -> Result<(), KeyStoreError> {
1875        self.add_pkcs12_inner(name, bytes, password, usages, resources, false)
1876    }
1877
1878    fn add_pkcs12_inner(
1879        &mut self,
1880        name: String,
1881        bytes: &[u8],
1882        password: &str,
1883        usages: KeyUsages,
1884        resources: &ResourcePolicy,
1885        auto_decrypt: bool,
1886    ) -> Result<(), KeyStoreError> {
1887        let limits = self.pkcs12_import_limits(&name, bytes, usages, resources)?;
1888        let contents = pkcs12_import::prepare_with_work(bytes, &limits, self.kdf_work)?
1889            .decrypt_with_work(password, password.len(), &mut self.kdf_work)?;
1890        self.add_pkcs12_contents(name, bytes.len(), contents, usages, resources, auto_decrypt)
1891    }
1892
1893    fn add_pkcs12_contents(
1894        &mut self,
1895        name: String,
1896        encoded_len: usize,
1897        mut contents: pkcs12_import::Contents,
1898        mut usages: KeyUsages,
1899        resources: &ResourcePolicy,
1900        auto_decrypt: bool,
1901    ) -> Result<(), KeyStoreError> {
1902        if contents.private_keys.len() != 1 {
1903            return Err(KeyStoreError::Selection(
1904                "PKCS#12 bundle must contain exactly one private key",
1905            ));
1906        }
1907        let private_key = contents
1908            .private_keys
1909            .pop()
1910            .ok_or(KeyStoreError::ProtectedContainer)?;
1911        let mut certificates = contents.certificates;
1912        let identity = PrivateKeyIdentity::decode(private_key.as_ref())?;
1913        if auto_decrypt {
1914            usages = identity.automatic_usages();
1915        } else if usages.allows(KeyUsage::Decrypt) && !identity.can_decrypt() {
1916            return Err(KeyStoreError::Selection(
1917                "private key cannot recover encryption keys",
1918            ));
1919        }
1920        identity.validate_usages(usages)?;
1921        let mut matching_leaf = None;
1922        for certificate in &certificates {
1923            let (rest, parsed) = X509Certificate::from_der(certificate)
1924                .map_err(|_| KeyStoreError::Selection("invalid certificate in PKCS#12"))?;
1925            if !rest.is_empty() {
1926                return Err(KeyStoreError::Selection("invalid certificate in PKCS#12"));
1927            }
1928            if identity.matches_spki(parsed.public_key().raw) {
1929                if matching_leaf.is_some_and(|leaf: &[u8]| leaf != certificate.as_slice()) {
1930                    return Err(KeyStoreError::Selection(
1931                        "ambiguous certificate for PKCS#12 private key",
1932                    ));
1933                }
1934                // RFC 7292 section 4.2 permits repeated certificate bags.
1935                // Identical DER is the same leaf, not a second candidate.
1936                matching_leaf = Some(certificate.as_slice());
1937            }
1938        }
1939        drop(identity);
1940        // PKCS#12 may carry unrelated CA certificates. They remain lookup
1941        // material; only an actual SPKI match is promoted to the leaf slot.
1942        let has_matching_leaf = matching_leaf.is_some();
1943        if let Some(leaf) = matching_leaf {
1944            let position = certificates
1945                .iter()
1946                .position(|candidate| candidate == leaf)
1947                .ok_or(KeyStoreError::ProtectedContainer)?;
1948            certificates.swap(0, position);
1949            let mut index = 1;
1950            while index < certificates.len() {
1951                if certificates[index] == certificates[0] {
1952                    certificates.remove(index);
1953                } else {
1954                    index += 1;
1955                }
1956            }
1957        }
1958        let decoded_bytes = certificates
1959            .iter()
1960            .try_fold(private_key.len(), |total, cert| {
1961                total
1962                    .checked_add(cert.len())
1963                    .ok_or(KeyStoreError::Selection("key material size overflow"))
1964            })?;
1965        let retained_candidates = 1_usize
1966            .checked_add(certificates.len())
1967            .ok_or(KeyStoreError::Selection("key candidate count overflow"))?;
1968        let remaining_candidates = resources.max_key_candidates - self.entry_count;
1969        if retained_candidates > remaining_candidates {
1970            return Err(crate::policy::PolicyViolation::ResourceLimitExceeded {
1971                resource: crate::policy::resource_name::KEY_CANDIDATES,
1972                maximum: remaining_candidates,
1973            }
1974            .into());
1975        }
1976        self.reserve_material(
1977            named_material_length(&name, decoded_bytes.max(encoded_len), 1)?,
1978            resources,
1979        )?;
1980        self.private_keys.push(StoredPrivateKey {
1981            name,
1982            pkcs8_der: private_key,
1983            usages,
1984            certificate_chain: certificates,
1985            has_matching_leaf,
1986        });
1987        self.entry_count += retained_candidates;
1988        Ok(())
1989    }
1990
1991    fn pkcs12_import_limits(
1992        &self,
1993        name: &str,
1994        bytes: &[u8],
1995        usages: KeyUsages,
1996        resources: &ResourcePolicy,
1997    ) -> Result<Pkcs12Limits, KeyStoreError> {
1998        self.check_new_name(name, resources)?;
1999        validate_private_key_usages(usages)?;
2000        if bytes.len() > resources.max_external_resource_bytes {
2001            return Err(KeyStoreError::Policy(
2002                crate::policy::PolicyViolation::ResourceLimitExceeded {
2003                    resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
2004                    maximum: resources.max_external_resource_bytes,
2005                },
2006            ));
2007        }
2008        let retained_with_input =
2009            self.check_material_capacity(named_material_length(name, bytes.len(), 1)?, resources)?;
2010        let remaining_candidates = resources.max_key_candidates - self.entry_count;
2011        Ok(Pkcs12Limits {
2012            resources: resources.clone(),
2013            candidates: remaining_candidates,
2014            // Borrowing the PFX does not end its lifetime during decryption.
2015            memory_available: resources.max_external_resource_total_bytes - retained_with_input,
2016        })
2017    }
2018
2019    fn check_new_name(&self, name: &str, resources: &ResourcePolicy) -> Result<(), KeyStoreError> {
2020        ensure_resource_policy(resources)?;
2021        if name.is_empty() {
2022            return Err(KeyStoreError::Selection("empty key name"));
2023        }
2024        if self.entry_count >= resources.max_key_candidates {
2025            return Err(import_resource_limit(
2026                crate::policy::resource_name::KEY_CANDIDATES,
2027                resources.max_key_candidates,
2028            ));
2029        }
2030        if name.len() > resources.max_external_resource_bytes {
2031            return Err(import_resource_limit(
2032                crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
2033                resources.max_external_resource_bytes,
2034            ));
2035        }
2036        self.check_material_capacity(name.len(), resources)?;
2037        if self.symmetric_keys.iter().any(|key| key.name == name)
2038            || self.public_keys.iter().any(|key| key.name == name)
2039            || self.private_keys.iter().any(|key| key.name == name)
2040        {
2041            return Err(KeyStoreError::Selection("duplicate key name"));
2042        }
2043        Ok(())
2044    }
2045
2046    fn remaining_kdf_resources(
2047        &self,
2048        resources: &ResourcePolicy,
2049    ) -> Result<ResourcePolicy, KeyStoreError> {
2050        if self.kdf_work > resources.max_key_import_kdf_work {
2051            return Err(import_resource_limit(
2052                crate::policy::resource_name::KEY_IMPORT_KDF_WORK,
2053                resources.max_key_import_kdf_work,
2054            ));
2055        }
2056        let mut remaining = resources.clone();
2057        remaining.max_key_import_kdf_work -= self.kdf_work;
2058        Ok(remaining)
2059    }
2060
2061    fn check_material_capacity(
2062        &self,
2063        length: usize,
2064        resources: &ResourcePolicy,
2065    ) -> Result<usize, KeyStoreError> {
2066        let total = self.material_bytes.checked_add(length).ok_or_else(|| {
2067            import_resource_limit(
2068                crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
2069                resources.max_external_resource_total_bytes,
2070            )
2071        })?;
2072        if total > resources.max_external_resource_total_bytes {
2073            return Err(import_resource_limit(
2074                crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
2075                resources.max_external_resource_total_bytes,
2076            ));
2077        }
2078        Ok(total)
2079    }
2080
2081    fn reserve_material(
2082        &mut self,
2083        length: usize,
2084        resources: &ResourcePolicy,
2085    ) -> Result<(), KeyStoreError> {
2086        self.material_bytes = self.check_material_capacity(length, resources)?;
2087        Ok(())
2088    }
2089
2090    /// Import a DER certificate as an untrusted lookup candidate or an
2091    /// explicitly caller-trusted anchor. Parsing alone never grants trust.
2092    pub fn add_certificate_der(
2093        &mut self,
2094        der: Vec<u8>,
2095        trusted_anchor: bool,
2096        resources: &ResourcePolicy,
2097    ) -> Result<(), KeyStoreError> {
2098        let material_bytes = self.preflight_x509_import(&der, resources)?;
2099        let (rest, _) = X509Certificate::from_der(&der)
2100            .map_err(|_| KeyStoreError::Selection("invalid X.509 certificate"))?;
2101        if !rest.is_empty() {
2102            return Err(KeyStoreError::Selection("invalid X.509 certificate"));
2103        }
2104        self.material_bytes = material_bytes;
2105        if trusted_anchor {
2106            self.trusted_certificates.push(der);
2107        } else {
2108            self.lookup_certificates.push(der);
2109        }
2110        self.entry_count += 1;
2111        Ok(())
2112    }
2113
2114    /// Import a DER CRL for policy-controlled revocation checks.
2115    pub fn add_crl_der(
2116        &mut self,
2117        der: Vec<u8>,
2118        resources: &ResourcePolicy,
2119    ) -> Result<(), KeyStoreError> {
2120        let material_bytes = self.preflight_x509_import(&der, resources)?;
2121        let (rest, _) = x509_parser::revocation_list::CertificateRevocationList::from_der(&der)
2122            .map_err(|_| KeyStoreError::Selection("invalid X.509 CRL"))?;
2123        if !rest.is_empty() {
2124            return Err(KeyStoreError::Selection("invalid X.509 CRL"));
2125        }
2126        self.material_bytes = material_bytes;
2127        self.crls.push(der);
2128        self.entry_count += 1;
2129        Ok(())
2130    }
2131
2132    fn preflight_x509_import(
2133        &self,
2134        der: &[u8],
2135        resources: &ResourcePolicy,
2136    ) -> Result<usize, KeyStoreError> {
2137        // Policy exhaustion is terminal before any candidate-local DER work.
2138        ensure_resource_policy(resources)?;
2139        if der.len() > resources.max_external_resource_bytes {
2140            return Err(import_resource_limit(
2141                crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
2142                resources.max_external_resource_bytes,
2143            ));
2144        }
2145        if self.entry_count >= resources.max_key_candidates {
2146            return Err(import_resource_limit(
2147                crate::policy::resource_name::KEY_CANDIDATES,
2148                resources.max_key_candidates,
2149            ));
2150        }
2151        self.check_material_capacity(der.len(), resources)
2152    }
2153    /// Import caller-owned XML bytes under the operation's XML and resource snapshot.
2154    pub fn from_xml_bytes<P: crate::document::XmlDocumentPolicy>(
2155        bytes: &[u8],
2156        policy: &P,
2157        backend: XmlBackend,
2158    ) -> Result<Self, KeyStoreError> {
2159        let budget = XmlParseWorkBudget::from_resources(policy.resource_policy());
2160        Self::from_xml_bytes_with_budget(bytes, policy, backend, &budget, &mut 0, 0)
2161    }
2162
2163    fn from_xml_bytes_with_budget<P: crate::document::XmlDocumentPolicy>(
2164        bytes: &[u8],
2165        policy: &P,
2166        backend: XmlBackend,
2167        budget: &XmlParseWorkBudget,
2168        inspected: &mut usize,
2169        live_material: usize,
2170    ) -> Result<Self, KeyStoreError> {
2171        let resources = policy.resource_policy();
2172        ensure_resource_policy(resources)?;
2173        if bytes.len() > resources.max_external_resource_bytes {
2174            return Err(import_resource_limit(
2175                crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
2176                resources.max_external_resource_bytes,
2177            ));
2178        }
2179        if bytes.len() > resources.max_external_resource_total_bytes - live_material {
2180            return Err(import_resource_limit(
2181                crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
2182                resources.max_external_resource_total_bytes,
2183            ));
2184        }
2185        let settings = DocumentParseSettings::from_policy(policy.xml_input_policy(), resources)
2186            .with_backend(backend);
2187        let text = crate::document::decode_xml_with_budget(
2188            bytes,
2189            resources
2190                .max_xml_document_bytes
2191                .min(resources.max_external_resource_bytes),
2192            Some(budget),
2193        )
2194        .map_err(|error| match error.into_policy_violation(settings) {
2195            Ok(violation) => KeyStoreError::Policy(violation),
2196            Err(error) => KeyStoreError::Invalid(error.to_string()),
2197        })?;
2198        let document = parse_borrowed_with_settings_and_budget(&text, settings, Some(budget))
2199            .map_err(|error| match error.into_policy_violation(settings) {
2200                Ok(violation) => KeyStoreError::Policy(violation),
2201                Err(error) => KeyStoreError::Invalid(error.to_string()),
2202            })?;
2203        let root = document.root_element();
2204        if !root.has_tag_name((XMLSEC_NS, "Keys")) {
2205            return Err(KeyStoreError::Invalid("expected xmlsec Keys root".into()));
2206        }
2207        let mut material_budget = XmlImportMaterialBudget {
2208            used: live_material + bytes.len(),
2209            maximum: resources.max_external_resource_total_bytes,
2210        };
2211        let mut names = HashSet::new();
2212        let mut store = Self::default();
2213        let mut entry_count = 0_usize;
2214        for info in root.children().filter(|child| child.is_element()) {
2215            if !info.has_tag_name((XMLDSIG_NS, "KeyInfo")) {
2216                return Err(KeyStoreError::Invalid("unexpected child of Keys".into()));
2217            }
2218            if *inspected >= resources.max_key_candidates {
2219                return Err(import_resource_limit(
2220                    crate::policy::resource_name::KEY_CANDIDATES,
2221                    resources.max_key_candidates,
2222                ));
2223            }
2224            *inspected += 1;
2225            entry_count += 1;
2226            preflight_xml_material(info, &mut material_budget)?;
2227            let mut name = None;
2228            let mut value = None;
2229            for child in info.children().filter(|child| child.is_element()) {
2230                if child.has_tag_name((XMLDSIG_NS, "KeyName")) {
2231                    if name.is_some() {
2232                        return Err(KeyStoreError::Invalid("duplicate KeyName".into()));
2233                    }
2234                    let text = element_text(child)?;
2235                    if text.is_empty() {
2236                        return Err(KeyStoreError::Invalid("empty KeyName".into()));
2237                    }
2238                    name = Some(text);
2239                } else if child.has_tag_name((XMLDSIG_NS, "KeyValue")) {
2240                    if value.is_some() {
2241                        return Err(KeyStoreError::Invalid("duplicate KeyValue".into()));
2242                    }
2243                    let mut values = child.children().filter(|node| node.is_element());
2244                    let key = values.next().ok_or_else(|| {
2245                        KeyStoreError::Invalid("KeyValue has no key material".into())
2246                    })?;
2247                    if values.next().is_some() {
2248                        return Err(KeyStoreError::Invalid("ambiguous KeyValue".into()));
2249                    }
2250                    let material = if key.has_tag_name((XMLSEC_NS, "HMACKeyValue")) {
2251                        Some(SymmetricKeyKind::Hmac)
2252                    } else if key.has_tag_name((XMLSEC_NS, "AESKeyValue")) {
2253                        Some(SymmetricKeyKind::Aes)
2254                    } else if key.has_tag_name((XMLSEC_NS, "DESKeyValue")) {
2255                        Some(SymmetricKeyKind::Des)
2256                    } else {
2257                        None
2258                    };
2259                    value = Some(if let Some(kind) = material {
2260                        ParsedMaterial::Symmetric(kind, decode_xml_base64_secret(key)?)
2261                    } else if key.has_tag_name((XMLDSIG_NS, "DSAKeyValue")) {
2262                        let (public, private) =
2263                            parse_xmlsec_dsa_key_value(key, &mut material_budget)?;
2264                        ParsedMaterial::Dsa(public, private)
2265                    } else if key.has_tag_name((XMLDSIG_NS, "RSAKeyValue"))
2266                        // XMLDSig 1.1 section 4.5.2.3 places ECKeyValue in dsig11:
2267                        // https://www.w3.org/TR/2013/REC-xmldsig-core1-20130411/#sec-ECKeyValue
2268                        || key.has_tag_name((XMLDSIG11_NS, "ECKeyValue"))
2269                    {
2270                        ParsedMaterial::Public(
2271                            crate::xmldsig::parse::parse_key_value_dispatch(child)
2272                                .map_err(|error| KeyStoreError::Invalid(error.to_string()))?,
2273                        )
2274                    } else {
2275                        ParsedMaterial::Unsupported
2276                    });
2277                } else if child.has_tag_name((XMLDSIG11_NS, "DEREncodedKeyValue")) {
2278                    // XMLDSig 1.1 §4.5.9: this contains base64 DER SPKI, not PKCS#8.
2279                    // https://www.w3.org/TR/xmldsig-core1/#sec-DEREncodedKeyValue
2280                    if value.is_some() {
2281                        return Err(KeyStoreError::Invalid("ambiguous key material".into()));
2282                    }
2283                    value = Some(ParsedMaterial::PublicDer(decode_xml_base64(child)?));
2284                } else {
2285                    return Err(KeyStoreError::Invalid("unsupported KeyInfo child".into()));
2286                }
2287            }
2288            let name = name.ok_or_else(|| KeyStoreError::Invalid("missing KeyName".into()))?;
2289            let material =
2290                value.ok_or_else(|| KeyStoreError::Invalid("missing KeyValue".into()))?;
2291            if !names.insert(name.clone()) {
2292                return Err(KeyStoreError::Invalid("duplicate key name".into()));
2293            }
2294            match material {
2295                ParsedMaterial::PublicDer(der) => {
2296                    store.add_public_der_payload(
2297                        name,
2298                        der,
2299                        None,
2300                        resources,
2301                        PublicDerFormat::SubjectPublicKeyInfo,
2302                    )?;
2303                }
2304                ParsedMaterial::Symmetric(kind, bytes) => {
2305                    if bytes.is_empty() {
2306                        return Err(KeyStoreError::Invalid("empty symmetric key".into()));
2307                    }
2308                    if kind == SymmetricKeyKind::Aes && !matches!(bytes.len(), 16 | 24 | 32) {
2309                        return Err(KeyStoreError::Invalid("invalid AES key length".into()));
2310                    }
2311                    let usages = match kind {
2312                        SymmetricKeyKind::Hmac => KeyUsages::SIGN.union(KeyUsages::VERIFY),
2313                        SymmetricKeyKind::Aes => KeyUsages::ENCRYPT.union(KeyUsages::DECRYPT),
2314                        SymmetricKeyKind::Des => des_key_usages(&bytes)?,
2315                    };
2316                    store.symmetric_keys.push(StoredSymmetricKey {
2317                        name,
2318                        kind,
2319                        bytes,
2320                        usages,
2321                    });
2322                }
2323                ParsedMaterial::Public(value) => {
2324                    let mut key_info = KeyInfo::default();
2325                    key_info.sources.push(KeyInfoSource::KeyName(name.clone()));
2326                    key_info.sources.push(KeyInfoSource::KeyValue(value));
2327                    let is_rsa = key_info
2328                        .sources
2329                        .iter()
2330                        .find_map(|source| match source {
2331                            KeyInfoSource::KeyValue(value) => Some(value),
2332                            _ => None,
2333                        })
2334                        .ok_or_else(|| KeyStoreError::Invalid("invalid public KeyValue".into()))?;
2335                    let is_rsa = crate::xmldsig::keys::supported_key_value_is_rsa(is_rsa)
2336                        .map_err(|_| KeyStoreError::Invalid("invalid public KeyValue".into()))?;
2337                    let usages = if is_rsa {
2338                        KeyUsages::VERIFY.union(KeyUsages::ENCRYPT)
2339                    } else {
2340                        KeyUsages::VERIFY
2341                    };
2342                    store.public_keys.push(StoredPublicKey {
2343                        name,
2344                        key_info,
2345                        usages,
2346                    });
2347                }
2348                ParsedMaterial::Dsa(public, private) => {
2349                    // This inventory has no external DSA parameter inheritance;
2350                    // its stored public tuple must be independently resolvable.
2351                    crate::xmldsig::keys::supported_key_value_is_rsa(&public)
2352                        .map_err(|_| KeyStoreError::Invalid("invalid public DSAKeyValue".into()))?;
2353                    let mut key_info = KeyInfo::default();
2354                    key_info.sources.push(KeyInfoSource::KeyName(name.clone()));
2355                    key_info.sources.push(KeyInfoSource::KeyValue(public));
2356                    if let Some(pkcs8_der) = private {
2357                        store.private_keys.push(StoredPrivateKey {
2358                            name: name.clone(),
2359                            pkcs8_der,
2360                            usages: KeyUsages::SIGN,
2361                            certificate_chain: Vec::new(),
2362                            has_matching_leaf: false,
2363                        });
2364                    }
2365                    store.public_keys.push(StoredPublicKey {
2366                        name,
2367                        key_info,
2368                        usages: KeyUsages::VERIFY,
2369                    });
2370                }
2371                ParsedMaterial::Unsupported => {}
2372            }
2373        }
2374        store.entry_count = entry_count;
2375        // Decoding can retain both public components and a derived private key.
2376        // Keep the input charge too, so compact XML never lowers the import budget.
2377        store.material_bytes = bytes
2378            .len()
2379            .checked_add(store.retained_material_bytes()?)
2380            .ok_or(KeyStoreError::Selection("key material size overflow"))?;
2381        if store.material_bytes > resources.max_external_resource_total_bytes - live_material {
2382            return Err(import_resource_limit(
2383                crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
2384                resources.max_external_resource_total_bytes,
2385            ));
2386        }
2387        Ok(store)
2388    }
2389}
2390
2391fn validate_private_key_usages(usages: KeyUsages) -> Result<(), KeyStoreError> {
2392    let permitted = KeyUsages::SIGN.union(KeyUsages::DECRYPT);
2393    if usages.0 == 0 || usages.0 & !permitted.0 != 0 {
2394        return Err(KeyStoreError::Selection(
2395            "private key usage is incompatible",
2396        ));
2397    }
2398    Ok(())
2399}
2400
2401fn import_resource_limit(resource: &'static str, maximum: usize) -> KeyStoreError {
2402    crate::policy::PolicyViolation::ResourceLimitExceeded { resource, maximum }.into()
2403}
2404
2405fn check_operation_material_size(
2406    length: usize,
2407    resources: &ResourcePolicy,
2408) -> Result<(), KeyStoreError> {
2409    if length > resources.max_external_resource_bytes {
2410        return Err(crate::policy::PolicyViolation::ResourceLimit {
2411            resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
2412            maximum: resources.max_external_resource_bytes,
2413            actual: length,
2414        }
2415        .into());
2416    }
2417    if length > resources.max_external_resource_total_bytes {
2418        return Err(crate::policy::PolicyViolation::ResourceLimit {
2419            resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
2420            maximum: resources.max_external_resource_total_bytes,
2421            actual: length,
2422        }
2423        .into());
2424    }
2425    Ok(())
2426}
2427
2428#[cfg(feature = "xmlenc")]
2429fn check_selected_material_size(
2430    length: usize,
2431    resources: &ResourcePolicy,
2432) -> Result<(), KeyStoreError> {
2433    check_operation_material_size(length, resources)
2434}
2435
2436#[cfg(feature = "xmlenc")]
2437fn check_encryption_material_size(
2438    length: usize,
2439    resources: &ResourcePolicy,
2440) -> Result<(), KeyStoreError> {
2441    check_operation_material_size(length, resources)
2442}
2443
2444#[cfg(feature = "xmlenc")]
2445fn rsa_recipient_preflight(
2446    modulus: &[u8],
2447    exponent: &[u8],
2448    policy: &crate::policy::EncryptionPolicy,
2449) -> Result<(), KeyStoreError> {
2450    let combined = modulus
2451        .len()
2452        .checked_add(exponent.len())
2453        .ok_or(KeyStoreError::Selection("encryption key size overflow"))?;
2454    check_encryption_material_size(combined, &policy.resources)?;
2455    policy
2456        .rsa_keys
2457        .validate_components("encryption", modulus, exponent)?;
2458    Ok(())
2459}
2460
2461#[cfg(feature = "xmlenc")]
2462fn rsa_recipient_from_components(
2463    modulus: &[u8],
2464    exponent: &[u8],
2465    policy: &crate::policy::EncryptionPolicy,
2466) -> Result<RsaPublicKey, KeyStoreError> {
2467    rsa_recipient_preflight(modulus, exponent, policy)?;
2468    let first_nonzero = modulus
2469        .iter()
2470        .position(|byte| *byte != 0)
2471        .ok_or(KeyStoreError::Selection("invalid RSA encryption key"))?;
2472    RsaPublicKey::new(
2473        BoxedUint::from_be_slice_vartime(&modulus[first_nonzero..]),
2474        BoxedUint::from_be_slice_vartime(exponent),
2475    )
2476    .map_err(|_| KeyStoreError::Selection("invalid RSA encryption key"))
2477}
2478
2479fn signing_policy_error(error: crate::xmldsig::SigningError) -> KeyStoreError {
2480    match error {
2481        crate::xmldsig::SigningError::Policy(violation) => violation.into(),
2482        _ => KeyStoreError::Selection("signing key violates operation policy"),
2483    }
2484}
2485
2486fn ensure_resource_policy(resources: &ResourcePolicy) -> Result<(), KeyStoreError> {
2487    resources.validate().map_err(Into::into)
2488}
2489
2490fn find_named_entry<'a, T>(
2491    entries: &'a [T],
2492    name: &str,
2493    resources: &ResourcePolicy,
2494    visited: &mut usize,
2495    entry_name: impl Fn(&T) -> &str,
2496) -> Result<Option<&'a T>, KeyStoreError> {
2497    for entry in entries {
2498        let next = visited
2499            .checked_add(1)
2500            .ok_or(KeyStoreError::Selection("key candidate count overflow"))?;
2501        resources.validate_key_candidates(next)?;
2502        *visited = next;
2503        if entry_name(entry) == name {
2504            return Ok(Some(entry));
2505        }
2506    }
2507    Ok(None)
2508}
2509
2510fn named_material_length(
2511    name: &str,
2512    payload: usize,
2513    retained_names: usize,
2514) -> Result<usize, KeyStoreError> {
2515    name.len()
2516        .checked_mul(retained_names)
2517        .and_then(|names| names.checked_add(payload))
2518        .ok_or(KeyStoreError::Selection("key material size overflow"))
2519}
2520
2521// Public identity stays borrowed (RSA), native (DSA), or stack-sized (EC).
2522// Import validation must not serialize an unaccounted SPKI beside live input.
2523#[cfg_attr(
2524    feature = "experimental-pq",
2525    expect(
2526        clippy::large_enum_variant,
2527        reason = "A transient, wire-bounded 2592-byte public key avoids a heap allocation during identity comparison"
2528    )
2529)]
2530enum PrivateKeyIdentity<'a> {
2531    Rsa(pkcs1::RsaPrivateKeyRef<'a>),
2532    #[cfg(feature = "experimental-pq")]
2533    Encapsulation {
2534        algorithm: pkcs8::AlgorithmIdentifierRef<'a>,
2535        public: [u8; 1568],
2536        length: usize,
2537    },
2538    Dsa {
2539        algorithm: pkcs8::AlgorithmIdentifierRef<'a>,
2540        key: NativeDsaSigningKey,
2541    },
2542    Ec {
2543        algorithm: pkcs8::AlgorithmIdentifierRef<'a>,
2544        point: [u8; 133],
2545        length: usize,
2546    },
2547    EdDsa {
2548        algorithm: pkcs8::AlgorithmIdentifierRef<'a>,
2549        public: [u8; 57],
2550        length: usize,
2551    },
2552    #[cfg(feature = "experimental-pq")]
2553    PostQuantum {
2554        algorithm: pkcs8::AlgorithmIdentifierRef<'a>,
2555        public: [u8; 2592],
2556        length: usize,
2557    },
2558}
2559
2560impl<'a> PrivateKeyIdentity<'a> {
2561    fn automatic_usages(&self) -> KeyUsages {
2562        match self {
2563            Self::Rsa(_) => KeyUsages::SIGN.union(KeyUsages::DECRYPT),
2564            #[cfg(feature = "experimental-pq")]
2565            Self::Encapsulation { .. } => KeyUsages::DECRYPT,
2566            _ => KeyUsages::SIGN,
2567        }
2568    }
2569
2570    fn can_decrypt(&self) -> bool {
2571        match self {
2572            Self::Rsa(_) => true,
2573            #[cfg(feature = "experimental-pq")]
2574            Self::Encapsulation { .. } => true,
2575            _ => false,
2576        }
2577    }
2578
2579    fn validate_usages(&self, usages: KeyUsages) -> Result<(), KeyStoreError> {
2580        #[cfg(feature = "experimental-pq")]
2581        if matches!(self, Self::Encapsulation { .. }) && usages != KeyUsages::DECRYPT {
2582            return Err(KeyStoreError::Selection(
2583                "ML-KEM private keys require DECRYPT-only usage",
2584            ));
2585        }
2586        #[cfg(not(feature = "experimental-pq"))]
2587        let _ = usages;
2588        Ok(())
2589    }
2590
2591    fn decode(der: &'a [u8]) -> Result<Self, KeyStoreError> {
2592        let info = PrivateKeyInfoRef::try_from(der)
2593            .map_err(|_| KeyStoreError::Selection("unsupported PKCS#12 private key"))?;
2594        #[cfg(feature = "experimental-pq")]
2595        if crate::provider::KeyEncapsulationAlgorithm::from_oid(info.algorithm.oid).is_some() {
2596            let key = crate::provider::RustCryptoMlKemPrivateKey::from_pkcs8_der(der)
2597                .map_err(|_| KeyStoreError::Selection("invalid ML-KEM private key"))?;
2598            // RFC 9935 sections 4 and 6: bind every private form to the same
2599            // raw encapsulation-key identity, with absent ASN.1 parameters.
2600            // https://www.rfc-editor.org/rfc/rfc9935.html#section-6
2601            let mut public = [0; 1568];
2602            let length = key
2603                .copy_public(&mut public)
2604                .map_err(|_| KeyStoreError::Selection("invalid ML-KEM identity"))?;
2605            return Ok(Self::Encapsulation {
2606                algorithm: info.algorithm,
2607                public,
2608                length,
2609            });
2610        }
2611        #[cfg(feature = "experimental-pq")]
2612        if let Some(parameter) = crate::xmldsig::PqAlgorithm::from_oid(info.algorithm.oid) {
2613            let key = crate::xmldsig::PostQuantumSigningKey::from_pkcs8_der(parameter, der)
2614                .map_err(|_| KeyStoreError::Selection("invalid PQ private key"))?;
2615            // RFC 9881 section 4 / RFC 9909 section 5: identity is the raw
2616            // public bytes, not a separately allocated ASN.1 serialization.
2617            let mut public = [0; 2592];
2618            let length = key.copy_public(&mut public);
2619            return Ok(Self::PostQuantum {
2620                algorithm: info.algorithm,
2621                public,
2622                length,
2623            });
2624        }
2625        // RFC 8410 sections 3 and 7 require absent parameters and a nested
2626        // octet string: https://www.rfc-editor.org/rfc/rfc8410.html#section-7
2627        // Primitive decoding validates that contract before deriving identity.
2628        if info.algorithm.oid == der::asn1::ObjectIdentifier::new_unwrap("1.3.101.112") {
2629            let key = ed25519_dalek::SigningKey::from_pkcs8_der(der)
2630                .map_err(|_| KeyStoreError::Selection("invalid Ed25519 private key"))?;
2631            let mut public = [0; 57];
2632            public[..32].copy_from_slice(&key.verifying_key().to_bytes());
2633            return Ok(Self::EdDsa {
2634                algorithm: info.algorithm,
2635                public,
2636                length: 32,
2637            });
2638        }
2639        if info.algorithm.oid == der::asn1::ObjectIdentifier::new_unwrap("1.3.101.113") {
2640            let key = ed448_goldilocks::SigningKey::from_pkcs8_der(der)
2641                .map_err(|_| KeyStoreError::Selection("invalid Ed448 private key"))?;
2642            return Ok(Self::EdDsa {
2643                algorithm: info.algorithm,
2644                public: key.verifying_key().to_bytes(),
2645                length: 57,
2646            });
2647        }
2648        if info.algorithm.oid == pkcs1::ALGORITHM_OID {
2649            preflight_rsa_pkcs1_components(info.private_key.as_bytes())?;
2650            RsaPrivateKey::from_pkcs8_der(der)
2651                .map_err(|_| KeyStoreError::Selection("invalid RSA private key"))?;
2652            return pkcs1::RsaPrivateKeyRef::from_der(info.private_key.as_bytes())
2653                .map(Self::Rsa)
2654                .map_err(|_| KeyStoreError::Selection("invalid RSA private key"));
2655        } else if info.algorithm.oid == dsa::OID {
2656            let algorithm = info.algorithm;
2657            return crate::xmldsig::sign::decode_dsa_signing_key(info)
2658                .map(|key| Self::Dsa { algorithm, key })
2659                .map_err(|_| KeyStoreError::Selection("invalid or oversized DSA private key"));
2660        }
2661        macro_rules! try_ec {
2662            ($key:ty) => {
2663                if let Ok(key) = <$key>::from_pkcs8_der(der) {
2664                    use p256::elliptic_curve::sec1::ToSec1Point as _;
2665                    let encoded = key.public_key().to_sec1_point(false);
2666                    let bytes = encoded.as_bytes();
2667                    let mut point = [0; 133];
2668                    point[..bytes.len()].copy_from_slice(bytes);
2669                    return Ok(Self::Ec {
2670                        algorithm: info.algorithm,
2671                        point,
2672                        length: bytes.len(),
2673                    });
2674                }
2675            };
2676        }
2677        try_ec!(p256::SecretKey);
2678        try_ec!(p384::SecretKey);
2679        try_ec!(p521::SecretKey);
2680        Err(KeyStoreError::Selection("unsupported PKCS#12 private key"))
2681    }
2682
2683    fn matches_spki(&self, der: &[u8]) -> bool {
2684        let Ok(spki) = pkcs8::SubjectPublicKeyInfoRef::from_der(der) else {
2685            return false;
2686        };
2687        let Some(bytes) = spki.subject_public_key.as_bytes() else {
2688            return false;
2689        };
2690        match self {
2691            #[cfg(feature = "experimental-pq")]
2692            Self::Encapsulation {
2693                algorithm,
2694                public,
2695                length,
2696            } => spki.algorithm == *algorithm && bytes == &public[..*length],
2697            #[cfg(feature = "experimental-pq")]
2698            Self::PostQuantum {
2699                algorithm,
2700                public,
2701                length,
2702            } => spki.algorithm == *algorithm && bytes == &public[..*length],
2703            Self::EdDsa {
2704                algorithm,
2705                public,
2706                length,
2707            } => spki.algorithm == *algorithm && bytes == &public[..*length],
2708            Self::Rsa(key) => {
2709                if spki.algorithm != pkcs1::ALGORITHM_ID {
2710                    return false;
2711                }
2712                let Ok(public) = pkcs1::RsaPublicKeyRef::from_der(bytes) else {
2713                    return false;
2714                };
2715                key.modulus == public.modulus && key.public_exponent == public.public_exponent
2716            }
2717            Self::Dsa { algorithm, key } => {
2718                if spki.algorithm != *algorithm {
2719                    return false;
2720                }
2721                let Ok(y) = der::asn1::UintRef::from_der(bytes) else {
2722                    return false;
2723                };
2724                // Compare native little-endian limbs as a byte iterator, without
2725                // materializing a second big integer or owned public encoding.
2726                key.verifying_key()
2727                    .y()
2728                    .as_words()
2729                    .iter()
2730                    .rev()
2731                    .flat_map(|word| word.to_be_bytes())
2732                    .skip_while(|byte| *byte == 0)
2733                    .eq(y.as_bytes().iter().copied())
2734            }
2735            Self::Ec {
2736                algorithm,
2737                point,
2738                length,
2739            } => spki.algorithm == *algorithm && bytes == &point[..*length],
2740        }
2741    }
2742}
2743
2744/// Validate borrowed PKCS#1 private components against process-safety ceilings.
2745///
2746/// Container adapters must call this before constructing native big integers.
2747/// This checks ingestion safety, not the operation's RSA algorithm/key policy.
2748pub fn preflight_rsa_pkcs1_components(der: &[u8]) -> Result<(), KeyStoreError> {
2749    let key = pkcs1::RsaPrivateKeyRef::from_der(der)
2750        .map_err(|_| KeyStoreError::Selection("invalid RSA private key"))?;
2751    let modulus = key.modulus.as_bytes();
2752    let maximum = crate::hard_limits::RSA_MODULUS_BIT_CEILING;
2753    if modulus.len() > maximum.div_ceil(8)
2754        || modulus
2755            .first()
2756            .is_some_and(|first| modulus.len() * 8 - first.leading_zeros() as usize > maximum)
2757    {
2758        return Err(KeyStoreError::Selection("RSA modulus exceeds safety limit"));
2759    }
2760    if [
2761        key.public_exponent,
2762        key.private_exponent,
2763        key.prime1,
2764        key.prime2,
2765        key.exponent1,
2766        key.exponent2,
2767        key.coefficient,
2768    ]
2769    .into_iter()
2770    .any(|component| component.as_bytes().len() > maximum.div_ceil(8))
2771        || key.other_prime_infos.as_ref().is_some_and(|infos| {
2772            infos.iter().any(|info| {
2773                [info.prime, info.exponent, info.coefficient]
2774                    .into_iter()
2775                    .any(|component| component.as_bytes().len() > maximum.div_ceil(8))
2776            })
2777        })
2778    {
2779        return Err(KeyStoreError::Selection(
2780            "RSA component exceeds safety limit",
2781        ));
2782    }
2783    Ok(())
2784}
2785
2786fn preflight_dsa_pkcs8_components(info: &PrivateKeyInfoRef<'_>) -> Result<(), KeyStoreError> {
2787    crate::xmldsig::sign::preflight_dsa_private_key(info)
2788        .map_err(|_| KeyStoreError::Selection("invalid or oversized DSA private key"))
2789}
2790
2791// Borrow the frame and stream whitespace-separated Base64 into one preflighted
2792// DER buffer. No normalized Base64 string or owned label/header copies are needed.
2793struct BorrowedPem<'a> {
2794    label: &'a str,
2795    data: &'a str,
2796    decoded_len: usize,
2797}
2798
2799impl<'a> BorrowedPem<'a> {
2800    fn parse(bytes: &'a [u8], maximum: usize) -> Result<Self, KeyStoreError> {
2801        if bytes.len() > maximum {
2802            return Err(import_resource_limit(
2803                crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
2804                maximum,
2805            ));
2806        }
2807        let invalid = || KeyStoreError::Selection("invalid PEM key");
2808        let text = std::str::from_utf8(bytes)
2809            .map_err(|_| invalid())?
2810            .trim_matches(|character: char| character.is_ascii_whitespace());
2811        let (label, rest) = text
2812            .strip_prefix("-----BEGIN ")
2813            .and_then(|rest| rest.split_once("-----"))
2814            .ok_or_else(invalid)?;
2815        let (payload, end) = rest
2816            .trim_start_matches([' ', '\t', '\n', '\r'])
2817            .split_once("-----END ")
2818            .ok_or_else(invalid)?;
2819        if end.strip_prefix(label) != Some("-----") || payload.contains("-----BEGIN ") {
2820            return Err(KeyStoreError::Selection(
2821                "PEM must contain one complete block",
2822            ));
2823        }
2824        // Preserve the existing importer's optional header and whitespace
2825        // acceptance without allocating header strings or a stripped body.
2826        let data = if let Some((headers, data)) = payload
2827            .split_once("\n\n")
2828            .or_else(|| payload.split_once("\r\n\r\n"))
2829        {
2830            if headers.lines().any(|line| !line.contains(':')) {
2831                return Err(invalid());
2832            }
2833            data
2834        } else {
2835            payload
2836        };
2837        let mut characters = 0_usize;
2838        let mut padding = 0_usize;
2839        for part in data.split_whitespace() {
2840            for byte in part.bytes() {
2841                if byte == b'=' {
2842                    padding += 1;
2843                } else if padding != 0
2844                    || !matches!(byte, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'+' | b'/')
2845                {
2846                    return Err(invalid());
2847                }
2848                characters += 1;
2849            }
2850        }
2851        if !characters.is_multiple_of(4) || padding > 2 {
2852            return Err(invalid());
2853        }
2854        let decoded_len = (characters / 4 * 3)
2855            .checked_sub(padding)
2856            .ok_or_else(invalid)?;
2857        Ok(Self {
2858            label,
2859            data,
2860            decoded_len,
2861        })
2862    }
2863
2864    fn decode(&self) -> Result<Vec<u8>, KeyStoreError> {
2865        let mut decoded = vec![0; self.decoded_len];
2866        self.decode_into(&mut decoded)?;
2867        Ok(decoded)
2868    }
2869
2870    fn decode_into(&self, decoded: &mut [u8]) -> Result<(), KeyStoreError> {
2871        use std::io::Read as _;
2872        let input = PemBase64Reader {
2873            parts: self.data.split_whitespace(),
2874            current: &[],
2875        };
2876        let mut decoder =
2877            base64::read::DecoderReader::new(input, &base64::engine::general_purpose::STANDARD);
2878        decoder
2879            .read_exact(decoded)
2880            .map_err(|_| KeyStoreError::Selection("invalid PEM key"))?;
2881        let mut eof = [0];
2882        if decoder
2883            .read(&mut eof)
2884            .map_err(|_| KeyStoreError::Selection("invalid PEM key"))?
2885            != 0
2886        {
2887            return Err(KeyStoreError::Selection("invalid PEM key"));
2888        }
2889        Ok(())
2890    }
2891}
2892
2893struct PemBase64Reader<'a> {
2894    parts: std::str::SplitWhitespace<'a>,
2895    current: &'a [u8],
2896}
2897
2898impl std::io::Read for PemBase64Reader<'_> {
2899    fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
2900        let mut written = 0;
2901        while written < buffer.len() {
2902            if self.current.is_empty() {
2903                let Some(part) = self.parts.next() else {
2904                    break;
2905                };
2906                self.current = part.as_bytes();
2907            }
2908            let length = self.current.len().min(buffer.len() - written);
2909            buffer[written..written + length].copy_from_slice(&self.current[..length]);
2910            self.current = &self.current[length..];
2911            written += length;
2912        }
2913        Ok(written)
2914    }
2915}
2916
2917#[cfg(test)]
2918fn single_pem_block(bytes: &[u8], maximum: usize) -> Result<pem::Pem, KeyStoreError> {
2919    if bytes.len() > maximum {
2920        return Err(import_resource_limit(
2921            crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
2922            maximum,
2923        ));
2924    }
2925    let text = std::str::from_utf8(bytes)
2926        .map_err(|_| KeyStoreError::Selection("PEM key is not ASCII text"))?
2927        .trim_matches(|character: char| character.is_ascii_whitespace());
2928    let block = pem::parse(text).map_err(|_| KeyStoreError::Selection("invalid PEM key"))?;
2929    let begin = format!("-----BEGIN {}-----", block.tag());
2930    let end = format!("-----END {}-----", block.tag());
2931    if !text.starts_with(&begin)
2932        || !text.ends_with(&end)
2933        || text.matches(&begin).count() != 1
2934        || text.matches(&end).count() != 1
2935    {
2936        return Err(KeyStoreError::Selection(
2937            "PEM must contain one complete block",
2938        ));
2939    }
2940    Ok(block)
2941}
2942
2943fn enforce_pkcs8_kdf_policy(
2944    encrypted: &EncryptedPrivateKeyInfoRef<'_>,
2945    resources: &ResourcePolicy,
2946    retained_with_input: usize,
2947) -> Result<usize, KeyStoreError> {
2948    use pkcs8::pkcs5::{EncryptionScheme, pbes2::Kdf};
2949    // RFC 8018 §6.2 leaves KDF iteration policy to the application. Reject
2950    // excessive work before decrypting attacker-supplied containers.
2951    // https://www.rfc-editor.org/rfc/rfc8018#section-6.2
2952    let EncryptionScheme::Pbes2(params) = &encrypted.encryption_algorithm else {
2953        return Err(KeyStoreError::ProtectedContainer);
2954    };
2955    // PBES2 decryption mutates a ciphertext-sized output buffer while the
2956    // encoded input remains live. Include that capacity before any password
2957    // callback or KDF, including failed padding/DER decoding.
2958    let live_with_output =
2959        retained_with_input.checked_add(encrypted.encrypted_data.as_bytes().len());
2960    if live_with_output.is_none_or(|total| total > resources.max_external_resource_total_bytes) {
2961        return Err(kdf_policy_violation(
2962            crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
2963            resources.max_external_resource_total_bytes,
2964            live_with_output.map(|total| total as u64),
2965        ));
2966    }
2967    let live_with_output = live_with_output.ok_or(KeyStoreError::ProtectedContainer)?;
2968    let work = match &params.kdf {
2969        Kdf::Pbkdf2(kdf) => {
2970            if kdf.iteration_count == 0 {
2971                return Err(KeyStoreError::ProtectedContainer);
2972            }
2973            use pkcs8::pkcs5::pbes2::Pbkdf2Prf;
2974            let hash_len = match kdf.prf {
2975                Pbkdf2Prf::HmacWithSha1 => 20,
2976                Pbkdf2Prf::HmacWithSha224 => 28,
2977                Pbkdf2Prf::HmacWithSha256 => 32,
2978                Pbkdf2Prf::HmacWithSha384 => 48,
2979                Pbkdf2Prf::HmacWithSha512 => 64,
2980                _ => return Err(KeyStoreError::ProtectedContainer),
2981            };
2982            // RFC 8018 5.2 steps 2-3: every ceil(dkLen/hLen) block runs c
2983            // PRFs. The cipher determines dkLen, not a caller's KDF hint.
2984            // https://www.rfc-editor.org/rfc/rfc8018#section-5.2
2985            let blocks = params.encryption.key_size().div_ceil(hash_len) as u64;
2986            let work = u64::from(kdf.iteration_count).checked_mul(blocks);
2987            if work.is_none_or(|work| work > resources.max_key_import_kdf_work as u64) {
2988                return Err(kdf_policy_violation(
2989                    crate::policy::resource_name::KEY_IMPORT_KDF_WORK,
2990                    resources.max_key_import_kdf_work,
2991                    work,
2992                ));
2993            }
2994            work.ok_or(KeyStoreError::ProtectedContainer)? as usize
2995        }
2996        Kdf::Scrypt(kdf) => enforce_scrypt_kdf_limits(
2997            kdf.cost_parameter,
2998            u64::from(kdf.block_size),
2999            u64::from(kdf.parallelization),
3000            resources,
3001            live_with_output,
3002        )?,
3003        _ => return Err(KeyStoreError::ProtectedContainer),
3004    };
3005    Ok(work)
3006}
3007
3008fn enforce_pkcs8_password_policy(
3009    encrypted: &EncryptedPrivateKeyInfoRef<'_>,
3010    password: &[u8],
3011    resources: &ResourcePolicy,
3012    live_bytes: usize,
3013) -> Result<usize, KeyStoreError> {
3014    if password.len() > resources.max_external_resource_bytes {
3015        return Err(kdf_policy_violation(
3016            crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
3017            resources.max_external_resource_bytes,
3018            Some(password.len() as u64),
3019        ));
3020    }
3021    let live = live_bytes.checked_add(password.len()).ok_or_else(|| {
3022        import_resource_limit(
3023            crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
3024            resources.max_external_resource_total_bytes,
3025        )
3026    })?;
3027    // Product work accounting: one unit per 64 input bytes conservatively
3028    // covers password hashing for the supported SHA PRFs, in addition to rounds.
3029    let hashing_passes = match &encrypted.encryption_algorithm {
3030        pkcs8::pkcs5::EncryptionScheme::Pbes2(params)
3031            if matches!(&params.kdf, pkcs8::pkcs5::pbes2::Kdf::Scrypt(_)) =>
3032        {
3033            2
3034        }
3035        _ => 1,
3036    };
3037    // Scrypt initializes password-keyed HMAC both before and after ROMix.
3038    let password_work = password.len().div_ceil(64) * hashing_passes;
3039    if password_work > resources.max_key_import_kdf_work {
3040        return Err(kdf_policy_violation(
3041            crate::policy::resource_name::KEY_IMPORT_KDF_WORK,
3042            resources.max_key_import_kdf_work,
3043            Some(password_work as u64),
3044        ));
3045    }
3046    let mut remaining = resources.clone();
3047    remaining.max_key_import_kdf_work -= password_work;
3048    enforce_pkcs8_kdf_policy(encrypted, &remaining, live)
3049        .map(|work| work + password_work)
3050        .map_err(|error| match error {
3051            KeyStoreError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3052                resource: crate::policy::resource_name::KEY_IMPORT_KDF_WORK,
3053                actual,
3054                ..
3055            }) => kdf_policy_violation(
3056                crate::policy::resource_name::KEY_IMPORT_KDF_WORK,
3057                resources.max_key_import_kdf_work,
3058                Some(actual.saturating_add(password_work) as u64),
3059            ),
3060            error => error,
3061        })
3062}
3063
3064fn enforce_scrypt_kdf_limits(
3065    n: u64,
3066    r: u64,
3067    p: u64,
3068    resources: &ResourcePolicy,
3069    retained_with_input: usize,
3070) -> Result<usize, KeyStoreError> {
3071    if n == 0 || r == 0 || p == 0 {
3072        return Err(KeyStoreError::ProtectedContainer);
3073    }
3074    let work = n.checked_mul(r).and_then(|value| value.checked_mul(p));
3075    if work.is_none_or(|value| value > resources.max_key_import_kdf_work as u64) {
3076        return Err(kdf_policy_violation(
3077            crate::policy::resource_name::KEY_IMPORT_KDF_WORK,
3078            resources.max_key_import_kdf_work,
3079            work,
3080        ));
3081    }
3082    // RustCrypto scrypt retains B[p*r] plus V[N*r] and T[r] per parallel
3083    // worker when its `parallel` feature is unified in a downstream build.
3084    let memory = n
3085        .checked_add(2)
3086        .and_then(|blocks| blocks.checked_mul(p))
3087        .and_then(|blocks| blocks.checked_mul(r))
3088        .and_then(|blocks| blocks.checked_mul(128));
3089    if memory.is_none_or(|value| value > resources.max_key_import_kdf_memory_bytes as u64) {
3090        return Err(kdf_policy_violation(
3091            crate::policy::resource_name::KEY_IMPORT_KDF_MEMORY,
3092            resources.max_key_import_kdf_memory_bytes,
3093            memory,
3094        ));
3095    }
3096    // The workspace is live alongside the existing inventory, name and
3097    // imported container. An independent KDF ceiling cannot waive that peak.
3098    let total = memory.and_then(|memory| memory.checked_add(retained_with_input as u64));
3099    if total.is_none_or(|total| total > resources.max_external_resource_total_bytes as u64) {
3100        return Err(kdf_policy_violation(
3101            crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
3102            resources.max_external_resource_total_bytes,
3103            total,
3104        ));
3105    }
3106    // The work check above proves this value exists and fits the usize ceiling.
3107    Ok(work.ok_or(KeyStoreError::ProtectedContainer)? as usize)
3108}
3109
3110fn kdf_policy_violation(
3111    resource: &'static str,
3112    maximum: usize,
3113    actual: Option<u64>,
3114) -> KeyStoreError {
3115    KeyStoreError::Policy(crate::policy::PolicyViolation::ResourceLimit {
3116        resource,
3117        maximum,
3118        actual: actual
3119            .and_then(|value| usize::try_from(value).ok())
3120            .unwrap_or(usize::MAX),
3121    })
3122}
3123
3124fn element_text(node: Node<'_, '_>) -> Result<String, KeyStoreError> {
3125    let length = node
3126        .children()
3127        .filter(|child| child.is_text())
3128        .filter_map(|child| child.text())
3129        .map(str::len)
3130        .sum();
3131    let mut text = String::with_capacity(length);
3132    for child in node.children() {
3133        if child.is_element() {
3134            return Err(KeyStoreError::Invalid("unexpected nested element".into()));
3135        }
3136        if child.is_text() {
3137            text.push_str(child.text().unwrap_or_default());
3138        }
3139    }
3140    Ok(text)
3141}
3142
3143fn decode_xml_base64(node: Node<'_, '_>) -> Result<Vec<u8>, KeyStoreError> {
3144    xml_base64_payload(node)?
3145        .decode()
3146        .map_err(|reason| KeyStoreError::Invalid(reason.into()))
3147}
3148
3149fn decode_xml_base64_secret(node: Node<'_, '_>) -> Result<Zeroizing<Vec<u8>>, KeyStoreError> {
3150    let payload = xml_base64_payload(node)?;
3151    // Own the zeroizing guard before decoding: malformed trailing bits can
3152    // fail after part of the secret has already been written into the buffer.
3153    let mut output = Zeroizing::new(vec![0; payload.decoded_len]);
3154    payload
3155        .decode_into(&mut output)
3156        .map_err(|reason| KeyStoreError::Invalid(reason.into()))?;
3157    Ok(output)
3158}
3159
3160fn xml_base64_payload<'a, 'input>(
3161    node: Node<'a, 'input>,
3162) -> Result<crate::xmldsig::whitespace::XmlBase64Payload<'a, 'input>, KeyStoreError> {
3163    crate::xmldsig::whitespace::XmlBase64Payload::new(node)
3164        .map_err(|reason| KeyStoreError::Invalid(reason.into()))
3165}
3166
3167struct XmlImportMaterialBudget {
3168    used: usize,
3169    maximum: usize,
3170}
3171
3172impl XmlImportMaterialBudget {
3173    fn reserve(&mut self, length: usize) -> Result<(), KeyStoreError> {
3174        if self.used > self.maximum || length > self.maximum - self.used {
3175            return Err(import_resource_limit(
3176                crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
3177                self.maximum,
3178            ));
3179        }
3180        self.used += length;
3181        Ok(())
3182    }
3183}
3184
3185fn preflight_xml_material(
3186    info: Node<'_, '_>,
3187    budget: &mut XmlImportMaterialBudget,
3188) -> Result<(), KeyStoreError> {
3189    // The source remains live while decoded entries and cloned names are
3190    // retained. Charge them before importing any entry, including failed ones.
3191    let key = info
3192        .children()
3193        .find(|child| child.has_tag_name((XMLDSIG_NS, "KeyValue")))
3194        .and_then(|value| value.children().find(|child| child.is_element()));
3195    let has_der = info
3196        .children()
3197        .any(|child| child.has_tag_name((XMLDSIG11_NS, "DEREncodedKeyValue")));
3198    let name_copies = match key {
3199        Some(key) if key.has_tag_name((XMLDSIG_NS, "DSAKeyValue")) => {
3200            if key
3201                .children()
3202                .any(|child| child.has_tag_name((XMLSEC_NS, "X")))
3203            {
3204                4
3205            } else {
3206                3
3207            }
3208        }
3209        Some(key)
3210            if key.has_tag_name((XMLDSIG_NS, "RSAKeyValue"))
3211                || key.has_tag_name((XMLDSIG11_NS, "ECKeyValue")) =>
3212        {
3213            3
3214        }
3215        _ if has_der => 3,
3216        _ => 2,
3217    };
3218    let uses_shared_public_parser = has_der
3219        || key.is_some_and(|key| {
3220            key.has_tag_name((XMLDSIG_NS, "RSAKeyValue"))
3221                || key.has_tag_name((XMLDSIG11_NS, "ECKeyValue"))
3222        });
3223    if uses_shared_public_parser {
3224        crate::xmldsig::parse::validate_key_info_container(info)
3225            .map_err(|error| KeyStoreError::Invalid(error.to_string()))?;
3226    }
3227    for child in info.children().filter(|child| child.is_element()) {
3228        if child.has_tag_name((XMLDSIG_NS, "KeyName")) {
3229            let length = child
3230                .children()
3231                .filter(|node| node.is_text())
3232                .filter_map(|node| node.text())
3233                .try_fold(0_usize, |sum, text| {
3234                    sum.checked_add(text.len())
3235                        .ok_or(KeyStoreError::Selection("key material size overflow"))
3236                })?;
3237            // Inventory, duplicate-name set, KeyInfo and private DSA name.
3238            if uses_shared_public_parser && length > crate::xmldsig::parse::MAX_KEY_NAME_TEXT_LEN {
3239                return Err(KeyStoreError::Invalid(
3240                    "KeyName exceeds maximum allowed text length".into(),
3241                ));
3242            }
3243            for _ in 0..name_copies {
3244                budget.reserve(length)?;
3245            }
3246        } else if child.has_tag_name((XMLDSIG11_NS, "DEREncodedKeyValue")) {
3247            let payload = xml_base64_payload(child)?;
3248            budget.reserve(payload.decoded_len)?;
3249        } else if child.has_tag_name((XMLDSIG_NS, "KeyValue")) {
3250            for key in child.children().filter(|node| node.is_element()) {
3251                if key.has_tag_name((XMLSEC_NS, "HMACKeyValue"))
3252                    || key.has_tag_name((XMLSEC_NS, "AESKeyValue"))
3253                    || key.has_tag_name((XMLSEC_NS, "DESKeyValue"))
3254                {
3255                    budget.reserve(xml_base64_payload(key)?.decoded_len)?;
3256                } else if key.has_tag_name((XMLDSIG_NS, "RSAKeyValue"))
3257                    || key.has_tag_name((XMLDSIG_NS, "DSAKeyValue"))
3258                    || key.has_tag_name((XMLDSIG11_NS, "ECKeyValue"))
3259                {
3260                    for component in key.children().filter(|node| node.is_element()) {
3261                        if component.has_tag_name((XMLDSIG11_NS, "NamedCurve")) {
3262                            budget.reserve(component.attribute("URI").map_or(0, str::len))?;
3263                        } else {
3264                            budget.reserve(xml_base64_payload(component)?.decoded_len)?;
3265                        }
3266                    }
3267                }
3268            }
3269        }
3270    }
3271    Ok(())
3272}
3273
3274fn parse_xmlsec_dsa_key_value(
3275    node: Node<'_, '_>,
3276    budget: &mut XmlImportMaterialBudget,
3277) -> Result<ParsedDsaKey, KeyStoreError> {
3278    let mut p = None;
3279    let mut q = None;
3280    let mut g = None;
3281    let mut y = None;
3282    let mut seed = None;
3283    let mut counter = None;
3284    let mut private_x = None;
3285    let mut previous_position = None;
3286    for child in node.children().filter(|child| child.is_element()) {
3287        let (position, slot) = if child.has_tag_name((XMLDSIG_NS, "P")) {
3288            (0, Some(&mut p))
3289        } else if child.has_tag_name((XMLDSIG_NS, "Q")) {
3290            (1, Some(&mut q))
3291        } else if child.has_tag_name((XMLDSIG_NS, "G")) {
3292            (2, Some(&mut g))
3293        } else if child.has_tag_name((XMLDSIG_NS, "Y")) {
3294            (4, Some(&mut y))
3295        } else if child.has_tag_name((XMLSEC_NS, "X")) {
3296            if private_x.is_some() {
3297                return Err(KeyStoreError::Invalid("duplicate DSA X".into()));
3298            }
3299            // XMLDSig 1.1 §4.5.2.1 has no private X field. libxmlsec's
3300            // keys.xml adds one before Y; only this store importer accepts it.
3301            // https://www.w3.org/TR/xmldsig-core1/#sec-DSAKeyValue
3302            private_x = Some(decode_xml_base64_secret(child)?);
3303            (3, None)
3304        } else if child.has_tag_name((XMLDSIG_NS, "J")) {
3305            (5, None)
3306        } else if child.has_tag_name((XMLDSIG_NS, "Seed")) {
3307            (6, Some(&mut seed))
3308        } else if child.has_tag_name((XMLDSIG_NS, "PgenCounter")) {
3309            (7, Some(&mut counter))
3310        } else {
3311            return Err(KeyStoreError::Invalid(
3312                "unsupported DSAKeyValue child".into(),
3313            ));
3314        };
3315        // XMLDSig 1.1 §4.5.2.1 defines a sequence, not an unordered set.
3316        if previous_position.is_some_and(|previous| position <= previous) {
3317            return Err(KeyStoreError::Invalid(
3318                "DSA parameters are out of order".into(),
3319            ));
3320        }
3321        previous_position = Some(position);
3322        if let Some(slot) = slot {
3323            if slot.replace(decode_xml_base64(child)?).is_some() {
3324                return Err(KeyStoreError::Invalid("duplicate DSA parameter".into()));
3325            }
3326        } else if position == 5 {
3327            let _ = decode_xml_base64(child)?;
3328        }
3329    }
3330    if p.is_some() != q.is_some() {
3331        return Err(KeyStoreError::Invalid(
3332            "DSA P and Q must occur together".into(),
3333        ));
3334    }
3335    if seed.is_some() != counter.is_some() {
3336        return Err(KeyStoreError::Invalid(
3337            "DSA Seed and PgenCounter must occur together".into(),
3338        ));
3339    }
3340    if [p.as_ref(), q.as_ref(), g.as_ref(), y.as_ref()]
3341        .into_iter()
3342        .flatten()
3343        .any(|component| component.len() > crate::hard_limits::DSA_KEY_COMPONENT_BYTE_CEILING)
3344        || private_x.as_ref().is_some_and(|component| {
3345            component.len() > crate::hard_limits::DSA_KEY_COMPONENT_BYTE_CEILING
3346        })
3347    {
3348        return Err(KeyStoreError::Invalid(
3349            "DSA component exceeds safety limit".into(),
3350        ));
3351    }
3352    let y = y.ok_or_else(|| KeyStoreError::Invalid("DSAKeyValue requires Y".into()))?;
3353    let private =
3354        if let Some(x) = private_x {
3355            let (Some(p), Some(q), Some(g)) = (&p, &q, &g) else {
3356                return Err(KeyStoreError::Invalid(
3357                    "private DSA key requires P, Q, and G".into(),
3358                ));
3359            };
3360            let components = DsaComponents::from_components(
3361                BoxedUint::from_be_slice_vartime(p),
3362                BoxedUint::from_be_slice_vartime(q),
3363                BoxedUint::from_be_slice_vartime(g),
3364            )
3365            .map_err(|_| KeyStoreError::Invalid("invalid DSA parameters".into()))?;
3366            let x_value = BoxedUint::from_be_slice_vartime(&x);
3367            let monty = BoxedMontyParams::new(components.p().clone());
3368            let expected_y = BoxedMontyForm::new((**components.g()).clone(), &monty)
3369                .pow(&x_value)
3370                .retrieve();
3371            if expected_y != BoxedUint::from_be_slice_vartime(&y) {
3372                return Err(KeyStoreError::Invalid(
3373                    "DSA private and public values differ".into(),
3374                ));
3375            }
3376            let public = DsaVerifyingKey::from_components(components, expected_y)
3377                .map_err(|_| KeyStoreError::Invalid("invalid DSA public key".into()))?;
3378            let private = NativeDsaSigningKey::from_components(public, x_value)
3379                .map_err(|_| KeyStoreError::Invalid("invalid DSA private key".into()))?;
3380            use der::Encode as _;
3381            // The native encoder owns parameter DER, padded X bytes, X DER and
3382            // PKCS#8 DER concurrently; the retained copy also exists before return.
3383            let parameters_len =
3384                usize::try_from(private.verifying_key().components().encoded_len().map_err(
3385                    |_| KeyStoreError::Invalid("DSA private key encoding failed".into()),
3386                )?)
3387                .map_err(|_| KeyStoreError::Selection("key material size overflow"))?;
3388            let x_bytes_len = usize::try_from(private.x().bits_precision() / 8)
3389                .map_err(|_| KeyStoreError::Selection("key material size overflow"))?;
3390            // DER length headers and the PKCS#8 algorithm identifier fit this
3391            // checked bound: each header is at most 1 tag + 1 count + sizeof(usize).
3392            let header = 2 + std::mem::size_of::<usize>();
3393            let x_der_len = x_bytes_len
3394                .checked_add(header + 1)
3395                .ok_or(KeyStoreError::Selection("key material size overflow"))?;
3396            let pkcs8_len = parameters_len
3397                .checked_add(x_der_len)
3398                .and_then(|length| length.checked_add(4 * header + 16))
3399                .ok_or(KeyStoreError::Selection("key material size overflow"))?;
3400            budget.reserve(parameters_len)?;
3401            budget.reserve(x_bytes_len)?;
3402            budget.reserve(x_der_len)?;
3403            budget.reserve(pkcs8_len)?;
3404            budget.reserve(pkcs8_len)?;
3405            Some(Zeroizing::new(
3406                private
3407                    .to_pkcs8_der()
3408                    .map_err(|_| KeyStoreError::Invalid("DSA private key encoding failed".into()))?
3409                    .as_bytes()
3410                    .to_vec(),
3411            ))
3412        } else {
3413            None
3414        };
3415    Ok((KeyValueInfo::Dsa { p, q, g, y }, private))
3416}
3417
3418#[cfg(test)]
3419mod tests {
3420    #[cfg(not(feature = "xmlenc"))]
3421    use crate::rsa_encoding::RsaPublicKeyEncoding as _;
3422    use pkcs8::EncodePublicKey as _;
3423    use rand_chacha::{ChaCha8Rng, rand_core::SeedableRng as _};
3424    use rsa::RsaPublicKey;
3425
3426    use super::*;
3427
3428    fn xml_policy(resources: ResourcePolicy) -> crate::policy::VerificationPolicy {
3429        crate::policy::VerificationPolicy {
3430            resources,
3431            ..crate::policy::VerificationPolicy::default()
3432        }
3433    }
3434
3435    #[test]
3436    fn inventory_merge_preserves_import_work() {
3437        // Moving inventories must not discard work spent before the merge.
3438        let resources = ResourcePolicy {
3439            max_key_import_kdf_work: 5,
3440            ..Default::default()
3441        };
3442        let mut inventory = KeyInventory {
3443            kdf_work: 2,
3444            ..Default::default()
3445        };
3446        inventory
3447            .extend(
3448                KeyInventory {
3449                    kdf_work: 3,
3450                    ..Default::default()
3451                },
3452                &resources,
3453            )
3454            .expect("exact work allowance");
3455        assert_eq!(inventory.key_import_kdf_work(), 5);
3456        assert!(matches!(
3457            inventory.extend(
3458                KeyInventory {
3459                    kdf_work: 1,
3460                    ..Default::default()
3461                },
3462                &resources
3463            ),
3464            Err(KeyStoreError::Policy(_))
3465        ));
3466        assert_eq!(inventory.key_import_kdf_work(), 5, "failed merge is atomic");
3467    }
3468
3469    #[test]
3470    fn repeated_pkcs12_imports_share_kdf_work() {
3471        // Each bundle fits by itself, but a session may not reset password work.
3472        let bytes = include_bytes!("../tests/fixtures/xmlenc/01-phaos-xmlenc-3/rsa-priv-key.p12");
3473        let resources = ResourcePolicy {
3474            max_key_import_kdf_work: 10_000,
3475            ..ResourcePolicy::default()
3476        };
3477        let mut inventory = KeyInventory::default();
3478        inventory
3479            .add_pkcs12("first".into(), bytes, "secret", &resources)
3480            .expect("first bundle fits");
3481        assert!(matches!(
3482            inventory.add_pkcs12("second".into(), bytes, "secret", &resources),
3483            Err(KeyStoreError::Policy(_))
3484        ));
3485        let work = inventory.key_import_kdf_work();
3486        let exact = ResourcePolicy {
3487            max_key_import_kdf_work: work * 2,
3488            ..resources
3489        };
3490        let mut inventory = KeyInventory::default();
3491        inventory
3492            .add_pkcs12("first".into(), bytes, "secret", &exact)
3493            .expect("first exact-boundary bundle");
3494        inventory
3495            .add_pkcs12("second".into(), bytes, "secret", &exact)
3496            .expect("second exact-boundary bundle");
3497        assert_eq!(inventory.key_import_kdf_work(), work * 2);
3498    }
3499
3500    #[test]
3501    fn imports_donor_pkcs12_and_rejects_wrong_password() {
3502        // A real upstream PHAOS bundle exercises MAC, password decoding, key
3503        // association, and certificate import rather than a synthetic ASN.1 stub.
3504        let bytes = include_bytes!("../tests/fixtures/xmlenc/01-phaos-xmlenc-3/rsa-priv-key.p12");
3505        let resources = ResourcePolicy::default();
3506        let mut inventory = KeyInventory::default();
3507        inventory
3508            .add_pkcs12("phaos".into(), bytes, "secret", &resources)
3509            .expect("donor PKCS#12 should import");
3510        assert_eq!(inventory.private_keys.len(), 1);
3511        assert!(!inventory.private_keys[0].certificate_chain.is_empty());
3512        assert!(inventory.private_keys[0].usages.allows(KeyUsage::Sign));
3513
3514        let mut wrong = KeyInventory::default();
3515        assert!(matches!(
3516            wrong.add_pkcs12("phaos".into(), bytes, "wrong", &resources),
3517            Err(KeyStoreError::ProtectedContainer)
3518        ));
3519        assert!(wrong.private_keys.is_empty());
3520
3521        let oversized = ResourcePolicy {
3522            max_external_resource_bytes: bytes.len() - 1,
3523            ..ResourcePolicy::default()
3524        };
3525        assert!(matches!(
3526            KeyInventory::default().add_pkcs12("phaos".into(), bytes, "secret", &oversized),
3527            Err(KeyStoreError::Policy(
3528                crate::policy::PolicyViolation::ResourceLimitExceeded {
3529                    resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
3530                    maximum,
3531                }
3532            )) if maximum == bytes.len() - 1
3533        ));
3534    }
3535
3536    #[test]
3537    fn pkcs12_kdf_limit_is_a_typed_policy_denial() {
3538        // A valid protected bundle that exceeds import work is not a bad password.
3539        let bytes = include_bytes!("../tests/fixtures/xmlenc/01-phaos-xmlenc-3/rsa-priv-key.p12");
3540        let resources = ResourcePolicy {
3541            max_key_import_kdf_work: 1,
3542            ..ResourcePolicy::default()
3543        };
3544        assert!(matches!(
3545            KeyInventory::default().add_pkcs12("phaos".into(), bytes, "secret", &resources),
3546            Err(KeyStoreError::Policy(
3547                crate::policy::PolicyViolation::KdfIterationsOutsideLimit { maximum: 1 }
3548            ))
3549        ));
3550        assert!(matches!(
3551            KeyInventory::default().add_pkcs12(
3552                "phaos".into(),
3553                bytes,
3554                "wrong",
3555                &ResourcePolicy::default(),
3556            ),
3557            Err(KeyStoreError::ProtectedContainer)
3558        ));
3559    }
3560
3561    #[test]
3562    fn pkcs12_visible_encryption_kdf_is_checked_before_password() {
3563        // Raising an unencrypted PBES2 iteration count must deny the import
3564        // before asking for a secret, even when MacData is within the limit.
3565        let mut bytes =
3566            include_bytes!("../tests/fixtures/xmlenc/01-phaos-xmlenc-3/rsa-priv-key.p12").to_vec();
3567        let offset = bytes
3568            .windows(4)
3569            .position(|v| v == [2, 2, 8, 0])
3570            .expect("PBKDF2 iterations");
3571        bytes[offset + 3] = 1;
3572        let resources = ResourcePolicy {
3573            max_key_import_kdf_work: 2048,
3574            ..ResourcePolicy::default()
3575        };
3576        let result = KeyInventory::default().add_pkcs12_with_password_callback(
3577            "limited".into(),
3578            &bytes,
3579            || panic!("visible KDF must be checked first"),
3580            KeyUsages::SIGN,
3581            &resources,
3582        );
3583        assert!(matches!(result, Err(KeyStoreError::Policy(_))));
3584    }
3585
3586    #[test]
3587    fn pkcs12_content_count_denial_is_not_a_password_error() {
3588        // AuthenticatedSafe has two content infos; its count is public and
3589        // must report the candidate policy rather than request a password.
3590        let bytes = include_bytes!("../tests/fixtures/xmlenc/01-phaos-xmlenc-3/rsa-priv-key.p12");
3591        let resources = ResourcePolicy {
3592            max_key_candidates: 1,
3593            ..ResourcePolicy::default()
3594        };
3595        let result = KeyInventory::default().add_pkcs12_with_password_callback(
3596            "limited".into(),
3597            bytes,
3598            || panic!("container limit must be checked first"),
3599            KeyUsages::SIGN,
3600            &resources,
3601        );
3602        assert!(matches!(
3603            result,
3604            Err(KeyStoreError::Policy(
3605                crate::policy::PolicyViolation::ResourceLimitExceeded {
3606                    resource: crate::policy::resource_name::KEY_CANDIDATES,
3607                    maximum: 1,
3608                }
3609            ))
3610        ));
3611    }
3612
3613    #[test]
3614    fn pkcs12_aggregate_kdf_work_preserves_policy_error() {
3615        // Individual counts fit, but MAC plus PBES2 exceeds one shared budget.
3616        let resources = ResourcePolicy {
3617            max_key_import_kdf_work: 3000,
3618            ..ResourcePolicy::default()
3619        };
3620        let bytes = include_bytes!("../tests/fixtures/xmlenc/01-phaos-xmlenc-3/rsa-priv-key.p12");
3621        assert!(matches!(
3622            KeyInventory::default().add_pkcs12("limited".into(), bytes, "secret", &resources),
3623            Err(KeyStoreError::Policy(
3624                crate::policy::PolicyViolation::ResourceLimitExceeded {
3625                    resource: crate::policy::resource_name::KEY_IMPORT_KDF_WORK,
3626                    maximum: 3000,
3627                }
3628            ))
3629        ));
3630    }
3631
3632    #[test]
3633    fn pkcs12_workspace_denial_preserves_policy_error() {
3634        // KDF workspace denial must not look like a wrong password.
3635        let resources = ResourcePolicy {
3636            max_key_import_kdf_memory_bytes: 1,
3637            ..ResourcePolicy::default()
3638        };
3639        let bytes = include_bytes!("../tests/fixtures/xmlenc/01-phaos-xmlenc-3/rsa-priv-key.p12");
3640        assert!(matches!(
3641            KeyInventory::default().add_pkcs12("limited".into(), bytes, "secret", &resources),
3642            Err(KeyStoreError::Policy(
3643                crate::policy::PolicyViolation::ResourceLimitExceeded {
3644                    resource: crate::policy::resource_name::KEY_IMPORT_KDF_MEMORY,
3645                    maximum: 1,
3646                }
3647            ))
3648        ));
3649    }
3650
3651    #[test]
3652    fn pkcs12_temporary_memory_shares_existing_inventory_budget() {
3653        // Encoded input fits, but decrypted buffers and retained vector slots
3654        // must not receive a fresh aggregate allowance beside existing keys.
3655        let resources = ResourcePolicy {
3656            max_external_resource_bytes: 3000,
3657            max_external_resource_total_bytes: 5000,
3658            ..ResourcePolicy::default()
3659        };
3660        let mut inventory = KeyInventory::default();
3661        inventory
3662            .add_symmetric(
3663                "hmac".into(),
3664                SymmetricKeyKind::Hmac,
3665                vec![1; 2000],
3666                KeyUsages::SIGN,
3667                &resources,
3668            )
3669            .expect("existing key fits");
3670        let bytes = include_bytes!("../tests/fixtures/xmlenc/01-phaos-xmlenc-3/rsa-priv-key.p12");
3671        assert!(matches!(
3672            inventory.add_pkcs12("bundle".into(), bytes, "secret", &resources),
3673            Err(KeyStoreError::Policy(
3674                crate::policy::PolicyViolation::ResourceLimitExceeded {
3675                    resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
3676                    maximum: 5000
3677                }
3678            ))
3679        ));
3680        assert_eq!(inventory.symmetric_keys().len(), 1);
3681        assert!(inventory.private_keys().is_empty());
3682    }
3683
3684    #[test]
3685    fn private_bundle_certificates_do_not_authorize_verification() {
3686        // A SIGN/DECRYPT-only PKCS#12 bundle must not implicitly make its
3687        // associated leaf available as a verification lookup candidate.
3688        let bytes = include_bytes!("../tests/fixtures/xmlenc/01-phaos-xmlenc-3/rsa-priv-key.p12");
3689        let resources = ResourcePolicy::default();
3690        let mut inventory = KeyInventory::default();
3691        inventory
3692            .add_pkcs12("private".into(), bytes, "secret", &resources)
3693            .expect("bundle imports");
3694        let leaf = inventory.private_keys()[0].certificate_chain[0].clone();
3695        let subject = crate::xmldsig::parse::parse_x509_certificate(&leaf)
3696            .expect("bundle leaf parses")
3697            .subject_dn;
3698        let key_info = KeyInfo {
3699            sources: vec![KeyInfoSource::X509Data(X509DataInfo {
3700                subject_names: vec![subject],
3701                ..X509DataInfo::default()
3702            })],
3703        };
3704        let resolver = inventory.verification_resolver();
3705        assert!(
3706            resolver
3707                .resolve(Some(&key_info), SignatureAlgorithm::RsaSha256)
3708                .expect("lookup completes")
3709                .is_none()
3710        );
3711
3712        inventory
3713            .add_certificate_der(leaf, false, &resources)
3714            .expect("explicit lookup certificate imports");
3715        assert!(
3716            inventory
3717                .verification_resolver()
3718                .resolve(Some(&key_info), SignatureAlgorithm::RsaSha256)
3719                .expect("explicit lookup completes")
3720                .is_some()
3721        );
3722    }
3723
3724    #[test]
3725    fn stored_signing_keys_obey_operation_material_limits() {
3726        // An import-time resource policy must not override stricter limits
3727        // selected for a later signing operation.
3728        let resources = ResourcePolicy::default();
3729        let mut inventory = KeyInventory::default();
3730        inventory
3731            .add_symmetric(
3732                "hmac".into(),
3733                SymmetricKeyKind::Hmac,
3734                b"0123456789abcdef0123456789abcdef".to_vec(),
3735                KeyUsages::SIGN,
3736                &resources,
3737            )
3738            .expect("HMAC imports");
3739        inventory
3740            .add_private_pem(
3741                "rsa".into(),
3742                include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-key.pem"),
3743                None,
3744                KeyUsages::SIGN,
3745                &resources,
3746            )
3747            .expect("RSA imports");
3748
3749        for (name, algorithm, length) in [
3750            (
3751                "hmac",
3752                SignatureAlgorithm::HmacSha256,
3753                inventory.symmetric_keys[0].bytes.len(),
3754            ),
3755            (
3756                "rsa",
3757                SignatureAlgorithm::RsaSha256,
3758                inventory.private_keys[0].pkcs8_der.len(),
3759            ),
3760        ] {
3761            let mut policy = crate::policy::SigningPolicy::default();
3762            policy.resources.max_external_resource_bytes = length - 1;
3763            assert!(matches!(
3764                inventory.signing_key(name, algorithm, &policy),
3765                Err(KeyStoreError::Policy(
3766                    crate::policy::PolicyViolation::ResourceLimit {
3767                        resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
3768                        maximum,
3769                        actual,
3770                    }
3771                )) if maximum == length - 1 && actual == length
3772            ));
3773
3774            policy.resources.max_external_resource_bytes = length;
3775            policy.resources.max_external_resource_total_bytes = length - 1;
3776            assert!(matches!(
3777                inventory.signing_key(name, algorithm, &policy),
3778                Err(KeyStoreError::Policy(
3779                    crate::policy::PolicyViolation::ResourceLimit {
3780                        resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
3781                        maximum,
3782                        actual,
3783                    }
3784                )) if maximum == length - 1 && actual == length
3785            ));
3786        }
3787    }
3788
3789    #[test]
3790    fn named_signing_lookup_obeys_active_candidate_budget() {
3791        // Import limits do not authorize a later operation to scan the full inventory.
3792        let resources = ResourcePolicy::default();
3793        let mut inventory = KeyInventory::default();
3794        for name in ["first", "second"] {
3795            inventory
3796                .add_symmetric(
3797                    name.into(),
3798                    SymmetricKeyKind::Hmac,
3799                    vec![0x42; 32],
3800                    KeyUsages::SIGN,
3801                    &resources,
3802                )
3803                .expect("HMAC imports");
3804        }
3805        let mut policy = crate::policy::SigningPolicy::default();
3806        policy.resources.max_key_candidates = 1;
3807        assert!(
3808            inventory
3809                .signing_key("first", SignatureAlgorithm::HmacSha256, &policy)
3810                .is_ok()
3811        );
3812        assert!(matches!(
3813            inventory.signing_key("second", SignatureAlgorithm::HmacSha256, &policy),
3814            Err(KeyStoreError::Policy(
3815                crate::policy::PolicyViolation::ResourceLimit {
3816                    resource: crate::policy::resource_name::KEY_CANDIDATES,
3817                    ..
3818                }
3819            ))
3820        ));
3821    }
3822
3823    #[cfg(feature = "xmlenc")]
3824    #[test]
3825    fn named_decryption_lookup_shares_candidate_budget_across_key_kinds() {
3826        // Scanning a symmetric miss must consume the same operation budget as
3827        // the subsequent private-key lookup.
3828        let resources = ResourcePolicy::default();
3829        let mut inventory = KeyInventory::default();
3830        inventory
3831            .add_symmetric(
3832                "aes".into(),
3833                SymmetricKeyKind::Aes,
3834                vec![0x42; 32],
3835                KeyUsages::DECRYPT,
3836                &resources,
3837            )
3838            .expect("AES imports");
3839        inventory
3840            .add_private_pem(
3841                "rsa".into(),
3842                include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-key.pem"),
3843                None,
3844                KeyUsages::DECRYPT,
3845                &resources,
3846            )
3847            .expect("RSA imports");
3848        let mut policy = crate::policy::DecryptionPolicy::default();
3849        policy.resources.max_key_candidates = 1;
3850        assert!(inventory.decryption_resolver("aes", &policy).is_ok());
3851        assert!(matches!(
3852            inventory.decryption_resolver("rsa", &policy),
3853            Err(KeyStoreError::Policy(
3854                crate::policy::PolicyViolation::ResourceLimit {
3855                    resource: crate::policy::resource_name::KEY_CANDIDATES,
3856                    ..
3857                }
3858            ))
3859        ));
3860    }
3861
3862    #[test]
3863    fn pkcs12_imports_share_candidate_budget() {
3864        // Two ContentInfos and two SafeBags cost four inspections. The retained
3865        // key/certificate use two inventory slots, leaving too little work for
3866        // another bundle even though two more retained slots would fit.
3867        let bundle = include_bytes!("../tests/fixtures/xmlenc/01-phaos-xmlenc-3/rsa-priv-key.p12");
3868        let resources = ResourcePolicy {
3869            max_key_candidates: 4,
3870            ..ResourcePolicy::default()
3871        };
3872        let mut inventory = KeyInventory::default();
3873        inventory
3874            .add_pkcs12("first".into(), bundle, "secret", &resources)
3875            .expect("first bundle fits");
3876        assert_eq!(inventory.entry_count(), 2);
3877        assert!(
3878            inventory.private_keys()[0]
3879                .matching_certificate_chain()
3880                .is_some()
3881        );
3882        assert!(
3883            inventory
3884                .add_pkcs12("second".into(), bundle, "secret", &resources)
3885                .is_err()
3886        );
3887    }
3888
3889    #[test]
3890    fn pkcs12_temporary_budget_excludes_live_input() {
3891        // The borrowed PFX remains live during both prepare and decrypt;
3892        // plaintext and KDF allocations must not reuse its allowance.
3893        let resources = ResourcePolicy::default();
3894        let mut inventory = KeyInventory::default();
3895        inventory
3896            .add_symmetric(
3897                "old".into(),
3898                SymmetricKeyKind::Hmac,
3899                vec![7; 32],
3900                KeyUsages::SIGN,
3901                &resources,
3902            )
3903            .expect("retained key");
3904        let input = [0; 128];
3905        let limits = inventory
3906            .pkcs12_import_limits("new", &input, KeyUsages::SIGN, &resources)
3907            .expect("import allowance");
3908        assert_eq!(
3909            limits.memory_available,
3910            resources.max_external_resource_total_bytes
3911                - inventory.material_bytes
3912                - 3
3913                - input.len()
3914        );
3915    }
3916
3917    #[test]
3918    fn pkcs12_input_consumes_aggregate_import_budget() {
3919        // Repeated containers must charge encoded bytes, even when decoded material is small.
3920        let bundle = include_bytes!("../tests/fixtures/xmlenc/01-phaos-xmlenc-3/rsa-priv-key.p12");
3921        let resources = ResourcePolicy::default();
3922        let mut inventory = KeyInventory::default();
3923        inventory
3924            .add_pkcs12("first".into(), bundle, "secret", &resources)
3925            .expect("first bundle imports");
3926        assert!(inventory.material_bytes >= bundle.len());
3927        let limited = ResourcePolicy {
3928            max_external_resource_total_bytes: bundle.len() * 2 - 1,
3929            ..resources
3930        };
3931        assert!(
3932            inventory
3933                .add_pkcs12("second".into(), bundle, "secret", &limited)
3934                .is_err()
3935        );
3936    }
3937
3938    #[test]
3939    fn pkcs12_unrelated_ca_does_not_block_private_key() {
3940        // Generated with OpenSSL from the tracked RSA key and unrelated CA;
3941        // the CA is lookup material, not a fabricated leaf certificate.
3942        let bundle = base64::engine::general_purpose::STANDARD
3943            .decode(
3944                include_str!("../tests/fixtures/keys/pkcs12/rsa-key-unrelated-ca.p12.b64").trim(),
3945            )
3946            .expect("fixture base64 decodes");
3947        let mut inventory = KeyInventory::default();
3948        inventory
3949            .add_pkcs12(
3950                "ca-only".into(),
3951                &bundle,
3952                "secret",
3953                &ResourcePolicy::default(),
3954            )
3955            .expect("unrelated CA must not invalidate the private key");
3956        assert_eq!(inventory.private_keys.len(), 1);
3957        assert_eq!(inventory.private_keys[0].certificate_chain.len(), 1);
3958        assert!(!inventory.private_keys[0].has_matching_leaf);
3959        assert!(
3960            inventory
3961                .signing_key(
3962                    "ca-only",
3963                    SignatureAlgorithm::RsaSha256,
3964                    &crate::policy::SigningPolicy::default(),
3965                )
3966                .is_ok()
3967        );
3968    }
3969
3970    #[test]
3971    fn pkcs12_identical_leaf_bags_are_one_candidate() {
3972        // OpenSSL exported the same certificate as both the leaf and an extra
3973        // cert bag; the inventory retains one copy for the matching key.
3974        let bundle = base64::engine::general_purpose::STANDARD
3975            .decode(include_str!("../tests/fixtures/keys/pkcs12/rsa-duplicate-leaf.p12.b64").trim())
3976            .expect("fixture base64 decodes");
3977        let mut inventory = KeyInventory::default();
3978        inventory
3979            .add_pkcs12(
3980                "duplicate-leaf".into(),
3981                &bundle,
3982                "secret",
3983                &ResourcePolicy::default(),
3984            )
3985            .expect("identical leaf bags are not ambiguous");
3986        assert_eq!(inventory.private_keys[0].certificate_chain.len(), 1);
3987        assert!(inventory.private_keys[0].has_matching_leaf);
3988    }
3989
3990    #[cfg(feature = "experimental-pq")]
3991    #[test]
3992    fn kem_pkcs12_auto_usage_is_decrypt_only() {
3993        // Convenience PKCS#12 import derives usages from the decoded key,
3994        // while an explicitly requested signing usage must remain forbidden.
3995        let key = crate::provider::RustCryptoMlKemPrivateKey::from_seed(
3996            crate::provider::KeyEncapsulationAlgorithm::MlKem512,
3997            &[3; 64],
3998        )
3999        .expect("valid ML-KEM seed");
4000        let der = key
4001            .to_pkcs8_der(crate::provider::MlKemPrivateKeyEncoding::Seed)
4002            .expect("seed PKCS#8 export");
4003        let contents = || pkcs12_import::Contents {
4004            private_keys: vec![Zeroizing::new(der.as_bytes().to_vec())],
4005            certificates: Vec::new(),
4006        };
4007        let resources = ResourcePolicy::default();
4008        let mut inventory = KeyInventory::default();
4009        inventory
4010            .add_pkcs12_contents(
4011                "kem".into(),
4012                der.as_bytes().len(),
4013                contents(),
4014                KeyUsages::SIGN.union(KeyUsages::DECRYPT),
4015                &resources,
4016                true,
4017            )
4018            .expect("automatic recipient usages");
4019        assert_eq!(inventory.private_keys()[0].usages, KeyUsages::DECRYPT);
4020        // Explicit imports must never advertise unsupported recipient operations,
4021        // including mixed usages that happen to contain DECRYPT.
4022        for usages in [
4023            KeyUsages::VERIFY,
4024            KeyUsages::ENCRYPT,
4025            KeyUsages::DECRYPT.union(KeyUsages::VERIFY),
4026            KeyUsages::DECRYPT.union(KeyUsages::ENCRYPT),
4027            KeyUsages::DECRYPT.union(KeyUsages::SIGN),
4028        ] {
4029            let mut rejected = KeyInventory::default();
4030            assert!(
4031                rejected
4032                    .add_pkcs12_contents(
4033                        "kem".into(),
4034                        der.as_bytes().len(),
4035                        contents(),
4036                        usages,
4037                        &resources,
4038                        false,
4039                    )
4040                    .is_err()
4041            );
4042            assert!(rejected.private_keys().is_empty());
4043        }
4044        assert!(
4045            KeyInventory::default()
4046                .add_pkcs12_contents(
4047                    "kem".into(),
4048                    der.as_bytes().len(),
4049                    contents(),
4050                    KeyUsages::SIGN,
4051                    &resources,
4052                    false
4053                )
4054                .is_err()
4055        );
4056    }
4057
4058    #[test]
4059    fn ec_pkcs12_import_is_sign_only() {
4060        // The convenience importer must not advertise RSA transport for EC.
4061        let bundle = base64::engine::general_purpose::STANDARD
4062            .decode(include_str!("../tests/fixtures/keys/pkcs12/ec-key.p12.b64").trim())
4063            .expect("fixture base64 decodes");
4064        let resources = ResourcePolicy::default();
4065        let mut inventory = KeyInventory::default();
4066        inventory
4067            .add_pkcs12("ec".into(), &bundle, "secret", &resources)
4068            .expect("EC signing bundle imports");
4069        assert!(inventory.private_keys()[0].usages.allows(KeyUsage::Sign));
4070        assert!(!inventory.private_keys()[0].usages.allows(KeyUsage::Decrypt));
4071        assert!(
4072            KeyInventory::default()
4073                .add_pkcs12_with_usages(
4074                    "ec".into(),
4075                    &bundle,
4076                    "secret",
4077                    KeyUsages::DECRYPT,
4078                    &resources,
4079                )
4080                .is_err()
4081        );
4082    }
4083
4084    #[test]
4085    fn password_callback_runs_for_protected_container() {
4086        // Password delivery is caller-owned and failures must not leak the
4087        // callback's diagnostic or retry a plaintext decoder.
4088        let bundle = include_bytes!("../tests/fixtures/xmlenc/01-phaos-xmlenc-3/rsa-priv-key.p12");
4089        let mut inventory = KeyInventory::default();
4090        inventory
4091            .add_pkcs12_with_password_callback(
4092                "bundle".into(),
4093                bundle,
4094                || Some(Zeroizing::new("secret".to_owned())),
4095                KeyUsages::SIGN,
4096                &ResourcePolicy::default(),
4097            )
4098            .expect("callback password decrypts donor bundle");
4099        assert!(inventory.private_keys[0].usages.allows(KeyUsage::Sign));
4100        assert!(matches!(
4101            KeyInventory::default().add_pkcs12_with_password_callback(
4102                "bundle".into(),
4103                bundle,
4104                || None,
4105                KeyUsages::SIGN,
4106                &ResourcePolicy::default(),
4107            ),
4108            Err(KeyStoreError::ProtectedContainer)
4109        ));
4110        let limited = ResourcePolicy {
4111            max_external_resource_total_bytes: bundle.len() - 1,
4112            ..ResourcePolicy::default()
4113        };
4114        assert!(matches!(
4115            KeyInventory::default().add_pkcs12_with_password_callback(
4116                "bundle".into(),
4117                bundle,
4118                || panic!("budget must be checked before password delivery"),
4119                KeyUsages::SIGN,
4120                &limited,
4121            ),
4122            Err(KeyStoreError::Policy(
4123                crate::policy::PolicyViolation::ResourceLimitExceeded {
4124                    resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
4125                    ..
4126                }
4127            ))
4128        ));
4129        let limited_kdf = ResourcePolicy {
4130            max_key_import_kdf_work: 1,
4131            ..ResourcePolicy::default()
4132        };
4133        assert!(matches!(
4134            KeyInventory::default().add_pkcs12_with_password_callback(
4135                "bundle".into(),
4136                bundle,
4137                || panic!("KDF denial must precede password delivery"),
4138                KeyUsages::SIGN,
4139                &limited_kdf,
4140            ),
4141            Err(KeyStoreError::Policy(
4142                crate::policy::PolicyViolation::KdfIterationsOutsideLimit { maximum: 1 }
4143            ))
4144        ));
4145        let oversized_bundle = ResourcePolicy {
4146            max_external_resource_bytes: bundle.len() - 1,
4147            ..ResourcePolicy::default()
4148        };
4149        assert!(matches!(
4150            KeyInventory::default().add_pkcs12_with_password_callback(
4151                "bundle".into(),
4152                bundle,
4153                || panic!("size denial must precede password delivery"),
4154                KeyUsages::SIGN,
4155                &oversized_bundle,
4156            ),
4157            Err(KeyStoreError::Policy(
4158                crate::policy::PolicyViolation::ResourceLimitExceeded {
4159                    resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
4160                    maximum,
4161                }
4162            )) if maximum == bundle.len() - 1
4163        ));
4164    }
4165
4166    #[test]
4167    fn pkcs12_callback_preflights_indefinite_outer_ber() {
4168        // The outer PFX may use BER indefinite length without changing MAC parameters.
4169        let bundle = include_bytes!("../tests/fixtures/xmlenc/01-phaos-xmlenc-3/rsa-priv-key.p12");
4170        assert_eq!(bundle[0], 0x30);
4171        let length_octets = usize::from(bundle[1] & 0x7f);
4172        assert!(bundle[1] & 0x80 != 0 && length_octets > 0);
4173        let mut ber = vec![0x30, 0x80];
4174        ber.extend_from_slice(&bundle[2 + length_octets..]);
4175        ber.extend_from_slice(&[0, 0]);
4176        let resources = ResourcePolicy {
4177            max_key_import_kdf_work: 1,
4178            ..ResourcePolicy::default()
4179        };
4180        assert!(matches!(
4181            KeyInventory::default().add_pkcs12_with_password_callback(
4182                "bundle".into(),
4183                &ber,
4184                || panic!("BER MAC KDF denial must precede password delivery"),
4185                KeyUsages::SIGN,
4186                &resources,
4187            ),
4188            Err(KeyStoreError::Policy(
4189                crate::policy::PolicyViolation::KdfIterationsOutsideLimit { maximum: 1 }
4190            ))
4191        ));
4192    }
4193
4194    #[test]
4195    fn donor_xml_store_preserves_private_dsa_material() {
4196        // xmlsec's non-standard DSA X must be checked against Y, not silently
4197        // dropped while presenting the named key as usable for signing.
4198        let bytes = include_bytes!("../tests/fixtures/keys/xmlsec/mixed-keys.xml");
4199        let inventory = KeyInventory::from_xml_bytes(
4200            bytes,
4201            &xml_policy(ResourcePolicy::default()),
4202            XmlBackend::default(),
4203        )
4204        .expect("donor key store should parse");
4205        let dsa = inventory
4206            .private_keys
4207            .iter()
4208            .find(|entry| entry.name == "test-dsa")
4209            .expect("DSA private X should be imported");
4210        assert!(dsa.usages.allows(KeyUsage::Sign));
4211        assert!(!dsa.usages.allows(KeyUsage::Decrypt));
4212        assert!(DsaSigningKey::from_pkcs8_der(&dsa.pkcs8_der).is_ok());
4213        let dsa_public = inventory
4214            .public_keys()
4215            .iter()
4216            .find(|entry| entry.name == "test-dsa")
4217            .expect("DSA public entry is retained");
4218        assert_eq!(dsa_public.usages, KeyUsages::VERIFY);
4219    }
4220
4221    #[test]
4222    fn xml_store_charges_retained_decoded_material() {
4223        // DSA retains public components, private PKCS#8 and three name copies.
4224        // A long valid name makes retained bytes exceed the encoded source.
4225        let source = include_str!("../tests/fixtures/keys/xmlsec/mixed-keys.xml");
4226        let marker = source.find("<KeyName>test-dsa</KeyName>").expect("DSA key");
4227        let start = source[..marker].rfind("<KeyInfo").expect("DSA KeyInfo");
4228        let end =
4229            marker + source[marker..].find("</KeyInfo>").expect("DSA end") + "</KeyInfo>".len();
4230        let xml = format!(
4231            "<Keys xmlns=\"{XMLSEC_NS}\">{}</Keys>",
4232            source[start..end]
4233                .replace('\n', "")
4234                .replace("test-dsa", &"name".repeat(512))
4235        );
4236        let inventory = KeyInventory::from_xml_bytes(
4237            xml.as_bytes(),
4238            &xml_policy(ResourcePolicy::default()),
4239            XmlBackend::default(),
4240        )
4241        .expect("compact DSA store imports");
4242        let retained = inventory.retained_material_bytes().expect("bounded tally");
4243        assert_eq!(inventory.material_bytes, xml.len() + retained);
4244        assert!(retained >= inventory.private_keys[0].pkcs8_der.len());
4245        assert!(
4246            retained > xml.len(),
4247            "fixture must distinguish retained bytes from source bytes"
4248        );
4249        let resources = ResourcePolicy {
4250            max_external_resource_total_bytes: retained - 1,
4251            ..ResourcePolicy::default()
4252        };
4253        assert!(matches!(
4254            KeyInventory::from_xml_bytes(
4255                xml.as_bytes(),
4256                &xml_policy(resources),
4257                XmlBackend::default()
4258            ),
4259            Err(KeyStoreError::Policy(
4260                crate::policy::PolicyViolation::ResourceLimitExceeded {
4261                    resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
4262                    ..
4263                }
4264            ))
4265        ));
4266    }
4267
4268    #[test]
4269    fn xml_import_session_keeps_failed_work_and_preflights_live_material() {
4270        // Retrying malformed input cannot refund candidate inspections; a
4271        // full live inventory rejects the next source before XML parsing.
4272        let xml = |value: &str| {
4273            format!(
4274                "<Keys xmlns=\"{XMLSEC_NS}\" xmlns:ds=\"{XMLDSIG_NS}\"><ds:KeyInfo><ds:KeyName>a</ds:KeyName><ds:KeyValue><HMACKeyValue>{value}</HMACKeyValue></ds:KeyValue></ds:KeyInfo></Keys>"
4275            )
4276        };
4277        let policy = xml_policy(ResourcePolicy {
4278            max_key_candidates: 1,
4279            ..ResourcePolicy::default()
4280        });
4281        let mut importer =
4282            XmlKeyStoreImporter::new(&policy, XmlBackend::default()).expect("session");
4283        assert!(matches!(
4284            importer.import(xml("!").as_bytes()),
4285            Err(KeyStoreError::Invalid(_))
4286        ));
4287        assert!(matches!(
4288            importer.import(xml("AA==").as_bytes()),
4289            Err(KeyStoreError::Policy(
4290                crate::policy::PolicyViolation::ResourceLimitExceeded {
4291                    resource: crate::policy::resource_name::KEY_CANDIDATES,
4292                    maximum: 1
4293                }
4294            ))
4295        ));
4296        assert_eq!(importer.finish().entry_count(), 0);
4297        let valid = xml("AA==");
4298        let policy = xml_policy(ResourcePolicy {
4299            // Source, decoded byte, inventory name, and duplicate-name copy.
4300            max_external_resource_total_bytes: valid.len() + 3,
4301            ..ResourcePolicy::default()
4302        });
4303        let mut importer =
4304            XmlKeyStoreImporter::new(&policy, XmlBackend::default()).expect("session");
4305        importer
4306            .import(valid.as_bytes())
4307            .expect("first source fits exactly");
4308        let consumed = importer.parse_work.consumed();
4309        assert!(matches!(
4310            importer.import(b"!!"),
4311            Err(KeyStoreError::Policy(
4312                crate::policy::PolicyViolation::ResourceLimitExceeded {
4313                    resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
4314                    ..
4315                }
4316            ))
4317        ));
4318        assert_eq!(
4319            importer.parse_work.consumed(),
4320            consumed,
4321            "no parser work after byte denial"
4322        );
4323        assert_eq!(importer.finish().entry_count(), 1);
4324    }
4325
4326    #[test]
4327    fn xml_store_rejects_empty_symmetric_material() {
4328        // Empty decoded secrets are invalid at the same boundary as direct imports.
4329        for kind in ["HMACKeyValue", "AESKeyValue", "DESKeyValue"] {
4330            let xml = format!(
4331                "<Keys xmlns=\"{XMLSEC_NS}\"><KeyInfo xmlns=\"{XMLDSIG_NS}\"><KeyName>empty</KeyName><KeyValue><{kind} xmlns=\"{XMLSEC_NS}\"/></KeyValue></KeyInfo></Keys>"
4332            );
4333            assert!(
4334                KeyInventory::from_xml_bytes(
4335                    xml.as_bytes(),
4336                    &xml_policy(ResourcePolicy::default()),
4337                    XmlBackend::default(),
4338                )
4339                .is_err()
4340            );
4341        }
4342    }
4343
4344    #[test]
4345    fn aes_imports_require_supported_key_widths() {
4346        // Both direct and XML key stores must reject widths unusable by AES-CBC/GCM.
4347        let resources = ResourcePolicy::default();
4348        for length in [1, 15, 17, 23, 25, 31, 33] {
4349            let mut inventory = KeyInventory::default();
4350            assert!(
4351                inventory
4352                    .add_symmetric(
4353                        "aes".into(),
4354                        SymmetricKeyKind::Aes,
4355                        vec![0; length],
4356                        KeyUsages::ENCRYPT,
4357                        &resources,
4358                    )
4359                    .is_err()
4360            );
4361            let xml = format!(
4362                "<Keys xmlns=\"{XMLSEC_NS}\"><KeyInfo xmlns=\"{XMLDSIG_NS}\"><KeyName>aes</KeyName><KeyValue><AESKeyValue xmlns=\"{XMLSEC_NS}\">{}</AESKeyValue></KeyValue></KeyInfo></Keys>",
4363                base64::Engine::encode(&base64::engine::general_purpose::STANDARD, vec![0; length])
4364            );
4365            assert!(
4366                KeyInventory::from_xml_bytes(
4367                    xml.as_bytes(),
4368                    &xml_policy(resources.clone()),
4369                    XmlBackend::default(),
4370                )
4371                .is_err()
4372            );
4373        }
4374        for length in [16, 24, 32] {
4375            let mut inventory = KeyInventory::default();
4376            inventory
4377                .add_symmetric(
4378                    "aes".into(),
4379                    SymmetricKeyKind::Aes,
4380                    vec![0; length],
4381                    KeyUsages::ENCRYPT,
4382                    &resources,
4383                )
4384                .expect("AES-128/192/256 key imports");
4385        }
4386    }
4387
4388    #[test]
4389    fn unsupported_des_material_is_not_authorized() {
4390        // A parsed legacy DES value must not advertise an operation that the
4391        // encryption pipeline cannot execute.
4392        let resources = ResourcePolicy::default();
4393        let mut inventory = KeyInventory::default();
4394        assert!(
4395            inventory
4396                .add_symmetric(
4397                    "des".into(),
4398                    SymmetricKeyKind::Des,
4399                    vec![0x42; 8],
4400                    KeyUsages::ENCRYPT,
4401                    &resources,
4402                )
4403                .is_err()
4404        );
4405        let xml = format!(
4406            "<Keys xmlns=\"{XMLSEC_NS}\"><KeyInfo xmlns=\"{XMLDSIG_NS}\"><KeyName>des</KeyName><KeyValue><DESKeyValue xmlns=\"{XMLSEC_NS}\">QkJCQkJCQkI=</DESKeyValue></KeyValue></KeyInfo></Keys>"
4407        );
4408        assert!(
4409            KeyInventory::from_xml_bytes(
4410                xml.as_bytes(),
4411                &xml_policy(resources.clone()),
4412                XmlBackend::default(),
4413            )
4414            .is_err()
4415        );
4416    }
4417
4418    #[test]
4419    fn xml_store_accepts_xmlsig11_ec_key_value() {
4420        // XMLDSig 1.1 ECKeyValue must be recognized as public material.
4421        let pem = pem::parse(include_bytes!(
4422            "../tests/fixtures/keys/ec/ec-prime256v1-pubkey.pem"
4423        ))
4424        .expect("EC public PEM decodes");
4425        let (_, spki) = x509_parser::x509::SubjectPublicKeyInfo::from_der(pem.contents())
4426            .expect("EC SPKI parses");
4427        let point =
4428            base64::engine::general_purpose::STANDARD.encode(spki.subject_public_key.data.as_ref());
4429        let xml = format!(
4430            "<Keys xmlns=\"{XMLSEC_NS}\"><KeyInfo xmlns=\"{XMLDSIG_NS}\"><KeyName>ec</KeyName><KeyValue><ECKeyValue xmlns=\"{XMLDSIG11_NS}\"><NamedCurve URI=\"urn:oid:1.2.840.10045.3.1.7\"/><PublicKey>{point}</PublicKey></ECKeyValue></KeyValue></KeyInfo></Keys>"
4431        );
4432        let store = KeyInventory::from_xml_bytes(
4433            xml.as_bytes(),
4434            &xml_policy(ResourcePolicy::default()),
4435            XmlBackend::default(),
4436        )
4437        .expect("ECKeyValue namespace is supported");
4438        assert_eq!(store.public_keys().len(), 1);
4439        assert_eq!(store.public_keys()[0].usages, KeyUsages::VERIFY);
4440        assert!(
4441            store.public_keys()[0]
4442                .key_info
4443                .sources
4444                .iter()
4445                .any(|source| matches!(source, KeyInfoSource::KeyValue(KeyValueInfo::Ec { .. })))
4446        );
4447    }
4448
4449    #[cfg(feature = "legacy-algorithms")]
4450    #[test]
4451    fn xml_store_imports_triple_des_without_granting_operation_permission() {
4452        // A supported key encoding grants usage, not algorithm policy permission;
4453        // an eight-byte DES key must never be mistaken for a three-key TDEA key.
4454        let encoded = base64::engine::general_purpose::STANDARD.encode([0x31; 24]);
4455        let xml = format!(
4456            "<Keys xmlns=\"{XMLSEC_NS}\"><KeyInfo xmlns=\"{XMLDSIG_NS}\"><KeyName>des</KeyName><KeyValue><DESKeyValue xmlns=\"{XMLSEC_NS}\">{encoded}</DESKeyValue></KeyValue></KeyInfo></Keys>"
4457        );
4458        let store = KeyInventory::from_xml_bytes(
4459            xml.as_bytes(),
4460            &xml_policy(ResourcePolicy::default()),
4461            XmlBackend::default(),
4462        )
4463        .expect("a three-key TDEA inventory entry is supported");
4464        assert_eq!(store.symmetric_keys()[0].kind, SymmetricKeyKind::Des);
4465        assert_eq!(store.symmetric_keys()[0].bytes.as_slice(), &[0x31; 24]);
4466        assert!(store.symmetric_keys()[0].usages.allows(KeyUsage::Decrypt));
4467    }
4468
4469    #[test]
4470    fn xml_store_rejects_unusable_public_key_values() {
4471        // Malformed supported public-key material must fail at import, not at verification.
4472        let cases = [
4473            "<RSAKeyValue><Modulus></Modulus><Exponent>AQAB</Exponent></RSAKeyValue>",
4474            "<ECKeyValue xmlns=\"http://www.w3.org/2009/xmldsig11#\"><NamedCurve URI=\"urn:oid:1.2.840.10045.3.1.7\"/><PublicKey>AQ==</PublicKey></ECKeyValue>",
4475        ];
4476        for key in cases {
4477            let xml = format!(
4478                "<Keys xmlns=\"{XMLSEC_NS}\"><KeyInfo xmlns=\"{XMLDSIG_NS}\"><KeyName>invalid</KeyName><KeyValue>{key}</KeyValue></KeyInfo></Keys>"
4479            );
4480            assert!(
4481                KeyInventory::from_xml_bytes(
4482                    xml.as_bytes(),
4483                    &xml_policy(ResourcePolicy::default()),
4484                    XmlBackend::default(),
4485                )
4486                .is_err()
4487            );
4488        }
4489    }
4490
4491    #[test]
4492    fn xml_store_import_limits_preserve_policy_errors() {
4493        // Limit denials must not look like malformed stores or candidate misses.
4494        let xml = format!(
4495            "<Keys xmlns=\"{XMLSEC_NS}\"><KeyInfo xmlns=\"{XMLDSIG_NS}\"><KeyName>key</KeyName><KeyValue><HMACKeyValue xmlns=\"{XMLSEC_NS}\">c2VjcmV0</HMACKeyValue></KeyValue></KeyInfo></Keys>"
4496        );
4497        let two = xml.replace(
4498            "</Keys>",
4499            &format!(
4500                "{} </Keys>",
4501                xml[xml.find("<KeyInfo").expect("fixture has KeyInfo")
4502                    ..xml.find("</Keys>").expect("fixture has Keys end tag")]
4503                    .replace(">key<", ">other<")
4504            ),
4505        );
4506        for (bytes, resources, expected) in [
4507            (
4508                xml.as_bytes(),
4509                ResourcePolicy {
4510                    max_external_resource_bytes: xml.len() - 1,
4511                    ..ResourcePolicy::default()
4512                },
4513                crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
4514            ),
4515            (
4516                xml.as_bytes(),
4517                ResourcePolicy {
4518                    max_external_resource_total_bytes: xml.len() - 1,
4519                    ..ResourcePolicy::default()
4520                },
4521                crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
4522            ),
4523            (
4524                two.as_bytes(),
4525                ResourcePolicy {
4526                    max_key_candidates: 1,
4527                    ..ResourcePolicy::default()
4528                },
4529                crate::policy::resource_name::KEY_CANDIDATES,
4530            ),
4531        ] {
4532            assert!(
4533                matches!(KeyInventory::from_xml_bytes(bytes, &xml_policy(resources), XmlBackend::default()),
4534                Err(KeyStoreError::Policy(crate::policy::PolicyViolation::ResourceLimitExceeded { resource, .. })) if resource == expected)
4535            );
4536        }
4537        let mut inventory = KeyInventory::default();
4538        let resources = ResourcePolicy {
4539            max_external_resource_total_bytes: 3,
4540            ..ResourcePolicy::default()
4541        };
4542        assert!(matches!(
4543            inventory.add_symmetric(
4544                "key".into(),
4545                SymmetricKeyKind::Hmac,
4546                vec![7; 32],
4547                KeyUsages::SIGN,
4548                &resources
4549            ),
4550            Err(KeyStoreError::Policy(
4551                crate::policy::PolicyViolation::ResourceLimitExceeded {
4552                    resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
4553                    ..
4554                }
4555            ))
4556        ));
4557    }
4558
4559    #[test]
4560    fn xml_store_enforces_all_parser_resource_limits() {
4561        // Import must not skip the depth, namespace or cumulative work limits.
4562        let xml = format!(
4563            "<Keys xmlns=\"{XMLSEC_NS}\" xmlns:a=\"urn:a\" xmlns:b=\"urn:b\"><KeyInfo xmlns=\"{XMLDSIG_NS}\"><KeyName>key</KeyName><KeyValue><HMACKeyValue xmlns=\"{XMLSEC_NS}\">c2VjcmV0</HMACKeyValue></KeyValue></KeyInfo></Keys>"
4564        );
4565        for resources in [
4566            ResourcePolicy {
4567                max_xml_depth: 2,
4568                ..ResourcePolicy::default()
4569            },
4570            ResourcePolicy {
4571                max_xml_namespace_bindings: 1,
4572                ..ResourcePolicy::default()
4573            },
4574            ResourcePolicy {
4575                max_xml_parse_work_bytes: 1,
4576                ..ResourcePolicy::default()
4577            },
4578        ] {
4579            assert!(
4580                KeyInventory::from_xml_bytes(
4581                    xml.as_bytes(),
4582                    &xml_policy(resources),
4583                    XmlBackend::default()
4584                )
4585                .is_err_and(|error| matches!(error, KeyStoreError::Policy(_))),
4586                "parser policy denial must retain its type in key stores"
4587            );
4588        }
4589    }
4590
4591    #[test]
4592    fn xml_store_bounds_source_before_utf16_decode() {
4593        // The source-byte ceiling must be checked before UTF-16 expansion or parsing.
4594        let xml = format!("<Keys xmlns=\"{XMLSEC_NS}\"/>");
4595        let mut bytes = vec![0xff, 0xfe];
4596        for unit in xml.encode_utf16() {
4597            bytes.extend_from_slice(&unit.to_le_bytes());
4598        }
4599        let resources = ResourcePolicy {
4600            max_xml_document_bytes: xml.len() + 1,
4601            ..ResourcePolicy::default()
4602        };
4603        assert!(bytes.len() > resources.max_xml_document_bytes);
4604        assert!(
4605            KeyInventory::from_xml_bytes(&bytes, &xml_policy(resources), XmlBackend::default())
4606                .is_err()
4607        );
4608    }
4609
4610    #[test]
4611    fn xml_store_uses_operation_xml_policy() {
4612        // Internal DTD permission must come from the operation snapshot, not
4613        // an importer-local default that rejects a caller-authorized store.
4614        let xml = format!(
4615            "<!DOCTYPE Keys [<!ENTITY key 'named'>]><Keys xmlns=\"{XMLSEC_NS}\"><KeyInfo xmlns=\"{XMLDSIG_NS}\"><KeyName>&key;</KeyName><KeyValue><HMACKeyValue xmlns=\"{XMLSEC_NS}\">c2VjcmV0</HMACKeyValue></KeyValue></KeyInfo></Keys>"
4616        );
4617        let denied = crate::policy::VerificationPolicy::default();
4618        assert!(
4619            KeyInventory::from_xml_bytes(xml.as_bytes(), &denied, XmlBackend::default()).is_err()
4620        );
4621        let mut allowed = denied;
4622        allowed.xml.allow_internal_dtd = true;
4623        assert!(
4624            KeyInventory::from_xml_bytes(xml.as_bytes(), &allowed, XmlBackend::default()).is_ok()
4625        );
4626    }
4627
4628    #[test]
4629    fn ec_public_key_cannot_authorize_encryption() {
4630        // EC material can verify signatures but cannot serve as an RSA recipient.
4631        let resources = ResourcePolicy::default();
4632        let ec = include_bytes!("../tests/fixtures/keys/ec/ec-prime256v1-pubkey.pem");
4633        let cert = include_bytes!("../tests/fixtures/keys/ec/ec-prime256v1-cert.pem");
4634        let mut store = KeyInventory::default();
4635        store
4636            .add_public_pem("ec".into(), ec, &resources)
4637            .expect("EC public key imports");
4638        assert_eq!(store.public_keys()[0].usages, KeyUsages::VERIFY);
4639        assert!(
4640            store
4641                .add_public_pem_with_usages("ec-encrypt".into(), ec, KeyUsages::ENCRYPT, &resources)
4642                .is_err()
4643        );
4644        let cert_der = pem::parse(cert)
4645            .expect("EC certificate PEM decodes")
4646            .into_contents();
4647        store
4648            .add_public_der("ec-cert".into(), cert_der.clone(), &resources)
4649            .expect("EC certificate imports");
4650        assert_eq!(store.public_keys()[1].usages, KeyUsages::VERIFY);
4651        assert!(
4652            store
4653                .add_public_der_with_usages(
4654                    "ec-cert-encrypt".into(),
4655                    cert_der,
4656                    KeyUsages::ENCRYPT,
4657                    &resources
4658                )
4659                .is_err()
4660        );
4661    }
4662
4663    #[test]
4664    fn xml_store_rejects_policy_above_absolute_ceiling() {
4665        // Public imports cannot bypass hard ceilings with an unvalidated policy.
4666        let resources = ResourcePolicy {
4667            max_xml_nodes: crate::hard_limits::XML_DOCUMENT_NODE_CEILING as usize + 1,
4668            ..ResourcePolicy::default()
4669        };
4670        let xml = format!("<Keys xmlns=\"{XMLSEC_NS}\"/>");
4671        assert!(
4672            KeyInventory::from_xml_bytes(
4673                xml.as_bytes(),
4674                &xml_policy(resources.clone()),
4675                XmlBackend::default(),
4676            )
4677            .is_err()
4678        );
4679        assert!(
4680            KeyInventory::default()
4681                .add_symmetric(
4682                    "key".into(),
4683                    SymmetricKeyKind::Hmac,
4684                    b"secret".to_vec(),
4685                    KeyUsages::SIGN,
4686                    &resources,
4687                )
4688                .is_err()
4689        );
4690    }
4691
4692    #[test]
4693    fn ec_private_key_cannot_advertise_rsa_decryption() {
4694        // Only RSA private material can satisfy the inventory's decrypt API.
4695        let pem = include_bytes!("../tests/fixtures/keys/ec/ec-prime256v1-key.pem");
4696        assert!(
4697            KeyInventory::default()
4698                .add_private_pem(
4699                    "ec".into(),
4700                    pem,
4701                    None,
4702                    KeyUsages::DECRYPT,
4703                    &ResourcePolicy::default(),
4704                )
4705                .is_err()
4706        );
4707    }
4708
4709    #[test]
4710    fn imports_rsa_pkcs1_pem_and_resolves_named_spki() {
4711        // Traditional RSA import and named public lookup share one inventory,
4712        // while the resolver still applies the operation's verification policy.
4713        let private_pem = include_str!("../tests/fixtures/keys/rsa/rsa-2048-key.pem");
4714        let rsa = RsaPrivateKey::from_pkcs8_pem(private_pem).expect("RSA fixture parses");
4715        let pkcs1 = rsa.to_pkcs1_der().expect("RSA fixture encodes as PKCS#1");
4716        let public = rsa
4717            .to_public_key()
4718            .to_public_key_der()
4719            .expect("RSA fixture has SPKI");
4720        let resources = ResourcePolicy::default();
4721        let mut inventory = KeyInventory::default();
4722        inventory
4723            .add_private_der(
4724                "key".into(),
4725                pkcs1.as_bytes(),
4726                None,
4727                KeyUsages::SIGN.union(KeyUsages::DECRYPT),
4728                &resources,
4729            )
4730            .expect("PKCS#1 private key imports");
4731        inventory
4732            .add_public_der("verify-key".into(), public.as_bytes().to_vec(), &resources)
4733            .expect("public SPKI imports");
4734        assert!(inventory.private_keys[0].usages.allows(KeyUsage::Sign));
4735        let mut info = KeyInfo::default();
4736        info.sources
4737            .push(KeyInfoSource::KeyName("verify-key".into()));
4738        let resolver = inventory.verification_resolver();
4739        let resolved = resolver
4740            .resolve(Some(&info), SignatureAlgorithm::RsaSha256)
4741            .expect("named public key resolves");
4742        assert!(resolved.is_some());
4743    }
4744
4745    #[test]
4746    fn rejects_unknown_pem_text_and_duplicate_names() {
4747        // PEM is a single armor block; unrelated trailing data must not be
4748        // silently skipped by the general-purpose PEM parser.
4749        let public = include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-pubkey.pem");
4750        let mut inventory = KeyInventory::default();
4751        let resources = ResourcePolicy::default();
4752        inventory
4753            .add_public_pem("first".into(), public, &resources)
4754            .expect("public PEM imports");
4755        assert!(matches!(
4756            inventory.add_public_pem("first".into(), public, &resources),
4757            Err(KeyStoreError::Selection("duplicate key name"))
4758        ));
4759        let mut trailing = public.to_vec();
4760        trailing.extend_from_slice(b"\nnot a key");
4761        assert!(
4762            inventory
4763                .add_public_pem("other".into(), &trailing, &resources)
4764                .is_err()
4765        );
4766    }
4767
4768    #[test]
4769    fn public_pem_label_rejects_certificate_payload() {
4770        // Strict label dispatch must not inherit generic DER's certificate fallback.
4771        let certificate = pem::parse(include_bytes!(
4772            "../tests/fixtures/keys/rsa/rsa-2048-cert.pem"
4773        ))
4774        .expect("certificate")
4775        .into_contents();
4776        let mislabeled = pem::encode(&pem::Pem::new("PUBLIC KEY", certificate.clone()));
4777        let resources = ResourcePolicy::default();
4778        for usages in [None, Some(KeyUsages::VERIFY)] {
4779            let mut inventory = KeyInventory::default();
4780            assert!(matches!(
4781                inventory.add_public_pem_inner(
4782                    "key".into(),
4783                    mislabeled.as_bytes(),
4784                    usages,
4785                    &resources
4786                ),
4787                Err(KeyStoreError::Selection("invalid PUBLIC KEY payload"))
4788            ));
4789            assert!(inventory.public_keys().is_empty());
4790        }
4791        KeyInventory::default()
4792            .add_public_der("cert".into(), certificate, &resources)
4793            .expect("generic DER intentionally accepts certificates");
4794        KeyInventory::default()
4795            .add_public_pem(
4796                "spki".into(),
4797                include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"),
4798                &resources,
4799            )
4800            .expect("correct SPKI label");
4801    }
4802
4803    #[test]
4804    fn rsa_spki_import_rejects_even_public_exponent() {
4805        // ASN.1 shape alone must not grant verify/encrypt usages to an unusable RSA key.
4806        let mut der = single_pem_block(
4807            include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"),
4808            ResourcePolicy::default().max_external_resource_bytes,
4809        )
4810        .expect("public key fixture")
4811        .into_contents();
4812        let exponent = der
4813            .windows(5)
4814            .position(|bytes| bytes == [0x02, 0x03, 0x01, 0x00, 0x01])
4815            .expect("RSA exponent in SPKI");
4816        der[exponent + 4] = 0;
4817        assert!(
4818            KeyInventory::default()
4819                .add_public_der("invalid".into(), der, &ResourcePolicy::default())
4820                .is_err()
4821        );
4822    }
4823
4824    #[test]
4825    fn rsa_public_pkcs1_pem_is_bounded_before_bigint_decode() {
4826        // The borrowed ASN.1 modulus is checked before RSA allocates integers.
4827        let modulus = vec![1_u8; crate::hard_limits::RSA_MODULUS_BIT_CEILING.div_ceil(8) + 1];
4828        let exponent = [1_u8, 0, 1];
4829        let public = pkcs1::RsaPublicKeyRef {
4830            modulus: der::asn1::UintRef::new(&modulus).expect("valid modulus"),
4831            public_exponent: der::asn1::UintRef::new(&exponent).expect("valid exponent"),
4832        };
4833        let pem = pem::encode(&pem::Pem::new(
4834            "RSA PUBLIC KEY",
4835            der::Encode::to_der(&public).expect("encodable RSA public key"),
4836        ));
4837        assert!(matches!(
4838            KeyInventory::default().add_public_pem(
4839                "oversized".into(),
4840                pem.as_bytes(),
4841                &ResourcePolicy::default(),
4842            ),
4843            Err(KeyStoreError::Selection(
4844                "RSA public key exceeds safety limit"
4845            ))
4846        ));
4847    }
4848
4849    #[test]
4850    fn direct_inventory_names_consume_resource_budget() {
4851        // Caller-owned names must not bypass per-resource or retained aggregate bounds.
4852        let resources = ResourcePolicy {
4853            max_external_resource_bytes: 64,
4854            max_external_resource_total_bytes: 100,
4855            ..ResourcePolicy::default()
4856        };
4857        let mut inventory = KeyInventory::default();
4858        assert!(
4859            inventory
4860                .add_symmetric(
4861                    "x".repeat(65),
4862                    SymmetricKeyKind::Hmac,
4863                    vec![7; 32],
4864                    KeyUsages::SIGN,
4865                    &resources,
4866                )
4867                .is_err()
4868        );
4869        inventory
4870            .add_symmetric(
4871                "a".repeat(40),
4872                SymmetricKeyKind::Hmac,
4873                vec![7; 32],
4874                KeyUsages::SIGN,
4875                &resources,
4876            )
4877            .expect("first named key fits");
4878        assert!(
4879            inventory
4880                .add_symmetric(
4881                    "b".repeat(40),
4882                    SymmetricKeyKind::Hmac,
4883                    vec![8; 32],
4884                    KeyUsages::SIGN,
4885                    &resources,
4886                )
4887                .is_err()
4888        );
4889    }
4890
4891    #[test]
4892    fn private_pem_label_cannot_enable_der_fallback() {
4893        // A protected label must never import plaintext, even with a password.
4894        let plain = pem::parse(include_bytes!(
4895            "../tests/fixtures/keys/rsa/rsa-2048-key.pem"
4896        ))
4897        .expect("fixture")
4898        .into_contents();
4899        let mislabeled = pem::encode(&pem::Pem::new("ENCRYPTED PRIVATE KEY", plain));
4900        for password in [None, Some(b"ignored".as_slice())] {
4901            let mut inventory = KeyInventory::default();
4902            assert!(matches!(
4903                inventory.add_private_pem(
4904                    "key".into(),
4905                    mislabeled.as_bytes(),
4906                    password,
4907                    KeyUsages::SIGN,
4908                    &ResourcePolicy::default()
4909                ),
4910                Err(KeyStoreError::ProtectedContainer)
4911            ));
4912            assert!(inventory.private_keys.is_empty());
4913            assert_eq!(inventory.material_bytes, 0);
4914        }
4915    }
4916
4917    #[test]
4918    fn encrypted_pkcs8_callback_checks_remaining_work() {
4919        use der::Encode as _;
4920        use pkcs8::pkcs5::{EncryptionScheme, pbes2};
4921        // Failed decryption consumes work. A later container must not request
4922        // another secret when its visible derivation exceeds the remainder.
4923        let envelope = EncryptedPrivateKeyInfoRef {
4924            encryption_algorithm: EncryptionScheme::Pbes2(pbes2::Parameters {
4925                kdf: pbes2::Kdf::Pbkdf2(pbes2::Pbkdf2Params {
4926                    salt: pbes2::Salt::new(b"12345678").expect("salt"),
4927                    iteration_count: 2,
4928                    key_length: None,
4929                    prf: pbes2::Pbkdf2Prf::HmacWithSha256,
4930                }),
4931                encryption: pbes2::EncryptionScheme::Aes256Cbc { iv: [0; 16] },
4932            }),
4933            encrypted_data: der::asn1::OctetStringRef::new(&[0; 64]).expect("ciphertext"),
4934        }
4935        .to_der()
4936        .expect("envelope");
4937        let resources = ResourcePolicy {
4938            max_key_import_kdf_work: 4,
4939            ..ResourcePolicy::default()
4940        };
4941        let mut inventory = KeyInventory::default();
4942        assert!(matches!(
4943            inventory.add_private_der(
4944                "first".into(),
4945                &envelope,
4946                Some(b"wrong"),
4947                KeyUsages::SIGN,
4948                &resources,
4949            ),
4950            Err(KeyStoreError::ProtectedContainer)
4951        ));
4952        assert_eq!(inventory.key_import_kdf_work(), 3);
4953        let calls = std::cell::Cell::new(0);
4954        let result = inventory.add_private_der_with_password_callback(
4955            "second".into(),
4956            &envelope,
4957            || {
4958                calls.set(calls.get() + 1);
4959                Some(Zeroizing::new(b"wrong".to_vec()))
4960            },
4961            KeyUsages::SIGN,
4962            &resources,
4963        );
4964        assert_eq!(calls.get(), 0, "preflight precedes secret acquisition");
4965        assert!(matches!(result, Err(KeyStoreError::Policy(_))));
4966        assert_eq!(inventory.key_import_kdf_work(), 3);
4967        // An exact remaining derivation is allowed to request the password.
4968        let exact = ResourcePolicy {
4969            max_key_import_kdf_work: 6,
4970            ..resources
4971        };
4972        assert!(matches!(
4973            inventory.add_private_der_with_password_callback(
4974                "third".into(),
4975                &envelope,
4976                || {
4977                    calls.set(calls.get() + 1);
4978                    Some(Zeroizing::new(b"wrong".to_vec()))
4979                },
4980                KeyUsages::SIGN,
4981                &exact,
4982            ),
4983            Err(KeyStoreError::ProtectedContainer)
4984        ));
4985        assert_eq!(calls.get(), 1);
4986        assert_eq!(inventory.key_import_kdf_work(), 6);
4987    }
4988
4989    #[test]
4990    fn encrypted_pkcs8_requires_correct_password_without_plaintext_fallback() {
4991        // Wrong passwords must not retry another format or leave a partial
4992        // registration in the caller-owned inventory.
4993        let private_pem = include_str!("../tests/fixtures/keys/rsa/rsa-2048-key.pem");
4994        let rsa = RsaPrivateKey::from_pkcs8_pem(private_pem).expect("RSA fixture parses");
4995        let plain = rsa.to_pkcs8_der().expect("RSA fixture encodes as PKCS#8");
4996        let mut rng = ChaCha8Rng::seed_from_u64(0xA11C_E501);
4997        let encrypted = PrivateKeyInfoRef::try_from(plain.as_bytes())
4998            .expect("PKCS#8 reference parses")
4999            .encrypt_with_rng(&mut rng, b"correct")
5000            .expect("PKCS#8 fixture encrypts");
5001        let resources = ResourcePolicy::default();
5002        let mut inventory = KeyInventory::default();
5003        let restricted = ResourcePolicy {
5004            max_key_import_kdf_work: 1,
5005            ..ResourcePolicy::default()
5006        };
5007        assert!(matches!(
5008            inventory.add_private_der(
5009                "restricted".into(),
5010                encrypted.as_bytes(),
5011                Some(b"correct"),
5012                KeyUsages::SIGN,
5013                &restricted,
5014            ),
5015            Err(KeyStoreError::Policy(
5016                crate::policy::PolicyViolation::ResourceLimit {
5017                    resource: crate::policy::resource_name::KEY_IMPORT_KDF_WORK,
5018                    maximum: 1,
5019                    ..
5020                }
5021            ))
5022        ));
5023        let callback_calls = std::cell::Cell::new(0);
5024        assert!(matches!(
5025            inventory.add_private_der_with_password_callback(
5026                "restricted-callback".into(),
5027                encrypted.as_bytes(),
5028                || {
5029                    callback_calls.set(callback_calls.get() + 1);
5030                    Some(Zeroizing::new(b"correct".to_vec()))
5031                },
5032                KeyUsages::SIGN,
5033                &restricted,
5034            ),
5035            Err(KeyStoreError::Policy(
5036                crate::policy::PolicyViolation::ResourceLimit {
5037                    resource: crate::policy::resource_name::KEY_IMPORT_KDF_WORK,
5038                    maximum: 1,
5039                    ..
5040                }
5041            ))
5042        ));
5043        assert_eq!(callback_calls.get(), 0);
5044        assert!(matches!(
5045            inventory.add_private_der(
5046                "protected".into(),
5047                encrypted.as_bytes(),
5048                Some(b"wrong"),
5049                KeyUsages::SIGN,
5050                &resources,
5051            ),
5052            Err(KeyStoreError::ProtectedContainer)
5053        ));
5054        assert!(inventory.private_keys.is_empty());
5055        inventory
5056            .add_private_der(
5057                "protected".into(),
5058                encrypted.as_bytes(),
5059                Some(b"correct"),
5060                KeyUsages::SIGN,
5061                &resources,
5062            )
5063            .expect("correct password imports encrypted PKCS#8");
5064        assert_eq!(inventory.private_keys.len(), 1);
5065        assert!(inventory.material_bytes >= encrypted.as_bytes().len());
5066        let mut callback_inventory = KeyInventory::default();
5067        callback_inventory
5068            .add_private_der_with_password_callback(
5069                "callback".into(),
5070                encrypted.as_bytes(),
5071                || Some(Zeroizing::new(b"correct".to_vec())),
5072                KeyUsages::SIGN,
5073                &resources,
5074            )
5075            .expect("callback decrypts encrypted PKCS#8");
5076        callback_inventory
5077            .add_private_der_with_password_callback(
5078                "plain".into(),
5079                plain.as_bytes(),
5080                || panic!("plaintext PKCS#8 must not request a password"),
5081                KeyUsages::SIGN,
5082                &resources,
5083            )
5084            .expect("plaintext import ignores password callback");
5085    }
5086
5087    #[test]
5088    fn invalid_private_usages_do_not_request_password() {
5089        // Caller-visible rejection must precede prompting or retrieving secrets.
5090        let private = pem::parse(include_bytes!(
5091            "../tests/fixtures/keys/rsa/rsa-2048-key.pem"
5092        ))
5093        .expect("key")
5094        .into_contents();
5095        let encrypted = PrivateKeyInfoRef::try_from(private.as_slice())
5096            .expect("PKCS8")
5097            .encrypt_with_rng(&mut ChaCha8Rng::seed_from_u64(42), b"correct")
5098            .expect("encrypted PKCS8");
5099        for usages in [
5100            KeyUsages::VERIFY,
5101            KeyUsages(0),
5102            KeyUsages::SIGN.union(KeyUsages::VERIFY),
5103        ] {
5104            let calls = std::cell::Cell::new(0);
5105            let mut inventory = KeyInventory::default();
5106            assert!(matches!(
5107                inventory.add_private_der_with_password_callback(
5108                    "invalid".into(),
5109                    encrypted.as_bytes(),
5110                    || {
5111                        calls.set(calls.get() + 1);
5112                        Some(Zeroizing::new(b"correct".to_vec()))
5113                    },
5114                    usages,
5115                    &ResourcePolicy::default()
5116                ),
5117                Err(KeyStoreError::Selection(
5118                    "private key usage is incompatible"
5119                ))
5120            ));
5121            assert_eq!(calls.get(), 0);
5122            assert_eq!(inventory.entry_count(), 0);
5123        }
5124    }
5125
5126    #[test]
5127    fn certificate_and_crl_capacity_precedes_der_parsing() {
5128        // Exhausted inventory limits are terminal policy errors even for
5129        // malformed DER. Failed imports must not change retained accounting.
5130        for candidates in [true, false] {
5131            for certificate in [true, false] {
5132                let mut inventory = KeyInventory::default();
5133                let resources = ResourcePolicy {
5134                    max_key_candidates: if candidates { 1 } else { 64 },
5135                    max_external_resource_total_bytes: 2,
5136                    ..ResourcePolicy::default()
5137                };
5138                inventory
5139                    .add_symmetric(
5140                        "a".into(),
5141                        SymmetricKeyKind::Hmac,
5142                        vec![0],
5143                        KeyUsages::SIGN,
5144                        &resources,
5145                    )
5146                    .expect("fill inventory");
5147                let result = if certificate {
5148                    inventory.add_certificate_der(vec![0], false, &resources)
5149                } else {
5150                    inventory.add_crl_der(vec![0], &resources)
5151                };
5152                let resource = if candidates {
5153                    crate::policy::resource_name::KEY_CANDIDATES
5154                } else {
5155                    crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES
5156                };
5157                assert!(matches!(result, Err(KeyStoreError::Policy(
5158                    crate::policy::PolicyViolation::ResourceLimitExceeded { resource: actual, .. }
5159                )) if actual == resource));
5160                assert_eq!(inventory.entry_count(), 1);
5161                assert_eq!(inventory.material_bytes, 2);
5162                assert!(inventory.lookup_certificates.is_empty());
5163                assert!(inventory.crls.is_empty());
5164            }
5165        }
5166        assert!(matches!(
5167            KeyInventory::default().add_crl_der(vec![0], &ResourcePolicy::default()),
5168            Err(KeyStoreError::Selection("invalid X.509 CRL"))
5169        ));
5170    }
5171
5172    #[test]
5173    fn pkcs8_pbkdf2_output_blocks_are_checked_before_password() {
5174        use der::Encode as _;
5175        // AES-256/SHA-1 needs two PBKDF2 blocks; three rounds cost six PRFs,
5176        // not three. Denial must precede the public password callback.
5177        use pkcs8::pkcs5::{EncryptionScheme, pbes2};
5178        let encrypted = EncryptedPrivateKeyInfoRef {
5179            encryption_algorithm: EncryptionScheme::Pbes2(pbes2::Parameters {
5180                kdf: pbes2::Kdf::Pbkdf2(pbes2::Pbkdf2Params {
5181                    salt: pbes2::Salt::new(b"12345678").expect("salt"),
5182                    iteration_count: 3,
5183                    key_length: None,
5184                    prf: pbes2::Pbkdf2Prf::HmacWithSha1,
5185                }),
5186                encryption: pbes2::EncryptionScheme::Aes256Cbc { iv: [0; 16] },
5187            }),
5188            encrypted_data: der::asn1::OctetStringRef::new(&[0; 16]).expect("ciphertext"),
5189        };
5190        let bytes = encrypted.to_der().expect("encrypted envelope");
5191        let resources = ResourcePolicy {
5192            max_key_import_kdf_work: 5,
5193            ..ResourcePolicy::default()
5194        };
5195        let calls = std::cell::Cell::new(0);
5196        let result = KeyInventory::default().add_private_der_with_password_callback(
5197            "key".into(),
5198            &bytes,
5199            || {
5200                calls.set(calls.get() + 1);
5201                None
5202            },
5203            KeyUsages::SIGN,
5204            &resources,
5205        );
5206        assert_eq!(calls.get(), 0);
5207        assert!(matches!(
5208            result,
5209            Err(KeyStoreError::Policy(
5210                crate::policy::PolicyViolation::ResourceLimit {
5211                    resource: crate::policy::resource_name::KEY_IMPORT_KDF_WORK,
5212                    maximum: 5,
5213                    actual: 6,
5214                }
5215            ))
5216        ));
5217        let exact = ResourcePolicy {
5218            max_key_import_kdf_work: 6,
5219            ..resources
5220        };
5221        assert!(enforce_pkcs8_kdf_policy(&encrypted, &exact, 0).is_ok());
5222        // Cover each PRF width and CBC key width, including single-block
5223        // cases so the fix cannot unconditionally double iteration charges.
5224        for (prf, hash_len) in [
5225            (pbes2::Pbkdf2Prf::HmacWithSha1, 20),
5226            (pbes2::Pbkdf2Prf::HmacWithSha224, 28),
5227            (pbes2::Pbkdf2Prf::HmacWithSha256, 32),
5228            (pbes2::Pbkdf2Prf::HmacWithSha384, 48),
5229            (pbes2::Pbkdf2Prf::HmacWithSha512, 64),
5230        ] {
5231            for cipher in [
5232                pbes2::EncryptionScheme::Aes128Cbc { iv: [0; 16] },
5233                pbes2::EncryptionScheme::Aes192Cbc { iv: [0; 16] },
5234                pbes2::EncryptionScheme::Aes256Cbc { iv: [0; 16] },
5235            ] {
5236                let mut envelope = encrypted.clone();
5237                let EncryptionScheme::Pbes2(params) = &mut envelope.encryption_algorithm else {
5238                    panic!("PBES2 fixture");
5239                };
5240                let pbes2::Kdf::Pbkdf2(kdf) = &mut params.kdf else {
5241                    panic!("PBKDF2 fixture");
5242                };
5243                kdf.prf = prf;
5244                params.encryption = cipher;
5245                let work = 3 * cipher.key_size().div_ceil(hash_len);
5246                let exact = ResourcePolicy {
5247                    max_key_import_kdf_work: work,
5248                    ..ResourcePolicy::default()
5249                };
5250                assert!(enforce_pkcs8_kdf_policy(&envelope, &exact, 0).is_ok());
5251                let tight = ResourcePolicy {
5252                    max_key_import_kdf_work: work - 1,
5253                    ..exact
5254                };
5255                assert!(matches!(enforce_pkcs8_kdf_policy(&envelope, &tight, 0),
5256                    Err(KeyStoreError::Policy(crate::policy::PolicyViolation::ResourceLimit {
5257                        resource: crate::policy::resource_name::KEY_IMPORT_KDF_WORK,
5258                        actual, ..
5259                    })) if actual == work));
5260            }
5261        }
5262    }
5263
5264    #[test]
5265    fn protected_password_consumes_live_byte_and_work_budgets() {
5266        use der::Encode as _;
5267        use pkcs8::pkcs5::{EncryptionScheme, pbes2};
5268        // Password hashing and retained callback capacity must be denied before
5269        // failed padding can hide the resource-policy failure.
5270        let envelope = EncryptedPrivateKeyInfoRef {
5271            encryption_algorithm: EncryptionScheme::Pbes2(pbes2::Parameters {
5272                kdf: pbes2::Kdf::Pbkdf2(pbes2::Pbkdf2Params {
5273                    salt: pbes2::Salt::new(b"12345678").expect("salt"),
5274                    iteration_count: 2,
5275                    key_length: None,
5276                    prf: pbes2::Pbkdf2Prf::HmacWithSha256,
5277                }),
5278                encryption: pbes2::EncryptionScheme::Aes256Cbc { iv: [0; 16] },
5279            }),
5280            encrypted_data: der::asn1::OctetStringRef::new(&[0; 64]).expect("ciphertext"),
5281        }
5282        .to_der()
5283        .expect("envelope");
5284        let peak = 3 + envelope.len() + 64;
5285        for resources in [
5286            ResourcePolicy {
5287                max_external_resource_bytes: 255,
5288                ..ResourcePolicy::default()
5289            },
5290            ResourcePolicy {
5291                max_external_resource_total_bytes: peak + 255,
5292                ..ResourcePolicy::default()
5293            },
5294            ResourcePolicy {
5295                max_key_import_kdf_work: 5,
5296                ..ResourcePolicy::default()
5297            },
5298        ] {
5299            let mut inventory = KeyInventory::default();
5300            assert!(matches!(
5301                inventory.add_private_der(
5302                    "new".into(),
5303                    &envelope,
5304                    Some(&[0; 256]),
5305                    KeyUsages::SIGN,
5306                    &resources
5307                ),
5308                Err(KeyStoreError::Policy(_))
5309            ));
5310            assert!(inventory.private_keys().is_empty());
5311        }
5312        let resources = ResourcePolicy {
5313            max_external_resource_total_bytes: peak + 255,
5314            ..ResourcePolicy::default()
5315        };
5316        let mut secret = Vec::with_capacity(256);
5317        secret.push(0);
5318        assert!(matches!(
5319            KeyInventory::default().add_private_der_with_password_callback(
5320                "new".into(),
5321                &envelope,
5322                || Some(Zeroizing::new(secret)),
5323                KeyUsages::SIGN,
5324                &resources
5325            ),
5326            Err(KeyStoreError::Policy(_))
5327        ));
5328        let exact = ResourcePolicy {
5329            max_external_resource_total_bytes: peak + 256,
5330            max_key_import_kdf_work: 6,
5331            ..ResourcePolicy::default()
5332        };
5333        assert!(matches!(
5334            KeyInventory::default().add_private_der(
5335                "new".into(),
5336                &envelope,
5337                Some(&[0; 256]),
5338                KeyUsages::SIGN,
5339                &exact
5340            ),
5341            Err(KeyStoreError::ProtectedContainer)
5342        ));
5343    }
5344
5345    #[test]
5346    fn private_identity_matches_without_owned_public_encoding() {
5347        // Compare every supported family against its canonical encoder, and
5348        // reject changed public material. Import itself retains no SPKI copy.
5349        let rsa_der = pem::parse(include_bytes!(
5350            "../tests/fixtures/keys/rsa/rsa-2048-key.pem"
5351        ))
5352        .expect("RSA PEM fixture")
5353        .into_contents();
5354        let rsa = RsaPrivateKey::from_pkcs8_der(&rsa_der).expect("RSA private key");
5355        let public = rsa
5356            .to_public_key()
5357            .to_public_key_der()
5358            .expect("RSA public encoding");
5359        let identity = PrivateKeyIdentity::decode(&rsa_der).expect("RSA identity");
5360        assert!(identity.matches_spki(public.as_bytes()));
5361        let mut changed = public.as_bytes().to_vec();
5362        *changed.last_mut().expect("nonempty RSA SPKI") ^= 2;
5363        assert!(!identity.matches_spki(&changed));
5364        assert!(!identity.matches_spki(b"invalid DER"));
5365
5366        macro_rules! check_ec {
5367            ($key:ty, $length:expr) => {{
5368                let mut scalar = [0; $length];
5369                scalar[$length - 1] = 1;
5370                let key = <$key>::from_slice(&scalar).expect("valid EC scalar");
5371                let private = key.to_pkcs8_der().expect("EC private encoding");
5372                let public = key
5373                    .public_key()
5374                    .to_public_key_der()
5375                    .expect("EC public encoding");
5376                let identity = PrivateKeyIdentity::decode(private.as_bytes()).expect("EC identity");
5377                assert!(identity.matches_spki(public.as_bytes()));
5378                let mut changed = public.as_bytes().to_vec();
5379                *changed.last_mut().expect("nonempty EC SPKI") ^= 1;
5380                assert!(!identity.matches_spki(&changed));
5381            }};
5382        }
5383        check_ec!(p256::SecretKey, 32);
5384        check_ec!(p384::SecretKey, 48);
5385        check_ec!(p521::SecretKey, 66);
5386
5387        let inventory = KeyInventory::from_xml_bytes(
5388            include_bytes!("../tests/fixtures/keys/xmlsec/mixed-keys.xml"),
5389            &xml_policy(ResourcePolicy::default()),
5390            XmlBackend::default(),
5391        )
5392        .expect("mixed store fixture");
5393        let private = inventory
5394            .private_keys()
5395            .iter()
5396            .find(|entry| entry.name == "test-dsa")
5397            .expect("stored DSA private key");
5398        let key = NativeDsaSigningKey::from_pkcs8_der(&private.pkcs8_der).expect("DSA private key");
5399        let public = key
5400            .verifying_key()
5401            .to_public_key_der()
5402            .expect("DSA public encoding");
5403        let identity = PrivateKeyIdentity::decode(&private.pkcs8_der).expect("DSA identity");
5404        assert!(identity.matches_spki(public.as_bytes()));
5405        let mut changed = public.as_bytes().to_vec();
5406        *changed.last_mut().expect("nonempty DSA SPKI") ^= 1;
5407        assert!(!identity.matches_spki(&changed));
5408    }
5409
5410    #[test]
5411    fn xml_import_counts_source_and_decoded_material_together() {
5412        // The caller still owns the Base64 XML while the decoded HMAC is live.
5413        let xml = format!(
5414            "<Keys xmlns='{XMLSEC_NS}' xmlns:ds='{XMLDSIG_NS}'><ds:KeyInfo><ds:KeyName>hmac</ds:KeyName><ds:KeyValue><HMACKeyValue>{}</HMACKeyValue></ds:KeyValue></ds:KeyInfo></Keys>",
5415            base64::engine::general_purpose::STANDARD.encode(vec![1; 1024])
5416        );
5417        let resources = ResourcePolicy {
5418            max_external_resource_total_bytes: xml.len(),
5419            ..Default::default()
5420        };
5421        assert!(matches!(
5422            KeyInventory::from_xml_bytes(
5423                xml.as_bytes(),
5424                &xml_policy(resources),
5425                XmlBackend::default()
5426            ),
5427            Err(KeyStoreError::Policy(_))
5428        ));
5429        // Exact peak includes both live name copies, not just retained bytes.
5430        let peak = xml.len() + 1024 + 2 * "hmac".len();
5431        for maximum in [peak - 1, peak] {
5432            let policy = xml_policy(ResourcePolicy {
5433                max_external_resource_total_bytes: maximum,
5434                ..Default::default()
5435            });
5436            let result =
5437                KeyInventory::from_xml_bytes(xml.as_bytes(), &policy, XmlBackend::default());
5438            if maximum == peak {
5439                let store = result.expect("exact live peak fits");
5440                assert_eq!(store.symmetric_keys[0].bytes.as_slice(), &[1; 1024]);
5441                assert_eq!(store.material_bytes, xml.len() + 1024 + "hmac".len());
5442            } else {
5443                assert!(matches!(result, Err(KeyStoreError::Policy(_))));
5444            }
5445        }
5446    }
5447
5448    #[test]
5449    fn private_pem_preflight_precedes_payload_parsing() {
5450        // Structurally invalid DER must never be reached when its allocation
5451        // already exceeds the allowance, for plain and encrypted labels alike.
5452        for label in ["PRIVATE KEY", "ENCRYPTED PRIVATE KEY", "RSA PRIVATE KEY"] {
5453            let pem = pem::encode(&pem::Pem::new(label, vec![1, 2, 3]));
5454            let resources = ResourcePolicy {
5455                max_external_resource_total_bytes: pem.len() + "key".len() + 2,
5456                ..Default::default()
5457            };
5458            let mut store = KeyInventory::default();
5459            assert!(matches!(
5460                store.add_private_pem(
5461                    "key".into(),
5462                    pem.as_bytes(),
5463                    None,
5464                    KeyUsages::SIGN,
5465                    &resources
5466                ),
5467                Err(KeyStoreError::Policy(_))
5468            ));
5469            assert_eq!(store.entry_count(), 0);
5470        }
5471    }
5472
5473    #[test]
5474    fn xml_public_key_import_preserves_key_info_metadata_validation() {
5475        // Importing KeyValue directly must not bypass the original KeyInfo
5476        // mixed-content and name-length validation performed by shared parsing.
5477        let source = include_str!("../tests/fixtures/keys/xmlsec/mixed-keys.xml");
5478        let marker = source.find("<RSAKeyValue").expect("RSA entry");
5479        let start = source[..marker].rfind("<KeyInfo").expect("RSA KeyInfo");
5480        let end =
5481            marker + source[marker..].find("</KeyInfo>").expect("RSA end") + "</KeyInfo>".len();
5482        let entry = &source[start..end];
5483        let name_start = entry.find("<KeyName>").expect("name") + "<KeyName>".len();
5484        let name_end = name_start + entry[name_start..].find("</KeyName>").expect("name end");
5485        for changed in [
5486            format!(
5487                "{}{}{}",
5488                &entry[..name_start],
5489                "a".repeat(4097),
5490                &entry[name_end..]
5491            ),
5492            entry.replacen("<KeyName>", "unexpected<KeyName>", 1),
5493        ] {
5494            let xml = format!("<Keys xmlns='{XMLSEC_NS}'>{changed}</Keys>");
5495            assert!(
5496                KeyInventory::from_xml_bytes(
5497                    xml.as_bytes(),
5498                    &xml_policy(ResourcePolicy::default()),
5499                    XmlBackend::default()
5500                )
5501                .is_err()
5502            );
5503        }
5504    }
5505
5506    #[test]
5507    fn public_pem_decoder_preserves_framing_and_whitespace_contract() {
5508        // Decode borrowed input without tightening the existing whitespace/header
5509        // contract; malformed framing, alphabet and padding remain rejected.
5510        for data in [
5511            "AQID",
5512            "A Q\tI\r\nD",
5513            "A\u{2003}QID",
5514            "Header: value\n\nAQID",
5515            "Header: value\r\n\r\nAQID",
5516        ] {
5517            let text = format!("-----BEGIN PUBLIC KEY-----\n{data}\n-----END PUBLIC KEY-----");
5518            let expected = single_pem_block(text.as_bytes(), 4096).expect("existing PEM contract");
5519            let frame = BorrowedPem::parse(text.as_bytes(), 4096).expect("borrowed frame");
5520            assert_eq!(
5521                frame.decode().expect("borrowed decode"),
5522                expected.contents()
5523            );
5524        }
5525        for data in ["AQI=", "AQ==", "AQID"] {
5526            let text =
5527                format!("-----BEGIN RSA PUBLIC KEY-----\n{data}\n-----END RSA PUBLIC KEY-----");
5528            let frame = BorrowedPem::parse(text.as_bytes(), 4096).expect("borrowed frame");
5529            assert_eq!(frame.decode().expect("decode").len(), frame.decoded_len);
5530        }
5531        // Exercise padding and the decoder's fixed-size staging boundaries;
5532        // whitespace-separated input must not truncate a multi-read payload.
5533        for length in [0, 1, 2, 3, 1023, 1024, 1025, 4096] {
5534            let bytes = vec![0xa5; length];
5535            let encoded = pem::encode(&pem::Pem::new("PUBLIC KEY", bytes.clone()));
5536            let frame = BorrowedPem::parse(encoded.as_bytes(), 8192).expect("frame");
5537            assert_eq!(frame.decoded_len, length);
5538            assert_eq!(frame.decode().expect("streamed decoding"), bytes);
5539        }
5540        for text in [
5541            "-----BEGIN PUBLIC KEY-----\nAQID\n-----END RSA PUBLIC KEY-----",
5542            "-----BEGIN PUBLIC KEY-----\nAQID\n-----END PUBLIC KEY-----junk",
5543            "-----BEGIN PUBLIC KEY-----\nAQID\n-----END PUBLIC KEY-----\n-----BEGIN PUBLIC KEY-----\nAQID\n-----END PUBLIC KEY-----",
5544            "-----BEGIN PUBLIC KEY-----\nAQI\n-----END PUBLIC KEY-----",
5545            "-----BEGIN PUBLIC KEY-----\nAQ$=\n-----END PUBLIC KEY-----",
5546            "-----BEGIN PUBLIC KEY-----\nAR==\n-----END PUBLIC KEY-----",
5547            "-----BEGIN PUBLIC KEY-----\nA===\n-----END PUBLIC KEY-----",
5548            "-----BEGIN PUBLIC KEY-----\nAQ=I\n-----END PUBLIC KEY-----",
5549            "-----BEGIN PUBLIC KEY-----\nnot a header\n\nAQID\n-----END PUBLIC KEY-----",
5550        ] {
5551            assert!(
5552                BorrowedPem::parse(text.as_bytes(), 4096)
5553                    .and_then(|frame| frame.decode())
5554                    .is_err(),
5555                "accepted {text}"
5556            );
5557        }
5558    }
5559
5560    #[test]
5561    fn public_pem_import_accounts_for_simultaneous_buffers() {
5562        // Caller PEM, decoded bytes and a normalized SPKI coexist; neither
5563        // prior inventory material nor any intermediate may reuse that budget.
5564        let pem = include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-pubkey.pem");
5565        let spki = pem::parse(pem).expect("public PEM fixture").into_contents();
5566        let key = RsaPublicKey::from_public_key_der(&spki).expect("RSA key");
5567        let pkcs1 = key.to_pkcs1_der().expect("PKCS1 encoding");
5568        for (label, decoded, extra) in [
5569            ("PUBLIC KEY", spki.as_slice(), 0),
5570            ("RSA PUBLIC KEY", pkcs1.as_bytes(), spki.len()),
5571        ] {
5572            let encoded = pem::encode(&pem::Pem::new(label, decoded.to_vec()));
5573            for exact in [false, true] {
5574                let mut inventory = KeyInventory::default();
5575                inventory
5576                    .add_symmetric(
5577                        "existing".into(),
5578                        SymmetricKeyKind::Hmac,
5579                        vec![1; 16],
5580                        KeyUsages::SIGN,
5581                        &ResourcePolicy::default(),
5582                    )
5583                    .expect("existing key");
5584                let retained = inventory.material_bytes;
5585                let resources = ResourcePolicy {
5586                    max_external_resource_total_bytes: retained
5587                        + 2 * "new".len()
5588                        + encoded.len()
5589                        + decoded.len()
5590                        + extra
5591                        - usize::from(!exact),
5592                    ..ResourcePolicy::default()
5593                };
5594                let result = inventory.add_public_pem("new".into(), encoded.as_bytes(), &resources);
5595                if exact {
5596                    result.expect("exact peak allowance");
5597                    assert_eq!(inventory.entry_count(), 2);
5598                } else {
5599                    assert!(matches!(result, Err(KeyStoreError::Policy(_))));
5600                    assert_eq!(inventory.entry_count(), 1);
5601                    assert_eq!(inventory.material_bytes, retained);
5602                }
5603            }
5604        }
5605    }
5606
5607    #[test]
5608    fn plaintext_private_import_checks_all_live_copies() {
5609        // Borrowed input, decoded PEM and retained DER coexist. Reject one
5610        // byte below that peak without retaining a key, and accept exactly it.
5611        let der = pem::parse(include_bytes!(
5612            "../tests/fixtures/keys/rsa/rsa-2048-key.pem"
5613        ))
5614        .expect("private key")
5615        .into_contents();
5616        let pkcs1 = RsaPrivateKey::from_pkcs8_der(&der)
5617            .expect("RSA")
5618            .to_pkcs1_der()
5619            .expect("PKCS1");
5620        for (label, input) in [
5621            ("PRIVATE KEY", der.as_slice()),
5622            ("RSA PRIVATE KEY", pkcs1.as_bytes()),
5623        ] {
5624            let pem = pem::encode(&pem::Pem::new(label, input.to_vec()));
5625            for encoded in [false, true] {
5626                let peak = 3 + input.len() + der.len() + if encoded { pem.len() } else { 0 };
5627                for exact in [false, true] {
5628                    let resources = ResourcePolicy {
5629                        max_external_resource_total_bytes: peak - usize::from(!exact),
5630                        ..ResourcePolicy::default()
5631                    };
5632                    let mut inventory = KeyInventory::default();
5633                    let result = if encoded {
5634                        inventory.add_private_pem(
5635                            "new".into(),
5636                            pem.as_bytes(),
5637                            None,
5638                            KeyUsages::SIGN,
5639                            &resources,
5640                        )
5641                    } else {
5642                        inventory.add_private_der(
5643                            "new".into(),
5644                            input,
5645                            None,
5646                            KeyUsages::SIGN,
5647                            &resources,
5648                        )
5649                    };
5650                    if exact {
5651                        result.expect("exact live-byte allowance");
5652                        assert_eq!(inventory.entry_count(), 1);
5653                    } else {
5654                        assert!(matches!(result, Err(KeyStoreError::Policy(_))));
5655                        assert_eq!(inventory.entry_count(), 0);
5656                        assert_eq!(inventory.material_bytes, 0);
5657                    }
5658                }
5659            }
5660        }
5661    }
5662
5663    #[test]
5664    fn encrypted_pkcs8_plaintext_is_reserved_before_password() {
5665        use der::Encode as _;
5666        use pkcs8::pkcs5::{EncryptionScheme, pbes2};
5667        // PBKDF2 has no large workspace, but decrypt still allocates a full
5668        // ciphertext-sized buffer, even for a wrong password/malformed key.
5669        let encrypted = EncryptedPrivateKeyInfoRef {
5670            encryption_algorithm: EncryptionScheme::Pbes2(pbes2::Parameters {
5671                kdf: pbes2::Kdf::Pbkdf2(pbes2::Pbkdf2Params {
5672                    salt: pbes2::Salt::new(b"12345678").expect("salt"),
5673                    iteration_count: 2,
5674                    key_length: None,
5675                    prf: pbes2::Pbkdf2Prf::HmacWithSha256,
5676                }),
5677                encryption: pbes2::EncryptionScheme::Aes256Cbc { iv: [0; 16] },
5678            }),
5679            encrypted_data: der::asn1::OctetStringRef::new(&[0; 64]).expect("ciphertext"),
5680        };
5681        let bytes = encrypted.to_der().expect("envelope");
5682        let peak = 3 + bytes.len() + 64;
5683        let tight = ResourcePolicy {
5684            max_external_resource_total_bytes: peak - 1,
5685            ..ResourcePolicy::default()
5686        };
5687        let calls = std::cell::Cell::new(0);
5688        let mut inventory = KeyInventory::default();
5689        let result = inventory.add_private_der_with_password_callback(
5690            "new".into(),
5691            &bytes,
5692            || {
5693                calls.set(calls.get() + 1);
5694                None
5695            },
5696            KeyUsages::SIGN,
5697            &tight,
5698        );
5699        assert_eq!(calls.get(), 0);
5700        assert!(
5701            matches!(result, Err(KeyStoreError::Policy(crate::policy::PolicyViolation::ResourceLimit {
5702            resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES, actual, ..
5703        })) if actual == peak)
5704        );
5705        assert!(matches!(
5706            inventory.add_private_der(
5707                "new".into(),
5708                &bytes,
5709                Some(b"wrong"),
5710                KeyUsages::SIGN,
5711                &tight
5712            ),
5713            Err(KeyStoreError::Policy(_))
5714        ));
5715        let exact = ResourcePolicy {
5716            max_external_resource_total_bytes: peak,
5717            ..tight
5718        };
5719        assert!(matches!(
5720            inventory.add_private_der_with_password_callback(
5721                "new".into(),
5722                &bytes,
5723                || {
5724                    calls.set(calls.get() + 1);
5725                    None
5726                },
5727                KeyUsages::SIGN,
5728                &exact
5729            ),
5730            Err(KeyStoreError::ProtectedContainer)
5731        ));
5732        assert_eq!(calls.get(), 1);
5733        assert!(inventory.private_keys().is_empty());
5734        assert_eq!(inventory.material_bytes, 0);
5735    }
5736
5737    #[test]
5738    fn encrypted_pem_workspace_accounts_for_live_encoded_input() {
5739        use der::Encode as _;
5740        use pkcs8::pkcs5::{EncryptionScheme, pbes2};
5741        // PEM and its decoded DER coexist during derivation. A DER-only
5742        // allowance must not authorize that larger peak or mutate inventory.
5743        let encrypted = EncryptedPrivateKeyInfoRef {
5744            encryption_algorithm: EncryptionScheme::Pbes2(pbes2::Parameters {
5745                kdf: pbes2::Kdf::Scrypt(pbes2::ScryptParams {
5746                    salt: pbes2::Salt::new(b"12345678").expect("salt"),
5747                    cost_parameter: 16,
5748                    block_size: 1,
5749                    parallelization: 1,
5750                    key_length: None,
5751                }),
5752                encryption: pbes2::EncryptionScheme::Aes256Cbc { iv: [0; 16] },
5753            }),
5754            encrypted_data: der::asn1::OctetStringRef::new(&[0; 16]).expect("ciphertext"),
5755        };
5756        let der = encrypted.to_der().expect("envelope");
5757        let pem = pem::encode(&pem::Pem::new("ENCRYPTED PRIVATE KEY", der.clone()));
5758        let peak = 3 + pem.len() + der.len() + 16 + 2304;
5759        let mut inventory = KeyInventory::default();
5760        let tight = ResourcePolicy {
5761            max_external_resource_total_bytes: peak - 1,
5762            ..ResourcePolicy::default()
5763        };
5764        assert!(matches!(
5765            inventory.add_private_pem("new".into(), pem.as_bytes(), None, KeyUsages::SIGN, &tight),
5766            Err(KeyStoreError::Policy(crate::policy::PolicyViolation::ResourceLimit {
5767                resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
5768                actual, ..
5769            })) if actual == peak
5770        ));
5771        let exact = ResourcePolicy {
5772            max_external_resource_total_bytes: peak,
5773            ..tight
5774        };
5775        assert!(matches!(
5776            inventory.add_private_pem("new".into(), pem.as_bytes(), None, KeyUsages::SIGN, &exact),
5777            Err(KeyStoreError::ProtectedContainer)
5778        ));
5779        assert!(matches!(
5780            inventory.add_private_der("new".into(), &der, None, KeyUsages::SIGN, &tight),
5781            Err(KeyStoreError::ProtectedContainer)
5782        ));
5783        assert!(inventory.private_keys().is_empty());
5784        assert_eq!(inventory.material_bytes, 0);
5785        // Exercise actual derivation/import as well as preflight: an exactly
5786        // fitting PEM peak succeeds with the correct password, not just framing.
5787        let plain = pem::parse(include_bytes!(
5788            "../tests/fixtures/keys/rsa/rsa-2048-key.pem"
5789        ))
5790        .expect("private key")
5791        .into_contents();
5792        let EncryptionScheme::Pbes2(params) = &encrypted.encryption_algorithm else {
5793            panic!("PBES2 envelope");
5794        };
5795        let ciphertext = params.encrypt(b"correct", &plain).expect("encrypt");
5796        let real = EncryptedPrivateKeyInfoRef {
5797            encryption_algorithm: encrypted.encryption_algorithm.clone(),
5798            encrypted_data: der::asn1::OctetStringRef::new(&ciphertext).expect("ciphertext"),
5799        }
5800        .to_der()
5801        .expect("envelope");
5802        let pem = pem::encode(&pem::Pem::new("ENCRYPTED PRIVATE KEY", real.clone()));
5803        let resources = ResourcePolicy {
5804            max_external_resource_total_bytes: 3
5805                + pem.len()
5806                + real.len()
5807                + ciphertext.len()
5808                + 2304
5809                + b"correct".len(),
5810            ..ResourcePolicy::default()
5811        };
5812        inventory
5813            .add_private_pem(
5814                "new".into(),
5815                pem.as_bytes(),
5816                Some(b"correct"),
5817                KeyUsages::SIGN,
5818                &resources,
5819            )
5820            .expect("exact PEM peak imports");
5821        assert_eq!(inventory.private_keys().len(), 1);
5822    }
5823
5824    #[test]
5825    fn pkcs8_scrypt_workspace_shares_retained_inventory_budget() {
5826        use der::Encode as _;
5827        // A workspace below its own ceiling must still fit alongside existing
5828        // inventory material; rejection must precede password delivery.
5829        use pkcs8::pkcs5::{EncryptionScheme, pbes2};
5830        let encrypted = EncryptedPrivateKeyInfoRef {
5831            encryption_algorithm: EncryptionScheme::Pbes2(pbes2::Parameters {
5832                kdf: pbes2::Kdf::Scrypt(pbes2::ScryptParams {
5833                    salt: pbes2::Salt::new(b"12345678").expect("salt"),
5834                    cost_parameter: 16,
5835                    block_size: 1,
5836                    parallelization: 1,
5837                    key_length: None,
5838                }),
5839                encryption: pbes2::EncryptionScheme::Aes256Cbc { iv: [0; 16] },
5840            }),
5841            encrypted_data: der::asn1::OctetStringRef::new(&[0; 16]).expect("ciphertext"),
5842        };
5843        let bytes = encrypted.to_der().expect("envelope");
5844        let mut inventory = KeyInventory::default();
5845        inventory
5846            .add_symmetric(
5847                "old".into(),
5848                SymmetricKeyKind::Hmac,
5849                vec![7; 1024],
5850                KeyUsages::SIGN,
5851                &ResourcePolicy::default(),
5852            )
5853            .expect("retained key");
5854        let resources = ResourcePolicy {
5855            max_external_resource_total_bytes: inventory.material_bytes + 2304,
5856            max_key_import_kdf_memory_bytes: 4096,
5857            ..ResourcePolicy::default()
5858        };
5859        let calls = std::cell::Cell::new(0);
5860        let result = inventory.add_private_der_with_password_callback(
5861            "new".into(),
5862            &bytes,
5863            || {
5864                calls.set(calls.get() + 1);
5865                None
5866            },
5867            KeyUsages::SIGN,
5868            &resources,
5869        );
5870        assert_eq!(calls.get(), 0);
5871        assert!(matches!(
5872            result,
5873            Err(KeyStoreError::Policy(
5874                crate::policy::PolicyViolation::ResourceLimit {
5875                    resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
5876                    ..
5877                }
5878            ))
5879        ));
5880        assert!(inventory.private_keys().is_empty());
5881        assert_eq!(inventory.symmetric_keys().len(), 1);
5882        let exact = ResourcePolicy {
5883            max_external_resource_total_bytes: inventory.material_bytes
5884                + bytes.len()
5885                + 3
5886                + 16
5887                + 2304,
5888            ..resources
5889        };
5890        assert!(matches!(
5891            inventory.add_private_der_with_password_callback(
5892                "new".into(),
5893                &bytes,
5894                || {
5895                    calls.set(calls.get() + 1);
5896                    None
5897                },
5898                KeyUsages::SIGN,
5899                &exact,
5900            ),
5901            Err(KeyStoreError::ProtectedContainer)
5902        ));
5903        assert_eq!(
5904            calls.get(),
5905            1,
5906            "an exactly fitting workspace reaches password delivery"
5907        );
5908    }
5909
5910    #[test]
5911    fn configured_x509_continuation_retains_first_deferred_error() {
5912        // Splitting at configured X509 must preserve the unsplit diagnostic
5913        // order, while a later terminal failure still stops immediately.
5914        let mut inventory = KeyInventory::default();
5915        inventory
5916            .add_certificate_der(
5917                single_pem_block(
5918                    include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-cert.pem"),
5919                    ResourcePolicy::default().max_external_resource_bytes,
5920                )
5921                .expect("certificate")
5922                .into_contents(),
5923                false,
5924                &ResourcePolicy::default(),
5925            )
5926            .expect("lookup");
5927        let mut info = KeyInfo {
5928            sources: vec![
5929                KeyInfoSource::KeyValue(KeyValueInfo::Dsa {
5930                    p: None,
5931                    q: None,
5932                    g: None,
5933                    y: vec![1],
5934                }),
5935                KeyInfoSource::X509Data(X509DataInfo {
5936                    subject_names: vec!["CN=absent".into()],
5937                    ..X509DataInfo::default()
5938                }),
5939                KeyInfoSource::KeyValue(KeyValueInfo::InvalidEcKeyValue),
5940            ],
5941        };
5942        let unsplit = DefaultKeyResolver::new(KeyResolverConfig::default())
5943            .resolve(Some(&info), SignatureAlgorithm::RsaSha256)
5944            .err()
5945            .expect("mismatch");
5946        let split = inventory
5947            .verification_resolver()
5948            .resolve(Some(&info), SignatureAlgorithm::RsaSha256)
5949            .err()
5950            .expect("mismatch");
5951        assert_eq!(format!("{split:?}"), format!("{unsplit:?}"));
5952        info.sources[2] = KeyInfoSource::DerEncodedKeyValue(vec![0]);
5953        let unsplit = DefaultKeyResolver::new(KeyResolverConfig::default())
5954            .resolve(Some(&info), SignatureAlgorithm::RsaSha256)
5955            .err()
5956            .expect("terminal");
5957        let split = inventory
5958            .verification_resolver()
5959            .resolve(Some(&info), SignatureAlgorithm::RsaSha256)
5960            .err()
5961            .expect("terminal");
5962        assert_eq!(format!("{split:?}"), format!("{unsplit:?}"));
5963    }
5964
5965    #[test]
5966    fn configured_x509_fallback_preserves_terminal_prefix_errors() {
5967        // Attaching configured certificates must not turn malformed earlier
5968        // DER key material into a successful certificate fallback.
5969        let resources = ResourcePolicy::default();
5970        let mut inventory = KeyInventory::default();
5971        let certificate = single_pem_block(
5972            include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-cert.pem"),
5973            resources.max_external_resource_bytes,
5974        )
5975        .expect("certificate")
5976        .into_contents();
5977        let subject = crate::xmldsig::parse::parse_x509_certificate(&certificate)
5978            .expect("leaf")
5979            .subject_dn;
5980        inventory
5981            .add_certificate_der(certificate, false, &resources)
5982            .expect("lookup");
5983        let mut info = KeyInfo {
5984            sources: vec![
5985                KeyInfoSource::DerEncodedKeyValue(vec![0]),
5986                KeyInfoSource::X509Data(X509DataInfo {
5987                    subject_names: vec![subject],
5988                    ..X509DataInfo::default()
5989                }),
5990            ],
5991        };
5992        assert!(
5993            inventory
5994                .verification_resolver()
5995                .resolve(Some(&info), SignatureAlgorithm::RsaSha256,)
5996                .is_err(),
5997            "a terminal prefix error must stop resolution"
5998        );
5999        // Unsupported EC material for an RSA method is explicitly deferrable;
6000        // a certificate can satisfy it, but a complete miss retains the error.
6001        info.sources[0] = KeyInfoSource::KeyValue(KeyValueInfo::InvalidEcKeyValue);
6002        assert!(
6003            inventory
6004                .verification_resolver()
6005                .resolve(Some(&info), SignatureAlgorithm::RsaSha256,)
6006                .expect("deferred prefix permits certificate fallback")
6007                .is_some()
6008        );
6009        let KeyInfoSource::X509Data(data) = &mut info.sources[1] else {
6010            panic!("X509 selector");
6011        };
6012        data.subject_names = vec!["CN=absent".into()];
6013        assert!(
6014            inventory
6015                .verification_resolver()
6016                .resolve(Some(&info), SignatureAlgorithm::RsaSha256,)
6017                .is_err(),
6018            "a complete miss retains its deferred error"
6019        );
6020    }
6021
6022    #[test]
6023    fn configured_x509_fallback_does_not_reinspect_prefix() {
6024        // One absent name, its document source, and the certificate inspection
6025        // fit exactly. Replaying the source prefix incorrectly exhausts it.
6026        let resources = ResourcePolicy::default();
6027        let mut inventory = KeyInventory::default();
6028        let certificate = single_pem_block(
6029            include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-cert.pem"),
6030            resources.max_external_resource_bytes,
6031        )
6032        .expect("certificate")
6033        .into_contents();
6034        let subject = crate::xmldsig::parse::parse_x509_certificate(&certificate)
6035            .expect("leaf")
6036            .subject_dn;
6037        inventory
6038            .add_certificate_der(certificate, false, &resources)
6039            .expect("lookup");
6040        let info = KeyInfo {
6041            sources: vec![
6042                KeyInfoSource::KeyName("absent".into()),
6043                KeyInfoSource::X509Data(X509DataInfo {
6044                    subject_names: vec![subject],
6045                    ..X509DataInfo::default()
6046                }),
6047            ],
6048        };
6049        let mut policy = crate::policy::VerificationPolicy::default();
6050        policy.resources.max_key_candidates = 3;
6051        assert!(
6052            inventory
6053                .verification_resolver()
6054                .resolve_with_policy_and_provider(
6055                    Some(&info),
6056                    SignatureAlgorithm::RsaSha256,
6057                    &policy,
6058                    crate::provider::default_provider(),
6059                )
6060                .expect("each source inspected once")
6061                .is_some()
6062        );
6063        policy.resources.max_key_candidates = 2;
6064        assert!(matches!(
6065            inventory
6066                .verification_resolver()
6067                .resolve_with_policy_and_provider(
6068                    Some(&info),
6069                    SignatureAlgorithm::RsaSha256,
6070                    &policy,
6071                    crate::provider::default_provider(),
6072                ),
6073            Err(DsigError::Policy(_))
6074        ));
6075    }
6076
6077    #[test]
6078    fn scrypt_parallel_buffers_are_checked_before_derivation() {
6079        // N*r fits a tiny limit, but p independent B/V/T workspaces do not.
6080        let mut resources = ResourcePolicy {
6081            max_key_import_kdf_work: 10_000,
6082            max_key_import_kdf_memory_bytes: 4_096,
6083            ..ResourcePolicy::default()
6084        };
6085        assert!(matches!(
6086            enforce_scrypt_kdf_limits(2, 1, 1_000, &resources, 0),
6087            Err(KeyStoreError::Policy(
6088                crate::policy::PolicyViolation::ResourceLimit {
6089                    resource: crate::policy::resource_name::KEY_IMPORT_KDF_MEMORY,
6090                    maximum: 4_096,
6091                    actual: 512_000,
6092                }
6093            ))
6094        ));
6095        resources.max_key_import_kdf_memory_bytes = 512_000;
6096        assert!(enforce_scrypt_kdf_limits(2, 1, 1_000, &resources, 0).is_ok());
6097    }
6098
6099    #[test]
6100    fn named_hmac_resolution_enforces_usage_and_method() {
6101        // A named secret may verify only when both its usage and the XMLDSig
6102        // method permit HMAC; the resolver must not fall back to another key.
6103        let resources = ResourcePolicy::default();
6104        let mut inventory = KeyInventory::default();
6105        inventory
6106            .add_symmetric(
6107                "verify".into(),
6108                SymmetricKeyKind::Hmac,
6109                b"sufficiently-long-hmac-secret".to_vec(),
6110                KeyUsages::VERIFY,
6111                &resources,
6112            )
6113            .expect("verification HMAC imports");
6114        inventory
6115            .add_symmetric(
6116                "sign-only".into(),
6117                SymmetricKeyKind::Hmac,
6118                b"another-hmac-secret".to_vec(),
6119                KeyUsages::SIGN,
6120                &resources,
6121            )
6122            .expect("sign-only HMAC imports");
6123        let resolver = inventory.verification_resolver();
6124        let named = |name: &str| KeyInfo {
6125            sources: vec![KeyInfoSource::KeyName(name.into())],
6126            ..KeyInfo::default()
6127        };
6128        assert!(
6129            resolver
6130                .resolve(Some(&named("verify")), SignatureAlgorithm::HmacSha256)
6131                .expect("named HMAC resolves")
6132                .is_some()
6133        );
6134        assert!(
6135            resolver
6136                .resolve(Some(&named("verify")), SignatureAlgorithm::RsaSha256)
6137                .is_err()
6138        );
6139        assert!(
6140            resolver
6141                .resolve(Some(&named("sign-only")), SignatureAlgorithm::HmacSha256)
6142                .is_err()
6143        );
6144    }
6145
6146    #[test]
6147    fn named_hmac_resolution_rejects_invalid_policy_snapshot() {
6148        // Early HMAC resolution must validate the entire snapshot even when
6149        // the selected key is small and otherwise permitted.
6150        let mut inventory = KeyInventory::default();
6151        inventory
6152            .add_symmetric(
6153                "verify".into(),
6154                SymmetricKeyKind::Hmac,
6155                b"sufficiently-long-hmac-secret".to_vec(),
6156                KeyUsages::VERIFY,
6157                &ResourcePolicy::default(),
6158            )
6159            .expect("verification HMAC imports");
6160        let info = KeyInfo {
6161            sources: vec![KeyInfoSource::KeyName("verify".into())],
6162            ..KeyInfo::default()
6163        };
6164        let mut policy = crate::policy::VerificationPolicy::default();
6165        policy.resources.max_external_resource_bytes = usize::MAX;
6166        assert!(matches!(
6167            inventory.verification_resolver().resolve_with_policy(
6168                Some(&info),
6169                SignatureAlgorithm::HmacSha256,
6170                &policy
6171            ),
6172            Err(DsigError::Policy(_))
6173        ));
6174    }
6175
6176    #[test]
6177    fn named_hmac_resolution_uses_active_resource_limits() {
6178        // A store imported under a broad policy must not bypass a later,
6179        // stricter verification snapshot when resolving a named secret.
6180        let resources = ResourcePolicy::default();
6181        let secret = b"sufficiently-long-hmac-secret";
6182        let mut inventory = KeyInventory::default();
6183        inventory
6184            .add_symmetric(
6185                "verify".into(),
6186                SymmetricKeyKind::Hmac,
6187                secret.to_vec(),
6188                KeyUsages::VERIFY,
6189                &resources,
6190            )
6191            .expect("verification HMAC imports");
6192        let key_info = KeyInfo {
6193            sources: vec![KeyInfoSource::KeyName("verify".into())],
6194            ..KeyInfo::default()
6195        };
6196        let resolver = inventory.verification_resolver();
6197        for (resource, aggregate) in [
6198            (crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES, false),
6199            (
6200                crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
6201                true,
6202            ),
6203        ] {
6204            let mut policy = crate::policy::VerificationPolicy::default();
6205            if aggregate {
6206                policy.resources.max_external_resource_total_bytes = secret.len() - 1;
6207            } else {
6208                policy.resources.max_external_resource_bytes = secret.len() - 1;
6209            }
6210            let error = resolver
6211                .resolve_with_policy(Some(&key_info), SignatureAlgorithm::HmacSha256, &policy)
6212                .err()
6213                .expect("active limit must reject stored HMAC material");
6214            assert!(
6215                matches!(
6216                    error,
6217                    DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
6218                        resource: actual,
6219                        maximum,
6220                        actual: size,
6221                    }) if actual == resource && maximum == secret.len() - 1 && size == secret.len()
6222                ),
6223                "{error:?}"
6224            );
6225        }
6226    }
6227
6228    #[test]
6229    fn one_named_verification_entry_fits_one_candidate() {
6230        // A name and the single entry it selects are one lookup, not two.
6231        let mut inventory = KeyInventory::default();
6232        let mut policy = crate::policy::VerificationPolicy::default();
6233        inventory
6234            .add_symmetric(
6235                "only".into(),
6236                SymmetricKeyKind::Hmac,
6237                b"sufficiently-long-hmac-secret".to_vec(),
6238                KeyUsages::VERIFY,
6239                &policy.resources,
6240            )
6241            .expect("HMAC imports");
6242        policy.resources.max_key_candidates = 1;
6243        let info = KeyInfo {
6244            sources: vec![KeyInfoSource::KeyName("only".into())],
6245        };
6246        assert!(
6247            inventory
6248                .verification_resolver()
6249                .resolve_with_policy(Some(&info), SignatureAlgorithm::HmacSha256, &policy)
6250                .expect("one lookup fits")
6251                .is_some()
6252        );
6253    }
6254
6255    #[test]
6256    fn pem_imports_charge_encoded_input_to_aggregate_budget() {
6257        // Repeated padded PEM inputs must consume the aggregate work budget
6258        // even when their decoded DER keys are much smaller.
6259        let public = include_str!("../tests/fixtures/keys/rsa/rsa-4096-pubkey.pem");
6260        let private = include_str!("../tests/fixtures/keys/rsa/rsa-4096-key.pem");
6261        for (pem, is_private) in [(public, false), (private, true)] {
6262            let padded = format!("{pem}{}", " ".repeat(16 * 1024));
6263            // Public imports need one live DER; private imports need two,
6264            // in addition to encoded input. Retaining the first input charge
6265            // still makes the second padded import exceed this exact peak.
6266            let workspace = pem::parse(pem).expect("PEM payload").contents().len()
6267                * if is_private { 2 } else { 1 };
6268            let resources = ResourcePolicy {
6269                max_external_resource_bytes: padded.len(),
6270                max_external_resource_total_bytes: padded.len()
6271                    + if is_private { 5 } else { 10 }
6272                    + workspace
6273                    + 1,
6274                ..ResourcePolicy::default()
6275            };
6276            let mut inventory = KeyInventory::default();
6277            let import = |inventory: &mut KeyInventory, name: &str| {
6278                if is_private {
6279                    inventory.add_private_pem(
6280                        name.into(),
6281                        padded.as_bytes(),
6282                        None,
6283                        KeyUsages::SIGN,
6284                        &resources,
6285                    )
6286                } else {
6287                    inventory.add_public_pem(name.into(), padded.as_bytes(), &resources)
6288                }
6289            };
6290            import(&mut inventory, "first").expect("first PEM import fits");
6291            assert!(
6292                matches!(
6293                    import(&mut inventory, "second"),
6294                    Err(KeyStoreError::Policy(
6295                        crate::policy::PolicyViolation::ResourceLimitExceeded {
6296                            resource:
6297                                crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
6298                            ..
6299                        }
6300                    ))
6301                ),
6302                "second PEM input must exceed aggregate budget"
6303            );
6304        }
6305    }
6306
6307    #[test]
6308    fn repeated_key_name_selects_one_inventory_entry() {
6309        // XMLDSig 1.1 section 4.5 permits repeated KeyInfo choices; duplicate
6310        // references to one entry are not two distinct verification keys.
6311        let resources = ResourcePolicy::default();
6312        let mut inventory = KeyInventory::default();
6313        inventory
6314            .add_symmetric(
6315                "secret".into(),
6316                SymmetricKeyKind::Hmac,
6317                b"sufficiently-long-hmac-secret".to_vec(),
6318                KeyUsages::VERIFY,
6319                &resources,
6320            )
6321            .expect("HMAC key imports");
6322        inventory
6323            .add_symmetric(
6324                "other-secret".into(),
6325                SymmetricKeyKind::Hmac,
6326                b"another-long-hmac-secret".to_vec(),
6327                KeyUsages::VERIFY,
6328                &resources,
6329            )
6330            .expect("second HMAC key imports");
6331        inventory
6332            .add_public_pem(
6333                "public".into(),
6334                include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"),
6335                &resources,
6336            )
6337            .expect("public key imports");
6338        let resolver = inventory.verification_resolver();
6339        for (name, algorithm) in [
6340            ("secret", SignatureAlgorithm::HmacSha256),
6341            ("public", SignatureAlgorithm::RsaSha256),
6342        ] {
6343            let info = KeyInfo {
6344                sources: vec![
6345                    KeyInfoSource::KeyName(name.into()),
6346                    KeyInfoSource::KeyName(name.into()),
6347                ],
6348            };
6349            assert!(resolver.resolve(Some(&info), algorithm).is_ok(), "{name}");
6350        }
6351        let distinct = KeyInfo {
6352            sources: vec![
6353                KeyInfoSource::KeyName("secret".into()),
6354                KeyInfoSource::KeyName("other-secret".into()),
6355            ],
6356        };
6357        assert!(
6358            resolver
6359                .resolve(Some(&distinct), SignatureAlgorithm::HmacSha256)
6360                .is_err()
6361        );
6362    }
6363
6364    #[test]
6365    fn named_lookup_charges_inspected_inventory_entries() {
6366        // A late KeyName must not bypass a stricter operation candidate limit.
6367        let mut inventory = KeyInventory::default();
6368        let resources = ResourcePolicy::default();
6369        for name in ["first", "second"] {
6370            inventory
6371                .add_symmetric(
6372                    name.into(),
6373                    SymmetricKeyKind::Hmac,
6374                    b"sufficiently-long-hmac-secret".to_vec(),
6375                    KeyUsages::VERIFY,
6376                    &resources,
6377                )
6378                .expect("key imports");
6379        }
6380        let key_info = KeyInfo {
6381            sources: vec![KeyInfoSource::KeyName("second".into())],
6382            ..KeyInfo::default()
6383        };
6384        let policy = crate::policy::VerificationPolicy {
6385            resources: ResourcePolicy {
6386                max_key_candidates: 1,
6387                ..ResourcePolicy::default()
6388            },
6389            ..crate::policy::VerificationPolicy::default()
6390        };
6391        let resolver = inventory.verification_resolver();
6392        assert!(
6393            resolver
6394                .resolve_with_policy_and_provider(
6395                    Some(&key_info),
6396                    SignatureAlgorithm::HmacSha256,
6397                    &policy,
6398                    crate::provider::default_provider(),
6399                )
6400                .is_err()
6401        );
6402    }
6403
6404    #[test]
6405    fn private_import_rejects_public_only_usage() {
6406        // Usage is part of the inventory contract: nonsensical permissions
6407        // must not survive import and later be interpreted by a resolver.
6408        let private = include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-key.pem");
6409        let mut inventory = KeyInventory::default();
6410        assert!(
6411            inventory
6412                .add_private_pem(
6413                    "wrong-use".into(),
6414                    private,
6415                    None,
6416                    KeyUsages::SIGN.union(KeyUsages::VERIFY),
6417                    &ResourcePolicy::default(),
6418                )
6419                .is_err()
6420        );
6421        assert!(inventory.private_keys.is_empty());
6422    }
6423
6424    #[test]
6425    fn normalized_private_key_must_fit_resource_limit() {
6426        // PKCS#8 wrapping may cross the per-resource ceiling even when PKCS#1 fits.
6427        let rsa = RsaPrivateKey::from_pkcs8_pem(include_str!(
6428            "../tests/fixtures/keys/rsa/rsa-2048-key.pem"
6429        ))
6430        .expect("RSA fixture parses");
6431        let pkcs1 = rsa.to_pkcs1_der().expect("PKCS#1 encodes");
6432        let pkcs8 = rsa.to_pkcs8_der().expect("PKCS#8 encodes");
6433        assert!(pkcs8.as_bytes().len() > pkcs1.as_bytes().len());
6434        let resources = ResourcePolicy {
6435            max_external_resource_bytes: pkcs1.as_bytes().len(),
6436            ..ResourcePolicy::default()
6437        };
6438        let mut inventory = KeyInventory::default();
6439        assert!(
6440            inventory
6441                .add_private_der(
6442                    "rsa".into(),
6443                    pkcs1.as_bytes(),
6444                    None,
6445                    KeyUsages::SIGN,
6446                    &resources,
6447                )
6448                .is_err()
6449        );
6450        assert!(inventory.private_keys.is_empty());
6451    }
6452
6453    #[test]
6454    fn oversized_rsa_components_are_rejected_before_private_key_decode() {
6455        // PKCS#8 wraps PKCS#1 integers; every component must be checked
6456        // before RustCrypto allocates big integers or validates CRT arithmetic.
6457        let pem = include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-key.pem");
6458        let block = single_pem_block(pem, ResourcePolicy::default().max_external_resource_bytes)
6459            .expect("fixture PEM");
6460        let info = PrivateKeyInfoRef::try_from(block.contents()).expect("fixture PKCS#8");
6461        let original =
6462            pkcs1::RsaPrivateKeyRef::from_der(info.private_key.as_bytes()).expect("fixture PKCS#1");
6463        let oversized_modulus = vec![1_u8; 1025];
6464        let oversized = pkcs1::RsaPrivateKeyRef {
6465            modulus: der::asn1::UintRef::new(&oversized_modulus).expect("positive modulus"),
6466            public_exponent: original.public_exponent,
6467            private_exponent: original.private_exponent,
6468            prime1: original.prime1,
6469            prime2: original.prime2,
6470            exponent1: original.exponent1,
6471            exponent2: original.exponent2,
6472            coefficient: original.coefficient,
6473            other_prime_infos: None,
6474        };
6475        let pkcs1 = der::Encode::to_der(&oversized).expect("PKCS#1 encodes");
6476        let octets = der::asn1::OctetStringRef::new(&pkcs1).expect("PKCS#8 octets");
6477        let pkcs8 = der::Encode::to_der(&PrivateKeyInfoRef::new(pkcs1::ALGORITHM_ID, octets))
6478            .expect("PKCS#8 encodes");
6479        let mut inventory = KeyInventory::default();
6480        let error = inventory
6481            .add_private_der(
6482                "oversized".into(),
6483                &pkcs8,
6484                None,
6485                KeyUsages::SIGN,
6486                &ResourcePolicy::default(),
6487            )
6488            .expect_err("oversized RSA modulus must fail at preflight");
6489        assert!(error.to_string().contains("safety limit"), "{error}");
6490
6491        let oversized_exponent = vec![1_u8; 1025];
6492        let oversized = pkcs1::RsaPrivateKeyRef {
6493            modulus: original.modulus,
6494            public_exponent: original.public_exponent,
6495            private_exponent: der::asn1::UintRef::new(&oversized_exponent)
6496                .expect("positive exponent"),
6497            prime1: original.prime1,
6498            prime2: original.prime2,
6499            exponent1: original.exponent1,
6500            exponent2: original.exponent2,
6501            coefficient: original.coefficient,
6502            other_prime_infos: None,
6503        };
6504        let pkcs1 = der::Encode::to_der(&oversized).expect("PKCS#1 encodes");
6505        let error = preflight_rsa_pkcs1_components(&pkcs1)
6506            .expect_err("oversized private exponent must fail before bigint decoding");
6507        assert!(error.to_string().contains("safety limit"), "{error}");
6508    }
6509
6510    #[test]
6511    fn named_certificate_import_is_not_a_trust_anchor() {
6512        // A certificate is usable as a named public-key source but importing
6513        // it must never grant certificate-chain trust implicitly.
6514        let pem = include_bytes!("../tests/fixtures/keys/rsa/rsa-4096-cert.pem");
6515        let certificate =
6516            single_pem_block(pem, ResourcePolicy::default().max_external_resource_bytes)
6517                .expect("certificate fixture is one PEM block");
6518        let mut inventory = KeyInventory::default();
6519        inventory
6520            .add_public_der(
6521                "recipient-cert".into(),
6522                certificate.contents().to_vec(),
6523                &ResourcePolicy::default(),
6524            )
6525            .expect("named X.509 certificate imports");
6526        #[cfg(feature = "xmlenc")]
6527        {
6528            assert!(
6529                inventory
6530                    .rsa_encryption_key(
6531                        "recipient-cert",
6532                        &crate::policy::EncryptionPolicy::default()
6533                    )
6534                    .is_ok()
6535            );
6536            let restricted = crate::policy::EncryptionPolicy {
6537                resources: ResourcePolicy {
6538                    max_external_resource_bytes: 64,
6539                    max_external_resource_total_bytes: 64,
6540                    ..ResourcePolicy::default()
6541                },
6542                ..crate::policy::EncryptionPolicy::default()
6543            };
6544            assert!(
6545                inventory
6546                    .rsa_encryption_key("recipient-cert", &restricted)
6547                    .is_err()
6548            );
6549        }
6550        assert!(inventory.trusted_certificates.is_empty());
6551    }
6552
6553    #[test]
6554    fn eddsa_private_identity_matches_only_its_corresponding_spki() {
6555        use pkcs8::{EncodePrivateKey, EncodePublicKey};
6556        // PKCS#12 leaf selection must bind exact algorithm and public bytes,
6557        // without retaining or allocating a second serialized public key.
6558        let key = ed25519_dalek::SigningKey::from_bytes(&[0x42; 32]);
6559        let der = key.to_pkcs8_der().expect("Ed25519 private encoding");
6560        let public = key
6561            .verifying_key()
6562            .to_public_key_der()
6563            .expect("Ed25519 public encoding");
6564        let identity = PrivateKeyIdentity::decode(der.as_bytes()).expect("Ed25519 identity");
6565        assert!(identity.matches_spki(public.as_bytes()));
6566        let wrong = ed25519_dalek::SigningKey::from_bytes(&[0x43; 32])
6567            .verifying_key()
6568            .to_public_key_der()
6569            .expect("different Ed25519 public encoding");
6570        assert!(!identity.matches_spki(wrong.as_bytes()));
6571        let key = ed448_goldilocks::SigningKey::try_from(&[0x42; 57][..]).expect("Ed448 seed");
6572        let der = key.to_pkcs8_der().expect("Ed448 private encoding");
6573        let public = key
6574            .verifying_key()
6575            .to_public_key_der()
6576            .expect("Ed448 public encoding");
6577        let identity = PrivateKeyIdentity::decode(der.as_bytes()).expect("Ed448 identity");
6578        assert!(identity.matches_spki(public.as_bytes()));
6579        assert!(!identity.matches_spki(wrong.as_bytes()));
6580    }
6581
6582    #[test]
6583    fn unsupported_spki_is_rejected_at_import() {
6584        // A syntactically valid X25519 agreement key is not an Ed25519
6585        // signing key and must not acquire VERIFY usage (RFC 8410 section 5).
6586        let mut agreement_spki = vec![
6587            0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e, 0x03, 0x21, 0x00,
6588        ];
6589        agreement_spki.extend_from_slice(&[1; 32]);
6590        let mut inventory = KeyInventory::default();
6591        assert!(
6592            inventory
6593                .add_public_der(
6594                    "unsupported".into(),
6595                    agreement_spki,
6596                    &ResourcePolicy::default()
6597                )
6598                .is_err()
6599        );
6600        assert!(inventory.public_keys.is_empty());
6601    }
6602
6603    #[test]
6604    fn named_certificate_resolution_preserves_document_crl() {
6605        // Named inventory selection must preserve document revocation evidence;
6606        // without it the same chain is valid and resolves successfully.
6607        use rcgen::{CertificateParams, KeyPair, KeyUsagePurpose, SerialNumber};
6608        let mut root_params = CertificateParams::new(Vec::new()).expect("root params");
6609        root_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
6610        root_params.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign];
6611        let root = rcgen::CertifiedIssuer::self_signed(
6612            root_params,
6613            KeyPair::generate().expect("root key"),
6614        )
6615        .expect("root certificate");
6616        let mut leaf_params = CertificateParams::new(Vec::new()).expect("leaf params");
6617        leaf_params.serial_number = Some(SerialNumber::from(42_u64));
6618        leaf_params.key_usages = vec![KeyUsagePurpose::DigitalSignature];
6619        let leaf = leaf_params
6620            .signed_by(&KeyPair::generate().expect("leaf key"), &root)
6621            .expect("leaf certificate");
6622        let now = time::OffsetDateTime::now_utc();
6623        let crl = rcgen::CertificateRevocationListParams {
6624            this_update: now - time::Duration::days(1),
6625            next_update: now + time::Duration::days(1),
6626            crl_number: SerialNumber::from(1_u64),
6627            issuing_distribution_point: None,
6628            revoked_certs: vec![rcgen::RevokedCertParams {
6629                serial_number: SerialNumber::from(42_u64),
6630                revocation_time: now - time::Duration::hours(1),
6631                reason_code: None,
6632                invalidity_date: None,
6633            }],
6634            key_identifier_method: rcgen::KeyIdMethod::Sha256,
6635        }
6636        .signed_by(&root)
6637        .expect("signed CRL");
6638        let resources = ResourcePolicy::default();
6639        let mut inventory = KeyInventory::default();
6640        inventory
6641            .add_public_der("leaf".into(), leaf.der().to_vec(), &resources)
6642            .expect("leaf imports");
6643        inventory
6644            .add_certificate_der(root.der().to_vec(), true, &resources)
6645            .expect("root imports");
6646        let policy = crate::policy::VerificationPolicy {
6647            key_trust: crate::policy::KeyTrustPolicy {
6648                verify_x509_chains: true,
6649                check_crls: true,
6650                verification_time: Some(std::time::SystemTime::now()),
6651                ..crate::policy::KeyTrustPolicy::default()
6652            },
6653            ..crate::policy::VerificationPolicy::default()
6654        };
6655        let mut info = KeyInfo {
6656            sources: vec![KeyInfoSource::KeyName("leaf".into())],
6657        };
6658        let resolver = inventory.verification_resolver();
6659        assert!(
6660            resolver
6661                .resolve_with_policy_and_provider(
6662                    Some(&info),
6663                    SignatureAlgorithm::EcdsaSha256,
6664                    &policy,
6665                    crate::provider::default_provider()
6666                )
6667                .expect("unrevoked chain resolves")
6668                .is_some()
6669        );
6670        info.sources.push(KeyInfoSource::X509Data(X509DataInfo {
6671            crls: vec![crl.der().to_vec()],
6672            ..X509DataInfo::default()
6673        }));
6674        let error = resolver
6675            .resolve_with_policy_and_provider(
6676                Some(&info),
6677                SignatureAlgorithm::EcdsaSha256,
6678                &policy,
6679                crate::provider::default_provider(),
6680            )
6681            .err()
6682            .expect("document CRL revokes leaf");
6683        assert!(
6684            error
6685                .to_string()
6686                .contains("certificate at chain position 0 is revoked"),
6687            "{error}"
6688        );
6689        let mut bounded = policy.clone();
6690        // The owned fallback coexists with the root and enabled document CRL.
6691        // Deny one byte below that peak; disabling CRLs releases both charges.
6692        bounded.resources.max_external_resource_total_bytes =
6693            leaf.der().len() + 2 * (root.der().len() + crl.der().len()) - 1;
6694        assert!(matches!(
6695            resolver.resolve_with_policy_and_provider(
6696                Some(&info),
6697                SignatureAlgorithm::EcdsaSha256,
6698                &bounded,
6699                crate::provider::default_provider()
6700            ),
6701            Err(DsigError::Policy(
6702                crate::policy::PolicyViolation::ResourceLimit {
6703                    resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
6704                    ..
6705                }
6706            ))
6707        ));
6708        bounded.key_trust.check_crls = false;
6709        assert!(
6710            resolver
6711                .resolve_with_policy_and_provider(
6712                    Some(&info),
6713                    SignatureAlgorithm::EcdsaSha256,
6714                    &bounded,
6715                    crate::provider::default_provider()
6716                )
6717                .expect("disabled CRL checks do not load CRLs")
6718                .is_some()
6719        );
6720    }
6721
6722    #[test]
6723    fn named_certificate_resolution_enforces_inventory_crl() {
6724        // A KeyName must not drop caller-supplied revocation evidence.
6725        fn cert(pem: &[u8]) -> Vec<u8> {
6726            let text = std::str::from_utf8(pem).expect("fixture is UTF-8");
6727            let start = text.find("-----BEGIN ").expect("fixture has PEM armor");
6728            single_pem_block(
6729                &text.as_bytes()[start..],
6730                ResourcePolicy::default().max_external_resource_bytes,
6731            )
6732            .expect("certificate PEM parses")
6733            .into_contents()
6734        }
6735        let resources = ResourcePolicy::default();
6736        let mut inventory = KeyInventory::default();
6737        inventory
6738            .add_public_der(
6739                "leaf".into(),
6740                cert(include_bytes!(
6741                    "../tests/fixtures/keys/rsa/rsa-2048-cert.pem"
6742                )),
6743                &resources,
6744            )
6745            .expect("leaf imports");
6746        for anchor in [
6747            include_bytes!("../tests/fixtures/keys/ca2cert.pem").as_slice(),
6748            include_bytes!("../tests/fixtures/keys/cacert.pem").as_slice(),
6749        ] {
6750            inventory
6751                .add_certificate_der(cert(anchor), true, &resources)
6752                .expect("anchor imports");
6753        }
6754        inventory
6755            .add_crl_der(
6756                cert(include_bytes!(
6757                    "../tests/fixtures/keys/rsa/rsa-2048-cert-revoked-crl.pem"
6758                )),
6759                &resources,
6760            )
6761            .expect("CRL imports");
6762        let policy = crate::policy::VerificationPolicy {
6763            key_trust: crate::policy::KeyTrustPolicy {
6764                verify_x509_chains: true,
6765                check_crls: true,
6766                max_x509_chain_depth: 3,
6767                verification_time: Some(
6768                    std::time::SystemTime::UNIX_EPOCH
6769                        + std::time::Duration::from_secs(1_773_964_800),
6770                ),
6771                ..crate::policy::KeyTrustPolicy::default()
6772            },
6773            ..crate::policy::VerificationPolicy::default()
6774        };
6775        let key_info = KeyInfo {
6776            sources: vec![KeyInfoSource::KeyName("leaf".into())],
6777            ..KeyInfo::default()
6778        };
6779        let error = inventory
6780            .verification_resolver()
6781            .resolve_with_policy_and_provider(
6782                Some(&key_info),
6783                SignatureAlgorithm::RsaSha256,
6784                &policy,
6785                crate::provider::default_provider(),
6786            )
6787            .err()
6788            .expect("revocation evidence must reject this chain");
6789        assert!(error.to_string().contains("cRLSign"), "{error}");
6790    }
6791
6792    #[test]
6793    fn hmac_resolution_does_not_load_unrelated_certificate_material() {
6794        // The active snapshot bounds selected material, not unrelated X.509
6795        // bytes that an HMAC resolver never needs to copy or inspect.
6796        let mut inventory = KeyInventory::default();
6797        inventory
6798            .add_symmetric(
6799                "hmac".into(),
6800                SymmetricKeyKind::Hmac,
6801                b"secret".to_vec(),
6802                KeyUsages::VERIFY,
6803                &ResourcePolicy::default(),
6804            )
6805            .expect("HMAC imports");
6806        let pem = include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
6807        let certificate =
6808            single_pem_block(pem, ResourcePolicy::default().max_external_resource_bytes)
6809                .expect("certificate fixture");
6810        inventory
6811            .add_certificate_der(
6812                certificate.contents().to_vec(),
6813                true,
6814                &ResourcePolicy::default(),
6815            )
6816            .expect("anchor imports");
6817        let policy = crate::policy::VerificationPolicy {
6818            resources: ResourcePolicy {
6819                max_external_resource_bytes: 64,
6820                max_external_resource_total_bytes: 64,
6821                ..ResourcePolicy::default()
6822            },
6823            ..crate::policy::VerificationPolicy::default()
6824        };
6825        let info = KeyInfo {
6826            sources: vec![KeyInfoSource::KeyName("hmac".into())],
6827            ..KeyInfo::default()
6828        };
6829        assert!(
6830            inventory
6831                .verification_resolver()
6832                .resolve_with_policy_and_provider(
6833                    Some(&info),
6834                    SignatureAlgorithm::HmacSha256,
6835                    &policy,
6836                    crate::provider::default_provider(),
6837                )
6838                .expect("unrelated certificates cannot consume HMAC budget")
6839                .is_some()
6840        );
6841    }
6842
6843    #[test]
6844    fn named_key_resolution_obeys_source_policy() {
6845        // Inventory lookup is not permission to use a KeyName source that the
6846        // operation's immutable verification policy has disabled.
6847        let resources = ResourcePolicy::default();
6848        let mut inventory = KeyInventory::default();
6849        inventory
6850            .add_symmetric(
6851                "blocked-name".into(),
6852                SymmetricKeyKind::Hmac,
6853                b"policy-guarded-hmac-secret".to_vec(),
6854                KeyUsages::VERIFY,
6855                &resources,
6856            )
6857            .expect("HMAC fixture imports");
6858        let info = KeyInfo {
6859            sources: vec![KeyInfoSource::KeyName("blocked-name".into())],
6860            ..KeyInfo::default()
6861        };
6862        let mut policy = crate::policy::VerificationPolicy::default();
6863        policy.key_sources.key_name = false;
6864        assert!(
6865            inventory
6866                .verification_resolver()
6867                .resolve_with_policy_and_provider(
6868                    Some(&info),
6869                    SignatureAlgorithm::HmacSha256,
6870                    &policy,
6871                    crate::provider::default_provider(),
6872                )
6873                .is_err()
6874        );
6875    }
6876
6877    #[test]
6878    fn named_lookup_and_fallback_share_one_candidate_budget() {
6879        // Finding a named inventory entry and resolving its embedded KeyValue
6880        // are one verification operation, not two independently budgeted scans.
6881        let mut inventory = KeyInventory::default();
6882        inventory.public_keys.push(StoredPublicKey {
6883            name: "named".into(),
6884            key_info: KeyInfo {
6885                sources: vec![
6886                    KeyInfoSource::KeyName("named".into()),
6887                    KeyInfoSource::KeyValue(KeyValueInfo::Rsa {
6888                        modulus: vec![3],
6889                        exponent: vec![3],
6890                    }),
6891                ],
6892            },
6893            usages: KeyUsages::VERIFY,
6894        });
6895        let info = KeyInfo {
6896            sources: vec![KeyInfoSource::KeyName("named".into())],
6897        };
6898        let mut policy = crate::policy::VerificationPolicy::default();
6899        policy.resources.max_key_candidates = 1;
6900        let error = inventory
6901            .verification_resolver()
6902            .resolve_with_policy_and_provider(
6903                Some(&info),
6904                SignatureAlgorithm::RsaSha256,
6905                &policy,
6906                crate::provider::default_provider(),
6907            )
6908            .err()
6909            .expect("delegation must not reset the candidate budget");
6910        assert!(matches!(
6911            error,
6912            DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
6913                resource: crate::policy::resource_name::KEY_CANDIDATES,
6914                maximum: 1,
6915                actual: 2,
6916            })
6917        ));
6918    }
6919
6920    #[test]
6921    fn prefix_retry_spends_the_same_candidate_budget() {
6922        // Unresolved sources preceding X.509 lookup are visited twice, so both
6923        // passes must charge the same operation budget.
6924        let mut sources = vec![KeyInfoSource::KeyName("missing".into()); 3];
6925        sources.push(KeyInfoSource::X509Data(X509DataInfo {
6926            subject_names: vec!["missing subject".into()],
6927            ..X509DataInfo::default()
6928        }));
6929        let info = KeyInfo { sources };
6930        let mut policy = crate::policy::VerificationPolicy::default();
6931        policy.resources.max_key_candidates = 5;
6932        let error = KeyInventory::default()
6933            .verification_resolver()
6934            .resolve_with_policy_and_provider(
6935                Some(&info),
6936                SignatureAlgorithm::RsaSha256,
6937                &policy,
6938                crate::provider::default_provider(),
6939            )
6940            .err()
6941            .expect("the repeated prefix must exhaust the candidate limit");
6942        assert!(matches!(
6943            error,
6944            DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
6945                resource: crate::policy::resource_name::KEY_CANDIDATES,
6946                maximum: 5,
6947                actual: 6,
6948            })
6949        ));
6950    }
6951
6952    #[test]
6953    fn selected_key_value_is_one_resource() {
6954        // Representation must not split one key into separately bounded
6955        // components; exact boundaries remain accepted for all KeyValue kinds.
6956        for value in [
6957            KeyValueInfo::Rsa {
6958                modulus: vec![1; 256],
6959                exponent: vec![1; 3],
6960            },
6961            KeyValueInfo::Dsa {
6962                p: Some(vec![1; 64]),
6963                q: Some(vec![1; 16]),
6964                g: Some(vec![1; 64]),
6965                y: vec![1; 64],
6966            },
6967            KeyValueInfo::Ec {
6968                curve_oid: "1.2.840.10045.3.1.7".into(),
6969                public_key: vec![1; 65],
6970            },
6971        ] {
6972            let info = KeyInfo {
6973                sources: vec![KeyInfoSource::KeyValue(value)],
6974            };
6975            let size =
6976                check_selected_public_material(&info, &ResourcePolicy::default()).expect("size");
6977            let resources = ResourcePolicy {
6978                max_external_resource_bytes: size,
6979                ..ResourcePolicy::default()
6980            };
6981            assert_eq!(
6982                check_selected_public_material(&info, &resources).expect("exact limit"),
6983                size
6984            );
6985            let resources = ResourcePolicy {
6986                max_external_resource_bytes: size - 1,
6987                ..resources
6988            };
6989            assert!(matches!(check_selected_public_material(&info, &resources),
6990                Err(DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
6991                    resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES, actual, ..
6992                })) if actual == size));
6993        }
6994    }
6995
6996    #[test]
6997    fn named_verification_bounds_complete_key_value() {
6998        // A broadly imported XML key must obey the tighter operation snapshot
6999        // before the resolver constructs an SPKI from its components.
7000        use rsa::traits::PublicKeyParts as _;
7001        let public = RsaPublicKey::from_public_key_pem(include_str!(
7002            "../tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"
7003        ))
7004        .expect("RSA fixture");
7005        let modulus = public.n().to_be_bytes_trimmed_vartime();
7006        let exponent = public.e().to_be_bytes_trimmed_vartime();
7007        let size = modulus.len() + exponent.len();
7008        let base64 = base64::engine::general_purpose::STANDARD;
7009        let xml = format!(
7010            "<Keys xmlns=\"{XMLSEC_NS}\"><KeyInfo xmlns=\"{XMLDSIG_NS}\"><KeyName>named</KeyName><KeyValue><RSAKeyValue><Modulus>{}</Modulus><Exponent>{}</Exponent></RSAKeyValue></KeyValue></KeyInfo></Keys>",
7011            base64.encode(modulus),
7012            base64.encode(exponent)
7013        );
7014        let inventory = KeyInventory::from_xml_bytes(
7015            xml.as_bytes(),
7016            &xml_policy(ResourcePolicy::default()),
7017            XmlBackend::default(),
7018        )
7019        .expect("broad import");
7020        let info = KeyInfo {
7021            sources: vec![KeyInfoSource::KeyName("named".into())],
7022        };
7023        let mut policy = crate::policy::VerificationPolicy::default();
7024        policy.resources.max_external_resource_bytes = size;
7025        assert!(
7026            inventory
7027                .verification_resolver()
7028                .resolve_with_policy_and_provider(
7029                    Some(&info),
7030                    SignatureAlgorithm::RsaSha256,
7031                    &policy,
7032                    crate::provider::default_provider()
7033                )
7034                .expect("exact complete-key limit")
7035                .is_some()
7036        );
7037        policy.resources.max_external_resource_bytes = size - 1;
7038        assert!(
7039            matches!(inventory.verification_resolver().resolve_with_policy_and_provider(Some(&info), SignatureAlgorithm::RsaSha256,
7040            &policy, crate::provider::default_provider()),
7041            Err(DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
7042                resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES, actual, ..
7043            })) if actual == size)
7044        );
7045    }
7046
7047    #[test]
7048    fn configured_x509_fallback_budgets_live_copies() {
7049        // Inventory bytes remain live while the owned fallback is assembled.
7050        // Deny before cloning even malformed CRLs; admit the exact live peak.
7051        let resources = ResourcePolicy::default();
7052        let certificate = single_pem_block(
7053            include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-cert.pem"),
7054            resources.max_external_resource_bytes,
7055        )
7056        .expect("certificate")
7057        .into_contents();
7058        for trusted in [false, true] {
7059            for with_crl in [false, true] {
7060                let mut inventory = KeyInventory::default();
7061                inventory
7062                    .add_certificate_der(certificate.clone(), trusted, &resources)
7063                    .expect("certificate imports");
7064                let document_crl = vec![0; 16];
7065                if with_crl {
7066                    inventory.crls.push(vec![0; 32]);
7067                }
7068                let info = KeyInfo {
7069                    sources: vec![KeyInfoSource::X509Data(X509DataInfo {
7070                        subject_names: vec!["CN=absent".into()],
7071                        crls: if with_crl {
7072                            vec![document_crl]
7073                        } else {
7074                            Vec::new()
7075                        },
7076                        ..X509DataInfo::default()
7077                    })],
7078                };
7079                let mut policy = crate::policy::VerificationPolicy::default();
7080                policy.key_trust.verify_x509_chains = true;
7081                policy.key_trust.check_crls = with_crl;
7082                let configured = certificate.len() + if with_crl { 32 } else { 0 };
7083                // Document evidence is not copied into fallback for an unnamed
7084                // source; the default resolver checks it with configured bytes.
7085                let peak = 2 * configured;
7086                policy.resources.max_external_resource_total_bytes = peak - 1;
7087                assert!(
7088                    matches!(
7089                        inventory
7090                            .verification_resolver()
7091                            .resolve_with_policy_and_provider(
7092                                Some(&info),
7093                                SignatureAlgorithm::RsaSha256,
7094                                &policy,
7095                                crate::provider::default_provider(),
7096                            ),
7097                        Err(DsigError::Policy(
7098                            crate::policy::PolicyViolation::ResourceLimit {
7099                                resource:
7100                                    crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
7101                                ..
7102                            }
7103                        ))
7104                    ),
7105                    "each owned fallback copy must fit before allocation"
7106                );
7107                policy.resources.max_external_resource_total_bytes = peak;
7108                let resolver = inventory.verification_resolver();
7109                let outcome = resolver.resolve_with_policy_and_provider(
7110                    Some(&info),
7111                    SignatureAlgorithm::RsaSha256,
7112                    &policy,
7113                    crate::provider::default_provider(),
7114                );
7115                assert!(
7116                    !matches!(outcome, Err(DsigError::Policy(_))),
7117                    "exact copy budget passes preflight"
7118                );
7119            }
7120        }
7121    }
7122
7123    #[test]
7124    fn named_x509_fallback_budgets_document_crl_copies() {
7125        // A named certificate substitutes document keys, not revocation
7126        // evidence. Both retained document CRLs and fallback copies coexist.
7127        let resources = ResourcePolicy::default();
7128        let certificate = single_pem_block(
7129            include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-cert.pem"),
7130            resources.max_external_resource_bytes,
7131        )
7132        .expect("certificate")
7133        .into_contents();
7134        let mut inventory = KeyInventory::default();
7135        inventory
7136            .add_public_der("leaf".into(), certificate.clone(), &resources)
7137            .expect("named certificate");
7138        let info = KeyInfo {
7139            sources: vec![
7140                KeyInfoSource::KeyName("leaf".into()),
7141                KeyInfoSource::X509Data(X509DataInfo {
7142                    crls: vec![vec![0; 32]],
7143                    ..X509DataInfo::default()
7144                }),
7145            ],
7146        };
7147        let mut policy = crate::policy::VerificationPolicy::default();
7148        policy.key_trust.verify_x509_chains = true;
7149        policy.key_trust.check_crls = true;
7150        policy.resources.max_external_resource_total_bytes = certificate.len() + 64 - 1;
7151        assert!(matches!(
7152            inventory
7153                .verification_resolver()
7154                .resolve_with_policy_and_provider(
7155                    Some(&info),
7156                    SignatureAlgorithm::RsaSha256,
7157                    &policy,
7158                    crate::provider::default_provider(),
7159                ),
7160            Err(DsigError::Policy(
7161                crate::policy::PolicyViolation::ResourceLimit {
7162                    resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
7163                    ..
7164                }
7165            ))
7166        ));
7167    }
7168
7169    #[test]
7170    fn selected_certificate_and_anchors_share_aggregate_budget() {
7171        // Selected named material and configured trust material are one
7172        // operation, even though they enter the resolver through separate paths.
7173        let certificate = single_pem_block(
7174            include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-cert.pem"),
7175            ResourcePolicy::default().max_external_resource_bytes,
7176        )
7177        .expect("leaf PEM")
7178        .into_contents();
7179        let anchor = single_pem_block(
7180            include_bytes!("../tests/fixtures/keys/rsa/rsa-4096-cert.pem"),
7181            ResourcePolicy::default().max_external_resource_bytes,
7182        )
7183        .expect("anchor PEM")
7184        .into_contents();
7185        let mut inventory = KeyInventory::default();
7186        inventory
7187            .add_public_der(
7188                "leaf".into(),
7189                certificate.clone(),
7190                &ResourcePolicy::default(),
7191            )
7192            .expect("leaf imports");
7193        inventory
7194            .add_certificate_der(anchor.clone(), true, &ResourcePolicy::default())
7195            .expect("anchor imports");
7196        let mut policy = crate::policy::VerificationPolicy::default();
7197        policy.key_trust.verify_x509_chains = true;
7198        policy.resources.max_external_resource_total_bytes = certificate.len() + anchor.len() - 1;
7199        let info = KeyInfo {
7200            sources: vec![KeyInfoSource::KeyName("leaf".into())],
7201        };
7202        let error = inventory
7203            .verification_resolver()
7204            .resolve_with_policy_and_provider(
7205                Some(&info),
7206                SignatureAlgorithm::RsaSha256,
7207                &policy,
7208                crate::provider::default_provider(),
7209            )
7210            .err()
7211            .expect("combined selected and configured material exceeds the budget");
7212        assert!(matches!(
7213            error,
7214            DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
7215                resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
7216                ..
7217            })
7218        ));
7219    }
7220
7221    #[test]
7222    fn named_key_does_not_bypass_other_source_permissions() {
7223        // Every source in a document KeyInfo is subject to the policy, even
7224        // when the inventory can resolve its KeyName without the other source.
7225        let resources = ResourcePolicy::default();
7226        let mut inventory = KeyInventory::default();
7227        inventory
7228            .add_symmetric(
7229                "named".into(),
7230                SymmetricKeyKind::Hmac,
7231                b"verification-secret".to_vec(),
7232                KeyUsages::VERIFY,
7233                &resources,
7234            )
7235            .expect("HMAC fixture imports");
7236        let info = KeyInfo {
7237            sources: vec![
7238                KeyInfoSource::KeyName("named".into()),
7239                KeyInfoSource::KeyValue(KeyValueInfo::Rsa {
7240                    modulus: vec![3],
7241                    exponent: vec![3],
7242                }),
7243            ],
7244            ..KeyInfo::default()
7245        };
7246        let mut policy = crate::policy::VerificationPolicy::default();
7247        policy.key_sources.key_value = false;
7248        assert!(
7249            inventory
7250                .verification_resolver()
7251                .resolve_with_policy_and_provider(
7252                    Some(&info),
7253                    SignatureAlgorithm::HmacSha256,
7254                    &policy,
7255                    crate::provider::default_provider(),
7256                )
7257                .is_err()
7258        );
7259    }
7260
7261    #[test]
7262    fn named_public_key_is_trusted_inventory_material() {
7263        // A document KeyName must not inherit the source restrictions of the
7264        // caller-owned public key's internal representation.
7265        let mut inventory = KeyInventory::default();
7266        inventory
7267            .add_public_pem(
7268                "named".into(),
7269                include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"),
7270                &ResourcePolicy::default(),
7271            )
7272            .expect("public key imports");
7273        let info = KeyInfo {
7274            sources: vec![KeyInfoSource::KeyName("named".into())],
7275            ..KeyInfo::default()
7276        };
7277        let mut policy = crate::policy::VerificationPolicy::default();
7278        policy.key_sources.key_value = false;
7279        policy.key_sources.der_encoded_key_value = false;
7280        policy.key_sources.x509_data = false;
7281        assert!(
7282            inventory
7283                .verification_resolver()
7284                .resolve_with_policy_and_provider(
7285                    Some(&info),
7286                    SignatureAlgorithm::RsaSha256,
7287                    &policy,
7288                    crate::provider::default_provider(),
7289                )
7290                .expect("trusted inventory material resolves")
7291                .is_some()
7292        );
7293    }
7294
7295    #[test]
7296    fn named_public_key_obeys_current_verification_limits() {
7297        // A permissive import policy cannot replace a later stricter operation snapshot.
7298        let mut inventory = KeyInventory::default();
7299        inventory
7300            .add_public_pem(
7301                "named".into(),
7302                include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"),
7303                &ResourcePolicy::default(),
7304            )
7305            .expect("public key imports");
7306        let info = KeyInfo {
7307            sources: vec![KeyInfoSource::KeyName("named".into())],
7308        };
7309        let mut policy = crate::policy::VerificationPolicy::default();
7310        policy.resources.max_external_resource_bytes = 1;
7311        assert!(
7312            inventory
7313                .verification_resolver()
7314                .resolve_with_policy(Some(&info), SignatureAlgorithm::RsaSha256, &policy)
7315                .is_err()
7316        );
7317        let certificate = pem::parse(include_bytes!(
7318            "../tests/fixtures/keys/rsa/rsa-2048-cert.pem"
7319        ))
7320        .expect("certificate PEM")
7321        .into_contents();
7322        let mut certificate_inventory = KeyInventory::default();
7323        certificate_inventory
7324            .add_public_der("named".into(), certificate, &ResourcePolicy::default())
7325            .expect("certificate imports as a named public key");
7326        assert!(
7327            certificate_inventory
7328                .verification_resolver()
7329                .resolve_with_policy(Some(&info), SignatureAlgorithm::RsaSha256, &policy)
7330                .is_err()
7331        );
7332    }
7333
7334    #[test]
7335    fn unused_certificates_do_not_block_earlier_public_keys() {
7336        // X.509 lookup bytes are irrelevant when a preceding DER key resolves.
7337        let mut inventory = KeyInventory::default();
7338        let certificate = pem::parse(include_bytes!(
7339            "../tests/fixtures/keys/rsa/rsa-2048-cert.pem"
7340        ))
7341        .expect("certificate PEM")
7342        .into_contents();
7343        inventory
7344            .add_certificate_der(certificate, false, &ResourcePolicy::default())
7345            .expect("certificate imports");
7346        let public = pem::parse(include_bytes!(
7347            "../tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"
7348        ))
7349        .expect("public PEM")
7350        .into_contents();
7351        let info = KeyInfo {
7352            sources: vec![
7353                KeyInfoSource::DerEncodedKeyValue(public.clone()),
7354                KeyInfoSource::X509Data(X509DataInfo {
7355                    subject_names: vec!["unused".into()],
7356                    ..X509DataInfo::default()
7357                }),
7358            ],
7359        };
7360        let mut policy = crate::policy::VerificationPolicy::default();
7361        policy.resources.max_external_resource_bytes = public.len();
7362        assert!(
7363            inventory
7364                .verification_resolver()
7365                .resolve_with_policy(Some(&info), SignatureAlgorithm::RsaSha256, &policy)
7366                .expect("earlier public key resolves")
7367                .is_some()
7368        );
7369    }
7370
7371    #[test]
7372    fn public_import_rejects_incompatible_usage() {
7373        // Public material may verify or encrypt, but cannot authorize signing.
7374        let resources = ResourcePolicy::default();
7375        let public = include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-pubkey.pem");
7376        let mut inventory = KeyInventory::default();
7377        assert!(
7378            inventory
7379                .add_public_pem_with_usages("invalid".into(), public, KeyUsages::SIGN, &resources,)
7380                .is_err()
7381        );
7382        assert_eq!(inventory.entry_count(), 0);
7383    }
7384
7385    #[test]
7386    fn public_certificate_import_accepts_supported_ed25519_key() {
7387        // Ed25519 can now acquire verification usage, but importing a
7388        // certificate still does not promote it into the trusted anchor set.
7389        let pair =
7390            rcgen::KeyPair::generate_for(&rcgen::PKCS_ED25519).expect("Ed25519 key generation");
7391        let params = rcgen::CertificateParams::new(vec!["example.test".into()])
7392            .expect("certificate parameters");
7393        let certificate = params.self_signed(&pair).expect("certificate generation");
7394        let mut inventory = KeyInventory::default();
7395        assert!(
7396            inventory
7397                .add_public_der(
7398                    "ed25519".into(),
7399                    certificate.der().to_vec(),
7400                    &ResourcePolicy::default()
7401                )
7402                .is_ok()
7403        );
7404        assert!(inventory.trusted_certificates.is_empty());
7405    }
7406
7407    #[cfg(feature = "xmlenc")]
7408    #[test]
7409    fn imported_usage_restricts_operation_selection() {
7410        // Explicit restrictions survive import and are enforced when selecting
7411        // material for the opposite operation.
7412        let resources = ResourcePolicy::default();
7413        let public = include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-pubkey.pem");
7414        let bundle = include_bytes!("../tests/fixtures/xmlenc/01-phaos-xmlenc-3/rsa-priv-key.p12");
7415        let mut inventory = KeyInventory::default();
7416        inventory
7417            .add_public_pem_with_usages("verify".into(), public, KeyUsages::VERIFY, &resources)
7418            .expect("verification-only public key imports");
7419        assert!(
7420            inventory
7421                .rsa_encryption_key("verify", &crate::policy::EncryptionPolicy::default())
7422                .is_err()
7423        );
7424        inventory
7425            .add_pkcs12_with_usages(
7426                "decrypt".into(),
7427                bundle,
7428                "secret",
7429                KeyUsages::DECRYPT,
7430                &resources,
7431            )
7432            .expect("decryption-only private key imports");
7433        assert!(
7434            inventory
7435                .signing_key(
7436                    "decrypt",
7437                    SignatureAlgorithm::RsaSha256,
7438                    &crate::policy::SigningPolicy::default(),
7439                )
7440                .is_err()
7441        );
7442        assert!(
7443            inventory
7444                .decryption_resolver("decrypt", &crate::policy::DecryptionPolicy::default())
7445                .is_ok()
7446        );
7447    }
7448
7449    #[cfg(feature = "xmlenc")]
7450    #[test]
7451    fn selected_weak_rsa_decryptor_obeys_default_policy() {
7452        // Importing a legacy key grants no exemption from operation minima.
7453        let mut inventory = KeyInventory::default();
7454        let weak = RsaPrivateKey::new(&mut ChaCha8Rng::seed_from_u64(0xA11C_E502), 1024)
7455            .expect("legacy key generation")
7456            .to_pkcs8_der()
7457            .expect("legacy key encoding");
7458        inventory
7459            .add_private_der(
7460                "weak".into(),
7461                weak.as_bytes(),
7462                None,
7463                KeyUsages::DECRYPT,
7464                &ResourcePolicy::default(),
7465            )
7466            .expect("legacy import");
7467        let error = inventory
7468            .decryption_resolver("weak", &crate::policy::DecryptionPolicy::default())
7469            .err()
7470            .expect("weak key is rejected");
7471        assert!(
7472            matches!(
7473                error,
7474                KeyStoreError::Policy(crate::policy::PolicyViolation::KeySize {
7475                    operation: "decryption",
7476                    ..
7477                })
7478            ),
7479            "{error:?}"
7480        );
7481        // A caller can deliberately authorize legacy input, but the snapshot
7482        // must survive inventory selection and the complete recovery pipeline.
7483        let key = RsaPrivateKey::from_pkcs8_der(weak.as_bytes()).expect("legacy decode");
7484        let mut encryption = crate::policy::EncryptionPolicy::default();
7485        encryption.rsa_keys.minimum_modulus_bits = 1024;
7486        let encrypted = crate::xmlenc::EncryptedDataBuilder::new(
7487            crate::xmlenc::DataEncryptionAlgorithm::Aes128Gcm,
7488        )
7489        .policy(encryption)
7490        .recipient_rsa_oaep(key.to_public_key())
7491        .encrypt_binary(b"legacy consent")
7492        .expect("explicit legacy encryption");
7493        let mut decryption = crate::policy::DecryptionPolicy::default();
7494        decryption.rsa_keys.minimum_modulus_bits = 1024;
7495        let resolver = inventory
7496            .decryption_resolver("weak", &decryption)
7497            .expect("explicit legacy selection");
7498        assert_eq!(
7499            crate::xmlenc::DecryptContext::new(resolver.as_ref())
7500                .policy(decryption)
7501                .decrypt(&encrypted.encrypted_data_xml)
7502                .expect("explicit legacy recovery"),
7503            crate::xmlenc::DecryptedContent::Bytes(b"legacy consent".to_vec())
7504        );
7505        assert!(matches!(
7506            crate::xmlenc::DecryptContext::new(resolver.as_ref())
7507                .decrypt(&encrypted.encrypted_data_xml),
7508            Err(crate::xmlenc::XmlEncError::Policy(
7509                crate::policy::PolicyViolation::KeySize {
7510                    actual_bits: 1024,
7511                    ..
7512                }
7513            ))
7514        ));
7515    }
7516
7517    #[cfg(feature = "xmlenc")]
7518    #[test]
7519    fn selected_rsa_recipient_obeys_operation_modulus_policy() {
7520        // Import permission does not override a stricter encryption snapshot.
7521        let mut inventory = KeyInventory::default();
7522        inventory
7523            .add_public_pem(
7524                "rsa".into(),
7525                include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"),
7526                &ResourcePolicy::default(),
7527            )
7528            .expect("RSA fixture imports");
7529        let mut policy = crate::policy::EncryptionPolicy::default();
7530        policy.rsa_keys.minimum_modulus_bits = 4096;
7531        assert!(matches!(
7532            inventory.rsa_encryption_key("rsa", &policy),
7533            Err(KeyStoreError::Policy(
7534                crate::policy::PolicyViolation::KeySize { .. }
7535            ))
7536        ));
7537        assert!(matches!(
7538            inventory.public_keys()[0].rsa_encryption_key(&policy),
7539            Err(KeyStoreError::Policy(
7540                crate::policy::PolicyViolation::KeySize { .. }
7541            ))
7542        ));
7543    }
7544
7545    #[test]
7546    fn inventory_merge_checks_names_and_budget_atomically() {
7547        // Combining independently parsed stores cannot bypass name or
7548        // aggregate-material limits, and a rejected merge is atomic.
7549        let resources = ResourcePolicy::default();
7550        let mut first = KeyInventory::default();
7551        first
7552            .add_symmetric(
7553                "one".into(),
7554                SymmetricKeyKind::Hmac,
7555                b"first-secret".to_vec(),
7556                KeyUsages::SIGN,
7557                &resources,
7558            )
7559            .expect("first key imports");
7560        let mut duplicate = KeyInventory::default();
7561        duplicate
7562            .add_symmetric(
7563                "one".into(),
7564                SymmetricKeyKind::Hmac,
7565                b"second-secret".to_vec(),
7566                KeyUsages::VERIFY,
7567                &resources,
7568            )
7569            .expect("independent duplicate imports");
7570        assert!(matches!(
7571            first.extend(duplicate, &resources),
7572            Err(KeyStoreError::Selection("duplicate key name"))
7573        ));
7574        assert_eq!(first.entry_count(), 1);
7575        let mut second = KeyInventory::default();
7576        second
7577            .add_symmetric(
7578                "two".into(),
7579                SymmetricKeyKind::Hmac,
7580                b"second-secret".to_vec(),
7581                KeyUsages::VERIFY,
7582                &resources,
7583            )
7584            .expect("second key imports");
7585        let constrained = ResourcePolicy {
7586            max_external_resource_total_bytes: b"first-secret".len(),
7587            ..ResourcePolicy::default()
7588        };
7589        assert!(matches!(
7590            first.extend(second, &constrained),
7591            Err(KeyStoreError::Policy(
7592                crate::policy::PolicyViolation::ResourceLimitExceeded {
7593                    resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
7594                    ..
7595                }
7596            ))
7597        ));
7598        assert_eq!(first.entry_count(), 1);
7599    }
7600
7601    #[test]
7602    fn public_dsa_store_entries_require_usable_parameters() {
7603        // The inventory has no implicit parameter inheritance. Do not grant
7604        // VERIFY to material that its own resolver cannot construct as a key.
7605        for fields in [
7606            "<Y>AQ==</Y>",
7607            "<P/><Q/><G/><Y>AQ==</Y>",
7608            "<P>AQ==</P><Q>AQ==</Q><G>AQ==</G><Y>AQ==</Y>",
7609        ] {
7610            let xml = format!(
7611                "<Keys xmlns=\"{XMLSEC_NS}\"><KeyInfo xmlns=\"{XMLDSIG_NS}\"><KeyName>dsa</KeyName><KeyValue><DSAKeyValue>{fields}</DSAKeyValue></KeyValue></KeyInfo></Keys>"
7612            );
7613            assert!(
7614                KeyInventory::from_xml_bytes(
7615                    xml.as_bytes(),
7616                    &xml_policy(ResourcePolicy::default()),
7617                    XmlBackend::default()
7618                )
7619                .is_err(),
7620                "accepted unusable DSA: {fields}"
7621            );
7622        }
7623    }
7624
7625    #[test]
7626    fn oversized_private_dsa_component_stops_before_big_integer_work() {
7627        // A bounded XML file can still contain an outsized exponent; reject
7628        // it before constructing a large modular exponentiation.
7629        let oversized = base64::engine::general_purpose::STANDARD.encode(vec![1_u8; 513]);
7630        let xml = format!(
7631            "<Keys xmlns=\"{XMLSEC_NS}\"><KeyInfo xmlns=\"{XMLDSIG_NS}\"><KeyName>dsa</KeyName><KeyValue><DSAKeyValue><P>{oversized}</P><Q>AQ==</Q><G>AQ==</G><X xmlns=\"{XMLSEC_NS}\">AQ==</X><Y>AQ==</Y></DSAKeyValue></KeyValue></KeyInfo></Keys>"
7632        );
7633        assert!(matches!(
7634            KeyInventory::from_xml_bytes(
7635                xml.as_bytes(),
7636                &xml_policy(ResourcePolicy::default()),
7637                XmlBackend::default(),
7638            ),
7639            Err(KeyStoreError::Invalid(message)) if message.contains("safety limit")
7640        ));
7641    }
7642
7643    #[test]
7644    fn oversized_pkcs8_dsa_parameters_stop_before_key_derivation() {
7645        // PKCS#8 uses the same component ceiling as xmlsec's DSAKeyValue.
7646        #[derive(der::Sequence)]
7647        struct DsaParameters<'a> {
7648            p: der::asn1::UintRef<'a>,
7649            q: der::asn1::UintRef<'a>,
7650            g: der::asn1::UintRef<'a>,
7651        }
7652        let oversized = vec![1_u8; crate::hard_limits::DSA_KEY_COMPONENT_BYTE_CEILING + 1];
7653        let one = [1_u8];
7654        let parameters = der::Encode::to_der(&DsaParameters {
7655            p: der::asn1::UintRef::new(&oversized).expect("positive P"),
7656            q: der::asn1::UintRef::new(&one).expect("positive Q"),
7657            g: der::asn1::UintRef::new(&one).expect("positive G"),
7658        })
7659        .expect("DSA parameters encode");
7660        let x = der::Encode::to_der(&der::asn1::UintRef::new(&one).expect("positive X"))
7661            .expect("DSA X encodes");
7662        let algorithm = pkcs8::AlgorithmIdentifierRef {
7663            oid: dsa::OID,
7664            parameters: Some(der::asn1::AnyRef::from_der(&parameters).expect("parameters")),
7665        };
7666        let private = der::asn1::OctetStringRef::new(&x).expect("private octets");
7667        let pkcs8 = der::Encode::to_der(&PrivateKeyInfoRef::new(algorithm, private))
7668            .expect("PKCS#8 encodes");
7669        let mut inventory = KeyInventory::default();
7670        let error = inventory
7671            .add_private_der(
7672                "oversized-dsa".into(),
7673                &pkcs8,
7674                None,
7675                KeyUsages::SIGN,
7676                &ResourcePolicy::default(),
7677            )
7678            .expect_err("oversized DSA parameter must fail preflight");
7679        assert!(
7680            matches!(
7681                error,
7682                KeyStoreError::Selection("invalid or oversized DSA private key")
7683            ),
7684            "{error}"
7685        );
7686    }
7687
7688    #[test]
7689    fn compressed_ec_spki_cannot_acquire_verify_usage() {
7690        // Import must enforce the same SEC1 profile as signature verification,
7691        // for every supported curve, rather than grant unusable VERIFY usage.
7692        for pem in [
7693            include_bytes!("../tests/fixtures/keys/ec/ec-prime256v1-pubkey.pem").as_slice(),
7694            include_bytes!("../tests/fixtures/keys/ec/ec-prime384v1-pubkey.pem").as_slice(),
7695            include_bytes!("../tests/fixtures/keys/ec/ec-prime521v1-pubkey.pem").as_slice(),
7696        ] {
7697            let original =
7698                single_pem_block(pem, ResourcePolicy::default().max_external_resource_bytes)
7699                    .expect("EC fixture")
7700                    .into_contents();
7701            let spki = pkcs8::SubjectPublicKeyInfoRef::from_der(&original).expect("SPKI");
7702            let point = spki
7703                .subject_public_key
7704                .as_bytes()
7705                .expect("octet-aligned point");
7706            let coordinate_len = (point.len() - 1) / 2;
7707            let mut compressed = vec![2 | (point.last().expect("Y coordinate") & 1)];
7708            compressed.extend_from_slice(&point[1..=coordinate_len]);
7709            let encoded = der::Encode::to_der(&pkcs8::SubjectPublicKeyInfoRef {
7710                algorithm: spki.algorithm,
7711                subject_public_key: der::asn1::BitStringRef::from_bytes(&compressed)
7712                    .expect("point"),
7713            })
7714            .expect("compressed SPKI");
7715            let mut inventory = KeyInventory::default();
7716            assert!(
7717                inventory
7718                    .add_public_der("compressed".into(), encoded, &ResourcePolicy::default())
7719                    .is_err()
7720            );
7721            assert!(inventory.public_keys().is_empty());
7722            inventory
7723                .add_public_der("uncompressed".into(), original, &ResourcePolicy::default())
7724                .expect("supported uncompressed encoding remains usable");
7725        }
7726        // A genuinely signed certificate wrapper must not bypass this profile.
7727        struct CompressedPoint<'a>(&'a [u8], &'static rcgen::SignatureAlgorithm);
7728        impl rcgen::PublicKeyData for CompressedPoint<'_> {
7729            fn der_bytes(&self) -> &[u8] {
7730                self.0
7731            }
7732            fn algorithm(&self) -> &'static rcgen::SignatureAlgorithm {
7733                self.1
7734            }
7735        }
7736        let mut root_params = rcgen::CertificateParams::new(Vec::new()).expect("root params");
7737        root_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
7738        let root = rcgen::CertifiedIssuer::self_signed(
7739            root_params,
7740            rcgen::KeyPair::generate().expect("root key"),
7741        )
7742        .expect("root certificate");
7743        for (pem, algorithm) in [
7744            (
7745                include_bytes!("../tests/fixtures/keys/ec/ec-prime256v1-cert.pem").as_slice(),
7746                &rcgen::PKCS_ECDSA_P256_SHA256,
7747            ),
7748            (
7749                include_bytes!("../tests/fixtures/keys/ec/ec-prime384v1-cert.pem").as_slice(),
7750                &rcgen::PKCS_ECDSA_P384_SHA384,
7751            ),
7752        ] {
7753            let original =
7754                single_pem_block(pem, ResourcePolicy::default().max_external_resource_bytes)
7755                    .expect("certificate fixture")
7756                    .into_contents();
7757            let (_, certificate) = X509Certificate::from_der(&original).expect("certificate");
7758            let point = certificate.public_key().subject_public_key.data.as_ref();
7759            let coordinate_len = (point.len() - 1) / 2;
7760            let mut compressed = vec![2 | (point.last().expect("Y coordinate") & 1)];
7761            compressed.extend_from_slice(&point[1..=coordinate_len]);
7762            let leaf = rcgen::CertificateParams::new(Vec::new())
7763                .expect("leaf params")
7764                .signed_by(&CompressedPoint(&compressed, algorithm), &root)
7765                .expect("signed compressed certificate");
7766            let encoded = leaf.der().to_vec();
7767            let mut inventory = KeyInventory::default();
7768            assert!(
7769                inventory
7770                    .add_public_der(
7771                        "compressed-cert".into(),
7772                        encoded,
7773                        &ResourcePolicy::default()
7774                    )
7775                    .is_err()
7776            );
7777            assert!(inventory.public_keys().is_empty());
7778            inventory
7779                .add_public_der(
7780                    "uncompressed-cert".into(),
7781                    original,
7782                    &ResourcePolicy::default(),
7783                )
7784                .expect("uncompressed certificate imports");
7785        }
7786    }
7787
7788    #[test]
7789    fn malformed_ec_point_cannot_acquire_verify_usage() {
7790        // A supported curve OID does not make an off-curve point usable.
7791        let block = single_pem_block(
7792            include_bytes!("../tests/fixtures/keys/ec/ec-prime256v1-pubkey.pem"),
7793            ResourcePolicy::default().max_external_resource_bytes,
7794        )
7795        .expect("EC SPKI fixture");
7796        let mut der = block.into_contents();
7797        let last = der.last_mut().expect("point bytes");
7798        *last ^= 1;
7799        let mut inventory = KeyInventory::default();
7800        assert!(
7801            inventory
7802                .add_public_der("off-curve".into(), der, &ResourcePolicy::default())
7803                .is_err()
7804        );
7805        assert!(inventory.public_keys().is_empty());
7806    }
7807
7808    #[cfg(feature = "xmlenc")]
7809    #[test]
7810    fn decryption_policy_rejection_retains_its_type() {
7811        // An invalid operation snapshot is not a candidate-local key miss.
7812        let mut policy = crate::policy::DecryptionPolicy::default();
7813        policy.resources.max_external_resource_bytes = usize::MAX;
7814        assert!(matches!(
7815            KeyInventory::default().decryption_resolver("missing", &policy),
7816            Err(KeyStoreError::Policy(_))
7817        ));
7818    }
7819
7820    #[cfg(feature = "xmlenc")]
7821    #[test]
7822    fn inventory_decryption_resolver_enforces_aes_usage_end_to_end() {
7823        // Inventory authorization is checked before the normal XMLEnc
7824        // decryptor receives an otherwise valid direct AES content key.
7825        use crate::xmlenc::{
7826            DataEncryptionAlgorithm, DecryptContext, DecryptedContent, EncryptedDataBuilder,
7827        };
7828
7829        let key = b"0123456789abcdef";
7830        let encrypted = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
7831            .direct_key(*key)
7832            .encrypt_binary(b"inventory decrypt payload")
7833            .expect("AES fixture encrypts");
7834        let resources = ResourcePolicy::default();
7835        let mut keys = KeyInventory::default();
7836        keys.add_symmetric(
7837            "decrypt".into(),
7838            SymmetricKeyKind::Aes,
7839            key.to_vec(),
7840            KeyUsages::DECRYPT,
7841            &resources,
7842        )
7843        .expect("decrypt key imports");
7844        let resolver = keys
7845            .decryption_resolver("decrypt", &crate::policy::DecryptionPolicy::default())
7846            .expect("decrypt use is allowed");
7847        let content = DecryptContext::new(resolver.as_ref())
7848            .decrypt(&encrypted.encrypted_data_xml)
7849            .expect("inventory key decrypts");
7850        assert!(
7851            matches!(content, DecryptedContent::Bytes(bytes) if bytes == b"inventory decrypt payload")
7852        );
7853
7854        let mut restricted = KeyInventory::default();
7855        restricted
7856            .add_symmetric(
7857                "encrypt-only".into(),
7858                SymmetricKeyKind::Aes,
7859                key.to_vec(),
7860                KeyUsages::ENCRYPT,
7861                &resources,
7862            )
7863            .expect("encrypt-only key imports");
7864        assert!(
7865            restricted
7866                .decryption_resolver("encrypt-only", &crate::policy::DecryptionPolicy::default())
7867                .is_err()
7868        );
7869    }
7870
7871    #[cfg(feature = "xmlenc")]
7872    #[test]
7873    fn inventory_direct_aes_rechecks_execution_policy() {
7874        // Selection does not freeze resource permission: reject a tighter
7875        // execution snapshot before copying bytes or consuming a candidate.
7876        use crate::xmlenc::{DataEncryptionAlgorithm, KeyCandidateBudget, XmlEncError};
7877        let mut inventory = KeyInventory::default();
7878        inventory
7879            .add_symmetric(
7880                "aes".into(),
7881                SymmetricKeyKind::Aes,
7882                vec![1; 16],
7883                KeyUsages::DECRYPT,
7884                &ResourcePolicy::default(),
7885            )
7886            .expect("AES imports");
7887        let resolver = inventory
7888            .decryption_resolver("aes", &crate::policy::DecryptionPolicy::default())
7889            .expect("AES resolver");
7890        let mut policy = crate::policy::DecryptionPolicy::default();
7891        policy.resources.max_external_resource_bytes = 15;
7892        let mut budget = KeyCandidateBudget::with_limit(1);
7893        assert!(matches!(
7894            resolver.resolve_key_candidates_with_policy(
7895                &crate::provider::RustCryptoProvider,
7896                DataEncryptionAlgorithm::Aes128Gcm,
7897                None,
7898                &policy,
7899                &mut budget
7900            ),
7901            Err(XmlEncError::Policy(_))
7902        ));
7903        assert_eq!(budget.remaining(), 1);
7904        policy.resources.max_external_resource_bytes = 16;
7905        assert_eq!(
7906            resolver
7907                .resolve_key_candidates_with_policy(
7908                    &crate::provider::RustCryptoProvider,
7909                    DataEncryptionAlgorithm::Aes128Gcm,
7910                    None,
7911                    &policy,
7912                    &mut budget
7913                )
7914                .expect("exact execution allowance"),
7915            vec![vec![1; 16]]
7916        );
7917        assert_eq!(budget.remaining(), 0);
7918    }
7919
7920    #[cfg(feature = "xmlenc")]
7921    #[test]
7922    fn inventory_direct_aes_does_not_consume_recipient_candidates() {
7923        // A direct AES key is not a wrapping key. Recipient traversal must
7924        // leave its sole candidate available for the later direct-key path.
7925        use crate::xmlenc::{
7926            CipherData, DataEncryptionAlgorithm, EncryptedKey, EncryptionMethod,
7927            KeyCandidateBudget, KeyTransportAlgorithm, XmlEncError,
7928        };
7929        let mut keys = KeyInventory::default();
7930        keys.add_symmetric(
7931            "direct".into(),
7932            SymmetricKeyKind::Aes,
7933            vec![1; 16],
7934            KeyUsages::DECRYPT,
7935            &ResourcePolicy::default(),
7936        )
7937        .expect("AES imports");
7938        let resolver = keys
7939            .decryption_resolver("direct", &crate::policy::DecryptionPolicy::default())
7940            .expect("AES resolver");
7941        let recipient = EncryptedKey {
7942            sources: Default::default(),
7943            id: None,
7944            recipient: None,
7945            key_name: None,
7946            encryption_method: EncryptionMethod {
7947                algorithm: KeyTransportAlgorithm::RsaOaep11.uri().into(),
7948                key_size_bits: None,
7949                oaep_digest: None,
7950                mgf_algorithm: None,
7951                oaep_params: None,
7952            },
7953            cipher_data: CipherData::Value {
7954                value: String::new(),
7955            },
7956            reference_list: None,
7957            carried_key_name: None,
7958        };
7959        let mut budget = KeyCandidateBudget::with_limit(1);
7960        let provider = crate::provider::RustCryptoProvider;
7961        for _ in 0..64 {
7962            assert!(matches!(
7963                resolver.resolve_key_candidates(
7964                    &provider,
7965                    DataEncryptionAlgorithm::Aes128Gcm,
7966                    Some(&recipient),
7967                    &mut budget
7968                ),
7969                Err(XmlEncError::KeyNotFound)
7970            ));
7971            assert_eq!(budget.remaining(), 1);
7972        }
7973        assert_eq!(
7974            resolver
7975                .resolve_key_candidates(
7976                    &provider,
7977                    DataEncryptionAlgorithm::Aes128Gcm,
7978                    None,
7979                    &mut budget
7980                )
7981                .expect("one direct candidate"),
7982            vec![vec![1; 16]]
7983        );
7984        assert_eq!(budget.remaining(), 0);
7985    }
7986
7987    #[cfg(feature = "xmlenc")]
7988    #[test]
7989    fn decryption_selection_checks_operation_limits_before_material_use() {
7990        // Reusing a broadly imported inventory with a tighter operation
7991        // snapshot must reject both AES and RSA material before copy/decode.
7992        let resources = ResourcePolicy::default();
7993        let mut keys = KeyInventory::default();
7994        keys.add_symmetric(
7995            "aes".into(),
7996            SymmetricKeyKind::Aes,
7997            vec![0x31; 16],
7998            KeyUsages::DECRYPT,
7999            &resources,
8000        )
8001        .expect("AES key imports");
8002        keys.add_private_pem(
8003            "rsa".into(),
8004            include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-key.pem"),
8005            None,
8006            KeyUsages::DECRYPT,
8007            &resources,
8008        )
8009        .expect("RSA key imports");
8010        let mut policy = crate::policy::DecryptionPolicy::default();
8011        policy.resources.max_external_resource_bytes = 8;
8012        policy.resources.max_external_resource_total_bytes = 8;
8013        assert!(keys.decryption_resolver("aes", &policy).is_err());
8014        assert!(keys.decryption_resolver("rsa", &policy).is_err());
8015    }
8016}