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