Skip to main content

xml_sec/
key_manager.rs

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