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