Skip to main content

xml_sec/
key_manager.rs

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