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