use std::collections::HashSet;
#[cfg(test)]
use crate::xmldsig::DsaSigningKey;
#[cfg(test)]
use base64::Engine as _;
use crypto_bigint::{
BoxedUint,
modular::{BoxedMontyForm, BoxedMontyParams},
};
use der::Decode as _;
use dsa::{
Components as DsaComponents, SigningKey as NativeDsaSigningKey,
VerifyingKey as DsaVerifyingKey, pkcs8::EncodePrivateKey as _,
};
mod pkcs12_import;
use crate::rsa_encoding::RsaPrivateKeyEncoding as _;
#[cfg(feature = "xmlenc")]
use crate::rsa_encoding::RsaPublicKeyEncoding as _;
use pkcs8::{DecodePrivateKey as _, EncryptedPrivateKeyInfoRef, PrivateKeyInfoRef};
use pkcs12_import::Limits as Pkcs12Limits;
use rsa::RsaPrivateKey;
#[cfg(feature = "xmlenc")]
use rsa::RsaPublicKey;
#[cfg(test)]
use x509_parser::prelude::FromDer as _;
#[cfg(test)]
use x509_parser::prelude::X509Certificate;
use zeroize::Zeroizing;
#[cfg(feature = "xmlenc")]
use crate::xmldsig::parse::X509PublicKeyInfo;
use crate::{
XmlBackend, XmlDomNode as Node,
document::{
DocumentParseSettings, XmlParseWorkBudget, parse_borrowed_with_settings_and_budget,
},
policy::ResourcePolicy,
xmldsig::keys::InspectedKeyCandidateBudget,
xmldsig::parse::XMLDSIG11_NS,
xmldsig::{
DefaultKeyResolver, DsigError, HmacSigningKey, HmacVerificationKey, KeyInfo, KeyInfoSource,
KeyResolver, KeyResolverConfig, KeyValueInfo, SignatureAlgorithm, SigningKey, VerifyingKey,
X509DataInfo, validate_signing_key,
},
};
const XMLSEC_NS: &str = "http://www.aleksey.com/xmlsec/2002";
const XMLDSIG_NS: &str = "http://www.w3.org/2000/09/xmldsig#";
enum PublicDerFormat {
KeyOrCertificate,
SubjectPublicKeyInfo,
}
fn check_selected_public_material(
info: &KeyInfo,
resources: &ResourcePolicy,
) -> Result<usize, DsigError> {
let mut total = 0_usize;
for source in &info.sources {
let mut charge = |length: usize| -> Result<(), DsigError> {
if length > resources.max_external_resource_bytes {
return Err(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
maximum: resources.max_external_resource_bytes,
actual: length,
}
.into());
}
total = total.checked_add(length).ok_or({
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
maximum: resources.max_external_resource_total_bytes,
actual: usize::MAX,
}
})?;
if total > resources.max_external_resource_total_bytes {
return Err(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
maximum: resources.max_external_resource_total_bytes,
actual: total,
}
.into());
}
Ok(())
};
match source {
KeyInfoSource::KeyValue(value) => {
let lengths = match value {
KeyValueInfo::Rsa { modulus, exponent } => {
[modulus.len(), exponent.len(), 0, 0, 0, 0]
}
KeyValueInfo::Dsa { p, q, g, y } => [
p.as_ref().map_or(0, Vec::len),
q.as_ref().map_or(0, Vec::len),
g.as_ref().map_or(0, Vec::len),
y.len(),
0,
0,
],
KeyValueInfo::Dh {
p,
q,
generator,
public,
seed,
pgen_counter,
} => [
p.as_ref().map_or(0, Vec::len),
q.as_ref().map_or(0, Vec::len),
generator.as_ref().map_or(0, Vec::len),
public.len(),
seed.as_ref().map_or(0, Vec::len),
pgen_counter.as_ref().map_or(0, Vec::len),
],
KeyValueInfo::Ec {
curve_oid,
public_key,
} => [curve_oid.len(), public_key.len(), 0, 0, 0, 0],
KeyValueInfo::InvalidEcKeyValue | KeyValueInfo::Unsupported { .. } => continue,
};
let length = lengths.into_iter().try_fold(0_usize, |sum, length| {
sum.checked_add(length)
.ok_or(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
maximum: resources.max_external_resource_bytes,
actual: usize::MAX,
})
})?;
charge(length)?;
}
KeyInfoSource::DerEncodedKeyValue(bytes) => charge(bytes.len())?,
KeyInfoSource::X509Data(data) => {
for certificate in &data.certificates {
charge(certificate.len())?;
}
for crl in &data.crls {
charge(crl.len())?;
}
}
_ => {}
}
}
Ok(total)
}
pub struct StoredSymmetricKey {
pub name: String,
pub kind: SymmetricKeyKind,
pub bytes: Zeroizing<Vec<u8>>,
pub usages: KeyUsages,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SymmetricKeyKind {
Hmac,
Aes,
Camellia,
ChaCha20,
Des,
}
impl SymmetricKeyKind {
pub fn is_encryption_key(self) -> bool {
matches!(self, Self::Aes | Self::Camellia | Self::ChaCha20)
|| (cfg!(feature = "legacy-algorithms") && self == Self::Des)
}
#[cfg(feature = "xmlenc")]
fn accepts_algorithm(self, algorithm: crate::xmlenc::DataEncryptionAlgorithm) -> bool {
self == algorithm.key_kind()
}
}
fn des_key_usages(bytes: &[u8]) -> Result<KeyUsages, KeyStoreError> {
if cfg!(feature = "legacy-algorithms") && bytes.len() == 24 {
Ok(KeyUsages::ENCRYPT.union(KeyUsages::DECRYPT))
} else {
Err(KeyStoreError::Selection(
"unsupported DES key length or mechanism",
))
}
}
#[derive(Default)]
pub struct KeyInventory {
entry_count: usize,
symmetric_keys: Vec<StoredSymmetricKey>,
public_keys: Vec<StoredPublicKey>,
private_keys: Vec<StoredPrivateKey>,
lookup_certificates: Vec<Vec<u8>>,
trusted_certificates: Vec<Vec<u8>>,
crls: Vec<Vec<u8>>,
material_bytes: usize,
kdf_work: usize,
}
pub struct XmlKeyStoreImporter<'a, P: crate::document::XmlDocumentPolicy> {
policy: &'a P,
backend: XmlBackend,
parse_work: XmlParseWorkBudget,
inspected: usize,
inventory: KeyInventory,
live_material_bytes: usize,
}
impl<'a, P: crate::document::XmlDocumentPolicy> XmlKeyStoreImporter<'a, P> {
pub fn new(policy: &'a P, backend: XmlBackend) -> Result<Self, KeyStoreError> {
Self::with_live_material(policy, backend, 0)
}
pub fn with_live_material(
policy: &'a P,
backend: XmlBackend,
live_material_bytes: usize,
) -> Result<Self, KeyStoreError> {
ensure_resource_policy(policy.resource_policy())?;
if live_material_bytes > policy.resource_policy().max_external_resource_total_bytes {
return Err(import_resource_limit(
crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
policy.resource_policy().max_external_resource_total_bytes,
));
}
Ok(Self {
policy,
backend,
parse_work: XmlParseWorkBudget::from_resources(policy.resource_policy()),
inspected: 0,
inventory: KeyInventory::default(),
live_material_bytes,
})
}
pub fn import(&mut self, bytes: &[u8]) -> Result<(), KeyStoreError> {
let store = KeyInventory::from_xml_bytes_with_budget(
bytes,
self.policy,
self.backend,
&self.parse_work,
&mut self.inspected,
self.inventory
.material_bytes
.checked_add(self.live_material_bytes)
.ok_or_else(|| {
import_resource_limit(
crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
self.policy
.resource_policy()
.max_external_resource_total_bytes,
)
})?,
)?;
self.inventory.extend(store, self.policy.resource_policy())
}
#[must_use]
pub fn finish(self) -> KeyInventory {
self.inventory
}
}
#[derive(Default)]
pub struct SigningLookupBudget {
inspected: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum KeyUsage {
Sign,
Verify,
Encrypt,
Decrypt,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct KeyUsages(u8);
impl KeyUsages {
pub const SIGN: Self = Self(1);
pub const VERIFY: Self = Self(2);
pub const ENCRYPT: Self = Self(4);
pub const DECRYPT: Self = Self(8);
#[must_use]
pub const fn union(self, other: Self) -> Self {
Self(self.0 | other.0)
}
#[must_use]
pub const fn allows(self, usage: KeyUsage) -> bool {
let bit = match usage {
KeyUsage::Sign => Self::SIGN.0,
KeyUsage::Verify => Self::VERIFY.0,
KeyUsage::Encrypt => Self::ENCRYPT.0,
KeyUsage::Decrypt => Self::DECRYPT.0,
};
self.0 & bit != 0
}
}
pub struct StoredPrivateKey {
pub name: String,
pub pkcs8_der: Zeroizing<Vec<u8>>,
pub usages: KeyUsages,
pub certificate_chain: Vec<Vec<u8>>,
has_matching_leaf: bool,
}
impl StoredPrivateKey {
pub fn decapsulation_key(
&self,
provider: &dyn crate::provider::CryptoProvider,
algorithm: crate::provider::KeyEncapsulationAlgorithm,
policy: &crate::policy::KeyEstablishmentPolicy,
resources: &ResourcePolicy,
) -> Result<std::sync::Arc<dyn crate::provider::KeyDecapsulationKey>, KeyStoreError> {
policy.validate()?;
policy.check_encapsulation(algorithm)?;
if !self.usages.allows(KeyUsage::Decrypt) {
return Err(KeyStoreError::Selection(
"key is not authorized for decapsulation",
));
}
check_operation_material_size(self.pkcs8_der.len(), resources)?;
provider
.import_decapsulation_key(algorithm, &self.pkcs8_der)
.map_err(KeyStoreError::Provider)
}
#[must_use]
pub fn matching_certificate_chain(&self) -> Option<&[Vec<u8>]> {
self.has_matching_leaf.then_some(&self.certificate_chain)
}
}
pub struct StoredPublicKey {
pub name: String,
pub key_info: KeyInfo,
pub usages: KeyUsages,
}
impl StoredPublicKey {
pub fn encapsulation_key(
&self,
provider: &dyn crate::provider::CryptoProvider,
algorithm: crate::provider::KeyEncapsulationAlgorithm,
policy: &crate::policy::KeyEstablishmentPolicy,
resources: &ResourcePolicy,
) -> Result<std::sync::Arc<dyn crate::provider::KeyEncapsulationKey>, KeyStoreError> {
policy.validate()?;
policy.check_encapsulation(algorithm)?;
if !self.usages.allows(KeyUsage::Encrypt) {
return Err(KeyStoreError::Selection(
"key is not authorized for encapsulation",
));
}
let der = self
.key_info
.sources
.iter()
.find_map(|source| match source {
KeyInfoSource::DerEncodedKeyValue(der) => Some(der.as_slice()),
_ => None,
})
.ok_or(KeyStoreError::Selection(
"recipient requires an ML-KEM SPKI",
))?;
check_operation_material_size(der.len(), resources)?;
provider
.import_encapsulation_key(algorithm, der)
.map_err(KeyStoreError::Provider)
}
#[cfg(feature = "xmlenc")]
pub fn rsa_encryption_key(
&self,
policy: &crate::policy::EncryptionPolicy,
) -> Result<RsaPublicKey, KeyStoreError> {
policy.validate()?;
if !self.usages.allows(KeyUsage::Encrypt) {
return Err(KeyStoreError::Selection(
"key is not authorized for encryption",
));
}
for source in &self.key_info.sources {
return match source {
KeyInfoSource::KeyValue(KeyValueInfo::Rsa { modulus, exponent }) => {
rsa_recipient_from_components(modulus, exponent, policy)
}
KeyInfoSource::DerEncodedKeyValue(der) => {
check_encryption_material_size(der.len(), &policy.resources)?;
let spki = pkcs8::SubjectPublicKeyInfoRef::from_der(der)
.map_err(|_| KeyStoreError::Selection("invalid RSA encryption key"))?;
if spki.algorithm.oid != pkcs1::ALGORITHM_OID {
return Err(KeyStoreError::Selection(
"named key is not an RSA public key",
));
}
let octets = spki
.subject_public_key
.as_bytes()
.ok_or(KeyStoreError::Selection("invalid RSA encryption key"))?;
let raw = pkcs1::RsaPublicKeyRef::from_der(octets)
.map_err(|_| KeyStoreError::Selection("invalid RSA encryption key"))?;
rsa_recipient_preflight(
raw.modulus.as_bytes(),
raw.public_exponent.as_bytes(),
policy,
)?;
RsaPublicKey::from_public_key_der(der)
.map_err(|_| KeyStoreError::Selection("invalid RSA encryption key"))
}
KeyInfoSource::X509Data(data) if data.parsed_certificates.len() == 1 => {
if let Some(certificate) = data.certificates.first() {
check_encryption_material_size(certificate.len(), &policy.resources)?;
}
match &data.parsed_certificates[0].public_key {
X509PublicKeyInfo::Rsa { modulus, exponent } => {
rsa_recipient_from_components(modulus, exponent, policy)
}
_ => Err(KeyStoreError::Selection("certificate does not contain RSA")),
}
}
_ => continue,
};
}
Err(KeyStoreError::Selection(
"named key is not an RSA public key",
))
}
}
pub struct InventoryVerificationResolver<'a> {
inventory: &'a KeyInventory,
}
impl<'a> KeyResolver for InventoryVerificationResolver<'a> {
fn resolve<'k>(
&'k self,
key_info: Option<&KeyInfo>,
algorithm: SignatureAlgorithm,
) -> Result<Option<Box<dyn VerifyingKey + 'k>>, DsigError> {
self.resolve_with_policy_and_provider(
key_info,
algorithm,
&crate::policy::VerificationPolicy::default(),
crate::provider::default_provider(),
)
}
fn resolve_with_policy<'k>(
&'k self,
key_info: Option<&KeyInfo>,
algorithm: SignatureAlgorithm,
policy: &crate::policy::VerificationPolicy,
) -> Result<Option<Box<dyn VerifyingKey + 'k>>, DsigError> {
self.resolve_with_policy_and_provider(
key_info,
algorithm,
policy,
crate::provider::default_provider(),
)
}
fn resolve_with_policy_and_provider<'k>(
&'k self,
key_info: Option<&KeyInfo>,
algorithm: SignatureAlgorithm,
policy: &crate::policy::VerificationPolicy,
provider: &dyn crate::provider::CryptoProvider,
) -> Result<Option<Box<dyn VerifyingKey + 'k>>, DsigError> {
self.resolve_for_verification(key_info, algorithm, policy, provider)
.map(|key| key.map(crate::xmldsig::ResolvedVerificationKey::into_key))
}
fn resolve_for_verification<'k>(
&'k self,
key_info: Option<&KeyInfo>,
algorithm: SignatureAlgorithm,
policy: &crate::policy::VerificationPolicy,
provider: &dyn crate::provider::CryptoProvider,
) -> Result<Option<crate::xmldsig::ResolvedVerificationKey<'k>>, DsigError> {
let mut budget = InspectedKeyCandidateBudget::new(policy);
self.resolve_for_verification_with_budget(
key_info,
algorithm,
policy,
provider,
&mut budget,
)
}
fn resolve_for_verification_with_budget<'k>(
&'k self,
key_info: Option<&KeyInfo>,
algorithm: SignatureAlgorithm,
policy: &crate::policy::VerificationPolicy,
provider: &dyn crate::provider::CryptoProvider,
inspected_candidates: &mut InspectedKeyCandidateBudget,
) -> Result<Option<crate::xmldsig::ResolvedVerificationKey<'k>>, DsigError> {
policy.validate()?;
inspected_candidates.bind_policy(policy)?;
if let Some(info) = key_info {
crate::xmldsig::keys::validate_key_info_source_permissions(info, policy.key_sources)?;
}
let mut candidate: Option<&StoredPublicKey> = None;
let mut secret_candidate: Option<&StoredSymmetricKey> = None;
for name in key_info
.into_iter()
.flat_map(|info| &info.sources)
.filter_map(|source| match source {
KeyInfoSource::KeyName(name) => Some(name.as_str()),
_ => None,
})
{
if self.inventory.symmetric_keys.is_empty() && self.inventory.public_keys.is_empty() {
inspected_candidates.charge()?;
}
let mut symmetric_match = None;
for entry in &self.inventory.symmetric_keys {
inspected_candidates.charge()?;
if entry.name == name {
symmetric_match = Some(entry);
break;
}
}
if let Some(found) = symmetric_match {
if !found.usages.allows(KeyUsage::Verify) || found.kind != SymmetricKeyKind::Hmac {
return Err(DsigError::InvalidStructure {
reason: "named key is not authorized for verification",
});
}
if candidate.is_some()
|| secret_candidate.is_some_and(|previous| !core::ptr::eq(previous, found))
{
return Err(DsigError::InvalidStructure {
reason: "more than one named verification key matches",
});
}
secret_candidate = Some(found);
continue;
}
let mut public_match = None;
for entry in &self.inventory.public_keys {
inspected_candidates.charge()?;
if entry.name == name {
public_match = Some(entry);
break;
}
}
if let Some(found) = public_match {
if !found.usages.allows(KeyUsage::Verify) {
return Err(DsigError::InvalidStructure {
reason: "named key is not authorized for verification",
});
}
if secret_candidate.is_some()
|| candidate.is_some_and(|previous| !core::ptr::eq(previous, found))
{
return Err(DsigError::InvalidStructure {
reason: "more than one named verification key matches",
});
}
candidate = Some(found);
}
}
if let Some(candidate) = secret_candidate {
if algorithm.hmac_output_bits().is_none() {
return Err(DsigError::InvalidStructure {
reason: "named HMAC key is incompatible with signature method",
});
}
for (resource, maximum) in [
(
crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
policy.resources.max_external_resource_bytes,
),
(
crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
policy.resources.max_external_resource_total_bytes,
),
] {
if candidate.bytes.len() > maximum {
return Err(crate::policy::PolicyViolation::ResourceLimit {
resource,
maximum,
actual: candidate.bytes.len(),
}
.into());
}
}
let key = HmacVerificationKey::new(candidate.bytes.to_vec()).map_err(|_| {
DsigError::InvalidStructure {
reason: "invalid named HMAC key",
}
})?;
return Ok(Some(
crate::xmldsig::ResolvedVerificationKey::CallerTrusted(Box::new(key)),
));
}
let selected_material_bytes = candidate
.map(|candidate| check_selected_public_material(&candidate.key_info, &policy.resources))
.transpose()?
.unwrap_or(0);
let selected_info = candidate.map_or(key_info, |entry| Some(&entry.key_info));
let configured_x509_index = selected_info.and_then(|info| {
info.sources.iter().position(|source| match source {
KeyInfoSource::X509Data(data) if data.certificate_chain.is_empty() => {
crate::xmldsig::parse::x509_data_has_lookup_identifiers(data)
}
KeyInfoSource::X509Data(_) => {
policy.key_trust.verify_x509_chains
|| !self.inventory.trusted_certificates.is_empty()
}
_ => false,
})
});
let mut prefix_error = None;
if let Some(info) = selected_info
&& let Some(first_x509) = configured_x509_index
&& first_x509 != 0
{
let prefix_resolver = DefaultKeyResolver::new(KeyResolverConfig::default());
let outcome = prefix_resolver.resolve_sources_with_candidate_budget(
info,
algorithm,
policy,
provider,
inspected_candidates,
if candidate.is_some() {
crate::xmldsig::keys::ResolutionScope::TrustedPrefix(first_x509)
} else {
crate::xmldsig::keys::ResolutionScope::DocumentPrefix(first_x509)
},
)?;
if let Some(key) = outcome.key {
return Ok(Some(key));
}
prefix_error = outcome.deferred_error;
}
let fallback = if configured_x509_index.is_some() {
let certificates = self
.inventory
.lookup_certificates
.iter()
.chain(&self.inventory.trusted_certificates);
let document_crls = key_info
.filter(|_| {
candidate.is_some()
&& policy.key_trust.check_crls
&& policy.key_trust.verify_x509_chains
})
.into_iter()
.flat_map(|info| &info.sources)
.filter_map(|source| match source {
KeyInfoSource::X509Data(data) => Some(data.crls.as_slice()),
_ => None,
})
.flatten();
let crls = self
.inventory
.crls
.iter()
.chain(document_crls)
.filter(|_| policy.key_trust.check_crls && policy.key_trust.verify_x509_chains);
let mut total = selected_material_bytes;
for material in certificates.chain(crls.clone()) {
if material.len() > policy.resources.max_external_resource_bytes {
return Err(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
maximum: policy.resources.max_external_resource_bytes,
actual: material.len(),
}
.into());
}
for _ in 0..2 {
debug_assert!(total <= policy.resources.max_external_resource_total_bytes);
if material.len() > policy.resources.max_external_resource_total_bytes - total {
return Err(crate::policy::PolicyViolation::ResourceLimit {
resource:
crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
maximum: policy.resources.max_external_resource_total_bytes,
actual: total.saturating_add(material.len()),
}
.into());
}
total += material.len();
}
}
DefaultKeyResolver::new(KeyResolverConfig {
lookup_certs: self.inventory.lookup_certificates.clone(),
trusted_certs: self.inventory.trusted_certificates.clone(),
crls: crls.cloned().collect(),
..KeyResolverConfig::default()
})
} else {
DefaultKeyResolver::new(KeyResolverConfig::default())
};
if let Some(first_x509) = configured_x509_index
&& let Some(info) = selected_info
{
let mut outcome = fallback.resolve_sources_with_candidate_budget(
info,
algorithm,
policy,
provider,
inspected_candidates,
if candidate.is_some() {
crate::xmldsig::keys::ResolutionScope::TrustedSuffix(first_x509)
} else {
crate::xmldsig::keys::ResolutionScope::DocumentSuffix(first_x509)
},
)?;
if outcome.key.is_none()
&& let Some(error) = prefix_error
{
outcome.deferred_error = Some(error);
}
return outcome.finish();
}
if let Some(candidate) = candidate {
return fallback.resolve_trusted_material_with_candidate_budget(
&candidate.key_info,
algorithm,
policy,
provider,
inspected_candidates,
);
}
fallback.resolve_with_candidate_budget(
key_info,
algorithm,
policy,
provider,
inspected_candidates,
)
}
fn consumes_document_key_info(&self) -> bool {
true
}
}
enum ParsedMaterial {
Symmetric(SymmetricKeyKind, Zeroizing<Vec<u8>>),
Public(KeyValueInfo),
PublicDer(Vec<u8>),
Dsa(KeyValueInfo, Option<Zeroizing<Vec<u8>>>),
Unsupported,
}
type ParsedDsaKey = (KeyValueInfo, Option<Zeroizing<Vec<u8>>>);
#[cfg(feature = "xmlenc")]
struct InventoryDirectSymmetric {
bytes: Zeroizing<Vec<u8>>,
kind: SymmetricKeyKind,
}
#[cfg(feature = "xmlenc")]
impl crate::xmlenc::DecryptionKeyResolver for InventoryDirectSymmetric {
fn resolve_key(
&self,
_provider: &dyn crate::provider::CryptoProvider,
algorithm: crate::xmlenc::DataEncryptionAlgorithm,
encrypted_key: Option<&crate::xmlenc::EncryptedKey>,
) -> Result<Vec<u8>, crate::xmlenc::XmlEncError> {
if encrypted_key.is_some() || !self.kind.accepts_algorithm(algorithm) {
return Err(crate::xmlenc::XmlEncError::KeyNotFound);
}
crate::xmlenc::validate_key_len(algorithm, &self.bytes)?;
Ok(self.bytes.to_vec())
}
fn resolve_key_candidates(
&self,
provider: &dyn crate::provider::CryptoProvider,
algorithm: crate::xmlenc::DataEncryptionAlgorithm,
encrypted_key: Option<&crate::xmlenc::EncryptedKey>,
budget: &mut crate::xmlenc::KeyCandidateBudget,
) -> Result<Vec<Vec<u8>>, crate::xmlenc::XmlEncError> {
if encrypted_key.is_some() {
return Err(crate::xmlenc::XmlEncError::KeyNotFound);
}
budget.consume(1)?;
self.resolve_key(provider, algorithm, None)
.map(|key| vec![key])
}
fn resolve_key_candidates_with_policy(
&self,
provider: &dyn crate::provider::CryptoProvider,
algorithm: crate::xmlenc::DataEncryptionAlgorithm,
encrypted_key: Option<&crate::xmlenc::EncryptedKey>,
policy: &crate::policy::DecryptionPolicy,
budget: &mut crate::xmlenc::KeyCandidateBudget,
) -> Result<Vec<Vec<u8>>, crate::xmlenc::XmlEncError> {
policy.validate()?;
if encrypted_key.is_some() {
return Err(crate::xmlenc::XmlEncError::KeyNotFound);
}
check_selected_material_size(self.bytes.len(), &policy.resources).map_err(|error| {
match error {
KeyStoreError::Policy(violation) => crate::xmlenc::XmlEncError::Policy(violation),
other => crate::xmlenc::XmlEncError::InvalidStructure(other.to_string()),
}
})?;
self.resolve_key_candidates(provider, algorithm, None, budget)
}
}
#[derive(Debug, thiserror::Error)]
pub enum KeyStoreError {
#[error(transparent)]
Provider(#[from] crate::provider::ProviderError),
#[error("invalid xmlsec keys.xml: {0}")]
Invalid(String),
#[error("key inventory selection failed: {0}")]
Selection(&'static str),
#[error("key inventory policy violation: {0}")]
Policy(#[from] crate::policy::PolicyViolation),
#[error("protected key container could not be decoded")]
ProtectedContainer,
}
impl KeyInventory {
#[must_use]
pub fn key_import_kdf_work(&self) -> usize {
self.kdf_work
}
fn retained_material_bytes(&self) -> Result<usize, KeyStoreError> {
let mut total = 0_usize;
let mut add = |length: usize| -> Result<(), KeyStoreError> {
total = total
.checked_add(length)
.ok_or(KeyStoreError::Selection("key material size overflow"))?;
Ok(())
};
for key in &self.symmetric_keys {
add(key.name.len())?;
add(key.bytes.len())?;
}
for key in &self.private_keys {
add(key.name.len())?;
add(key.pkcs8_der.len())?;
for certificate in &key.certificate_chain {
add(certificate.len())?;
}
}
for key in &self.public_keys {
add(key.name.len())?;
for source in &key.key_info.sources {
match source {
KeyInfoSource::KeyName(name) => add(name.len())?,
KeyInfoSource::KeyValue(KeyValueInfo::Dsa { p, q, g, y }) => {
add(p.as_ref().map_or(0, Vec::len))?;
add(q.as_ref().map_or(0, Vec::len))?;
add(g.as_ref().map_or(0, Vec::len))?;
add(y.len())?;
}
KeyInfoSource::KeyValue(KeyValueInfo::Rsa { modulus, exponent }) => {
add(modulus.len())?;
add(exponent.len())?;
}
KeyInfoSource::KeyValue(KeyValueInfo::Ec {
curve_oid,
public_key,
}) => {
add(curve_oid.len())?;
add(public_key.len())?;
}
KeyInfoSource::DerEncodedKeyValue(bytes) => add(bytes.len())?,
_ => {}
}
}
}
for certificate in self
.lookup_certificates
.iter()
.chain(&self.trusted_certificates)
{
add(certificate.len())?;
}
for crl in &self.crls {
add(crl.len())?;
}
Ok(total)
}
#[must_use]
pub fn entry_count(&self) -> usize {
self.entry_count
}
#[must_use]
pub fn symmetric_keys(&self) -> &[StoredSymmetricKey] {
&self.symmetric_keys
}
#[must_use]
pub fn public_keys(&self) -> &[StoredPublicKey] {
&self.public_keys
}
#[must_use]
pub fn private_keys(&self) -> &[StoredPrivateKey] {
&self.private_keys
}
pub fn extend(
&mut self,
mut other: Self,
resources: &ResourcePolicy,
) -> Result<(), KeyStoreError> {
ensure_resource_policy(resources)?;
let work = self
.kdf_work
.checked_add(other.kdf_work)
.filter(|work| *work <= resources.max_key_import_kdf_work)
.ok_or_else(|| {
import_resource_limit(
crate::policy::resource_name::KEY_IMPORT_KDF_WORK,
resources.max_key_import_kdf_work,
)
})?;
let candidates = self
.entry_count
.checked_add(other.entry_count)
.ok_or(KeyStoreError::Selection("key candidate count overflow"))?;
if candidates > resources.max_key_candidates {
return Err(import_resource_limit(
crate::policy::resource_name::KEY_CANDIDATES,
resources.max_key_candidates,
));
}
let bytes = self
.material_bytes
.checked_add(other.material_bytes)
.ok_or(KeyStoreError::Selection("key material size overflow"))?;
if bytes > resources.max_external_resource_total_bytes {
return Err(import_resource_limit(
crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
resources.max_external_resource_total_bytes,
));
}
let mut names = HashSet::new();
for name in other
.symmetric_keys
.iter()
.map(|entry| entry.name.as_str())
.chain(other.public_keys.iter().map(|entry| entry.name.as_str()))
.chain(other.private_keys.iter().map(|entry| entry.name.as_str()))
{
names.insert(name);
}
if names.iter().any(|name| {
self.symmetric_keys.iter().any(|entry| entry.name == *name)
|| self.public_keys.iter().any(|entry| entry.name == *name)
|| self.private_keys.iter().any(|entry| entry.name == *name)
}) {
return Err(KeyStoreError::Selection("duplicate key name"));
}
self.entry_count = candidates;
self.material_bytes = bytes;
self.kdf_work = work;
self.symmetric_keys.append(&mut other.symmetric_keys);
self.public_keys.append(&mut other.public_keys);
self.private_keys.append(&mut other.private_keys);
self.lookup_certificates
.append(&mut other.lookup_certificates);
self.trusted_certificates
.append(&mut other.trusted_certificates);
self.crls.append(&mut other.crls);
Ok(())
}
#[cfg(feature = "xmlenc")]
pub fn rsa_encryption_key(
&self,
name: &str,
policy: &crate::policy::EncryptionPolicy,
) -> Result<RsaPublicKey, KeyStoreError> {
policy.validate()?;
let entry = find_named_entry(
&self.public_keys,
name,
&policy.resources,
&mut 0,
|entry| &entry.name,
)?
.ok_or(KeyStoreError::Selection("named encryption key not found"))?;
entry.rsa_encryption_key(policy)
}
pub fn signing_key(
&self,
name: &str,
algorithm: SignatureAlgorithm,
policy: &crate::policy::SigningPolicy,
) -> Result<Box<dyn SigningKey>, KeyStoreError> {
self.signing_key_with_budget(name, algorithm, policy, &mut SigningLookupBudget::default())
}
pub fn signing_key_with_budget(
&self,
name: &str,
algorithm: SignatureAlgorithm,
policy: &crate::policy::SigningPolicy,
budget: &mut SigningLookupBudget,
) -> Result<Box<dyn SigningKey>, KeyStoreError> {
self.signing_key_with_provider_and_budget(
name,
algorithm,
policy,
crate::provider::default_provider(),
budget,
)
}
pub fn signing_key_with_provider(
&self,
name: &str,
algorithm: SignatureAlgorithm,
policy: &crate::policy::SigningPolicy,
provider: &dyn crate::provider::CryptoProvider,
) -> Result<Box<dyn SigningKey>, KeyStoreError> {
self.signing_key_with_provider_and_budget(
name,
algorithm,
policy,
provider,
&mut SigningLookupBudget::default(),
)
}
pub fn signing_key_with_provider_and_budget(
&self,
name: &str,
algorithm: SignatureAlgorithm,
policy: &crate::policy::SigningPolicy,
provider: &dyn crate::provider::CryptoProvider,
budget: &mut SigningLookupBudget,
) -> Result<Box<dyn SigningKey>, KeyStoreError> {
policy.validate()?;
policy.check_signature_algorithm(algorithm)?;
provider
.require_capability(crate::provider::ProviderCapability::Sign(algorithm))
.map_err(|_| KeyStoreError::Selection("provider does not support signing method"))?;
if algorithm.hmac_output_bits().is_some() {
let entry = find_named_entry(
&self.symmetric_keys,
name,
&policy.resources,
&mut budget.inspected,
|entry| &entry.name,
)?
.ok_or(KeyStoreError::Selection("named signing key not found"))?;
if entry.kind != SymmetricKeyKind::Hmac || !entry.usages.allows(KeyUsage::Sign) {
return Err(KeyStoreError::Selection(
"key is not authorized for signing",
));
}
check_operation_material_size(entry.bytes.len(), &policy.resources)?;
let key = HmacSigningKey::new(entry.bytes.to_vec())
.map_err(|_| KeyStoreError::Selection("invalid HMAC key"))?;
validate_signing_key(&key, algorithm, policy).map_err(signing_policy_error)?;
return Ok(Box::new(key));
}
let entry = find_named_entry(
&self.private_keys,
name,
&policy.resources,
&mut budget.inspected,
|entry| &entry.name,
)?
.ok_or(KeyStoreError::Selection("named signing key not found"))?;
if !entry.usages.allows(KeyUsage::Sign) {
return Err(KeyStoreError::Selection(
"key is not authorized for signing",
));
}
let der = entry.pkcs8_der.as_slice();
check_operation_material_size(der.len(), &policy.resources)?;
if let Ok(info) = PrivateKeyInfoRef::try_from(der)
&& info.algorithm.oid == dsa::OID
{
preflight_dsa_pkcs8_components(&info)?;
}
let key = provider
.import_signing_key(algorithm, der)
.map_err(|_| KeyStoreError::Selection("incompatible signing key for provider"))?;
validate_signing_key(key.as_ref(), algorithm, policy).map_err(signing_policy_error)?;
Ok(key)
}
#[cfg(feature = "xmlenc")]
pub fn decryption_resolver(
&self,
name: &str,
policy: &crate::policy::DecryptionPolicy,
) -> Result<Box<dyn crate::xmlenc::DecryptionKeyResolver>, KeyStoreError> {
self.decryption_resolver_with_provider(name, policy, crate::provider::default_provider())
}
#[cfg(feature = "xmlenc")]
pub fn decryption_resolver_with_provider(
&self,
name: &str,
policy: &crate::policy::DecryptionPolicy,
provider: &dyn crate::provider::CryptoProvider,
) -> Result<Box<dyn crate::xmlenc::DecryptionKeyResolver>, KeyStoreError> {
policy.validate()?;
let mut visited = 0;
if let Some(entry) = find_named_entry(
&self.symmetric_keys,
name,
&policy.resources,
&mut visited,
|entry| &entry.name,
)? {
if !entry.kind.is_encryption_key() || !entry.usages.allows(KeyUsage::Decrypt) {
return Err(KeyStoreError::Selection(
"key is not authorized for decryption",
));
}
check_selected_material_size(entry.bytes.len(), &policy.resources)?;
return Ok(Box::new(InventoryDirectSymmetric {
bytes: Zeroizing::new(entry.bytes.to_vec()),
kind: entry.kind,
}));
}
let entry = find_named_entry(
&self.private_keys,
name,
&policy.resources,
&mut visited,
|entry| &entry.name,
)?
.ok_or(KeyStoreError::Selection("named decryption key not found"))?;
if !entry.usages.allows(KeyUsage::Decrypt) {
return Err(KeyStoreError::Selection(
"key is not authorized for decryption",
));
}
check_selected_material_size(entry.pkcs8_der.len(), &policy.resources)?;
let info = PrivateKeyInfoRef::try_from(entry.pkcs8_der.as_slice())
.map_err(|_| KeyStoreError::Selection("incompatible RSA decryption key"))?;
if let Some(algorithm) =
crate::provider::KeyEncapsulationAlgorithm::from_oid(info.algorithm.oid)
{
let key = entry.decapsulation_key(
provider,
algorithm,
&policy.key_establishment,
&policy.resources,
)?;
return Ok(Box::new(
crate::xmlenc::EncapsulationDecryptor::provider_key(key),
));
}
let components = pkcs1::RsaPrivateKeyRef::from_der(info.private_key.as_bytes())
.map_err(|_| KeyStoreError::Selection("incompatible RSA decryption key"))?;
policy.rsa_keys.validate_components(
"decryption",
components.modulus.as_bytes(),
components.public_exponent.as_bytes(),
)?;
let key = provider
.import_recovery_key(&entry.pkcs8_der)
.map_err(|_| KeyStoreError::Selection("incompatible RSA decryption key"))?;
Ok(Box::new(crate::xmlenc::PrivateKeyDecryptor::provider_key(
key,
)))
}
#[must_use]
pub fn verification_resolver(&self) -> InventoryVerificationResolver<'_> {
InventoryVerificationResolver { inventory: self }
}
pub fn add_symmetric(
&mut self,
name: String,
kind: SymmetricKeyKind,
bytes: Vec<u8>,
usages: KeyUsages,
resources: &ResourcePolicy,
) -> Result<(), KeyStoreError> {
self.check_new_name(&name, resources)?;
if bytes.len() > resources.max_external_resource_bytes {
return Err(import_resource_limit(
crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
resources.max_external_resource_bytes,
));
}
if bytes.is_empty()
|| (matches!(kind, SymmetricKeyKind::Aes | SymmetricKeyKind::Camellia)
&& !matches!(bytes.len(), 16 | 24 | 32))
|| (kind == SymmetricKeyKind::ChaCha20 && bytes.len() != 32)
{
return Err(KeyStoreError::Selection("invalid symmetric key length"));
}
let permitted = match kind {
SymmetricKeyKind::Hmac => KeyUsages::SIGN.union(KeyUsages::VERIFY),
SymmetricKeyKind::Aes | SymmetricKeyKind::Camellia | SymmetricKeyKind::ChaCha20 => {
KeyUsages::ENCRYPT.union(KeyUsages::DECRYPT)
}
SymmetricKeyKind::Des => des_key_usages(&bytes)?,
};
if usages.0 == 0 || usages.0 & !permitted.0 != 0 {
return Err(KeyStoreError::Selection(
"symmetric key usage is incompatible",
));
}
self.reserve_material(named_material_length(&name, bytes.len(), 1)?, resources)?;
self.symmetric_keys.push(StoredSymmetricKey {
name,
kind,
bytes: Zeroizing::new(bytes),
usages,
});
self.entry_count += 1;
Ok(())
}
pub fn add_public_der(
&mut self,
name: String,
der: Vec<u8>,
resources: &ResourcePolicy,
) -> Result<(), KeyStoreError> {
self.add_public_der_inner(name, der, None, resources)
}
pub fn add_public_der_with_usages(
&mut self,
name: String,
der: Vec<u8>,
usages: KeyUsages,
resources: &ResourcePolicy,
) -> Result<(), KeyStoreError> {
self.add_public_der_inner(name, der, Some(usages), resources)
}
fn add_public_der_inner(
&mut self,
name: String,
der: Vec<u8>,
usages: Option<KeyUsages>,
resources: &ResourcePolicy,
) -> Result<(), KeyStoreError> {
self.add_public_der_payload(
name,
der,
usages,
resources,
PublicDerFormat::KeyOrCertificate,
&crate::xmldsig::pkix::CertificateSession::new(resources),
)
}
fn add_public_der_payload(
&mut self,
name: String,
der: Vec<u8>,
usages: Option<KeyUsages>,
resources: &ResourcePolicy,
format: PublicDerFormat,
session: &crate::xmldsig::pkix::CertificateSession,
) -> Result<(), KeyStoreError> {
self.check_new_name(&name, resources)?;
let permitted = KeyUsages::VERIFY.union(KeyUsages::ENCRYPT);
if usages.is_some_and(|usages| usages.0 == 0 || usages.0 & !permitted.0 != 0) {
return Err(KeyStoreError::Selection("public key usage is incompatible"));
}
if der.len() > resources.max_external_resource_bytes {
return Err(import_resource_limit(
crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
resources.max_external_resource_bytes,
));
}
self.check_material_capacity(named_material_length(&name, der.len(), 2)?, resources)?;
#[cfg(feature = "experimental-pq")]
if let Ok(spki) = pkcs8::SubjectPublicKeyInfoRef::from_der(&der)
&& crate::provider::KeyEncapsulationAlgorithm::from_oid(spki.algorithm.oid).is_some()
{
crate::provider::RustCryptoMlKemPublicKey::from_spki_der(&der)
.map_err(|_| KeyStoreError::Selection("invalid ML-KEM public key"))?;
let usages = usages.unwrap_or(KeyUsages::ENCRYPT);
if usages != KeyUsages::ENCRYPT {
return Err(KeyStoreError::Selection(
"ML-KEM public keys establish keys, not signatures",
));
}
self.reserve_material(named_material_length(&name, der.len(), 2)?, resources)?;
self.public_keys.push(StoredPublicKey {
key_info: KeyInfo {
sources: vec![
KeyInfoSource::KeyName(name.clone()),
KeyInfoSource::DerEncodedKeyValue(der),
],
..KeyInfo::default()
},
name,
usages,
});
self.entry_count += 1;
return Ok(());
}
let material_len = der.len();
let mut key_info = KeyInfo::default();
key_info.sources.push(KeyInfoSource::KeyName(name.clone()));
let is_rsa = if let Ok(spki) = pkcs8::SubjectPublicKeyInfoRef::from_der(&der) {
let is_rsa = crate::xmldsig::keys::supported_parsed_spki_is_rsa(session, &spki, &der)
.map_err(|error| {
key_import_error(error, || {
KeyStoreError::Selection("unsupported public key algorithm")
})
})?;
key_info
.sources
.push(KeyInfoSource::DerEncodedKeyValue(der));
is_rsa
} else {
if matches!(format, PublicDerFormat::SubjectPublicKeyInfo) {
return Err(KeyStoreError::Selection("invalid PUBLIC KEY payload"));
}
let certificate = session.read(&der).map_err(certificate_import_error)?;
let parsed = crate::xmldsig::parse::certificate_metadata(&certificate, session)
.map_err(|error| match error {
crate::xmldsig::ParseError::Policy(error) => KeyStoreError::Policy(error),
_ => KeyStoreError::Selection("unsupported X.509 certificate key"),
})?;
let spki_der = certificate.tbs().public_key().as_bytes();
let spki = pkcs8::SubjectPublicKeyInfoRef::from_der(spki_der)
.map_err(|_| KeyStoreError::Selection("unsupported X.509 certificate key"))?;
let is_rsa =
crate::xmldsig::keys::supported_parsed_spki_is_rsa(session, &spki, spki_der)
.map_err(|error| {
key_import_error(error, || {
KeyStoreError::Selection("unsupported X.509 certificate key")
})
})?;
drop(certificate);
key_info.sources.push(KeyInfoSource::X509Data(X509DataInfo {
certificates: vec![der],
parsed_certificates: vec![parsed],
certificate_chain: vec![0],
..X509DataInfo::default()
}));
is_rsa
};
let usages = usages.unwrap_or(if is_rsa { permitted } else { KeyUsages::VERIFY });
if usages.allows(KeyUsage::Encrypt) && !is_rsa {
return Err(KeyStoreError::Selection(
"only RSA public keys can be used for encryption",
));
}
self.reserve_material(named_material_length(&name, material_len, 2)?, resources)?;
self.public_keys.push(StoredPublicKey {
name,
key_info,
usages,
});
self.entry_count += 1;
Ok(())
}
pub fn add_public_pem(
&mut self,
name: String,
bytes: &[u8],
resources: &ResourcePolicy,
) -> Result<(), KeyStoreError> {
self.add_public_pem_inner(name, bytes, None, resources)
}
pub fn add_public_pem_with_usages(
&mut self,
name: String,
bytes: &[u8],
usages: KeyUsages,
resources: &ResourcePolicy,
) -> Result<(), KeyStoreError> {
self.add_public_pem_inner(name, bytes, Some(usages), resources)
}
fn add_public_pem_inner(
&mut self,
name: String,
bytes: &[u8],
usages: Option<KeyUsages>,
resources: &ResourcePolicy,
) -> Result<(), KeyStoreError> {
ensure_resource_policy(resources)?;
self.check_new_name(&name, resources)?;
self.check_material_capacity(named_material_length(&name, bytes.len(), 2)?, resources)?;
let block = BorrowedPem::parse(bytes, resources.max_external_resource_bytes)?;
if !matches!(block.label, "PUBLIC KEY" | "RSA PUBLIC KEY") {
return Err(KeyStoreError::Selection("unsupported public PEM label"));
}
self.check_material_capacity(
named_material_length(
&name,
bytes
.len()
.checked_add(block.decoded_len)
.ok_or(KeyStoreError::Selection("key material size overflow"))?,
2,
)?,
resources,
)?;
let decoded = block.decode()?;
let der = match block.label {
"PUBLIC KEY" => decoded,
"RSA PUBLIC KEY" => {
use der::Encode as _;
let components = pkcs1::RsaPublicKeyRef::from_der(&decoded)
.map_err(|_| KeyStoreError::Selection("invalid RSA public key"))?;
crate::xmldsig::keys::bounded_rsa_public_components(
components.modulus.as_bytes(),
components.public_exponent.as_bytes(),
)
.map_err(|_| KeyStoreError::Selection("RSA public key exceeds safety limit"))?;
let spki = pkcs8::SubjectPublicKeyInfoRef {
algorithm: pkcs1::ALGORITHM_ID,
subject_public_key: der::asn1::BitStringRef::new(0, &decoded)
.map_err(|_| KeyStoreError::Selection("invalid RSA public key"))?,
};
let encoded_len = usize::try_from(
spki.encoded_len()
.map_err(|_| KeyStoreError::Selection("invalid RSA public key"))?,
)
.map_err(|_| KeyStoreError::Selection("invalid RSA public key"))?;
if encoded_len > resources.max_external_resource_bytes {
return Err(import_resource_limit(
crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
resources.max_external_resource_bytes,
));
}
self.check_material_capacity(
named_material_length(
&name,
bytes
.len()
.checked_add(decoded.capacity())
.and_then(|total| total.checked_add(encoded_len))
.ok_or(KeyStoreError::Selection("key material size overflow"))?,
2,
)?,
resources,
)?;
spki.to_der()
.map_err(|_| KeyStoreError::Selection("invalid RSA public key"))?
}
_ => return Err(KeyStoreError::Selection("unsupported public PEM label")),
};
let previous_total = self.material_bytes;
let charged_total = self.check_material_capacity(
named_material_length(&name, bytes.len().max(der.len()), 2)?,
resources,
)?;
self.add_public_der_payload(
name,
der,
usages,
resources,
PublicDerFormat::SubjectPublicKeyInfo,
&crate::xmldsig::pkix::CertificateSession::new(resources),
)?;
debug_assert!(self.material_bytes >= previous_total);
self.material_bytes = charged_total;
Ok(())
}
pub fn add_private_der(
&mut self,
name: String,
bytes: &[u8],
password: Option<&[u8]>,
usages: KeyUsages,
resources: &ResourcePolicy,
) -> Result<(), KeyStoreError> {
self.add_private_der_inner(name, bytes, password, usages, resources, 0)
}
fn add_private_der_inner(
&mut self,
name: String,
bytes: &[u8],
password: Option<&[u8]>,
usages: KeyUsages,
resources: &ResourcePolicy,
live_encoded_bytes: usize,
) -> Result<(), KeyStoreError> {
self.check_new_name(&name, resources)?;
if bytes.len() > resources.max_external_resource_bytes {
return Err(import_resource_limit(
crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
resources.max_external_resource_bytes,
));
}
let retained_with_input =
self.check_material_capacity(named_material_length(&name, bytes.len(), 1)?, resources)?;
validate_private_key_usages(usages)?;
let check_plain_output = |output: usize| {
if output > resources.max_external_resource_bytes {
return Err(import_resource_limit(
crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
resources.max_external_resource_bytes,
));
}
let live = bytes
.len()
.checked_add(live_encoded_bytes)
.and_then(|live| live.checked_add(output))
.ok_or_else(|| {
import_resource_limit(
crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
resources.max_external_resource_total_bytes,
)
})?;
self.check_material_capacity(named_material_length(&name, live, 1)?, resources)
};
let der = if PrivateKeyInfoRef::try_from(bytes).is_ok() {
check_plain_output(bytes.len())?;
Zeroizing::new(bytes.to_vec())
} else if let Ok(encrypted) = EncryptedPrivateKeyInfoRef::try_from(bytes) {
let kdf_live_bytes = retained_with_input
.checked_add(live_encoded_bytes)
.ok_or_else(|| {
import_resource_limit(
crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
resources.max_external_resource_total_bytes,
)
})?;
let remaining = self.remaining_kdf_resources(resources)?;
enforce_pkcs8_kdf_policy(&encrypted, &remaining, kdf_live_bytes)?;
let password = password.ok_or(KeyStoreError::ProtectedContainer)?;
let work =
enforce_pkcs8_password_policy(&encrypted, password, &remaining, kdf_live_bytes)?;
self.kdf_work += work;
let mut plain = Zeroizing::new(encrypted.encrypted_data.as_bytes().to_vec());
let plaintext_len = encrypted
.encryption_algorithm
.decrypt_in_place(password, &mut plain)
.map_err(|_| KeyStoreError::ProtectedContainer)?;
let plaintext_len = plaintext_len.len();
plain.truncate(plaintext_len);
PrivateKeyInfoRef::try_from(plain.as_slice())
.map_err(|_| KeyStoreError::ProtectedContainer)?;
plain
} else {
use der::Encode as _;
preflight_rsa_pkcs1_components(bytes)?;
let normalized = PrivateKeyInfoRef::new(
pkcs1::ALGORITHM_ID,
der::asn1::OctetStringRef::new(bytes)
.map_err(|_| KeyStoreError::Selection("invalid RSA private key"))?,
);
let size = usize::try_from(
normalized
.encoded_len()
.map_err(|_| KeyStoreError::Selection("invalid RSA private key"))?,
)
.map_err(|_| KeyStoreError::Selection("key material size overflow"))?;
check_plain_output(size)?;
let normalized = normalized
.to_der()
.map_err(|_| KeyStoreError::Selection("invalid RSA private key"))?;
Zeroizing::new(normalized)
};
if der.len() > resources.max_external_resource_bytes {
return Err(import_resource_limit(
crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
resources.max_external_resource_bytes,
));
}
let identity = PrivateKeyIdentity::decode(&der)?;
if usages.allows(KeyUsage::Decrypt) && !identity.can_decrypt() {
return Err(KeyStoreError::Selection(
"private key cannot recover encryption keys",
));
}
identity.validate_usages(usages)?;
drop(identity);
self.reserve_material(
named_material_length(&name, bytes.len().max(der.len()), 1)?,
resources,
)?;
self.private_keys.push(StoredPrivateKey {
name,
pkcs8_der: der,
usages,
certificate_chain: Vec::new(),
has_matching_leaf: false,
});
self.entry_count += 1;
Ok(())
}
pub fn add_private_der_with_password_callback<F>(
&mut self,
name: String,
bytes: &[u8],
password: F,
usages: KeyUsages,
resources: &ResourcePolicy,
) -> Result<(), KeyStoreError>
where
F: FnOnce() -> Option<Zeroizing<Vec<u8>>>,
{
self.check_new_name(&name, resources)?;
validate_private_key_usages(usages)?;
if bytes.len() > resources.max_external_resource_bytes {
return Err(import_resource_limit(
crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
resources.max_external_resource_bytes,
));
}
let retained_with_input =
self.check_material_capacity(named_material_length(&name, bytes.len(), 1)?, resources)?;
let encrypted = EncryptedPrivateKeyInfoRef::try_from(bytes).ok();
let secret = if let Some(encrypted) = &encrypted {
let remaining = self.remaining_kdf_resources(resources)?;
enforce_pkcs8_kdf_policy(encrypted, &remaining, retained_with_input)?;
Some(password().ok_or(KeyStoreError::ProtectedContainer)?)
} else {
None
};
if let Some(secret) = &secret {
self.check_material_capacity(
named_material_length(
&name,
bytes.len().checked_add(secret.capacity()).ok_or_else(|| {
import_resource_limit(
crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
resources.max_external_resource_total_bytes,
)
})?,
1,
)?,
resources,
)?;
let encrypted = encrypted
.as_ref()
.ok_or(KeyStoreError::ProtectedContainer)?;
enforce_pkcs8_password_policy(
encrypted,
secret,
&self.remaining_kdf_resources(resources)?,
retained_with_input + secret.capacity() - secret.len(),
)?;
}
self.add_private_der(
name,
bytes,
secret.as_deref().map(Vec::as_slice),
usages,
resources,
)
}
pub fn add_private_pem(
&mut self,
name: String,
bytes: &[u8],
password: Option<&[u8]>,
usages: KeyUsages,
resources: &ResourcePolicy,
) -> Result<(), KeyStoreError> {
ensure_resource_policy(resources)?;
self.check_new_name(&name, resources)?;
self.check_material_capacity(named_material_length(&name, bytes.len(), 1)?, resources)?;
let block = BorrowedPem::parse(bytes, resources.max_external_resource_bytes)?;
validate_private_key_usages(usages)?;
self.check_material_capacity(
named_material_length(
&name,
bytes
.len()
.checked_add(block.decoded_len)
.ok_or(KeyStoreError::Selection("key material size overflow"))?,
1,
)?,
resources,
)?;
enum Payload {
Plain,
Encrypted,
Rsa,
}
let payload = match block.label {
"PRIVATE KEY" => Payload::Plain,
"ENCRYPTED PRIVATE KEY" => Payload::Encrypted,
"RSA PRIVATE KEY" => Payload::Rsa,
_ => return Err(KeyStoreError::Selection("unsupported private PEM label")),
};
let mut der = Zeroizing::new(vec![0; block.decoded_len]);
block.decode_into(&mut der)?;
match payload {
Payload::Encrypted => {
EncryptedPrivateKeyInfoRef::try_from(der.as_slice())
.map_err(|_| KeyStoreError::ProtectedContainer)?;
}
Payload::Plain => {
PrivateKeyInfoRef::try_from(der.as_slice())
.map_err(|_| KeyStoreError::Selection("invalid PRIVATE KEY payload"))?;
}
Payload::Rsa => preflight_rsa_pkcs1_components(&der)?,
}
let previous_total = self.material_bytes;
let name_len = name.len();
self.add_private_der_inner(name, &der, password, usages, resources, bytes.len())?;
let retained_len = self.material_bytes - previous_total;
self.material_bytes = previous_total + name_len + bytes.len().max(retained_len - name_len);
Ok(())
}
pub fn add_pkcs12(
&mut self,
name: String,
bytes: &[u8],
password: &str,
resources: &ResourcePolicy,
) -> Result<(), KeyStoreError> {
self.add_pkcs12_inner(
name,
bytes,
password,
KeyUsages::SIGN.union(KeyUsages::DECRYPT),
resources,
true,
)
}
pub fn add_pkcs12_with_password_callback<F>(
&mut self,
name: String,
bytes: &[u8],
password: F,
usages: KeyUsages,
resources: &ResourcePolicy,
) -> Result<(), KeyStoreError>
where
F: FnOnce() -> Option<Zeroizing<String>>,
{
let limits = self.pkcs12_import_limits(&name, bytes, usages, resources)?;
let prepared = pkcs12_import::prepare_with_work(bytes, &limits, self.kdf_work)?;
let secret = password().ok_or(KeyStoreError::ProtectedContainer)?;
let contents =
prepared.decrypt_with_work(&secret, secret.capacity(), &mut self.kdf_work)?;
self.add_pkcs12_contents(name, bytes.len(), contents, usages, resources, false)
}
pub fn add_pkcs12_with_usages(
&mut self,
name: String,
bytes: &[u8],
password: &str,
usages: KeyUsages,
resources: &ResourcePolicy,
) -> Result<(), KeyStoreError> {
self.add_pkcs12_inner(name, bytes, password, usages, resources, false)
}
fn add_pkcs12_inner(
&mut self,
name: String,
bytes: &[u8],
password: &str,
usages: KeyUsages,
resources: &ResourcePolicy,
auto_decrypt: bool,
) -> Result<(), KeyStoreError> {
let limits = self.pkcs12_import_limits(&name, bytes, usages, resources)?;
let contents = pkcs12_import::prepare_with_work(bytes, &limits, self.kdf_work)?
.decrypt_with_work(password, password.len(), &mut self.kdf_work)?;
self.add_pkcs12_contents(name, bytes.len(), contents, usages, resources, auto_decrypt)
}
fn add_pkcs12_contents(
&mut self,
name: String,
encoded_len: usize,
mut contents: pkcs12_import::Contents,
mut usages: KeyUsages,
resources: &ResourcePolicy,
auto_decrypt: bool,
) -> Result<(), KeyStoreError> {
if contents.private_keys.len() != 1 {
return Err(KeyStoreError::Selection(
"PKCS#12 bundle must contain exactly one private key",
));
}
let private_key = contents
.private_keys
.pop()
.ok_or(KeyStoreError::ProtectedContainer)?;
let mut certificates = contents.certificates;
let identity = PrivateKeyIdentity::decode(private_key.as_ref())?;
if auto_decrypt {
usages = identity.automatic_usages();
} else if usages.allows(KeyUsage::Decrypt) && !identity.can_decrypt() {
return Err(KeyStoreError::Selection(
"private key cannot recover encryption keys",
));
}
identity.validate_usages(usages)?;
let mut matching_leaf = None;
let session = crate::xmldsig::pkix::CertificateSession::new(resources);
for certificate in &certificates {
let parsed = session
.read(certificate)
.map_err(certificate_import_error)?;
if identity.matches_spki(parsed.tbs().public_key().as_bytes()) {
if matching_leaf.is_some_and(|leaf: &[u8]| leaf != certificate.as_slice()) {
return Err(KeyStoreError::Selection(
"ambiguous certificate for PKCS#12 private key",
));
}
matching_leaf = Some(certificate.as_slice());
}
}
drop(identity);
let has_matching_leaf = matching_leaf.is_some();
if let Some(leaf) = matching_leaf {
let position = certificates
.iter()
.position(|candidate| candidate == leaf)
.ok_or(KeyStoreError::ProtectedContainer)?;
certificates.swap(0, position);
let mut index = 1;
while index < certificates.len() {
if certificates[index] == certificates[0] {
certificates.remove(index);
} else {
index += 1;
}
}
}
let decoded_bytes = certificates
.iter()
.try_fold(private_key.len(), |total, cert| {
total
.checked_add(cert.len())
.ok_or(KeyStoreError::Selection("key material size overflow"))
})?;
let retained_candidates = 1_usize
.checked_add(certificates.len())
.ok_or(KeyStoreError::Selection("key candidate count overflow"))?;
let remaining_candidates = resources.max_key_candidates - self.entry_count;
if retained_candidates > remaining_candidates {
return Err(crate::policy::PolicyViolation::ResourceLimitExceeded {
resource: crate::policy::resource_name::KEY_CANDIDATES,
maximum: remaining_candidates,
}
.into());
}
self.reserve_material(
named_material_length(&name, decoded_bytes.max(encoded_len), 1)?,
resources,
)?;
self.private_keys.push(StoredPrivateKey {
name,
pkcs8_der: private_key,
usages,
certificate_chain: certificates,
has_matching_leaf,
});
self.entry_count += retained_candidates;
Ok(())
}
fn pkcs12_import_limits(
&self,
name: &str,
bytes: &[u8],
usages: KeyUsages,
resources: &ResourcePolicy,
) -> Result<Pkcs12Limits, KeyStoreError> {
self.check_new_name(name, resources)?;
validate_private_key_usages(usages)?;
if bytes.len() > resources.max_external_resource_bytes {
return Err(KeyStoreError::Policy(
crate::policy::PolicyViolation::ResourceLimitExceeded {
resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
maximum: resources.max_external_resource_bytes,
},
));
}
let retained_with_input =
self.check_material_capacity(named_material_length(name, bytes.len(), 1)?, resources)?;
let remaining_candidates = resources.max_key_candidates - self.entry_count;
Ok(Pkcs12Limits {
resources: resources.clone(),
candidates: remaining_candidates,
memory_available: resources.max_external_resource_total_bytes - retained_with_input,
})
}
fn check_new_name(&self, name: &str, resources: &ResourcePolicy) -> Result<(), KeyStoreError> {
ensure_resource_policy(resources)?;
if name.is_empty() {
return Err(KeyStoreError::Selection("empty key name"));
}
if self.entry_count >= resources.max_key_candidates {
return Err(import_resource_limit(
crate::policy::resource_name::KEY_CANDIDATES,
resources.max_key_candidates,
));
}
if name.len() > resources.max_external_resource_bytes {
return Err(import_resource_limit(
crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
resources.max_external_resource_bytes,
));
}
self.check_material_capacity(name.len(), resources)?;
if self.symmetric_keys.iter().any(|key| key.name == name)
|| self.public_keys.iter().any(|key| key.name == name)
|| self.private_keys.iter().any(|key| key.name == name)
{
return Err(KeyStoreError::Selection("duplicate key name"));
}
Ok(())
}
fn remaining_kdf_resources(
&self,
resources: &ResourcePolicy,
) -> Result<ResourcePolicy, KeyStoreError> {
if self.kdf_work > resources.max_key_import_kdf_work {
return Err(import_resource_limit(
crate::policy::resource_name::KEY_IMPORT_KDF_WORK,
resources.max_key_import_kdf_work,
));
}
let mut remaining = resources.clone();
remaining.max_key_import_kdf_work -= self.kdf_work;
Ok(remaining)
}
fn check_material_capacity(
&self,
length: usize,
resources: &ResourcePolicy,
) -> Result<usize, KeyStoreError> {
let total = self.material_bytes.checked_add(length).ok_or_else(|| {
import_resource_limit(
crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
resources.max_external_resource_total_bytes,
)
})?;
if total > resources.max_external_resource_total_bytes {
return Err(import_resource_limit(
crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
resources.max_external_resource_total_bytes,
));
}
Ok(total)
}
fn reserve_material(
&mut self,
length: usize,
resources: &ResourcePolicy,
) -> Result<(), KeyStoreError> {
self.material_bytes = self.check_material_capacity(length, resources)?;
Ok(())
}
pub fn add_certificate_der(
&mut self,
der: Vec<u8>,
trusted_anchor: bool,
resources: &ResourcePolicy,
) -> Result<(), KeyStoreError> {
let material_bytes = self.preflight_x509_import(&der, resources)?;
let session = crate::xmldsig::pkix::CertificateSession::new(resources);
drop(session.read(&der).map_err(certificate_import_error)?);
self.material_bytes = material_bytes;
if trusted_anchor {
self.trusted_certificates.push(der);
} else {
self.lookup_certificates.push(der);
}
self.entry_count += 1;
Ok(())
}
pub fn add_crl_der(
&mut self,
der: Vec<u8>,
resources: &ResourcePolicy,
) -> Result<(), KeyStoreError> {
let material_bytes = self.preflight_x509_import(&der, resources)?;
let session = crate::xmldsig::pkix::CertificateSession::new(resources);
drop(session.read_crl(&der).map_err(|error| match error {
crate::xmldsig::X509ChainError::Policy(error) => KeyStoreError::Policy(error),
_ => KeyStoreError::Selection("invalid X.509 CRL"),
})?);
self.material_bytes = material_bytes;
self.crls.push(der);
self.entry_count += 1;
Ok(())
}
fn preflight_x509_import(
&self,
der: &[u8],
resources: &ResourcePolicy,
) -> Result<usize, KeyStoreError> {
ensure_resource_policy(resources)?;
if der.len() > resources.max_external_resource_bytes {
return Err(import_resource_limit(
crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
resources.max_external_resource_bytes,
));
}
if self.entry_count >= resources.max_key_candidates {
return Err(import_resource_limit(
crate::policy::resource_name::KEY_CANDIDATES,
resources.max_key_candidates,
));
}
self.check_material_capacity(der.len(), resources)
}
pub fn from_xml_bytes<P: crate::document::XmlDocumentPolicy>(
bytes: &[u8],
policy: &P,
backend: XmlBackend,
) -> Result<Self, KeyStoreError> {
let budget = XmlParseWorkBudget::from_resources(policy.resource_policy());
Self::from_xml_bytes_with_budget(bytes, policy, backend, &budget, &mut 0, 0)
}
fn from_xml_bytes_with_budget<P: crate::document::XmlDocumentPolicy>(
bytes: &[u8],
policy: &P,
backend: XmlBackend,
budget: &XmlParseWorkBudget,
inspected: &mut usize,
live_material: usize,
) -> Result<Self, KeyStoreError> {
let resources = policy.resource_policy();
ensure_resource_policy(resources)?;
if bytes.len() > resources.max_external_resource_bytes {
return Err(import_resource_limit(
crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
resources.max_external_resource_bytes,
));
}
if bytes.len() > resources.max_external_resource_total_bytes - live_material {
return Err(import_resource_limit(
crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
resources.max_external_resource_total_bytes,
));
}
let settings = DocumentParseSettings::from_policy(policy.xml_input_policy(), resources)
.with_backend(backend);
let text = crate::document::decode_xml_with_budget(
bytes,
resources
.max_xml_document_bytes
.min(resources.max_external_resource_bytes),
Some(budget),
)
.map_err(|error| match error.into_policy_violation(settings) {
Ok(violation) => KeyStoreError::Policy(violation),
Err(error) => KeyStoreError::Invalid(error.to_string()),
})?;
let document = parse_borrowed_with_settings_and_budget(&text, settings, Some(budget))
.map_err(|error| match error.into_policy_violation(settings) {
Ok(violation) => KeyStoreError::Policy(violation),
Err(error) => KeyStoreError::Invalid(error.to_string()),
})?;
let root = document.root_element();
if !root.has_tag_name((XMLSEC_NS, "Keys")) {
return Err(KeyStoreError::Invalid("expected xmlsec Keys root".into()));
}
let mut material_budget = XmlImportMaterialBudget {
used: live_material + bytes.len(),
maximum: resources.max_external_resource_total_bytes,
};
let mut names = HashSet::new();
let mut store = Self::default();
let mut entry_count = 0_usize;
let session = crate::xmldsig::pkix::CertificateSession::new(resources);
for info in root.children().filter(|child| child.is_element()) {
if !info.has_tag_name((XMLDSIG_NS, "KeyInfo")) {
return Err(KeyStoreError::Invalid("unexpected child of Keys".into()));
}
if *inspected >= resources.max_key_candidates {
return Err(import_resource_limit(
crate::policy::resource_name::KEY_CANDIDATES,
resources.max_key_candidates,
));
}
*inspected += 1;
entry_count += 1;
preflight_xml_material(info, &mut material_budget)?;
let mut name = None;
let mut value = None;
for child in info.children().filter(|child| child.is_element()) {
if child.has_tag_name((XMLDSIG_NS, "KeyName")) {
if name.is_some() {
return Err(KeyStoreError::Invalid("duplicate KeyName".into()));
}
let text = element_text(child)?;
if text.is_empty() {
return Err(KeyStoreError::Invalid("empty KeyName".into()));
}
name = Some(text);
} else if child.has_tag_name((XMLDSIG_NS, "KeyValue")) {
if value.is_some() {
return Err(KeyStoreError::Invalid("duplicate KeyValue".into()));
}
let mut values = child.children().filter(|node| node.is_element());
let key = values.next().ok_or_else(|| {
KeyStoreError::Invalid("KeyValue has no key material".into())
})?;
if values.next().is_some() {
return Err(KeyStoreError::Invalid("ambiguous KeyValue".into()));
}
let material = if key.has_tag_name((XMLSEC_NS, "HMACKeyValue")) {
Some(SymmetricKeyKind::Hmac)
} else if key.has_tag_name((XMLSEC_NS, "AESKeyValue")) {
Some(SymmetricKeyKind::Aes)
} else if key.has_tag_name((XMLSEC_NS, "CamelliaKeyValue")) {
Some(SymmetricKeyKind::Camellia)
} else if key.has_tag_name((XMLSEC_NS, "ChaCha20KeyValue")) {
Some(SymmetricKeyKind::ChaCha20)
} else if key.has_tag_name((XMLSEC_NS, "DESKeyValue")) {
Some(SymmetricKeyKind::Des)
} else {
None
};
value = Some(if let Some(kind) = material {
ParsedMaterial::Symmetric(kind, decode_xml_base64_secret(key)?)
} else if key.has_tag_name((XMLDSIG_NS, "DSAKeyValue")) {
let (public, private) =
parse_xmlsec_dsa_key_value(key, &mut material_budget)?;
ParsedMaterial::Dsa(public, private)
} else if key.has_tag_name((XMLDSIG_NS, "RSAKeyValue"))
|| key.has_tag_name((XMLDSIG11_NS, "ECKeyValue"))
{
ParsedMaterial::Public(
crate::xmldsig::parse::parse_key_value_dispatch(child)
.map_err(|error| KeyStoreError::Invalid(error.to_string()))?,
)
} else {
ParsedMaterial::Unsupported
});
} else if child.has_tag_name((XMLDSIG11_NS, "DEREncodedKeyValue")) {
if value.is_some() {
return Err(KeyStoreError::Invalid("ambiguous key material".into()));
}
value = Some(ParsedMaterial::PublicDer(decode_xml_base64(child)?));
} else {
return Err(KeyStoreError::Invalid("unsupported KeyInfo child".into()));
}
}
let name = name.ok_or_else(|| KeyStoreError::Invalid("missing KeyName".into()))?;
let material =
value.ok_or_else(|| KeyStoreError::Invalid("missing KeyValue".into()))?;
if !names.insert(name.clone()) {
return Err(KeyStoreError::Invalid("duplicate key name".into()));
}
match material {
ParsedMaterial::PublicDer(der) => {
store.add_public_der_payload(
name,
der,
None,
resources,
PublicDerFormat::SubjectPublicKeyInfo,
&session,
)?;
}
ParsedMaterial::Symmetric(kind, bytes) => {
if bytes.is_empty() {
return Err(KeyStoreError::Invalid("empty symmetric key".into()));
}
if matches!(kind, SymmetricKeyKind::Aes | SymmetricKeyKind::Camellia)
&& !matches!(bytes.len(), 16 | 24 | 32)
{
return Err(KeyStoreError::Invalid(
"invalid block-cipher key length".into(),
));
}
if kind == SymmetricKeyKind::ChaCha20 && bytes.len() != 32 {
return Err(KeyStoreError::Invalid(
"ChaCha20 requires a 256-bit key".into(),
));
}
let usages = match kind {
SymmetricKeyKind::Hmac => KeyUsages::SIGN.union(KeyUsages::VERIFY),
SymmetricKeyKind::Aes
| SymmetricKeyKind::Camellia
| SymmetricKeyKind::ChaCha20 => {
KeyUsages::ENCRYPT.union(KeyUsages::DECRYPT)
}
SymmetricKeyKind::Des => des_key_usages(&bytes)?,
};
store.symmetric_keys.push(StoredSymmetricKey {
name,
kind,
bytes,
usages,
});
}
ParsedMaterial::Public(value) => {
let mut key_info = KeyInfo::default();
key_info.sources.push(KeyInfoSource::KeyName(name.clone()));
key_info.sources.push(KeyInfoSource::KeyValue(value));
let is_rsa = key_info
.sources
.iter()
.find_map(|source| match source {
KeyInfoSource::KeyValue(value) => Some(value),
_ => None,
})
.ok_or_else(|| KeyStoreError::Invalid("invalid public KeyValue".into()))?;
let is_rsa = crate::xmldsig::keys::supported_key_value_is_rsa(&session, is_rsa)
.map_err(|error| {
key_import_error(error, || {
KeyStoreError::Invalid("invalid public KeyValue".into())
})
})?;
let usages = if is_rsa {
KeyUsages::VERIFY.union(KeyUsages::ENCRYPT)
} else {
KeyUsages::VERIFY
};
store.public_keys.push(StoredPublicKey {
name,
key_info,
usages,
});
}
ParsedMaterial::Dsa(public, private) => {
crate::xmldsig::keys::supported_key_value_is_rsa(&session, &public).map_err(
|error| {
key_import_error(error, || {
KeyStoreError::Invalid("invalid public DSAKeyValue".into())
})
},
)?;
let mut key_info = KeyInfo::default();
key_info.sources.push(KeyInfoSource::KeyName(name.clone()));
key_info.sources.push(KeyInfoSource::KeyValue(public));
if let Some(pkcs8_der) = private {
store.private_keys.push(StoredPrivateKey {
name: name.clone(),
pkcs8_der,
usages: KeyUsages::SIGN,
certificate_chain: Vec::new(),
has_matching_leaf: false,
});
}
store.public_keys.push(StoredPublicKey {
name,
key_info,
usages: KeyUsages::VERIFY,
});
}
ParsedMaterial::Unsupported => {}
}
}
store.entry_count = entry_count;
store.material_bytes = bytes
.len()
.checked_add(store.retained_material_bytes()?)
.ok_or(KeyStoreError::Selection("key material size overflow"))?;
if store.material_bytes > resources.max_external_resource_total_bytes - live_material {
return Err(import_resource_limit(
crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
resources.max_external_resource_total_bytes,
));
}
Ok(store)
}
}
fn key_import_error(
error: crate::xmldsig::KeyResolutionError,
otherwise: impl FnOnce() -> KeyStoreError,
) -> KeyStoreError {
match error {
crate::xmldsig::KeyResolutionError::Chain(crate::xmldsig::X509ChainError::Policy(
error,
)) => KeyStoreError::Policy(error),
_ => otherwise(),
}
}
fn certificate_import_error(error: crate::xmldsig::X509ChainError) -> KeyStoreError {
match error {
crate::xmldsig::X509ChainError::Policy(error) => KeyStoreError::Policy(error),
_ => KeyStoreError::Selection("invalid X.509 certificate"),
}
}
fn validate_private_key_usages(usages: KeyUsages) -> Result<(), KeyStoreError> {
let permitted = KeyUsages::SIGN.union(KeyUsages::DECRYPT);
if usages.0 == 0 || usages.0 & !permitted.0 != 0 {
return Err(KeyStoreError::Selection(
"private key usage is incompatible",
));
}
Ok(())
}
fn import_resource_limit(resource: &'static str, maximum: usize) -> KeyStoreError {
crate::policy::PolicyViolation::ResourceLimitExceeded { resource, maximum }.into()
}
fn check_operation_material_size(
length: usize,
resources: &ResourcePolicy,
) -> Result<(), KeyStoreError> {
if length > resources.max_external_resource_bytes {
return Err(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
maximum: resources.max_external_resource_bytes,
actual: length,
}
.into());
}
if length > resources.max_external_resource_total_bytes {
return Err(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
maximum: resources.max_external_resource_total_bytes,
actual: length,
}
.into());
}
Ok(())
}
#[cfg(feature = "xmlenc")]
fn check_selected_material_size(
length: usize,
resources: &ResourcePolicy,
) -> Result<(), KeyStoreError> {
check_operation_material_size(length, resources)
}
#[cfg(feature = "xmlenc")]
fn check_encryption_material_size(
length: usize,
resources: &ResourcePolicy,
) -> Result<(), KeyStoreError> {
check_operation_material_size(length, resources)
}
#[cfg(feature = "xmlenc")]
fn rsa_recipient_preflight(
modulus: &[u8],
exponent: &[u8],
policy: &crate::policy::EncryptionPolicy,
) -> Result<(), KeyStoreError> {
let combined = modulus
.len()
.checked_add(exponent.len())
.ok_or(KeyStoreError::Selection("encryption key size overflow"))?;
check_encryption_material_size(combined, &policy.resources)?;
policy
.rsa_keys
.validate_components("encryption", modulus, exponent)?;
Ok(())
}
#[cfg(feature = "xmlenc")]
fn rsa_recipient_from_components(
modulus: &[u8],
exponent: &[u8],
policy: &crate::policy::EncryptionPolicy,
) -> Result<RsaPublicKey, KeyStoreError> {
rsa_recipient_preflight(modulus, exponent, policy)?;
let first_nonzero = modulus
.iter()
.position(|byte| *byte != 0)
.ok_or(KeyStoreError::Selection("invalid RSA encryption key"))?;
RsaPublicKey::new(
BoxedUint::from_be_slice_vartime(&modulus[first_nonzero..]),
BoxedUint::from_be_slice_vartime(exponent),
)
.map_err(|_| KeyStoreError::Selection("invalid RSA encryption key"))
}
fn signing_policy_error(error: crate::xmldsig::SigningError) -> KeyStoreError {
match error {
crate::xmldsig::SigningError::Policy(violation) => violation.into(),
_ => KeyStoreError::Selection("signing key violates operation policy"),
}
}
fn ensure_resource_policy(resources: &ResourcePolicy) -> Result<(), KeyStoreError> {
resources.validate().map_err(Into::into)
}
fn find_named_entry<'a, T>(
entries: &'a [T],
name: &str,
resources: &ResourcePolicy,
visited: &mut usize,
entry_name: impl Fn(&T) -> &str,
) -> Result<Option<&'a T>, KeyStoreError> {
for entry in entries {
let next = visited
.checked_add(1)
.ok_or(KeyStoreError::Selection("key candidate count overflow"))?;
resources.validate_key_candidates(next)?;
*visited = next;
if entry_name(entry) == name {
return Ok(Some(entry));
}
}
Ok(None)
}
fn named_material_length(
name: &str,
payload: usize,
retained_names: usize,
) -> Result<usize, KeyStoreError> {
name.len()
.checked_mul(retained_names)
.and_then(|names| names.checked_add(payload))
.ok_or(KeyStoreError::Selection("key material size overflow"))
}
#[cfg_attr(
feature = "experimental-pq",
expect(
clippy::large_enum_variant,
reason = "A transient, wire-bounded 2592-byte public key avoids a heap allocation during identity comparison"
)
)]
enum PrivateKeyIdentity<'a> {
Rsa(pkcs1::RsaPrivateKeyRef<'a>),
#[cfg(feature = "experimental-pq")]
Encapsulation {
algorithm: pkcs8::AlgorithmIdentifierRef<'a>,
public: [u8; 1568],
length: usize,
},
Dsa {
algorithm: pkcs8::AlgorithmIdentifierRef<'a>,
key: NativeDsaSigningKey,
},
Ec {
algorithm: pkcs8::AlgorithmIdentifierRef<'a>,
point: [u8; 133],
length: usize,
},
EdDsa {
algorithm: pkcs8::AlgorithmIdentifierRef<'a>,
public: [u8; 57],
length: usize,
},
#[cfg(feature = "experimental-pq")]
PostQuantum {
algorithm: pkcs8::AlgorithmIdentifierRef<'a>,
public: [u8; 2592],
length: usize,
},
}
impl<'a> PrivateKeyIdentity<'a> {
fn automatic_usages(&self) -> KeyUsages {
match self {
Self::Rsa(_) => KeyUsages::SIGN.union(KeyUsages::DECRYPT),
#[cfg(feature = "experimental-pq")]
Self::Encapsulation { .. } => KeyUsages::DECRYPT,
_ => KeyUsages::SIGN,
}
}
fn can_decrypt(&self) -> bool {
match self {
Self::Rsa(_) => true,
#[cfg(feature = "experimental-pq")]
Self::Encapsulation { .. } => true,
_ => false,
}
}
fn validate_usages(&self, usages: KeyUsages) -> Result<(), KeyStoreError> {
#[cfg(feature = "experimental-pq")]
if matches!(self, Self::Encapsulation { .. }) && usages != KeyUsages::DECRYPT {
return Err(KeyStoreError::Selection(
"ML-KEM private keys require DECRYPT-only usage",
));
}
#[cfg(not(feature = "experimental-pq"))]
let _ = usages;
Ok(())
}
fn decode(der: &'a [u8]) -> Result<Self, KeyStoreError> {
let info = PrivateKeyInfoRef::try_from(der)
.map_err(|_| KeyStoreError::Selection("unsupported PKCS#12 private key"))?;
#[cfg(feature = "experimental-pq")]
if crate::provider::KeyEncapsulationAlgorithm::from_oid(info.algorithm.oid).is_some() {
let key = crate::provider::RustCryptoMlKemPrivateKey::from_pkcs8_der(der)
.map_err(|_| KeyStoreError::Selection("invalid ML-KEM private key"))?;
let mut public = [0; 1568];
let length = key
.copy_public(&mut public)
.map_err(|_| KeyStoreError::Selection("invalid ML-KEM identity"))?;
return Ok(Self::Encapsulation {
algorithm: info.algorithm,
public,
length,
});
}
#[cfg(feature = "experimental-pq")]
if let Some(parameter) = crate::xmldsig::PqAlgorithm::from_oid(info.algorithm.oid) {
let key = crate::xmldsig::PostQuantumSigningKey::from_pkcs8_der(parameter, der)
.map_err(|_| KeyStoreError::Selection("invalid PQ private key"))?;
let mut public = [0; 2592];
let length = key.copy_public(&mut public);
return Ok(Self::PostQuantum {
algorithm: info.algorithm,
public,
length,
});
}
if info.algorithm.oid == der::asn1::ObjectIdentifier::new_unwrap("1.3.101.112") {
let key = ed25519_dalek::SigningKey::from_pkcs8_der(der)
.map_err(|_| KeyStoreError::Selection("invalid Ed25519 private key"))?;
let mut public = [0; 57];
public[..32].copy_from_slice(&key.verifying_key().to_bytes());
return Ok(Self::EdDsa {
algorithm: info.algorithm,
public,
length: 32,
});
}
if info.algorithm.oid == der::asn1::ObjectIdentifier::new_unwrap("1.3.101.113") {
let key = ed448_goldilocks::SigningKey::from_pkcs8_der(der)
.map_err(|_| KeyStoreError::Selection("invalid Ed448 private key"))?;
return Ok(Self::EdDsa {
algorithm: info.algorithm,
public: key.verifying_key().to_bytes(),
length: 57,
});
}
if info.algorithm.oid == pkcs1::ALGORITHM_OID {
preflight_rsa_pkcs1_components(info.private_key.as_bytes())?;
RsaPrivateKey::from_pkcs8_der(der)
.map_err(|_| KeyStoreError::Selection("invalid RSA private key"))?;
return pkcs1::RsaPrivateKeyRef::from_der(info.private_key.as_bytes())
.map(Self::Rsa)
.map_err(|_| KeyStoreError::Selection("invalid RSA private key"));
} else if info.algorithm.oid == dsa::OID {
let algorithm = info.algorithm;
return crate::xmldsig::sign::decode_dsa_signing_key(info)
.map(|key| Self::Dsa { algorithm, key })
.map_err(|_| KeyStoreError::Selection("invalid or oversized DSA private key"));
}
macro_rules! try_ec {
($key:ty) => {
if let Ok(key) = <$key>::from_pkcs8_der(der) {
use p256::elliptic_curve::sec1::ToSec1Point as _;
let encoded = key.public_key().to_sec1_point(false);
let bytes = encoded.as_bytes();
let mut point = [0; 133];
point[..bytes.len()].copy_from_slice(bytes);
return Ok(Self::Ec {
algorithm: info.algorithm,
point,
length: bytes.len(),
});
}
};
}
try_ec!(p256::SecretKey);
try_ec!(p384::SecretKey);
try_ec!(p521::SecretKey);
Err(KeyStoreError::Selection("unsupported PKCS#12 private key"))
}
fn matches_spki(&self, der: &[u8]) -> bool {
let Ok(spki) = pkcs8::SubjectPublicKeyInfoRef::from_der(der) else {
return false;
};
let Some(bytes) = spki.subject_public_key.as_bytes() else {
return false;
};
match self {
#[cfg(feature = "experimental-pq")]
Self::Encapsulation {
algorithm,
public,
length,
} => spki.algorithm == *algorithm && bytes == &public[..*length],
#[cfg(feature = "experimental-pq")]
Self::PostQuantum {
algorithm,
public,
length,
} => spki.algorithm == *algorithm && bytes == &public[..*length],
Self::EdDsa {
algorithm,
public,
length,
} => spki.algorithm == *algorithm && bytes == &public[..*length],
Self::Rsa(key) => {
if spki.algorithm != pkcs1::ALGORITHM_ID {
return false;
}
let Ok(public) = pkcs1::RsaPublicKeyRef::from_der(bytes) else {
return false;
};
key.modulus == public.modulus && key.public_exponent == public.public_exponent
}
Self::Dsa { algorithm, key } => {
if spki.algorithm != *algorithm {
return false;
}
let Ok(y) = der::asn1::UintRef::from_der(bytes) else {
return false;
};
key.verifying_key()
.y()
.as_words()
.iter()
.rev()
.flat_map(|word| word.to_be_bytes())
.skip_while(|byte| *byte == 0)
.eq(y.as_bytes().iter().copied())
}
Self::Ec {
algorithm,
point,
length,
} => spki.algorithm == *algorithm && bytes == &point[..*length],
}
}
}
pub fn preflight_rsa_pkcs1_components(der: &[u8]) -> Result<(), KeyStoreError> {
let key = pkcs1::RsaPrivateKeyRef::from_der(der)
.map_err(|_| KeyStoreError::Selection("invalid RSA private key"))?;
let modulus = key.modulus.as_bytes();
let maximum = crate::hard_limits::RSA_MODULUS_BIT_CEILING;
if modulus.len() > maximum.div_ceil(8)
|| modulus
.first()
.is_some_and(|first| modulus.len() * 8 - first.leading_zeros() as usize > maximum)
{
return Err(KeyStoreError::Selection("RSA modulus exceeds safety limit"));
}
if [
key.public_exponent,
key.private_exponent,
key.prime1,
key.prime2,
key.exponent1,
key.exponent2,
key.coefficient,
]
.into_iter()
.any(|component| component.as_bytes().len() > maximum.div_ceil(8))
|| key.other_prime_infos.as_ref().is_some_and(|infos| {
infos.iter().any(|info| {
[info.prime, info.exponent, info.coefficient]
.into_iter()
.any(|component| component.as_bytes().len() > maximum.div_ceil(8))
})
})
{
return Err(KeyStoreError::Selection(
"RSA component exceeds safety limit",
));
}
Ok(())
}
fn preflight_dsa_pkcs8_components(info: &PrivateKeyInfoRef<'_>) -> Result<(), KeyStoreError> {
crate::xmldsig::sign::preflight_dsa_private_key(info)
.map_err(|_| KeyStoreError::Selection("invalid or oversized DSA private key"))
}
struct BorrowedPem<'a> {
label: &'a str,
data: &'a str,
decoded_len: usize,
}
impl<'a> BorrowedPem<'a> {
fn parse(bytes: &'a [u8], maximum: usize) -> Result<Self, KeyStoreError> {
if bytes.len() > maximum {
return Err(import_resource_limit(
crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
maximum,
));
}
let invalid = || KeyStoreError::Selection("invalid PEM key");
let text = std::str::from_utf8(bytes)
.map_err(|_| invalid())?
.trim_matches(|character: char| character.is_ascii_whitespace());
let (label, rest) = text
.strip_prefix("-----BEGIN ")
.and_then(|rest| rest.split_once("-----"))
.ok_or_else(invalid)?;
let (payload, end) = rest
.trim_start_matches([' ', '\t', '\n', '\r'])
.split_once("-----END ")
.ok_or_else(invalid)?;
if end.strip_prefix(label) != Some("-----") || payload.contains("-----BEGIN ") {
return Err(KeyStoreError::Selection(
"PEM must contain one complete block",
));
}
let data = if let Some((headers, data)) = payload
.split_once("\n\n")
.or_else(|| payload.split_once("\r\n\r\n"))
{
if headers.lines().any(|line| !line.contains(':')) {
return Err(invalid());
}
data
} else {
payload
};
let mut characters = 0_usize;
let mut padding = 0_usize;
for part in data.split_whitespace() {
for byte in part.bytes() {
if byte == b'=' {
padding += 1;
} else if padding != 0
|| !matches!(byte, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'+' | b'/')
{
return Err(invalid());
}
characters += 1;
}
}
if !characters.is_multiple_of(4) || padding > 2 {
return Err(invalid());
}
let decoded_len = (characters / 4 * 3)
.checked_sub(padding)
.ok_or_else(invalid)?;
Ok(Self {
label,
data,
decoded_len,
})
}
fn decode(&self) -> Result<Vec<u8>, KeyStoreError> {
let mut decoded = vec![0; self.decoded_len];
self.decode_into(&mut decoded)?;
Ok(decoded)
}
fn decode_into(&self, decoded: &mut [u8]) -> Result<(), KeyStoreError> {
use std::io::Read as _;
let input = PemBase64Reader {
parts: self.data.split_whitespace(),
current: &[],
};
let mut decoder =
base64::read::DecoderReader::new(input, &base64::engine::general_purpose::STANDARD);
decoder
.read_exact(decoded)
.map_err(|_| KeyStoreError::Selection("invalid PEM key"))?;
let mut eof = [0];
if decoder
.read(&mut eof)
.map_err(|_| KeyStoreError::Selection("invalid PEM key"))?
!= 0
{
return Err(KeyStoreError::Selection("invalid PEM key"));
}
Ok(())
}
}
struct PemBase64Reader<'a> {
parts: std::str::SplitWhitespace<'a>,
current: &'a [u8],
}
impl std::io::Read for PemBase64Reader<'_> {
fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
let mut written = 0;
while written < buffer.len() {
if self.current.is_empty() {
let Some(part) = self.parts.next() else {
break;
};
self.current = part.as_bytes();
}
let length = self.current.len().min(buffer.len() - written);
buffer[written..written + length].copy_from_slice(&self.current[..length]);
self.current = &self.current[length..];
written += length;
}
Ok(written)
}
}
#[cfg(test)]
fn single_pem_block(bytes: &[u8], maximum: usize) -> Result<pem::Pem, KeyStoreError> {
if bytes.len() > maximum {
return Err(import_resource_limit(
crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
maximum,
));
}
let text = std::str::from_utf8(bytes)
.map_err(|_| KeyStoreError::Selection("PEM key is not ASCII text"))?
.trim_matches(|character: char| character.is_ascii_whitespace());
let block = pem::parse(text).map_err(|_| KeyStoreError::Selection("invalid PEM key"))?;
let begin = format!("-----BEGIN {}-----", block.tag());
let end = format!("-----END {}-----", block.tag());
if !text.starts_with(&begin)
|| !text.ends_with(&end)
|| text.matches(&begin).count() != 1
|| text.matches(&end).count() != 1
{
return Err(KeyStoreError::Selection(
"PEM must contain one complete block",
));
}
Ok(block)
}
fn enforce_pkcs8_kdf_policy(
encrypted: &EncryptedPrivateKeyInfoRef<'_>,
resources: &ResourcePolicy,
retained_with_input: usize,
) -> Result<usize, KeyStoreError> {
use pkcs8::pkcs5::{EncryptionScheme, pbes2::Kdf};
let EncryptionScheme::Pbes2(params) = &encrypted.encryption_algorithm else {
return Err(KeyStoreError::ProtectedContainer);
};
let live_with_output =
retained_with_input.checked_add(encrypted.encrypted_data.as_bytes().len());
if live_with_output.is_none_or(|total| total > resources.max_external_resource_total_bytes) {
return Err(kdf_policy_violation(
crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
resources.max_external_resource_total_bytes,
live_with_output.map(|total| total as u64),
));
}
let live_with_output = live_with_output.ok_or(KeyStoreError::ProtectedContainer)?;
let work = match ¶ms.kdf {
Kdf::Pbkdf2(kdf) => {
if kdf.iteration_count == 0 {
return Err(KeyStoreError::ProtectedContainer);
}
use pkcs8::pkcs5::pbes2::Pbkdf2Prf;
let hash_len = match kdf.prf {
Pbkdf2Prf::HmacWithSha1 => 20,
Pbkdf2Prf::HmacWithSha224 => 28,
Pbkdf2Prf::HmacWithSha256 => 32,
Pbkdf2Prf::HmacWithSha384 => 48,
Pbkdf2Prf::HmacWithSha512 => 64,
_ => return Err(KeyStoreError::ProtectedContainer),
};
let blocks = params.encryption.key_size().div_ceil(hash_len) as u64;
let work = u64::from(kdf.iteration_count).checked_mul(blocks);
if work.is_none_or(|work| work > resources.max_key_import_kdf_work as u64) {
return Err(kdf_policy_violation(
crate::policy::resource_name::KEY_IMPORT_KDF_WORK,
resources.max_key_import_kdf_work,
work,
));
}
work.ok_or(KeyStoreError::ProtectedContainer)? as usize
}
Kdf::Scrypt(kdf) => enforce_scrypt_kdf_limits(
kdf.cost_parameter,
u64::from(kdf.block_size),
u64::from(kdf.parallelization),
resources,
live_with_output,
)?,
_ => return Err(KeyStoreError::ProtectedContainer),
};
Ok(work)
}
fn enforce_pkcs8_password_policy(
encrypted: &EncryptedPrivateKeyInfoRef<'_>,
password: &[u8],
resources: &ResourcePolicy,
live_bytes: usize,
) -> Result<usize, KeyStoreError> {
if password.len() > resources.max_external_resource_bytes {
return Err(kdf_policy_violation(
crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
resources.max_external_resource_bytes,
Some(password.len() as u64),
));
}
let live = live_bytes.checked_add(password.len()).ok_or_else(|| {
import_resource_limit(
crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
resources.max_external_resource_total_bytes,
)
})?;
let hashing_passes = match &encrypted.encryption_algorithm {
pkcs8::pkcs5::EncryptionScheme::Pbes2(params)
if matches!(¶ms.kdf, pkcs8::pkcs5::pbes2::Kdf::Scrypt(_)) =>
{
2
}
_ => 1,
};
let password_work = password.len().div_ceil(64) * hashing_passes;
if password_work > resources.max_key_import_kdf_work {
return Err(kdf_policy_violation(
crate::policy::resource_name::KEY_IMPORT_KDF_WORK,
resources.max_key_import_kdf_work,
Some(password_work as u64),
));
}
let mut remaining = resources.clone();
remaining.max_key_import_kdf_work -= password_work;
enforce_pkcs8_kdf_policy(encrypted, &remaining, live)
.map(|work| work + password_work)
.map_err(|error| match error {
KeyStoreError::Policy(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::KEY_IMPORT_KDF_WORK,
actual,
..
}) => kdf_policy_violation(
crate::policy::resource_name::KEY_IMPORT_KDF_WORK,
resources.max_key_import_kdf_work,
Some(actual.saturating_add(password_work) as u64),
),
error => error,
})
}
fn enforce_scrypt_kdf_limits(
n: u64,
r: u64,
p: u64,
resources: &ResourcePolicy,
retained_with_input: usize,
) -> Result<usize, KeyStoreError> {
if n == 0 || r == 0 || p == 0 {
return Err(KeyStoreError::ProtectedContainer);
}
let work = n.checked_mul(r).and_then(|value| value.checked_mul(p));
if work.is_none_or(|value| value > resources.max_key_import_kdf_work as u64) {
return Err(kdf_policy_violation(
crate::policy::resource_name::KEY_IMPORT_KDF_WORK,
resources.max_key_import_kdf_work,
work,
));
}
let memory = n
.checked_add(2)
.and_then(|blocks| blocks.checked_mul(p))
.and_then(|blocks| blocks.checked_mul(r))
.and_then(|blocks| blocks.checked_mul(128));
if memory.is_none_or(|value| value > resources.max_key_import_kdf_memory_bytes as u64) {
return Err(kdf_policy_violation(
crate::policy::resource_name::KEY_IMPORT_KDF_MEMORY,
resources.max_key_import_kdf_memory_bytes,
memory,
));
}
let total = memory.and_then(|memory| memory.checked_add(retained_with_input as u64));
if total.is_none_or(|total| total > resources.max_external_resource_total_bytes as u64) {
return Err(kdf_policy_violation(
crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
resources.max_external_resource_total_bytes,
total,
));
}
Ok(work.ok_or(KeyStoreError::ProtectedContainer)? as usize)
}
fn kdf_policy_violation(
resource: &'static str,
maximum: usize,
actual: Option<u64>,
) -> KeyStoreError {
KeyStoreError::Policy(crate::policy::PolicyViolation::ResourceLimit {
resource,
maximum,
actual: actual
.and_then(|value| usize::try_from(value).ok())
.unwrap_or(usize::MAX),
})
}
fn element_text(node: Node<'_, '_>) -> Result<String, KeyStoreError> {
let length = node
.children()
.filter(|child| child.is_text())
.filter_map(|child| child.text())
.map(str::len)
.sum();
let mut text = String::with_capacity(length);
for child in node.children() {
if child.is_element() {
return Err(KeyStoreError::Invalid("unexpected nested element".into()));
}
if child.is_text() {
text.push_str(child.text().unwrap_or_default());
}
}
Ok(text)
}
fn decode_xml_base64(node: Node<'_, '_>) -> Result<Vec<u8>, KeyStoreError> {
xml_base64_payload(node)?
.decode()
.map_err(|reason| KeyStoreError::Invalid(reason.into()))
}
fn decode_xml_base64_secret(node: Node<'_, '_>) -> Result<Zeroizing<Vec<u8>>, KeyStoreError> {
let payload = xml_base64_payload(node)?;
let mut output = Zeroizing::new(vec![0; payload.decoded_len]);
payload
.decode_into(&mut output)
.map_err(|reason| KeyStoreError::Invalid(reason.into()))?;
Ok(output)
}
fn xml_base64_payload<'a, 'input>(
node: Node<'a, 'input>,
) -> Result<crate::xmldsig::whitespace::XmlBase64Payload<'a, 'input>, KeyStoreError> {
crate::xmldsig::whitespace::XmlBase64Payload::new(node)
.map_err(|reason| KeyStoreError::Invalid(reason.into()))
}
struct XmlImportMaterialBudget {
used: usize,
maximum: usize,
}
impl XmlImportMaterialBudget {
fn reserve(&mut self, length: usize) -> Result<(), KeyStoreError> {
if self.used > self.maximum || length > self.maximum - self.used {
return Err(import_resource_limit(
crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
self.maximum,
));
}
self.used += length;
Ok(())
}
}
fn preflight_xml_material(
info: Node<'_, '_>,
budget: &mut XmlImportMaterialBudget,
) -> Result<(), KeyStoreError> {
let key = info
.children()
.find(|child| child.has_tag_name((XMLDSIG_NS, "KeyValue")))
.and_then(|value| value.children().find(|child| child.is_element()));
let has_der = info
.children()
.any(|child| child.has_tag_name((XMLDSIG11_NS, "DEREncodedKeyValue")));
let name_copies = match key {
Some(key) if key.has_tag_name((XMLDSIG_NS, "DSAKeyValue")) => {
if key
.children()
.any(|child| child.has_tag_name((XMLSEC_NS, "X")))
{
4
} else {
3
}
}
Some(key)
if key.has_tag_name((XMLDSIG_NS, "RSAKeyValue"))
|| key.has_tag_name((XMLDSIG11_NS, "ECKeyValue")) =>
{
3
}
_ if has_der => 3,
_ => 2,
};
let uses_shared_public_parser = has_der
|| key.is_some_and(|key| {
key.has_tag_name((XMLDSIG_NS, "RSAKeyValue"))
|| key.has_tag_name((XMLDSIG11_NS, "ECKeyValue"))
});
if uses_shared_public_parser {
crate::xmldsig::parse::validate_key_info_container(info)
.map_err(|error| KeyStoreError::Invalid(error.to_string()))?;
}
for child in info.children().filter(|child| child.is_element()) {
if child.has_tag_name((XMLDSIG_NS, "KeyName")) {
let length = child
.children()
.filter(|node| node.is_text())
.filter_map(|node| node.text())
.try_fold(0_usize, |sum, text| {
sum.checked_add(text.len())
.ok_or(KeyStoreError::Selection("key material size overflow"))
})?;
if uses_shared_public_parser && length > crate::xmldsig::parse::MAX_KEY_NAME_TEXT_LEN {
return Err(KeyStoreError::Invalid(
"KeyName exceeds maximum allowed text length".into(),
));
}
for _ in 0..name_copies {
budget.reserve(length)?;
}
} else if child.has_tag_name((XMLDSIG11_NS, "DEREncodedKeyValue")) {
let payload = xml_base64_payload(child)?;
budget.reserve(payload.decoded_len)?;
} else if child.has_tag_name((XMLDSIG_NS, "KeyValue")) {
for key in child.children().filter(|node| node.is_element()) {
if key.has_tag_name((XMLSEC_NS, "HMACKeyValue"))
|| key.has_tag_name((XMLSEC_NS, "AESKeyValue"))
|| key.has_tag_name((XMLSEC_NS, "DESKeyValue"))
{
budget.reserve(xml_base64_payload(key)?.decoded_len)?;
} else if key.has_tag_name((XMLDSIG_NS, "RSAKeyValue"))
|| key.has_tag_name((XMLDSIG_NS, "DSAKeyValue"))
|| key.has_tag_name((XMLDSIG11_NS, "ECKeyValue"))
{
for component in key.children().filter(|node| node.is_element()) {
if component.has_tag_name((XMLDSIG11_NS, "NamedCurve")) {
budget.reserve(component.attribute("URI").map_or(0, str::len))?;
} else {
budget.reserve(xml_base64_payload(component)?.decoded_len)?;
}
}
}
}
}
}
Ok(())
}
fn parse_xmlsec_dsa_key_value(
node: Node<'_, '_>,
budget: &mut XmlImportMaterialBudget,
) -> Result<ParsedDsaKey, KeyStoreError> {
let mut p = None;
let mut q = None;
let mut g = None;
let mut y = None;
let mut seed = None;
let mut counter = None;
let mut private_x = None;
let mut previous_position = None;
for child in node.children().filter(|child| child.is_element()) {
let (position, slot) = if child.has_tag_name((XMLDSIG_NS, "P")) {
(0, Some(&mut p))
} else if child.has_tag_name((XMLDSIG_NS, "Q")) {
(1, Some(&mut q))
} else if child.has_tag_name((XMLDSIG_NS, "G")) {
(2, Some(&mut g))
} else if child.has_tag_name((XMLDSIG_NS, "Y")) {
(4, Some(&mut y))
} else if child.has_tag_name((XMLSEC_NS, "X")) {
if private_x.is_some() {
return Err(KeyStoreError::Invalid("duplicate DSA X".into()));
}
private_x = Some(decode_xml_base64_secret(child)?);
(3, None)
} else if child.has_tag_name((XMLDSIG_NS, "J")) {
(5, None)
} else if child.has_tag_name((XMLDSIG_NS, "Seed")) {
(6, Some(&mut seed))
} else if child.has_tag_name((XMLDSIG_NS, "PgenCounter")) {
(7, Some(&mut counter))
} else {
return Err(KeyStoreError::Invalid(
"unsupported DSAKeyValue child".into(),
));
};
if previous_position.is_some_and(|previous| position <= previous) {
return Err(KeyStoreError::Invalid(
"DSA parameters are out of order".into(),
));
}
previous_position = Some(position);
if let Some(slot) = slot {
if slot.replace(decode_xml_base64(child)?).is_some() {
return Err(KeyStoreError::Invalid("duplicate DSA parameter".into()));
}
} else if position == 5 {
let _ = decode_xml_base64(child)?;
}
}
if p.is_some() != q.is_some() {
return Err(KeyStoreError::Invalid(
"DSA P and Q must occur together".into(),
));
}
if seed.is_some() != counter.is_some() {
return Err(KeyStoreError::Invalid(
"DSA Seed and PgenCounter must occur together".into(),
));
}
if [p.as_ref(), q.as_ref(), g.as_ref(), y.as_ref()]
.into_iter()
.flatten()
.any(|component| component.len() > crate::hard_limits::DSA_KEY_COMPONENT_BYTE_CEILING)
|| private_x.as_ref().is_some_and(|component| {
component.len() > crate::hard_limits::DSA_KEY_COMPONENT_BYTE_CEILING
})
{
return Err(KeyStoreError::Invalid(
"DSA component exceeds safety limit".into(),
));
}
let y = y.ok_or_else(|| KeyStoreError::Invalid("DSAKeyValue requires Y".into()))?;
let private =
if let Some(x) = private_x {
let (Some(p), Some(q), Some(g)) = (&p, &q, &g) else {
return Err(KeyStoreError::Invalid(
"private DSA key requires P, Q, and G".into(),
));
};
let components = DsaComponents::from_components(
BoxedUint::from_be_slice_vartime(p),
BoxedUint::from_be_slice_vartime(q),
BoxedUint::from_be_slice_vartime(g),
)
.map_err(|_| KeyStoreError::Invalid("invalid DSA parameters".into()))?;
let x_value = BoxedUint::from_be_slice_vartime(&x);
let monty = BoxedMontyParams::new(components.p().clone());
let expected_y = BoxedMontyForm::new((**components.g()).clone(), &monty)
.pow(&x_value)
.retrieve();
if expected_y != BoxedUint::from_be_slice_vartime(&y) {
return Err(KeyStoreError::Invalid(
"DSA private and public values differ".into(),
));
}
let public = DsaVerifyingKey::from_components(components, expected_y)
.map_err(|_| KeyStoreError::Invalid("invalid DSA public key".into()))?;
let private = NativeDsaSigningKey::from_components(public, x_value)
.map_err(|_| KeyStoreError::Invalid("invalid DSA private key".into()))?;
use der::Encode as _;
let parameters_len =
usize::try_from(private.verifying_key().components().encoded_len().map_err(
|_| KeyStoreError::Invalid("DSA private key encoding failed".into()),
)?)
.map_err(|_| KeyStoreError::Selection("key material size overflow"))?;
let x_bytes_len = usize::try_from(private.x().bits_precision() / 8)
.map_err(|_| KeyStoreError::Selection("key material size overflow"))?;
let header = 2 + std::mem::size_of::<usize>();
let x_der_len = x_bytes_len
.checked_add(header + 1)
.ok_or(KeyStoreError::Selection("key material size overflow"))?;
let pkcs8_len = parameters_len
.checked_add(x_der_len)
.and_then(|length| length.checked_add(4 * header + 16))
.ok_or(KeyStoreError::Selection("key material size overflow"))?;
budget.reserve(parameters_len)?;
budget.reserve(x_bytes_len)?;
budget.reserve(x_der_len)?;
budget.reserve(pkcs8_len)?;
budget.reserve(pkcs8_len)?;
Some(Zeroizing::new(
private
.to_pkcs8_der()
.map_err(|_| KeyStoreError::Invalid("DSA private key encoding failed".into()))?
.as_bytes()
.to_vec(),
))
} else {
None
};
Ok((KeyValueInfo::Dsa { p, q, g, y }, private))
}
#[cfg(test)]
mod tests {
#[cfg(not(feature = "xmlenc"))]
use crate::rsa_encoding::RsaPublicKeyEncoding as _;
use pkcs8::EncodePublicKey as _;
use rand_chacha::{ChaCha8Rng, rand_core::SeedableRng as _};
use rsa::RsaPublicKey;
use super::*;
fn xml_policy(resources: ResourcePolicy) -> crate::policy::VerificationPolicy {
crate::policy::VerificationPolicy {
resources,
..crate::policy::VerificationPolicy::default()
}
}
#[test]
fn inventory_merge_preserves_import_work() {
let resources = ResourcePolicy {
max_key_import_kdf_work: 5,
..Default::default()
};
let mut inventory = KeyInventory {
kdf_work: 2,
..Default::default()
};
inventory
.extend(
KeyInventory {
kdf_work: 3,
..Default::default()
},
&resources,
)
.expect("exact work allowance");
assert_eq!(inventory.key_import_kdf_work(), 5);
assert!(matches!(
inventory.extend(
KeyInventory {
kdf_work: 1,
..Default::default()
},
&resources
),
Err(KeyStoreError::Policy(_))
));
assert_eq!(inventory.key_import_kdf_work(), 5, "failed merge is atomic");
}
#[test]
fn repeated_pkcs12_imports_share_kdf_work() {
let bytes = include_bytes!("../tests/fixtures/xmlenc/01-phaos-xmlenc-3/rsa-priv-key.p12");
let resources = ResourcePolicy {
max_key_import_kdf_work: 10_000,
..ResourcePolicy::default()
};
let mut inventory = KeyInventory::default();
inventory
.add_pkcs12("first".into(), bytes, "secret", &resources)
.expect("first bundle fits");
assert!(matches!(
inventory.add_pkcs12("second".into(), bytes, "secret", &resources),
Err(KeyStoreError::Policy(_))
));
let work = inventory.key_import_kdf_work();
let exact = ResourcePolicy {
max_key_import_kdf_work: work * 2,
..resources
};
let mut inventory = KeyInventory::default();
inventory
.add_pkcs12("first".into(), bytes, "secret", &exact)
.expect("first exact-boundary bundle");
inventory
.add_pkcs12("second".into(), bytes, "secret", &exact)
.expect("second exact-boundary bundle");
assert_eq!(inventory.key_import_kdf_work(), work * 2);
}
#[test]
fn imports_donor_pkcs12_and_rejects_wrong_password() {
let bytes = include_bytes!("../tests/fixtures/xmlenc/01-phaos-xmlenc-3/rsa-priv-key.p12");
let resources = ResourcePolicy::default();
let mut inventory = KeyInventory::default();
inventory
.add_pkcs12("phaos".into(), bytes, "secret", &resources)
.expect("donor PKCS#12 should import");
assert_eq!(inventory.private_keys.len(), 1);
assert!(!inventory.private_keys[0].certificate_chain.is_empty());
assert!(inventory.private_keys[0].usages.allows(KeyUsage::Sign));
let mut wrong = KeyInventory::default();
assert!(matches!(
wrong.add_pkcs12("phaos".into(), bytes, "wrong", &resources),
Err(KeyStoreError::ProtectedContainer)
));
assert!(wrong.private_keys.is_empty());
let oversized = ResourcePolicy {
max_external_resource_bytes: bytes.len() - 1,
..ResourcePolicy::default()
};
assert!(matches!(
KeyInventory::default().add_pkcs12("phaos".into(), bytes, "secret", &oversized),
Err(KeyStoreError::Policy(
crate::policy::PolicyViolation::ResourceLimitExceeded {
resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
maximum,
}
)) if maximum == bytes.len() - 1
));
}
#[test]
fn pkcs12_kdf_limit_is_a_typed_policy_denial() {
let bytes = include_bytes!("../tests/fixtures/xmlenc/01-phaos-xmlenc-3/rsa-priv-key.p12");
let resources = ResourcePolicy {
max_key_import_kdf_work: 1,
..ResourcePolicy::default()
};
assert!(matches!(
KeyInventory::default().add_pkcs12("phaos".into(), bytes, "secret", &resources),
Err(KeyStoreError::Policy(
crate::policy::PolicyViolation::KdfIterationsOutsideLimit { maximum: 1 }
))
));
assert!(matches!(
KeyInventory::default().add_pkcs12(
"phaos".into(),
bytes,
"wrong",
&ResourcePolicy::default(),
),
Err(KeyStoreError::ProtectedContainer)
));
}
#[test]
fn pkcs12_visible_encryption_kdf_is_checked_before_password() {
let mut bytes =
include_bytes!("../tests/fixtures/xmlenc/01-phaos-xmlenc-3/rsa-priv-key.p12").to_vec();
let offset = bytes
.windows(4)
.position(|v| v == [2, 2, 8, 0])
.expect("PBKDF2 iterations");
bytes[offset + 3] = 1;
let resources = ResourcePolicy {
max_key_import_kdf_work: 2048,
..ResourcePolicy::default()
};
let result = KeyInventory::default().add_pkcs12_with_password_callback(
"limited".into(),
&bytes,
|| panic!("visible KDF must be checked first"),
KeyUsages::SIGN,
&resources,
);
assert!(matches!(result, Err(KeyStoreError::Policy(_))));
}
#[test]
fn pkcs12_content_count_denial_is_not_a_password_error() {
let bytes = include_bytes!("../tests/fixtures/xmlenc/01-phaos-xmlenc-3/rsa-priv-key.p12");
let resources = ResourcePolicy {
max_key_candidates: 1,
..ResourcePolicy::default()
};
let result = KeyInventory::default().add_pkcs12_with_password_callback(
"limited".into(),
bytes,
|| panic!("container limit must be checked first"),
KeyUsages::SIGN,
&resources,
);
assert!(matches!(
result,
Err(KeyStoreError::Policy(
crate::policy::PolicyViolation::ResourceLimitExceeded {
resource: crate::policy::resource_name::KEY_CANDIDATES,
maximum: 1,
}
))
));
}
#[test]
fn pkcs12_aggregate_kdf_work_preserves_policy_error() {
let resources = ResourcePolicy {
max_key_import_kdf_work: 3000,
..ResourcePolicy::default()
};
let bytes = include_bytes!("../tests/fixtures/xmlenc/01-phaos-xmlenc-3/rsa-priv-key.p12");
assert!(matches!(
KeyInventory::default().add_pkcs12("limited".into(), bytes, "secret", &resources),
Err(KeyStoreError::Policy(
crate::policy::PolicyViolation::ResourceLimitExceeded {
resource: crate::policy::resource_name::KEY_IMPORT_KDF_WORK,
maximum: 3000,
}
))
));
}
#[test]
fn pkcs12_workspace_denial_preserves_policy_error() {
let resources = ResourcePolicy {
max_key_import_kdf_memory_bytes: 1,
..ResourcePolicy::default()
};
let bytes = include_bytes!("../tests/fixtures/xmlenc/01-phaos-xmlenc-3/rsa-priv-key.p12");
assert!(matches!(
KeyInventory::default().add_pkcs12("limited".into(), bytes, "secret", &resources),
Err(KeyStoreError::Policy(
crate::policy::PolicyViolation::ResourceLimitExceeded {
resource: crate::policy::resource_name::KEY_IMPORT_KDF_MEMORY,
maximum: 1,
}
))
));
}
#[test]
fn pkcs12_temporary_memory_shares_existing_inventory_budget() {
let resources = ResourcePolicy {
max_external_resource_bytes: 3000,
max_external_resource_total_bytes: 5000,
..ResourcePolicy::default()
};
let mut inventory = KeyInventory::default();
inventory
.add_symmetric(
"hmac".into(),
SymmetricKeyKind::Hmac,
vec![1; 2000],
KeyUsages::SIGN,
&resources,
)
.expect("existing key fits");
let bytes = include_bytes!("../tests/fixtures/xmlenc/01-phaos-xmlenc-3/rsa-priv-key.p12");
assert!(matches!(
inventory.add_pkcs12("bundle".into(), bytes, "secret", &resources),
Err(KeyStoreError::Policy(
crate::policy::PolicyViolation::ResourceLimitExceeded {
resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
maximum: 5000
}
))
));
assert_eq!(inventory.symmetric_keys().len(), 1);
assert!(inventory.private_keys().is_empty());
}
#[test]
fn private_bundle_certificates_do_not_authorize_verification() {
let bytes = include_bytes!("../tests/fixtures/xmlenc/01-phaos-xmlenc-3/rsa-priv-key.p12");
let resources = ResourcePolicy::default();
let mut inventory = KeyInventory::default();
inventory
.add_pkcs12("private".into(), bytes, "secret", &resources)
.expect("bundle imports");
let leaf = inventory.private_keys()[0].certificate_chain[0].clone();
let subject = crate::xmldsig::parse::parse_x509_certificate(&leaf)
.expect("bundle leaf parses")
.subject_dn;
let key_info = KeyInfo {
sources: vec![KeyInfoSource::X509Data(X509DataInfo {
subject_names: vec![subject],
..X509DataInfo::default()
})],
};
let resolver = inventory.verification_resolver();
assert!(
resolver
.resolve(Some(&key_info), SignatureAlgorithm::RsaSha256)
.expect("lookup completes")
.is_none()
);
inventory
.add_certificate_der(leaf, false, &resources)
.expect("explicit lookup certificate imports");
assert!(
inventory
.verification_resolver()
.resolve(Some(&key_info), SignatureAlgorithm::RsaSha256)
.expect("explicit lookup completes")
.is_some()
);
}
#[test]
fn stored_signing_keys_obey_operation_material_limits() {
let resources = ResourcePolicy::default();
let mut inventory = KeyInventory::default();
inventory
.add_symmetric(
"hmac".into(),
SymmetricKeyKind::Hmac,
b"0123456789abcdef0123456789abcdef".to_vec(),
KeyUsages::SIGN,
&resources,
)
.expect("HMAC imports");
inventory
.add_private_pem(
"rsa".into(),
include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-key.pem"),
None,
KeyUsages::SIGN,
&resources,
)
.expect("RSA imports");
for (name, algorithm, length) in [
(
"hmac",
SignatureAlgorithm::HmacSha256,
inventory.symmetric_keys[0].bytes.len(),
),
(
"rsa",
SignatureAlgorithm::RsaSha256,
inventory.private_keys[0].pkcs8_der.len(),
),
] {
let mut policy = crate::policy::SigningPolicy::default();
policy.resources.max_external_resource_bytes = length - 1;
assert!(matches!(
inventory.signing_key(name, algorithm, &policy),
Err(KeyStoreError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
maximum,
actual,
}
)) if maximum == length - 1 && actual == length
));
policy.resources.max_external_resource_bytes = length;
policy.resources.max_external_resource_total_bytes = length - 1;
assert!(matches!(
inventory.signing_key(name, algorithm, &policy),
Err(KeyStoreError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
maximum,
actual,
}
)) if maximum == length - 1 && actual == length
));
}
}
#[test]
fn named_signing_lookup_obeys_active_candidate_budget() {
let resources = ResourcePolicy::default();
let mut inventory = KeyInventory::default();
for name in ["first", "second"] {
inventory
.add_symmetric(
name.into(),
SymmetricKeyKind::Hmac,
vec![0x42; 32],
KeyUsages::SIGN,
&resources,
)
.expect("HMAC imports");
}
let mut policy = crate::policy::SigningPolicy::default();
policy.resources.max_key_candidates = 1;
assert!(
inventory
.signing_key("first", SignatureAlgorithm::HmacSha256, &policy)
.is_ok()
);
assert!(matches!(
inventory.signing_key("second", SignatureAlgorithm::HmacSha256, &policy),
Err(KeyStoreError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::KEY_CANDIDATES,
..
}
))
));
}
#[cfg(feature = "xmlenc")]
#[test]
fn named_decryption_lookup_shares_candidate_budget_across_key_kinds() {
let resources = ResourcePolicy::default();
let mut inventory = KeyInventory::default();
inventory
.add_symmetric(
"aes".into(),
SymmetricKeyKind::Aes,
vec![0x42; 32],
KeyUsages::DECRYPT,
&resources,
)
.expect("AES imports");
inventory
.add_private_pem(
"rsa".into(),
include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-key.pem"),
None,
KeyUsages::DECRYPT,
&resources,
)
.expect("RSA imports");
let mut policy = crate::policy::DecryptionPolicy::default();
policy.resources.max_key_candidates = 1;
assert!(inventory.decryption_resolver("aes", &policy).is_ok());
assert!(matches!(
inventory.decryption_resolver("rsa", &policy),
Err(KeyStoreError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::KEY_CANDIDATES,
..
}
))
));
}
#[test]
fn pkcs12_imports_share_candidate_budget() {
let bundle = include_bytes!("../tests/fixtures/xmlenc/01-phaos-xmlenc-3/rsa-priv-key.p12");
let resources = ResourcePolicy {
max_key_candidates: 4,
..ResourcePolicy::default()
};
let mut inventory = KeyInventory::default();
inventory
.add_pkcs12("first".into(), bundle, "secret", &resources)
.expect("first bundle fits");
assert_eq!(inventory.entry_count(), 2);
assert!(
inventory.private_keys()[0]
.matching_certificate_chain()
.is_some()
);
assert!(
inventory
.add_pkcs12("second".into(), bundle, "secret", &resources)
.is_err()
);
}
#[test]
fn pkcs12_temporary_budget_excludes_live_input() {
let resources = ResourcePolicy::default();
let mut inventory = KeyInventory::default();
inventory
.add_symmetric(
"old".into(),
SymmetricKeyKind::Hmac,
vec![7; 32],
KeyUsages::SIGN,
&resources,
)
.expect("retained key");
let input = [0; 128];
let limits = inventory
.pkcs12_import_limits("new", &input, KeyUsages::SIGN, &resources)
.expect("import allowance");
assert_eq!(
limits.memory_available,
resources.max_external_resource_total_bytes
- inventory.material_bytes
- 3
- input.len()
);
}
#[test]
fn pkcs12_input_consumes_aggregate_import_budget() {
let bundle = include_bytes!("../tests/fixtures/xmlenc/01-phaos-xmlenc-3/rsa-priv-key.p12");
let resources = ResourcePolicy::default();
let mut inventory = KeyInventory::default();
inventory
.add_pkcs12("first".into(), bundle, "secret", &resources)
.expect("first bundle imports");
assert!(inventory.material_bytes >= bundle.len());
let limited = ResourcePolicy {
max_external_resource_total_bytes: bundle.len() * 2 - 1,
..resources
};
assert!(
inventory
.add_pkcs12("second".into(), bundle, "secret", &limited)
.is_err()
);
}
#[test]
fn pkcs12_unrelated_ca_does_not_block_private_key() {
let bundle = base64::engine::general_purpose::STANDARD
.decode(
include_str!("../tests/fixtures/keys/pkcs12/rsa-key-unrelated-ca.p12.b64").trim(),
)
.expect("fixture base64 decodes");
let mut inventory = KeyInventory::default();
inventory
.add_pkcs12(
"ca-only".into(),
&bundle,
"secret",
&ResourcePolicy::default(),
)
.expect("unrelated CA must not invalidate the private key");
assert_eq!(inventory.private_keys.len(), 1);
assert_eq!(inventory.private_keys[0].certificate_chain.len(), 1);
assert!(!inventory.private_keys[0].has_matching_leaf);
assert!(
inventory
.signing_key(
"ca-only",
SignatureAlgorithm::RsaSha256,
&crate::policy::SigningPolicy::default(),
)
.is_ok()
);
}
#[test]
fn pkcs12_identical_leaf_bags_are_one_candidate() {
let bundle = base64::engine::general_purpose::STANDARD
.decode(include_str!("../tests/fixtures/keys/pkcs12/rsa-duplicate-leaf.p12.b64").trim())
.expect("fixture base64 decodes");
let mut inventory = KeyInventory::default();
inventory
.add_pkcs12(
"duplicate-leaf".into(),
&bundle,
"secret",
&ResourcePolicy::default(),
)
.expect("identical leaf bags are not ambiguous");
assert_eq!(inventory.private_keys[0].certificate_chain.len(), 1);
assert!(inventory.private_keys[0].has_matching_leaf);
}
#[cfg(feature = "experimental-pq")]
#[test]
fn kem_pkcs12_auto_usage_is_decrypt_only() {
let key = crate::provider::RustCryptoMlKemPrivateKey::from_seed(
crate::provider::KeyEncapsulationAlgorithm::MlKem512,
&[3; 64],
)
.expect("valid ML-KEM seed");
let der = key
.to_pkcs8_der(crate::provider::MlKemPrivateKeyEncoding::Seed)
.expect("seed PKCS#8 export");
let contents = || pkcs12_import::Contents {
private_keys: vec![Zeroizing::new(der.as_bytes().to_vec())],
certificates: Vec::new(),
};
let resources = ResourcePolicy::default();
let mut inventory = KeyInventory::default();
inventory
.add_pkcs12_contents(
"kem".into(),
der.as_bytes().len(),
contents(),
KeyUsages::SIGN.union(KeyUsages::DECRYPT),
&resources,
true,
)
.expect("automatic recipient usages");
assert_eq!(inventory.private_keys()[0].usages, KeyUsages::DECRYPT);
for usages in [
KeyUsages::VERIFY,
KeyUsages::ENCRYPT,
KeyUsages::DECRYPT.union(KeyUsages::VERIFY),
KeyUsages::DECRYPT.union(KeyUsages::ENCRYPT),
KeyUsages::DECRYPT.union(KeyUsages::SIGN),
] {
let mut rejected = KeyInventory::default();
assert!(
rejected
.add_pkcs12_contents(
"kem".into(),
der.as_bytes().len(),
contents(),
usages,
&resources,
false,
)
.is_err()
);
assert!(rejected.private_keys().is_empty());
}
assert!(
KeyInventory::default()
.add_pkcs12_contents(
"kem".into(),
der.as_bytes().len(),
contents(),
KeyUsages::SIGN,
&resources,
false
)
.is_err()
);
}
#[test]
fn ec_pkcs12_import_is_sign_only() {
let bundle = base64::engine::general_purpose::STANDARD
.decode(include_str!("../tests/fixtures/keys/pkcs12/ec-key.p12.b64").trim())
.expect("fixture base64 decodes");
let resources = ResourcePolicy::default();
let mut inventory = KeyInventory::default();
inventory
.add_pkcs12("ec".into(), &bundle, "secret", &resources)
.expect("EC signing bundle imports");
assert!(inventory.private_keys()[0].usages.allows(KeyUsage::Sign));
assert!(!inventory.private_keys()[0].usages.allows(KeyUsage::Decrypt));
assert!(
KeyInventory::default()
.add_pkcs12_with_usages(
"ec".into(),
&bundle,
"secret",
KeyUsages::DECRYPT,
&resources,
)
.is_err()
);
}
#[test]
fn password_callback_runs_for_protected_container() {
let bundle = include_bytes!("../tests/fixtures/xmlenc/01-phaos-xmlenc-3/rsa-priv-key.p12");
let mut inventory = KeyInventory::default();
inventory
.add_pkcs12_with_password_callback(
"bundle".into(),
bundle,
|| Some(Zeroizing::new("secret".to_owned())),
KeyUsages::SIGN,
&ResourcePolicy::default(),
)
.expect("callback password decrypts donor bundle");
assert!(inventory.private_keys[0].usages.allows(KeyUsage::Sign));
assert!(matches!(
KeyInventory::default().add_pkcs12_with_password_callback(
"bundle".into(),
bundle,
|| None,
KeyUsages::SIGN,
&ResourcePolicy::default(),
),
Err(KeyStoreError::ProtectedContainer)
));
let limited = ResourcePolicy {
max_external_resource_total_bytes: bundle.len() - 1,
..ResourcePolicy::default()
};
assert!(matches!(
KeyInventory::default().add_pkcs12_with_password_callback(
"bundle".into(),
bundle,
|| panic!("budget must be checked before password delivery"),
KeyUsages::SIGN,
&limited,
),
Err(KeyStoreError::Policy(
crate::policy::PolicyViolation::ResourceLimitExceeded {
resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
..
}
))
));
let limited_kdf = ResourcePolicy {
max_key_import_kdf_work: 1,
..ResourcePolicy::default()
};
assert!(matches!(
KeyInventory::default().add_pkcs12_with_password_callback(
"bundle".into(),
bundle,
|| panic!("KDF denial must precede password delivery"),
KeyUsages::SIGN,
&limited_kdf,
),
Err(KeyStoreError::Policy(
crate::policy::PolicyViolation::KdfIterationsOutsideLimit { maximum: 1 }
))
));
let oversized_bundle = ResourcePolicy {
max_external_resource_bytes: bundle.len() - 1,
..ResourcePolicy::default()
};
assert!(matches!(
KeyInventory::default().add_pkcs12_with_password_callback(
"bundle".into(),
bundle,
|| panic!("size denial must precede password delivery"),
KeyUsages::SIGN,
&oversized_bundle,
),
Err(KeyStoreError::Policy(
crate::policy::PolicyViolation::ResourceLimitExceeded {
resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
maximum,
}
)) if maximum == bundle.len() - 1
));
}
#[test]
fn pkcs12_callback_preflights_indefinite_outer_ber() {
let bundle = include_bytes!("../tests/fixtures/xmlenc/01-phaos-xmlenc-3/rsa-priv-key.p12");
assert_eq!(bundle[0], 0x30);
let length_octets = usize::from(bundle[1] & 0x7f);
assert!(bundle[1] & 0x80 != 0 && length_octets > 0);
let mut ber = vec![0x30, 0x80];
ber.extend_from_slice(&bundle[2 + length_octets..]);
ber.extend_from_slice(&[0, 0]);
let resources = ResourcePolicy {
max_key_import_kdf_work: 1,
..ResourcePolicy::default()
};
assert!(matches!(
KeyInventory::default().add_pkcs12_with_password_callback(
"bundle".into(),
&ber,
|| panic!("BER MAC KDF denial must precede password delivery"),
KeyUsages::SIGN,
&resources,
),
Err(KeyStoreError::Policy(
crate::policy::PolicyViolation::KdfIterationsOutsideLimit { maximum: 1 }
))
));
}
#[test]
fn donor_xml_store_preserves_private_dsa_material() {
let bytes = include_bytes!("../tests/fixtures/keys/xmlsec/mixed-keys.xml");
let inventory = KeyInventory::from_xml_bytes(
bytes,
&xml_policy(ResourcePolicy::default()),
XmlBackend::default(),
)
.expect("donor key store should parse");
let dsa = inventory
.private_keys
.iter()
.find(|entry| entry.name == "test-dsa")
.expect("DSA private X should be imported");
assert!(dsa.usages.allows(KeyUsage::Sign));
assert!(!dsa.usages.allows(KeyUsage::Decrypt));
assert!(DsaSigningKey::from_pkcs8_der(&dsa.pkcs8_der).is_ok());
let dsa_public = inventory
.public_keys()
.iter()
.find(|entry| entry.name == "test-dsa")
.expect("DSA public entry is retained");
assert_eq!(dsa_public.usages, KeyUsages::VERIFY);
}
#[test]
fn xml_store_charges_retained_decoded_material() {
let source = include_str!("../tests/fixtures/keys/xmlsec/mixed-keys.xml");
let marker = source.find("<KeyName>test-dsa</KeyName>").expect("DSA key");
let start = source[..marker].rfind("<KeyInfo").expect("DSA KeyInfo");
let end =
marker + source[marker..].find("</KeyInfo>").expect("DSA end") + "</KeyInfo>".len();
let xml = format!(
"<Keys xmlns=\"{XMLSEC_NS}\">{}</Keys>",
source[start..end]
.replace('\n', "")
.replace("test-dsa", &"name".repeat(512))
);
let inventory = KeyInventory::from_xml_bytes(
xml.as_bytes(),
&xml_policy(ResourcePolicy::default()),
XmlBackend::default(),
)
.expect("compact DSA store imports");
let retained = inventory.retained_material_bytes().expect("bounded tally");
assert_eq!(inventory.material_bytes, xml.len() + retained);
assert!(retained >= inventory.private_keys[0].pkcs8_der.len());
assert!(
retained > xml.len(),
"fixture must distinguish retained bytes from source bytes"
);
let resources = ResourcePolicy {
max_external_resource_total_bytes: retained - 1,
..ResourcePolicy::default()
};
assert!(matches!(
KeyInventory::from_xml_bytes(
xml.as_bytes(),
&xml_policy(resources),
XmlBackend::default()
),
Err(KeyStoreError::Policy(
crate::policy::PolicyViolation::ResourceLimitExceeded {
resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
..
}
))
));
}
#[test]
fn xml_import_session_keeps_failed_work_and_preflights_live_material() {
let xml = |value: &str| {
format!(
"<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>"
)
};
let policy = xml_policy(ResourcePolicy {
max_key_candidates: 1,
..ResourcePolicy::default()
});
let mut importer =
XmlKeyStoreImporter::new(&policy, XmlBackend::default()).expect("session");
assert!(matches!(
importer.import(xml("!").as_bytes()),
Err(KeyStoreError::Invalid(_))
));
assert!(matches!(
importer.import(xml("AA==").as_bytes()),
Err(KeyStoreError::Policy(
crate::policy::PolicyViolation::ResourceLimitExceeded {
resource: crate::policy::resource_name::KEY_CANDIDATES,
maximum: 1
}
))
));
assert_eq!(importer.finish().entry_count(), 0);
let valid = xml("AA==");
let policy = xml_policy(ResourcePolicy {
max_external_resource_total_bytes: valid.len() + 3,
..ResourcePolicy::default()
});
let mut importer =
XmlKeyStoreImporter::new(&policy, XmlBackend::default()).expect("session");
importer
.import(valid.as_bytes())
.expect("first source fits exactly");
let consumed = importer.parse_work.consumed();
assert!(matches!(
importer.import(b"!!"),
Err(KeyStoreError::Policy(
crate::policy::PolicyViolation::ResourceLimitExceeded {
resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
..
}
))
));
assert_eq!(
importer.parse_work.consumed(),
consumed,
"no parser work after byte denial"
);
assert_eq!(importer.finish().entry_count(), 1);
}
#[test]
fn xml_store_rejects_empty_symmetric_material() {
for kind in ["HMACKeyValue", "AESKeyValue", "DESKeyValue"] {
let xml = format!(
"<Keys xmlns=\"{XMLSEC_NS}\"><KeyInfo xmlns=\"{XMLDSIG_NS}\"><KeyName>empty</KeyName><KeyValue><{kind} xmlns=\"{XMLSEC_NS}\"/></KeyValue></KeyInfo></Keys>"
);
assert!(
KeyInventory::from_xml_bytes(
xml.as_bytes(),
&xml_policy(ResourcePolicy::default()),
XmlBackend::default(),
)
.is_err()
);
}
}
#[test]
fn aes_imports_require_supported_key_widths() {
let resources = ResourcePolicy::default();
for length in [1, 15, 17, 23, 25, 31, 33] {
let mut inventory = KeyInventory::default();
assert!(
inventory
.add_symmetric(
"aes".into(),
SymmetricKeyKind::Aes,
vec![0; length],
KeyUsages::ENCRYPT,
&resources,
)
.is_err()
);
let xml = format!(
"<Keys xmlns=\"{XMLSEC_NS}\"><KeyInfo xmlns=\"{XMLDSIG_NS}\"><KeyName>aes</KeyName><KeyValue><AESKeyValue xmlns=\"{XMLSEC_NS}\">{}</AESKeyValue></KeyValue></KeyInfo></Keys>",
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, vec![0; length])
);
assert!(
KeyInventory::from_xml_bytes(
xml.as_bytes(),
&xml_policy(resources.clone()),
XmlBackend::default(),
)
.is_err()
);
}
for length in [16, 24, 32] {
let mut inventory = KeyInventory::default();
inventory
.add_symmetric(
"aes".into(),
SymmetricKeyKind::Aes,
vec![0; length],
KeyUsages::ENCRYPT,
&resources,
)
.expect("AES-128/192/256 key imports");
}
}
#[test]
fn unsupported_des_material_is_not_authorized() {
let resources = ResourcePolicy::default();
let mut inventory = KeyInventory::default();
assert!(
inventory
.add_symmetric(
"des".into(),
SymmetricKeyKind::Des,
vec![0x42; 8],
KeyUsages::ENCRYPT,
&resources,
)
.is_err()
);
let xml = format!(
"<Keys xmlns=\"{XMLSEC_NS}\"><KeyInfo xmlns=\"{XMLDSIG_NS}\"><KeyName>des</KeyName><KeyValue><DESKeyValue xmlns=\"{XMLSEC_NS}\">QkJCQkJCQkI=</DESKeyValue></KeyValue></KeyInfo></Keys>"
);
assert!(
KeyInventory::from_xml_bytes(
xml.as_bytes(),
&xml_policy(resources.clone()),
XmlBackend::default(),
)
.is_err()
);
}
#[test]
fn xml_store_accepts_xmlsig11_ec_key_value() {
let pem = pem::parse(include_bytes!(
"../tests/fixtures/keys/ec/ec-prime256v1-pubkey.pem"
))
.expect("EC public PEM decodes");
let (_, spki) = x509_parser::x509::SubjectPublicKeyInfo::from_der(pem.contents())
.expect("EC SPKI parses");
let point =
base64::engine::general_purpose::STANDARD.encode(spki.subject_public_key.data.as_ref());
let xml = format!(
"<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>"
);
let store = KeyInventory::from_xml_bytes(
xml.as_bytes(),
&xml_policy(ResourcePolicy::default()),
XmlBackend::default(),
)
.expect("ECKeyValue namespace is supported");
assert_eq!(store.public_keys().len(), 1);
assert_eq!(store.public_keys()[0].usages, KeyUsages::VERIFY);
assert!(
store.public_keys()[0]
.key_info
.sources
.iter()
.any(|source| matches!(source, KeyInfoSource::KeyValue(KeyValueInfo::Ec { .. })))
);
}
#[test]
fn xml_store_der_keys_share_one_x509_work_budget() {
let encoded = base64::engine::general_purpose::STANDARD.encode(include_bytes!(
"../tests/fixtures/xmlenc/keys/ec/ec-prime256v1-pubkey.der"
));
let store = |keys: usize| {
let mut xml = format!("<Keys xmlns=\"{XMLSEC_NS}\">");
for index in 0..keys {
xml.push_str(&format!(
"<KeyInfo xmlns=\"{XMLDSIG_NS}\"><KeyName>ec{index}</KeyName><DEREncodedKeyValue xmlns=\"{XMLDSIG11_NS}\">{encoded}</DEREncodedKeyValue></KeyInfo>"
));
}
xml.push_str("</Keys>");
xml
};
let import = |xml: &str, max_x509_work: usize| {
KeyInventory::from_xml_bytes(
xml.as_bytes(),
&xml_policy(ResourcePolicy {
max_x509_work,
..ResourcePolicy::default()
}),
XmlBackend::default(),
)
};
let (one, two) = (store(1), store(2));
let (mut refused, mut admitted) = (0_usize, ResourcePolicy::default().max_x509_work);
assert!(import(&one, admitted).is_ok(), "the default admits one key");
while admitted - refused > 1 {
let middle = refused + (admitted - refused) / 2;
if import(&one, middle).is_ok() {
admitted = middle;
} else {
refused = middle;
}
}
assert!(matches!(
import(&two, admitted),
Err(KeyStoreError::Policy(
crate::policy::PolicyViolation::ResourceLimitExceeded {
resource: crate::policy::resource_name::X509_WORK,
..
}
))
));
}
#[cfg(feature = "legacy-algorithms")]
#[test]
fn xml_store_imports_triple_des_without_granting_operation_permission() {
let encoded = base64::engine::general_purpose::STANDARD.encode([0x31; 24]);
let xml = format!(
"<Keys xmlns=\"{XMLSEC_NS}\"><KeyInfo xmlns=\"{XMLDSIG_NS}\"><KeyName>des</KeyName><KeyValue><DESKeyValue xmlns=\"{XMLSEC_NS}\">{encoded}</DESKeyValue></KeyValue></KeyInfo></Keys>"
);
let store = KeyInventory::from_xml_bytes(
xml.as_bytes(),
&xml_policy(ResourcePolicy::default()),
XmlBackend::default(),
)
.expect("a three-key TDEA inventory entry is supported");
assert_eq!(store.symmetric_keys()[0].kind, SymmetricKeyKind::Des);
assert_eq!(store.symmetric_keys()[0].bytes.as_slice(), &[0x31; 24]);
assert!(store.symmetric_keys()[0].usages.allows(KeyUsage::Decrypt));
}
#[test]
fn xml_store_rejects_unusable_public_key_values() {
let cases = [
"<RSAKeyValue><Modulus></Modulus><Exponent>AQAB</Exponent></RSAKeyValue>",
"<ECKeyValue xmlns=\"http://www.w3.org/2009/xmldsig11#\"><NamedCurve URI=\"urn:oid:1.2.840.10045.3.1.7\"/><PublicKey>AQ==</PublicKey></ECKeyValue>",
];
for key in cases {
let xml = format!(
"<Keys xmlns=\"{XMLSEC_NS}\"><KeyInfo xmlns=\"{XMLDSIG_NS}\"><KeyName>invalid</KeyName><KeyValue>{key}</KeyValue></KeyInfo></Keys>"
);
assert!(
KeyInventory::from_xml_bytes(
xml.as_bytes(),
&xml_policy(ResourcePolicy::default()),
XmlBackend::default(),
)
.is_err()
);
}
}
#[test]
fn xml_store_import_limits_preserve_policy_errors() {
let xml = format!(
"<Keys xmlns=\"{XMLSEC_NS}\"><KeyInfo xmlns=\"{XMLDSIG_NS}\"><KeyName>key</KeyName><KeyValue><HMACKeyValue xmlns=\"{XMLSEC_NS}\">c2VjcmV0</HMACKeyValue></KeyValue></KeyInfo></Keys>"
);
let two = xml.replace(
"</Keys>",
&format!(
"{} </Keys>",
xml[xml.find("<KeyInfo").expect("fixture has KeyInfo")
..xml.find("</Keys>").expect("fixture has Keys end tag")]
.replace(">key<", ">other<")
),
);
for (bytes, resources, expected) in [
(
xml.as_bytes(),
ResourcePolicy {
max_external_resource_bytes: xml.len() - 1,
..ResourcePolicy::default()
},
crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
),
(
xml.as_bytes(),
ResourcePolicy {
max_external_resource_total_bytes: xml.len() - 1,
..ResourcePolicy::default()
},
crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
),
(
two.as_bytes(),
ResourcePolicy {
max_key_candidates: 1,
..ResourcePolicy::default()
},
crate::policy::resource_name::KEY_CANDIDATES,
),
] {
assert!(
matches!(KeyInventory::from_xml_bytes(bytes, &xml_policy(resources), XmlBackend::default()),
Err(KeyStoreError::Policy(crate::policy::PolicyViolation::ResourceLimitExceeded { resource, .. })) if resource == expected)
);
}
let mut inventory = KeyInventory::default();
let resources = ResourcePolicy {
max_external_resource_total_bytes: 3,
..ResourcePolicy::default()
};
assert!(matches!(
inventory.add_symmetric(
"key".into(),
SymmetricKeyKind::Hmac,
vec![7; 32],
KeyUsages::SIGN,
&resources
),
Err(KeyStoreError::Policy(
crate::policy::PolicyViolation::ResourceLimitExceeded {
resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
..
}
))
));
}
#[test]
fn xml_store_enforces_all_parser_resource_limits() {
let xml = format!(
"<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>"
);
for resources in [
ResourcePolicy {
max_xml_depth: 2,
..ResourcePolicy::default()
},
ResourcePolicy {
max_xml_namespace_bindings: 1,
..ResourcePolicy::default()
},
ResourcePolicy {
max_xml_parse_work_bytes: 1,
..ResourcePolicy::default()
},
] {
assert!(
KeyInventory::from_xml_bytes(
xml.as_bytes(),
&xml_policy(resources),
XmlBackend::default()
)
.is_err_and(|error| matches!(error, KeyStoreError::Policy(_))),
"parser policy denial must retain its type in key stores"
);
}
}
#[test]
fn xml_store_bounds_source_before_utf16_decode() {
let xml = format!("<Keys xmlns=\"{XMLSEC_NS}\"/>");
let mut bytes = vec![0xff, 0xfe];
for unit in xml.encode_utf16() {
bytes.extend_from_slice(&unit.to_le_bytes());
}
let resources = ResourcePolicy {
max_xml_document_bytes: xml.len() + 1,
..ResourcePolicy::default()
};
assert!(bytes.len() > resources.max_xml_document_bytes);
assert!(
KeyInventory::from_xml_bytes(&bytes, &xml_policy(resources), XmlBackend::default())
.is_err()
);
}
#[test]
fn xml_store_uses_operation_xml_policy() {
let xml = format!(
"<!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>"
);
let denied = crate::policy::VerificationPolicy::default();
assert!(
KeyInventory::from_xml_bytes(xml.as_bytes(), &denied, XmlBackend::default()).is_err()
);
let mut allowed = denied;
allowed.xml.allow_internal_dtd = true;
assert!(
KeyInventory::from_xml_bytes(xml.as_bytes(), &allowed, XmlBackend::default()).is_ok()
);
}
#[test]
fn ec_public_key_cannot_authorize_encryption() {
let resources = ResourcePolicy::default();
let ec = include_bytes!("../tests/fixtures/keys/ec/ec-prime256v1-pubkey.pem");
let cert = include_bytes!("../tests/fixtures/keys/ec/ec-prime256v1-cert.pem");
let mut store = KeyInventory::default();
store
.add_public_pem("ec".into(), ec, &resources)
.expect("EC public key imports");
assert_eq!(store.public_keys()[0].usages, KeyUsages::VERIFY);
assert!(
store
.add_public_pem_with_usages("ec-encrypt".into(), ec, KeyUsages::ENCRYPT, &resources)
.is_err()
);
let cert_der = pem::parse(cert)
.expect("EC certificate PEM decodes")
.into_contents();
store
.add_public_der("ec-cert".into(), cert_der.clone(), &resources)
.expect("EC certificate imports");
assert_eq!(store.public_keys()[1].usages, KeyUsages::VERIFY);
assert!(
store
.add_public_der_with_usages(
"ec-cert-encrypt".into(),
cert_der,
KeyUsages::ENCRYPT,
&resources
)
.is_err()
);
}
#[test]
fn xml_store_rejects_policy_above_absolute_ceiling() {
let resources = ResourcePolicy {
max_xml_nodes: crate::hard_limits::XML_DOCUMENT_NODE_CEILING as usize + 1,
..ResourcePolicy::default()
};
let xml = format!("<Keys xmlns=\"{XMLSEC_NS}\"/>");
assert!(
KeyInventory::from_xml_bytes(
xml.as_bytes(),
&xml_policy(resources.clone()),
XmlBackend::default(),
)
.is_err()
);
assert!(
KeyInventory::default()
.add_symmetric(
"key".into(),
SymmetricKeyKind::Hmac,
b"secret".to_vec(),
KeyUsages::SIGN,
&resources,
)
.is_err()
);
}
#[test]
fn ec_private_key_cannot_advertise_rsa_decryption() {
let pem = include_bytes!("../tests/fixtures/keys/ec/ec-prime256v1-key.pem");
assert!(
KeyInventory::default()
.add_private_pem(
"ec".into(),
pem,
None,
KeyUsages::DECRYPT,
&ResourcePolicy::default(),
)
.is_err()
);
}
#[test]
fn imports_rsa_pkcs1_pem_and_resolves_named_spki() {
let private_pem = include_str!("../tests/fixtures/keys/rsa/rsa-2048-key.pem");
let rsa = RsaPrivateKey::from_pkcs8_pem(private_pem).expect("RSA fixture parses");
let pkcs1 = rsa.to_pkcs1_der().expect("RSA fixture encodes as PKCS#1");
let public = rsa
.to_public_key()
.to_public_key_der()
.expect("RSA fixture has SPKI");
let resources = ResourcePolicy::default();
let mut inventory = KeyInventory::default();
inventory
.add_private_der(
"key".into(),
pkcs1.as_bytes(),
None,
KeyUsages::SIGN.union(KeyUsages::DECRYPT),
&resources,
)
.expect("PKCS#1 private key imports");
inventory
.add_public_der("verify-key".into(), public.as_bytes().to_vec(), &resources)
.expect("public SPKI imports");
assert!(inventory.private_keys[0].usages.allows(KeyUsage::Sign));
let mut info = KeyInfo::default();
info.sources
.push(KeyInfoSource::KeyName("verify-key".into()));
let resolver = inventory.verification_resolver();
let resolved = resolver
.resolve(Some(&info), SignatureAlgorithm::RsaSha256)
.expect("named public key resolves");
assert!(resolved.is_some());
}
#[test]
fn rejects_unknown_pem_text_and_duplicate_names() {
let public = include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-pubkey.pem");
let mut inventory = KeyInventory::default();
let resources = ResourcePolicy::default();
inventory
.add_public_pem("first".into(), public, &resources)
.expect("public PEM imports");
assert!(matches!(
inventory.add_public_pem("first".into(), public, &resources),
Err(KeyStoreError::Selection("duplicate key name"))
));
let mut trailing = public.to_vec();
trailing.extend_from_slice(b"\nnot a key");
assert!(
inventory
.add_public_pem("other".into(), &trailing, &resources)
.is_err()
);
}
#[test]
fn public_pem_label_rejects_certificate_payload() {
let certificate = pem::parse(include_bytes!(
"../tests/fixtures/keys/rsa/rsa-2048-cert.pem"
))
.expect("certificate")
.into_contents();
let mislabeled = pem::encode(&pem::Pem::new("PUBLIC KEY", certificate.clone()));
let resources = ResourcePolicy::default();
for usages in [None, Some(KeyUsages::VERIFY)] {
let mut inventory = KeyInventory::default();
assert!(matches!(
inventory.add_public_pem_inner(
"key".into(),
mislabeled.as_bytes(),
usages,
&resources
),
Err(KeyStoreError::Selection("invalid PUBLIC KEY payload"))
));
assert!(inventory.public_keys().is_empty());
}
KeyInventory::default()
.add_public_der("cert".into(), certificate, &resources)
.expect("generic DER intentionally accepts certificates");
KeyInventory::default()
.add_public_pem(
"spki".into(),
include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"),
&resources,
)
.expect("correct SPKI label");
}
#[test]
fn rsa_spki_import_rejects_even_public_exponent() {
let mut der = single_pem_block(
include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"),
ResourcePolicy::default().max_external_resource_bytes,
)
.expect("public key fixture")
.into_contents();
let exponent = der
.windows(5)
.position(|bytes| bytes == [0x02, 0x03, 0x01, 0x00, 0x01])
.expect("RSA exponent in SPKI");
der[exponent + 4] = 0;
assert!(
KeyInventory::default()
.add_public_der("invalid".into(), der, &ResourcePolicy::default())
.is_err()
);
}
#[test]
fn rsa_public_pkcs1_pem_is_bounded_before_bigint_decode() {
let modulus = vec![1_u8; crate::hard_limits::RSA_MODULUS_BIT_CEILING.div_ceil(8) + 1];
let exponent = [1_u8, 0, 1];
let public = pkcs1::RsaPublicKeyRef {
modulus: der::asn1::UintRef::new(&modulus).expect("valid modulus"),
public_exponent: der::asn1::UintRef::new(&exponent).expect("valid exponent"),
};
let pem = pem::encode(&pem::Pem::new(
"RSA PUBLIC KEY",
der::Encode::to_der(&public).expect("encodable RSA public key"),
));
assert!(matches!(
KeyInventory::default().add_public_pem(
"oversized".into(),
pem.as_bytes(),
&ResourcePolicy::default(),
),
Err(KeyStoreError::Selection(
"RSA public key exceeds safety limit"
))
));
}
#[test]
fn direct_inventory_names_consume_resource_budget() {
let resources = ResourcePolicy {
max_external_resource_bytes: 64,
max_external_resource_total_bytes: 100,
..ResourcePolicy::default()
};
let mut inventory = KeyInventory::default();
assert!(
inventory
.add_symmetric(
"x".repeat(65),
SymmetricKeyKind::Hmac,
vec![7; 32],
KeyUsages::SIGN,
&resources,
)
.is_err()
);
inventory
.add_symmetric(
"a".repeat(40),
SymmetricKeyKind::Hmac,
vec![7; 32],
KeyUsages::SIGN,
&resources,
)
.expect("first named key fits");
assert!(
inventory
.add_symmetric(
"b".repeat(40),
SymmetricKeyKind::Hmac,
vec![8; 32],
KeyUsages::SIGN,
&resources,
)
.is_err()
);
}
#[test]
fn private_pem_label_cannot_enable_der_fallback() {
let plain = pem::parse(include_bytes!(
"../tests/fixtures/keys/rsa/rsa-2048-key.pem"
))
.expect("fixture")
.into_contents();
let mislabeled = pem::encode(&pem::Pem::new("ENCRYPTED PRIVATE KEY", plain));
for password in [None, Some(b"ignored".as_slice())] {
let mut inventory = KeyInventory::default();
assert!(matches!(
inventory.add_private_pem(
"key".into(),
mislabeled.as_bytes(),
password,
KeyUsages::SIGN,
&ResourcePolicy::default()
),
Err(KeyStoreError::ProtectedContainer)
));
assert!(inventory.private_keys.is_empty());
assert_eq!(inventory.material_bytes, 0);
}
}
#[test]
fn encrypted_pkcs8_callback_checks_remaining_work() {
use der::Encode as _;
use pkcs8::pkcs5::{EncryptionScheme, pbes2};
let envelope = EncryptedPrivateKeyInfoRef {
encryption_algorithm: EncryptionScheme::Pbes2(pbes2::Parameters {
kdf: pbes2::Kdf::Pbkdf2(pbes2::Pbkdf2Params {
salt: pbes2::Salt::new(b"12345678").expect("salt"),
iteration_count: 2,
key_length: None,
prf: pbes2::Pbkdf2Prf::HmacWithSha256,
}),
encryption: pbes2::EncryptionScheme::Aes256Cbc { iv: [0; 16] },
}),
encrypted_data: der::asn1::OctetStringRef::new(&[0; 64]).expect("ciphertext"),
}
.to_der()
.expect("envelope");
let resources = ResourcePolicy {
max_key_import_kdf_work: 4,
..ResourcePolicy::default()
};
let mut inventory = KeyInventory::default();
assert!(matches!(
inventory.add_private_der(
"first".into(),
&envelope,
Some(b"wrong"),
KeyUsages::SIGN,
&resources,
),
Err(KeyStoreError::ProtectedContainer)
));
assert_eq!(inventory.key_import_kdf_work(), 3);
let calls = std::cell::Cell::new(0);
let result = inventory.add_private_der_with_password_callback(
"second".into(),
&envelope,
|| {
calls.set(calls.get() + 1);
Some(Zeroizing::new(b"wrong".to_vec()))
},
KeyUsages::SIGN,
&resources,
);
assert_eq!(calls.get(), 0, "preflight precedes secret acquisition");
assert!(matches!(result, Err(KeyStoreError::Policy(_))));
assert_eq!(inventory.key_import_kdf_work(), 3);
let exact = ResourcePolicy {
max_key_import_kdf_work: 6,
..resources
};
assert!(matches!(
inventory.add_private_der_with_password_callback(
"third".into(),
&envelope,
|| {
calls.set(calls.get() + 1);
Some(Zeroizing::new(b"wrong".to_vec()))
},
KeyUsages::SIGN,
&exact,
),
Err(KeyStoreError::ProtectedContainer)
));
assert_eq!(calls.get(), 1);
assert_eq!(inventory.key_import_kdf_work(), 6);
}
#[test]
fn encrypted_pkcs8_requires_correct_password_without_plaintext_fallback() {
let private_pem = include_str!("../tests/fixtures/keys/rsa/rsa-2048-key.pem");
let rsa = RsaPrivateKey::from_pkcs8_pem(private_pem).expect("RSA fixture parses");
let plain = rsa.to_pkcs8_der().expect("RSA fixture encodes as PKCS#8");
let mut rng = ChaCha8Rng::seed_from_u64(0xA11C_E501);
let encrypted = PrivateKeyInfoRef::try_from(plain.as_bytes())
.expect("PKCS#8 reference parses")
.encrypt_with_rng(&mut rng, b"correct")
.expect("PKCS#8 fixture encrypts");
let resources = ResourcePolicy::default();
let mut inventory = KeyInventory::default();
let restricted = ResourcePolicy {
max_key_import_kdf_work: 1,
..ResourcePolicy::default()
};
assert!(matches!(
inventory.add_private_der(
"restricted".into(),
encrypted.as_bytes(),
Some(b"correct"),
KeyUsages::SIGN,
&restricted,
),
Err(KeyStoreError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::KEY_IMPORT_KDF_WORK,
maximum: 1,
..
}
))
));
let callback_calls = std::cell::Cell::new(0);
assert!(matches!(
inventory.add_private_der_with_password_callback(
"restricted-callback".into(),
encrypted.as_bytes(),
|| {
callback_calls.set(callback_calls.get() + 1);
Some(Zeroizing::new(b"correct".to_vec()))
},
KeyUsages::SIGN,
&restricted,
),
Err(KeyStoreError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::KEY_IMPORT_KDF_WORK,
maximum: 1,
..
}
))
));
assert_eq!(callback_calls.get(), 0);
assert!(matches!(
inventory.add_private_der(
"protected".into(),
encrypted.as_bytes(),
Some(b"wrong"),
KeyUsages::SIGN,
&resources,
),
Err(KeyStoreError::ProtectedContainer)
));
assert!(inventory.private_keys.is_empty());
inventory
.add_private_der(
"protected".into(),
encrypted.as_bytes(),
Some(b"correct"),
KeyUsages::SIGN,
&resources,
)
.expect("correct password imports encrypted PKCS#8");
assert_eq!(inventory.private_keys.len(), 1);
assert!(inventory.material_bytes >= encrypted.as_bytes().len());
let mut callback_inventory = KeyInventory::default();
callback_inventory
.add_private_der_with_password_callback(
"callback".into(),
encrypted.as_bytes(),
|| Some(Zeroizing::new(b"correct".to_vec())),
KeyUsages::SIGN,
&resources,
)
.expect("callback decrypts encrypted PKCS#8");
callback_inventory
.add_private_der_with_password_callback(
"plain".into(),
plain.as_bytes(),
|| panic!("plaintext PKCS#8 must not request a password"),
KeyUsages::SIGN,
&resources,
)
.expect("plaintext import ignores password callback");
}
#[test]
fn invalid_private_usages_do_not_request_password() {
let private = pem::parse(include_bytes!(
"../tests/fixtures/keys/rsa/rsa-2048-key.pem"
))
.expect("key")
.into_contents();
let encrypted = PrivateKeyInfoRef::try_from(private.as_slice())
.expect("PKCS8")
.encrypt_with_rng(&mut ChaCha8Rng::seed_from_u64(42), b"correct")
.expect("encrypted PKCS8");
for usages in [
KeyUsages::VERIFY,
KeyUsages(0),
KeyUsages::SIGN.union(KeyUsages::VERIFY),
] {
let calls = std::cell::Cell::new(0);
let mut inventory = KeyInventory::default();
assert!(matches!(
inventory.add_private_der_with_password_callback(
"invalid".into(),
encrypted.as_bytes(),
|| {
calls.set(calls.get() + 1);
Some(Zeroizing::new(b"correct".to_vec()))
},
usages,
&ResourcePolicy::default()
),
Err(KeyStoreError::Selection(
"private key usage is incompatible"
))
));
assert_eq!(calls.get(), 0);
assert_eq!(inventory.entry_count(), 0);
}
}
#[test]
fn certificate_and_crl_capacity_precedes_der_parsing() {
for candidates in [true, false] {
for certificate in [true, false] {
let mut inventory = KeyInventory::default();
let resources = ResourcePolicy {
max_key_candidates: if candidates { 1 } else { 64 },
max_external_resource_total_bytes: 2,
..ResourcePolicy::default()
};
inventory
.add_symmetric(
"a".into(),
SymmetricKeyKind::Hmac,
vec![0],
KeyUsages::SIGN,
&resources,
)
.expect("fill inventory");
let result = if certificate {
inventory.add_certificate_der(vec![0], false, &resources)
} else {
inventory.add_crl_der(vec![0], &resources)
};
let resource = if candidates {
crate::policy::resource_name::KEY_CANDIDATES
} else {
crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES
};
assert!(matches!(result, Err(KeyStoreError::Policy(
crate::policy::PolicyViolation::ResourceLimitExceeded { resource: actual, .. }
)) if actual == resource));
assert_eq!(inventory.entry_count(), 1);
assert_eq!(inventory.material_bytes, 2);
assert!(inventory.lookup_certificates.is_empty());
assert!(inventory.crls.is_empty());
}
}
assert!(matches!(
KeyInventory::default().add_crl_der(vec![0], &ResourcePolicy::default()),
Err(KeyStoreError::Selection("invalid X.509 CRL"))
));
}
#[test]
fn pkcs8_pbkdf2_output_blocks_are_checked_before_password() {
use der::Encode as _;
use pkcs8::pkcs5::{EncryptionScheme, pbes2};
let encrypted = EncryptedPrivateKeyInfoRef {
encryption_algorithm: EncryptionScheme::Pbes2(pbes2::Parameters {
kdf: pbes2::Kdf::Pbkdf2(pbes2::Pbkdf2Params {
salt: pbes2::Salt::new(b"12345678").expect("salt"),
iteration_count: 3,
key_length: None,
prf: pbes2::Pbkdf2Prf::HmacWithSha1,
}),
encryption: pbes2::EncryptionScheme::Aes256Cbc { iv: [0; 16] },
}),
encrypted_data: der::asn1::OctetStringRef::new(&[0; 16]).expect("ciphertext"),
};
let bytes = encrypted.to_der().expect("encrypted envelope");
let resources = ResourcePolicy {
max_key_import_kdf_work: 5,
..ResourcePolicy::default()
};
let calls = std::cell::Cell::new(0);
let result = KeyInventory::default().add_private_der_with_password_callback(
"key".into(),
&bytes,
|| {
calls.set(calls.get() + 1);
None
},
KeyUsages::SIGN,
&resources,
);
assert_eq!(calls.get(), 0);
assert!(matches!(
result,
Err(KeyStoreError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::KEY_IMPORT_KDF_WORK,
maximum: 5,
actual: 6,
}
))
));
let exact = ResourcePolicy {
max_key_import_kdf_work: 6,
..resources
};
assert!(enforce_pkcs8_kdf_policy(&encrypted, &exact, 0).is_ok());
for (prf, hash_len) in [
(pbes2::Pbkdf2Prf::HmacWithSha1, 20),
(pbes2::Pbkdf2Prf::HmacWithSha224, 28),
(pbes2::Pbkdf2Prf::HmacWithSha256, 32),
(pbes2::Pbkdf2Prf::HmacWithSha384, 48),
(pbes2::Pbkdf2Prf::HmacWithSha512, 64),
] {
for cipher in [
pbes2::EncryptionScheme::Aes128Cbc { iv: [0; 16] },
pbes2::EncryptionScheme::Aes192Cbc { iv: [0; 16] },
pbes2::EncryptionScheme::Aes256Cbc { iv: [0; 16] },
] {
let mut envelope = encrypted.clone();
let EncryptionScheme::Pbes2(params) = &mut envelope.encryption_algorithm else {
panic!("PBES2 fixture");
};
let pbes2::Kdf::Pbkdf2(kdf) = &mut params.kdf else {
panic!("PBKDF2 fixture");
};
kdf.prf = prf;
params.encryption = cipher;
let work = 3 * cipher.key_size().div_ceil(hash_len);
let exact = ResourcePolicy {
max_key_import_kdf_work: work,
..ResourcePolicy::default()
};
assert!(enforce_pkcs8_kdf_policy(&envelope, &exact, 0).is_ok());
let tight = ResourcePolicy {
max_key_import_kdf_work: work - 1,
..exact
};
assert!(matches!(enforce_pkcs8_kdf_policy(&envelope, &tight, 0),
Err(KeyStoreError::Policy(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::KEY_IMPORT_KDF_WORK,
actual, ..
})) if actual == work));
}
}
}
#[test]
fn protected_password_consumes_live_byte_and_work_budgets() {
use der::Encode as _;
use pkcs8::pkcs5::{EncryptionScheme, pbes2};
let envelope = EncryptedPrivateKeyInfoRef {
encryption_algorithm: EncryptionScheme::Pbes2(pbes2::Parameters {
kdf: pbes2::Kdf::Pbkdf2(pbes2::Pbkdf2Params {
salt: pbes2::Salt::new(b"12345678").expect("salt"),
iteration_count: 2,
key_length: None,
prf: pbes2::Pbkdf2Prf::HmacWithSha256,
}),
encryption: pbes2::EncryptionScheme::Aes256Cbc { iv: [0; 16] },
}),
encrypted_data: der::asn1::OctetStringRef::new(&[0; 64]).expect("ciphertext"),
}
.to_der()
.expect("envelope");
let peak = 3 + envelope.len() + 64;
for resources in [
ResourcePolicy {
max_external_resource_bytes: 255,
..ResourcePolicy::default()
},
ResourcePolicy {
max_external_resource_total_bytes: peak + 255,
..ResourcePolicy::default()
},
ResourcePolicy {
max_key_import_kdf_work: 5,
..ResourcePolicy::default()
},
] {
let mut inventory = KeyInventory::default();
assert!(matches!(
inventory.add_private_der(
"new".into(),
&envelope,
Some(&[0; 256]),
KeyUsages::SIGN,
&resources
),
Err(KeyStoreError::Policy(_))
));
assert!(inventory.private_keys().is_empty());
}
let resources = ResourcePolicy {
max_external_resource_total_bytes: peak + 255,
..ResourcePolicy::default()
};
let mut secret = Vec::with_capacity(256);
secret.push(0);
assert!(matches!(
KeyInventory::default().add_private_der_with_password_callback(
"new".into(),
&envelope,
|| Some(Zeroizing::new(secret)),
KeyUsages::SIGN,
&resources
),
Err(KeyStoreError::Policy(_))
));
let exact = ResourcePolicy {
max_external_resource_total_bytes: peak + 256,
max_key_import_kdf_work: 6,
..ResourcePolicy::default()
};
assert!(matches!(
KeyInventory::default().add_private_der(
"new".into(),
&envelope,
Some(&[0; 256]),
KeyUsages::SIGN,
&exact
),
Err(KeyStoreError::ProtectedContainer)
));
}
#[test]
fn private_identity_matches_without_owned_public_encoding() {
let rsa_der = pem::parse(include_bytes!(
"../tests/fixtures/keys/rsa/rsa-2048-key.pem"
))
.expect("RSA PEM fixture")
.into_contents();
let rsa = RsaPrivateKey::from_pkcs8_der(&rsa_der).expect("RSA private key");
let public = rsa
.to_public_key()
.to_public_key_der()
.expect("RSA public encoding");
let identity = PrivateKeyIdentity::decode(&rsa_der).expect("RSA identity");
assert!(identity.matches_spki(public.as_bytes()));
let mut changed = public.as_bytes().to_vec();
*changed.last_mut().expect("nonempty RSA SPKI") ^= 2;
assert!(!identity.matches_spki(&changed));
assert!(!identity.matches_spki(b"invalid DER"));
macro_rules! check_ec {
($key:ty, $length:expr) => {{
let mut scalar = [0; $length];
scalar[$length - 1] = 1;
let key = <$key>::from_slice(&scalar).expect("valid EC scalar");
let private = key.to_pkcs8_der().expect("EC private encoding");
let public = key
.public_key()
.to_public_key_der()
.expect("EC public encoding");
let identity = PrivateKeyIdentity::decode(private.as_bytes()).expect("EC identity");
assert!(identity.matches_spki(public.as_bytes()));
let mut changed = public.as_bytes().to_vec();
*changed.last_mut().expect("nonempty EC SPKI") ^= 1;
assert!(!identity.matches_spki(&changed));
}};
}
check_ec!(p256::SecretKey, 32);
check_ec!(p384::SecretKey, 48);
check_ec!(p521::SecretKey, 66);
let inventory = KeyInventory::from_xml_bytes(
include_bytes!("../tests/fixtures/keys/xmlsec/mixed-keys.xml"),
&xml_policy(ResourcePolicy::default()),
XmlBackend::default(),
)
.expect("mixed store fixture");
let private = inventory
.private_keys()
.iter()
.find(|entry| entry.name == "test-dsa")
.expect("stored DSA private key");
let key = NativeDsaSigningKey::from_pkcs8_der(&private.pkcs8_der).expect("DSA private key");
let public = key
.verifying_key()
.to_public_key_der()
.expect("DSA public encoding");
let identity = PrivateKeyIdentity::decode(&private.pkcs8_der).expect("DSA identity");
assert!(identity.matches_spki(public.as_bytes()));
let mut changed = public.as_bytes().to_vec();
*changed.last_mut().expect("nonempty DSA SPKI") ^= 1;
assert!(!identity.matches_spki(&changed));
}
#[test]
fn xml_import_counts_source_and_decoded_material_together() {
let xml = format!(
"<Keys xmlns='{XMLSEC_NS}' xmlns:ds='{XMLDSIG_NS}'><ds:KeyInfo><ds:KeyName>hmac</ds:KeyName><ds:KeyValue><HMACKeyValue>{}</HMACKeyValue></ds:KeyValue></ds:KeyInfo></Keys>",
base64::engine::general_purpose::STANDARD.encode(vec![1; 1024])
);
let resources = ResourcePolicy {
max_external_resource_total_bytes: xml.len(),
..Default::default()
};
assert!(matches!(
KeyInventory::from_xml_bytes(
xml.as_bytes(),
&xml_policy(resources),
XmlBackend::default()
),
Err(KeyStoreError::Policy(_))
));
let peak = xml.len() + 1024 + 2 * "hmac".len();
for maximum in [peak - 1, peak] {
let policy = xml_policy(ResourcePolicy {
max_external_resource_total_bytes: maximum,
..Default::default()
});
let result =
KeyInventory::from_xml_bytes(xml.as_bytes(), &policy, XmlBackend::default());
if maximum == peak {
let store = result.expect("exact live peak fits");
assert_eq!(store.symmetric_keys[0].bytes.as_slice(), &[1; 1024]);
assert_eq!(store.material_bytes, xml.len() + 1024 + "hmac".len());
} else {
assert!(matches!(result, Err(KeyStoreError::Policy(_))));
}
}
}
#[test]
fn private_pem_preflight_precedes_payload_parsing() {
for label in ["PRIVATE KEY", "ENCRYPTED PRIVATE KEY", "RSA PRIVATE KEY"] {
let pem = pem::encode(&pem::Pem::new(label, vec![1, 2, 3]));
let resources = ResourcePolicy {
max_external_resource_total_bytes: pem.len() + "key".len() + 2,
..Default::default()
};
let mut store = KeyInventory::default();
assert!(matches!(
store.add_private_pem(
"key".into(),
pem.as_bytes(),
None,
KeyUsages::SIGN,
&resources
),
Err(KeyStoreError::Policy(_))
));
assert_eq!(store.entry_count(), 0);
}
}
#[test]
fn xml_public_key_import_preserves_key_info_metadata_validation() {
let source = include_str!("../tests/fixtures/keys/xmlsec/mixed-keys.xml");
let marker = source.find("<RSAKeyValue").expect("RSA entry");
let start = source[..marker].rfind("<KeyInfo").expect("RSA KeyInfo");
let end =
marker + source[marker..].find("</KeyInfo>").expect("RSA end") + "</KeyInfo>".len();
let entry = &source[start..end];
let name_start = entry.find("<KeyName>").expect("name") + "<KeyName>".len();
let name_end = name_start + entry[name_start..].find("</KeyName>").expect("name end");
for changed in [
format!(
"{}{}{}",
&entry[..name_start],
"a".repeat(4097),
&entry[name_end..]
),
entry.replacen("<KeyName>", "unexpected<KeyName>", 1),
] {
let xml = format!("<Keys xmlns='{XMLSEC_NS}'>{changed}</Keys>");
assert!(
KeyInventory::from_xml_bytes(
xml.as_bytes(),
&xml_policy(ResourcePolicy::default()),
XmlBackend::default()
)
.is_err()
);
}
}
#[test]
fn public_pem_decoder_preserves_framing_and_whitespace_contract() {
for data in [
"AQID",
"A Q\tI\r\nD",
"A\u{2003}QID",
"Header: value\n\nAQID",
"Header: value\r\n\r\nAQID",
] {
let text = format!("-----BEGIN PUBLIC KEY-----\n{data}\n-----END PUBLIC KEY-----");
let expected = single_pem_block(text.as_bytes(), 4096).expect("existing PEM contract");
let frame = BorrowedPem::parse(text.as_bytes(), 4096).expect("borrowed frame");
assert_eq!(
frame.decode().expect("borrowed decode"),
expected.contents()
);
}
for data in ["AQI=", "AQ==", "AQID"] {
let text =
format!("-----BEGIN RSA PUBLIC KEY-----\n{data}\n-----END RSA PUBLIC KEY-----");
let frame = BorrowedPem::parse(text.as_bytes(), 4096).expect("borrowed frame");
assert_eq!(frame.decode().expect("decode").len(), frame.decoded_len);
}
for length in [0, 1, 2, 3, 1023, 1024, 1025, 4096] {
let bytes = vec![0xa5; length];
let encoded = pem::encode(&pem::Pem::new("PUBLIC KEY", bytes.clone()));
let frame = BorrowedPem::parse(encoded.as_bytes(), 8192).expect("frame");
assert_eq!(frame.decoded_len, length);
assert_eq!(frame.decode().expect("streamed decoding"), bytes);
}
for text in [
"-----BEGIN PUBLIC KEY-----\nAQID\n-----END RSA PUBLIC KEY-----",
"-----BEGIN PUBLIC KEY-----\nAQID\n-----END PUBLIC KEY-----junk",
"-----BEGIN PUBLIC KEY-----\nAQID\n-----END PUBLIC KEY-----\n-----BEGIN PUBLIC KEY-----\nAQID\n-----END PUBLIC KEY-----",
"-----BEGIN PUBLIC KEY-----\nAQI\n-----END PUBLIC KEY-----",
"-----BEGIN PUBLIC KEY-----\nAQ$=\n-----END PUBLIC KEY-----",
"-----BEGIN PUBLIC KEY-----\nAR==\n-----END PUBLIC KEY-----",
"-----BEGIN PUBLIC KEY-----\nA===\n-----END PUBLIC KEY-----",
"-----BEGIN PUBLIC KEY-----\nAQ=I\n-----END PUBLIC KEY-----",
"-----BEGIN PUBLIC KEY-----\nnot a header\n\nAQID\n-----END PUBLIC KEY-----",
] {
assert!(
BorrowedPem::parse(text.as_bytes(), 4096)
.and_then(|frame| frame.decode())
.is_err(),
"accepted {text}"
);
}
}
#[test]
fn public_pem_import_accounts_for_simultaneous_buffers() {
let pem = include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-pubkey.pem");
let spki = pem::parse(pem).expect("public PEM fixture").into_contents();
let key = RsaPublicKey::from_public_key_der(&spki).expect("RSA key");
let pkcs1 = key.to_pkcs1_der().expect("PKCS1 encoding");
for (label, decoded, extra) in [
("PUBLIC KEY", spki.as_slice(), 0),
("RSA PUBLIC KEY", pkcs1.as_bytes(), spki.len()),
] {
let encoded = pem::encode(&pem::Pem::new(label, decoded.to_vec()));
for exact in [false, true] {
let mut inventory = KeyInventory::default();
inventory
.add_symmetric(
"existing".into(),
SymmetricKeyKind::Hmac,
vec![1; 16],
KeyUsages::SIGN,
&ResourcePolicy::default(),
)
.expect("existing key");
let retained = inventory.material_bytes;
let resources = ResourcePolicy {
max_external_resource_total_bytes: retained
+ 2 * "new".len()
+ encoded.len()
+ decoded.len()
+ extra
- usize::from(!exact),
..ResourcePolicy::default()
};
let result = inventory.add_public_pem("new".into(), encoded.as_bytes(), &resources);
if exact {
result.expect("exact peak allowance");
assert_eq!(inventory.entry_count(), 2);
} else {
assert!(matches!(result, Err(KeyStoreError::Policy(_))));
assert_eq!(inventory.entry_count(), 1);
assert_eq!(inventory.material_bytes, retained);
}
}
}
}
#[test]
fn plaintext_private_import_checks_all_live_copies() {
let der = pem::parse(include_bytes!(
"../tests/fixtures/keys/rsa/rsa-2048-key.pem"
))
.expect("private key")
.into_contents();
let pkcs1 = RsaPrivateKey::from_pkcs8_der(&der)
.expect("RSA")
.to_pkcs1_der()
.expect("PKCS1");
for (label, input) in [
("PRIVATE KEY", der.as_slice()),
("RSA PRIVATE KEY", pkcs1.as_bytes()),
] {
let pem = pem::encode(&pem::Pem::new(label, input.to_vec()));
for encoded in [false, true] {
let peak = 3 + input.len() + der.len() + if encoded { pem.len() } else { 0 };
for exact in [false, true] {
let resources = ResourcePolicy {
max_external_resource_total_bytes: peak - usize::from(!exact),
..ResourcePolicy::default()
};
let mut inventory = KeyInventory::default();
let result = if encoded {
inventory.add_private_pem(
"new".into(),
pem.as_bytes(),
None,
KeyUsages::SIGN,
&resources,
)
} else {
inventory.add_private_der(
"new".into(),
input,
None,
KeyUsages::SIGN,
&resources,
)
};
if exact {
result.expect("exact live-byte allowance");
assert_eq!(inventory.entry_count(), 1);
} else {
assert!(matches!(result, Err(KeyStoreError::Policy(_))));
assert_eq!(inventory.entry_count(), 0);
assert_eq!(inventory.material_bytes, 0);
}
}
}
}
}
#[test]
fn encrypted_pkcs8_plaintext_is_reserved_before_password() {
use der::Encode as _;
use pkcs8::pkcs5::{EncryptionScheme, pbes2};
let encrypted = EncryptedPrivateKeyInfoRef {
encryption_algorithm: EncryptionScheme::Pbes2(pbes2::Parameters {
kdf: pbes2::Kdf::Pbkdf2(pbes2::Pbkdf2Params {
salt: pbes2::Salt::new(b"12345678").expect("salt"),
iteration_count: 2,
key_length: None,
prf: pbes2::Pbkdf2Prf::HmacWithSha256,
}),
encryption: pbes2::EncryptionScheme::Aes256Cbc { iv: [0; 16] },
}),
encrypted_data: der::asn1::OctetStringRef::new(&[0; 64]).expect("ciphertext"),
};
let bytes = encrypted.to_der().expect("envelope");
let peak = 3 + bytes.len() + 64;
let tight = ResourcePolicy {
max_external_resource_total_bytes: peak - 1,
..ResourcePolicy::default()
};
let calls = std::cell::Cell::new(0);
let mut inventory = KeyInventory::default();
let result = inventory.add_private_der_with_password_callback(
"new".into(),
&bytes,
|| {
calls.set(calls.get() + 1);
None
},
KeyUsages::SIGN,
&tight,
);
assert_eq!(calls.get(), 0);
assert!(
matches!(result, Err(KeyStoreError::Policy(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES, actual, ..
})) if actual == peak)
);
assert!(matches!(
inventory.add_private_der(
"new".into(),
&bytes,
Some(b"wrong"),
KeyUsages::SIGN,
&tight
),
Err(KeyStoreError::Policy(_))
));
let exact = ResourcePolicy {
max_external_resource_total_bytes: peak,
..tight
};
assert!(matches!(
inventory.add_private_der_with_password_callback(
"new".into(),
&bytes,
|| {
calls.set(calls.get() + 1);
None
},
KeyUsages::SIGN,
&exact
),
Err(KeyStoreError::ProtectedContainer)
));
assert_eq!(calls.get(), 1);
assert!(inventory.private_keys().is_empty());
assert_eq!(inventory.material_bytes, 0);
}
#[test]
fn encrypted_pem_workspace_accounts_for_live_encoded_input() {
use der::Encode as _;
use pkcs8::pkcs5::{EncryptionScheme, pbes2};
let encrypted = EncryptedPrivateKeyInfoRef {
encryption_algorithm: EncryptionScheme::Pbes2(pbes2::Parameters {
kdf: pbes2::Kdf::Scrypt(pbes2::ScryptParams {
salt: pbes2::Salt::new(b"12345678").expect("salt"),
cost_parameter: 16,
block_size: 1,
parallelization: 1,
key_length: None,
}),
encryption: pbes2::EncryptionScheme::Aes256Cbc { iv: [0; 16] },
}),
encrypted_data: der::asn1::OctetStringRef::new(&[0; 16]).expect("ciphertext"),
};
let der = encrypted.to_der().expect("envelope");
let pem = pem::encode(&pem::Pem::new("ENCRYPTED PRIVATE KEY", der.clone()));
let peak = 3 + pem.len() + der.len() + 16 + 2304;
let mut inventory = KeyInventory::default();
let tight = ResourcePolicy {
max_external_resource_total_bytes: peak - 1,
..ResourcePolicy::default()
};
assert!(matches!(
inventory.add_private_pem("new".into(), pem.as_bytes(), None, KeyUsages::SIGN, &tight),
Err(KeyStoreError::Policy(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
actual, ..
})) if actual == peak
));
let exact = ResourcePolicy {
max_external_resource_total_bytes: peak,
..tight
};
assert!(matches!(
inventory.add_private_pem("new".into(), pem.as_bytes(), None, KeyUsages::SIGN, &exact),
Err(KeyStoreError::ProtectedContainer)
));
assert!(matches!(
inventory.add_private_der("new".into(), &der, None, KeyUsages::SIGN, &tight),
Err(KeyStoreError::ProtectedContainer)
));
assert!(inventory.private_keys().is_empty());
assert_eq!(inventory.material_bytes, 0);
let plain = pem::parse(include_bytes!(
"../tests/fixtures/keys/rsa/rsa-2048-key.pem"
))
.expect("private key")
.into_contents();
let EncryptionScheme::Pbes2(params) = &encrypted.encryption_algorithm else {
panic!("PBES2 envelope");
};
let ciphertext = params.encrypt(b"correct", &plain).expect("encrypt");
let real = EncryptedPrivateKeyInfoRef {
encryption_algorithm: encrypted.encryption_algorithm.clone(),
encrypted_data: der::asn1::OctetStringRef::new(&ciphertext).expect("ciphertext"),
}
.to_der()
.expect("envelope");
let pem = pem::encode(&pem::Pem::new("ENCRYPTED PRIVATE KEY", real.clone()));
let resources = ResourcePolicy {
max_external_resource_total_bytes: 3
+ pem.len()
+ real.len()
+ ciphertext.len()
+ 2304
+ b"correct".len(),
..ResourcePolicy::default()
};
inventory
.add_private_pem(
"new".into(),
pem.as_bytes(),
Some(b"correct"),
KeyUsages::SIGN,
&resources,
)
.expect("exact PEM peak imports");
assert_eq!(inventory.private_keys().len(), 1);
}
#[test]
fn pkcs8_scrypt_workspace_shares_retained_inventory_budget() {
use der::Encode as _;
use pkcs8::pkcs5::{EncryptionScheme, pbes2};
let encrypted = EncryptedPrivateKeyInfoRef {
encryption_algorithm: EncryptionScheme::Pbes2(pbes2::Parameters {
kdf: pbes2::Kdf::Scrypt(pbes2::ScryptParams {
salt: pbes2::Salt::new(b"12345678").expect("salt"),
cost_parameter: 16,
block_size: 1,
parallelization: 1,
key_length: None,
}),
encryption: pbes2::EncryptionScheme::Aes256Cbc { iv: [0; 16] },
}),
encrypted_data: der::asn1::OctetStringRef::new(&[0; 16]).expect("ciphertext"),
};
let bytes = encrypted.to_der().expect("envelope");
let mut inventory = KeyInventory::default();
inventory
.add_symmetric(
"old".into(),
SymmetricKeyKind::Hmac,
vec![7; 1024],
KeyUsages::SIGN,
&ResourcePolicy::default(),
)
.expect("retained key");
let resources = ResourcePolicy {
max_external_resource_total_bytes: inventory.material_bytes + 2304,
max_key_import_kdf_memory_bytes: 4096,
..ResourcePolicy::default()
};
let calls = std::cell::Cell::new(0);
let result = inventory.add_private_der_with_password_callback(
"new".into(),
&bytes,
|| {
calls.set(calls.get() + 1);
None
},
KeyUsages::SIGN,
&resources,
);
assert_eq!(calls.get(), 0);
assert!(matches!(
result,
Err(KeyStoreError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
..
}
))
));
assert!(inventory.private_keys().is_empty());
assert_eq!(inventory.symmetric_keys().len(), 1);
let exact = ResourcePolicy {
max_external_resource_total_bytes: inventory.material_bytes
+ bytes.len()
+ 3
+ 16
+ 2304,
..resources
};
assert!(matches!(
inventory.add_private_der_with_password_callback(
"new".into(),
&bytes,
|| {
calls.set(calls.get() + 1);
None
},
KeyUsages::SIGN,
&exact,
),
Err(KeyStoreError::ProtectedContainer)
));
assert_eq!(
calls.get(),
1,
"an exactly fitting workspace reaches password delivery"
);
}
#[test]
fn configured_x509_continuation_retains_first_deferred_error() {
let mut inventory = KeyInventory::default();
inventory
.add_certificate_der(
single_pem_block(
include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-cert.pem"),
ResourcePolicy::default().max_external_resource_bytes,
)
.expect("certificate")
.into_contents(),
false,
&ResourcePolicy::default(),
)
.expect("lookup");
let mut info = KeyInfo {
sources: vec![
KeyInfoSource::KeyValue(KeyValueInfo::Dsa {
p: None,
q: None,
g: None,
y: vec![1],
}),
KeyInfoSource::X509Data(X509DataInfo {
subject_names: vec!["CN=absent".into()],
..X509DataInfo::default()
}),
KeyInfoSource::KeyValue(KeyValueInfo::InvalidEcKeyValue),
],
};
let unsplit = DefaultKeyResolver::new(KeyResolverConfig::default())
.resolve(Some(&info), SignatureAlgorithm::RsaSha256)
.err()
.expect("mismatch");
let split = inventory
.verification_resolver()
.resolve(Some(&info), SignatureAlgorithm::RsaSha256)
.err()
.expect("mismatch");
assert_eq!(format!("{split:?}"), format!("{unsplit:?}"));
info.sources[2] = KeyInfoSource::DerEncodedKeyValue(vec![0]);
let unsplit = DefaultKeyResolver::new(KeyResolverConfig::default())
.resolve(Some(&info), SignatureAlgorithm::RsaSha256)
.err()
.expect("terminal");
let split = inventory
.verification_resolver()
.resolve(Some(&info), SignatureAlgorithm::RsaSha256)
.err()
.expect("terminal");
assert_eq!(format!("{split:?}"), format!("{unsplit:?}"));
}
#[test]
fn configured_x509_fallback_preserves_terminal_prefix_errors() {
let resources = ResourcePolicy::default();
let mut inventory = KeyInventory::default();
let certificate = single_pem_block(
include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-cert.pem"),
resources.max_external_resource_bytes,
)
.expect("certificate")
.into_contents();
let subject = crate::xmldsig::parse::parse_x509_certificate(&certificate)
.expect("leaf")
.subject_dn;
inventory
.add_certificate_der(certificate, false, &resources)
.expect("lookup");
let mut info = KeyInfo {
sources: vec![
KeyInfoSource::DerEncodedKeyValue(vec![0]),
KeyInfoSource::X509Data(X509DataInfo {
subject_names: vec![subject],
..X509DataInfo::default()
}),
],
};
assert!(
inventory
.verification_resolver()
.resolve(Some(&info), SignatureAlgorithm::RsaSha256,)
.is_err(),
"a terminal prefix error must stop resolution"
);
info.sources[0] = KeyInfoSource::KeyValue(KeyValueInfo::InvalidEcKeyValue);
assert!(
inventory
.verification_resolver()
.resolve(Some(&info), SignatureAlgorithm::RsaSha256,)
.expect("deferred prefix permits certificate fallback")
.is_some()
);
let KeyInfoSource::X509Data(data) = &mut info.sources[1] else {
panic!("X509 selector");
};
data.subject_names = vec!["CN=absent".into()];
assert!(
inventory
.verification_resolver()
.resolve(Some(&info), SignatureAlgorithm::RsaSha256,)
.is_err(),
"a complete miss retains its deferred error"
);
}
#[test]
fn configured_x509_fallback_does_not_reinspect_prefix() {
let resources = ResourcePolicy::default();
let mut inventory = KeyInventory::default();
let certificate = single_pem_block(
include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-cert.pem"),
resources.max_external_resource_bytes,
)
.expect("certificate")
.into_contents();
let subject = crate::xmldsig::parse::parse_x509_certificate(&certificate)
.expect("leaf")
.subject_dn;
inventory
.add_certificate_der(certificate, false, &resources)
.expect("lookup");
let info = KeyInfo {
sources: vec![
KeyInfoSource::KeyName("absent".into()),
KeyInfoSource::X509Data(X509DataInfo {
subject_names: vec![subject],
..X509DataInfo::default()
}),
],
};
let mut policy = crate::policy::VerificationPolicy::default();
policy.resources.max_key_candidates = 3;
assert!(
inventory
.verification_resolver()
.resolve_with_policy_and_provider(
Some(&info),
SignatureAlgorithm::RsaSha256,
&policy,
crate::provider::default_provider(),
)
.expect("each source inspected once")
.is_some()
);
policy.resources.max_key_candidates = 2;
assert!(matches!(
inventory
.verification_resolver()
.resolve_with_policy_and_provider(
Some(&info),
SignatureAlgorithm::RsaSha256,
&policy,
crate::provider::default_provider(),
),
Err(DsigError::Policy(_))
));
}
#[test]
fn scrypt_parallel_buffers_are_checked_before_derivation() {
let mut resources = ResourcePolicy {
max_key_import_kdf_work: 10_000,
max_key_import_kdf_memory_bytes: 4_096,
..ResourcePolicy::default()
};
assert!(matches!(
enforce_scrypt_kdf_limits(2, 1, 1_000, &resources, 0),
Err(KeyStoreError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::KEY_IMPORT_KDF_MEMORY,
maximum: 4_096,
actual: 512_000,
}
))
));
resources.max_key_import_kdf_memory_bytes = 512_000;
assert!(enforce_scrypt_kdf_limits(2, 1, 1_000, &resources, 0).is_ok());
}
#[test]
fn named_hmac_resolution_enforces_usage_and_method() {
let resources = ResourcePolicy::default();
let mut inventory = KeyInventory::default();
inventory
.add_symmetric(
"verify".into(),
SymmetricKeyKind::Hmac,
b"sufficiently-long-hmac-secret".to_vec(),
KeyUsages::VERIFY,
&resources,
)
.expect("verification HMAC imports");
inventory
.add_symmetric(
"sign-only".into(),
SymmetricKeyKind::Hmac,
b"another-hmac-secret".to_vec(),
KeyUsages::SIGN,
&resources,
)
.expect("sign-only HMAC imports");
let resolver = inventory.verification_resolver();
let named = |name: &str| KeyInfo {
sources: vec![KeyInfoSource::KeyName(name.into())],
..KeyInfo::default()
};
assert!(
resolver
.resolve(Some(&named("verify")), SignatureAlgorithm::HmacSha256)
.expect("named HMAC resolves")
.is_some()
);
assert!(
resolver
.resolve(Some(&named("verify")), SignatureAlgorithm::RsaSha256)
.is_err()
);
assert!(
resolver
.resolve(Some(&named("sign-only")), SignatureAlgorithm::HmacSha256)
.is_err()
);
}
#[test]
fn named_hmac_resolution_rejects_invalid_policy_snapshot() {
let mut inventory = KeyInventory::default();
inventory
.add_symmetric(
"verify".into(),
SymmetricKeyKind::Hmac,
b"sufficiently-long-hmac-secret".to_vec(),
KeyUsages::VERIFY,
&ResourcePolicy::default(),
)
.expect("verification HMAC imports");
let info = KeyInfo {
sources: vec![KeyInfoSource::KeyName("verify".into())],
..KeyInfo::default()
};
let mut policy = crate::policy::VerificationPolicy::default();
policy.resources.max_external_resource_bytes = usize::MAX;
assert!(matches!(
inventory.verification_resolver().resolve_with_policy(
Some(&info),
SignatureAlgorithm::HmacSha256,
&policy
),
Err(DsigError::Policy(_))
));
}
#[test]
fn named_hmac_resolution_uses_active_resource_limits() {
let resources = ResourcePolicy::default();
let secret = b"sufficiently-long-hmac-secret";
let mut inventory = KeyInventory::default();
inventory
.add_symmetric(
"verify".into(),
SymmetricKeyKind::Hmac,
secret.to_vec(),
KeyUsages::VERIFY,
&resources,
)
.expect("verification HMAC imports");
let key_info = KeyInfo {
sources: vec![KeyInfoSource::KeyName("verify".into())],
..KeyInfo::default()
};
let resolver = inventory.verification_resolver();
for (resource, aggregate) in [
(crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES, false),
(
crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
true,
),
] {
let mut policy = crate::policy::VerificationPolicy::default();
if aggregate {
policy.resources.max_external_resource_total_bytes = secret.len() - 1;
} else {
policy.resources.max_external_resource_bytes = secret.len() - 1;
}
let error = resolver
.resolve_with_policy(Some(&key_info), SignatureAlgorithm::HmacSha256, &policy)
.err()
.expect("active limit must reject stored HMAC material");
assert!(
matches!(
error,
DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
resource: actual,
maximum,
actual: size,
}) if actual == resource && maximum == secret.len() - 1 && size == secret.len()
),
"{error:?}"
);
}
}
#[test]
fn one_named_verification_entry_fits_one_candidate() {
let mut inventory = KeyInventory::default();
let mut policy = crate::policy::VerificationPolicy::default();
inventory
.add_symmetric(
"only".into(),
SymmetricKeyKind::Hmac,
b"sufficiently-long-hmac-secret".to_vec(),
KeyUsages::VERIFY,
&policy.resources,
)
.expect("HMAC imports");
policy.resources.max_key_candidates = 1;
let info = KeyInfo {
sources: vec![KeyInfoSource::KeyName("only".into())],
};
assert!(
inventory
.verification_resolver()
.resolve_with_policy(Some(&info), SignatureAlgorithm::HmacSha256, &policy)
.expect("one lookup fits")
.is_some()
);
}
#[test]
fn pem_imports_charge_encoded_input_to_aggregate_budget() {
let public = include_str!("../tests/fixtures/keys/rsa/rsa-4096-pubkey.pem");
let private = include_str!("../tests/fixtures/keys/rsa/rsa-4096-key.pem");
for (pem, is_private) in [(public, false), (private, true)] {
let padded = format!("{pem}{}", " ".repeat(16 * 1024));
let workspace = pem::parse(pem).expect("PEM payload").contents().len()
* if is_private { 2 } else { 1 };
let resources = ResourcePolicy {
max_external_resource_bytes: padded.len(),
max_external_resource_total_bytes: padded.len()
+ if is_private { 5 } else { 10 }
+ workspace
+ 1,
..ResourcePolicy::default()
};
let mut inventory = KeyInventory::default();
let import = |inventory: &mut KeyInventory, name: &str| {
if is_private {
inventory.add_private_pem(
name.into(),
padded.as_bytes(),
None,
KeyUsages::SIGN,
&resources,
)
} else {
inventory.add_public_pem(name.into(), padded.as_bytes(), &resources)
}
};
import(&mut inventory, "first").expect("first PEM import fits");
assert!(
matches!(
import(&mut inventory, "second"),
Err(KeyStoreError::Policy(
crate::policy::PolicyViolation::ResourceLimitExceeded {
resource:
crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
..
}
))
),
"second PEM input must exceed aggregate budget"
);
}
}
#[test]
fn repeated_key_name_selects_one_inventory_entry() {
let resources = ResourcePolicy::default();
let mut inventory = KeyInventory::default();
inventory
.add_symmetric(
"secret".into(),
SymmetricKeyKind::Hmac,
b"sufficiently-long-hmac-secret".to_vec(),
KeyUsages::VERIFY,
&resources,
)
.expect("HMAC key imports");
inventory
.add_symmetric(
"other-secret".into(),
SymmetricKeyKind::Hmac,
b"another-long-hmac-secret".to_vec(),
KeyUsages::VERIFY,
&resources,
)
.expect("second HMAC key imports");
inventory
.add_public_pem(
"public".into(),
include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"),
&resources,
)
.expect("public key imports");
let resolver = inventory.verification_resolver();
for (name, algorithm) in [
("secret", SignatureAlgorithm::HmacSha256),
("public", SignatureAlgorithm::RsaSha256),
] {
let info = KeyInfo {
sources: vec![
KeyInfoSource::KeyName(name.into()),
KeyInfoSource::KeyName(name.into()),
],
};
assert!(resolver.resolve(Some(&info), algorithm).is_ok(), "{name}");
}
let distinct = KeyInfo {
sources: vec![
KeyInfoSource::KeyName("secret".into()),
KeyInfoSource::KeyName("other-secret".into()),
],
};
assert!(
resolver
.resolve(Some(&distinct), SignatureAlgorithm::HmacSha256)
.is_err()
);
}
#[test]
fn named_lookup_charges_inspected_inventory_entries() {
let mut inventory = KeyInventory::default();
let resources = ResourcePolicy::default();
for name in ["first", "second"] {
inventory
.add_symmetric(
name.into(),
SymmetricKeyKind::Hmac,
b"sufficiently-long-hmac-secret".to_vec(),
KeyUsages::VERIFY,
&resources,
)
.expect("key imports");
}
let key_info = KeyInfo {
sources: vec![KeyInfoSource::KeyName("second".into())],
..KeyInfo::default()
};
let policy = crate::policy::VerificationPolicy {
resources: ResourcePolicy {
max_key_candidates: 1,
..ResourcePolicy::default()
},
..crate::policy::VerificationPolicy::default()
};
let resolver = inventory.verification_resolver();
assert!(
resolver
.resolve_with_policy_and_provider(
Some(&key_info),
SignatureAlgorithm::HmacSha256,
&policy,
crate::provider::default_provider(),
)
.is_err()
);
}
#[test]
fn private_import_rejects_public_only_usage() {
let private = include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-key.pem");
let mut inventory = KeyInventory::default();
assert!(
inventory
.add_private_pem(
"wrong-use".into(),
private,
None,
KeyUsages::SIGN.union(KeyUsages::VERIFY),
&ResourcePolicy::default(),
)
.is_err()
);
assert!(inventory.private_keys.is_empty());
}
#[test]
fn normalized_private_key_must_fit_resource_limit() {
let rsa = RsaPrivateKey::from_pkcs8_pem(include_str!(
"../tests/fixtures/keys/rsa/rsa-2048-key.pem"
))
.expect("RSA fixture parses");
let pkcs1 = rsa.to_pkcs1_der().expect("PKCS#1 encodes");
let pkcs8 = rsa.to_pkcs8_der().expect("PKCS#8 encodes");
assert!(pkcs8.as_bytes().len() > pkcs1.as_bytes().len());
let resources = ResourcePolicy {
max_external_resource_bytes: pkcs1.as_bytes().len(),
..ResourcePolicy::default()
};
let mut inventory = KeyInventory::default();
assert!(
inventory
.add_private_der(
"rsa".into(),
pkcs1.as_bytes(),
None,
KeyUsages::SIGN,
&resources,
)
.is_err()
);
assert!(inventory.private_keys.is_empty());
}
#[test]
fn oversized_rsa_components_are_rejected_before_private_key_decode() {
let pem = include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-key.pem");
let block = single_pem_block(pem, ResourcePolicy::default().max_external_resource_bytes)
.expect("fixture PEM");
let info = PrivateKeyInfoRef::try_from(block.contents()).expect("fixture PKCS#8");
let original =
pkcs1::RsaPrivateKeyRef::from_der(info.private_key.as_bytes()).expect("fixture PKCS#1");
let oversized_modulus = vec![1_u8; 1025];
let oversized = pkcs1::RsaPrivateKeyRef {
modulus: der::asn1::UintRef::new(&oversized_modulus).expect("positive modulus"),
public_exponent: original.public_exponent,
private_exponent: original.private_exponent,
prime1: original.prime1,
prime2: original.prime2,
exponent1: original.exponent1,
exponent2: original.exponent2,
coefficient: original.coefficient,
other_prime_infos: None,
};
let pkcs1 = der::Encode::to_der(&oversized).expect("PKCS#1 encodes");
let octets = der::asn1::OctetStringRef::new(&pkcs1).expect("PKCS#8 octets");
let pkcs8 = der::Encode::to_der(&PrivateKeyInfoRef::new(pkcs1::ALGORITHM_ID, octets))
.expect("PKCS#8 encodes");
let mut inventory = KeyInventory::default();
let error = inventory
.add_private_der(
"oversized".into(),
&pkcs8,
None,
KeyUsages::SIGN,
&ResourcePolicy::default(),
)
.expect_err("oversized RSA modulus must fail at preflight");
assert!(error.to_string().contains("safety limit"), "{error}");
let oversized_exponent = vec![1_u8; 1025];
let oversized = pkcs1::RsaPrivateKeyRef {
modulus: original.modulus,
public_exponent: original.public_exponent,
private_exponent: der::asn1::UintRef::new(&oversized_exponent)
.expect("positive exponent"),
prime1: original.prime1,
prime2: original.prime2,
exponent1: original.exponent1,
exponent2: original.exponent2,
coefficient: original.coefficient,
other_prime_infos: None,
};
let pkcs1 = der::Encode::to_der(&oversized).expect("PKCS#1 encodes");
let error = preflight_rsa_pkcs1_components(&pkcs1)
.expect_err("oversized private exponent must fail before bigint decoding");
assert!(error.to_string().contains("safety limit"), "{error}");
}
#[test]
fn named_certificate_import_is_not_a_trust_anchor() {
let pem = include_bytes!("../tests/fixtures/keys/rsa/rsa-4096-cert.pem");
let certificate =
single_pem_block(pem, ResourcePolicy::default().max_external_resource_bytes)
.expect("certificate fixture is one PEM block");
let mut inventory = KeyInventory::default();
inventory
.add_public_der(
"recipient-cert".into(),
certificate.contents().to_vec(),
&ResourcePolicy::default(),
)
.expect("named X.509 certificate imports");
#[cfg(feature = "xmlenc")]
{
assert!(
inventory
.rsa_encryption_key(
"recipient-cert",
&crate::policy::EncryptionPolicy::default()
)
.is_ok()
);
let restricted = crate::policy::EncryptionPolicy {
resources: ResourcePolicy {
max_external_resource_bytes: 64,
max_external_resource_total_bytes: 64,
..ResourcePolicy::default()
},
..crate::policy::EncryptionPolicy::default()
};
assert!(
inventory
.rsa_encryption_key("recipient-cert", &restricted)
.is_err()
);
}
assert!(inventory.trusted_certificates.is_empty());
}
#[test]
fn eddsa_private_identity_matches_only_its_corresponding_spki() {
use pkcs8::{EncodePrivateKey, EncodePublicKey};
let key = ed25519_dalek::SigningKey::from_bytes(&[0x42; 32]);
let der = key.to_pkcs8_der().expect("Ed25519 private encoding");
let public = key
.verifying_key()
.to_public_key_der()
.expect("Ed25519 public encoding");
let identity = PrivateKeyIdentity::decode(der.as_bytes()).expect("Ed25519 identity");
assert!(identity.matches_spki(public.as_bytes()));
let wrong = ed25519_dalek::SigningKey::from_bytes(&[0x43; 32])
.verifying_key()
.to_public_key_der()
.expect("different Ed25519 public encoding");
assert!(!identity.matches_spki(wrong.as_bytes()));
let key = ed448_goldilocks::SigningKey::try_from(&[0x42; 57][..]).expect("Ed448 seed");
let der = key.to_pkcs8_der().expect("Ed448 private encoding");
let public = key
.verifying_key()
.to_public_key_der()
.expect("Ed448 public encoding");
let identity = PrivateKeyIdentity::decode(der.as_bytes()).expect("Ed448 identity");
assert!(identity.matches_spki(public.as_bytes()));
assert!(!identity.matches_spki(wrong.as_bytes()));
}
#[test]
fn unsupported_spki_is_rejected_at_import() {
let mut agreement_spki = vec![
0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e, 0x03, 0x21, 0x00,
];
agreement_spki.extend_from_slice(&[1; 32]);
let mut inventory = KeyInventory::default();
assert!(
inventory
.add_public_der(
"unsupported".into(),
agreement_spki,
&ResourcePolicy::default()
)
.is_err()
);
assert!(inventory.public_keys.is_empty());
}
#[test]
fn named_certificate_resolution_preserves_document_crl() {
use rcgen::{CertificateParams, KeyPair, KeyUsagePurpose, SerialNumber};
let mut root_params = CertificateParams::new(Vec::new()).expect("root params");
root_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
root_params.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign];
let root = rcgen::CertifiedIssuer::self_signed(
root_params,
KeyPair::generate().expect("root key"),
)
.expect("root certificate");
let mut leaf_params = CertificateParams::new(Vec::new()).expect("leaf params");
leaf_params.serial_number = Some(SerialNumber::from(42_u64));
leaf_params.key_usages = vec![KeyUsagePurpose::DigitalSignature];
let leaf = leaf_params
.signed_by(&KeyPair::generate().expect("leaf key"), &root)
.expect("leaf certificate");
let now = time::OffsetDateTime::now_utc();
let crl = rcgen::CertificateRevocationListParams {
this_update: now - time::Duration::days(1),
next_update: now + time::Duration::days(1),
crl_number: SerialNumber::from(1_u64),
issuing_distribution_point: None,
revoked_certs: vec![rcgen::RevokedCertParams {
serial_number: SerialNumber::from(42_u64),
revocation_time: now - time::Duration::hours(1),
reason_code: None,
invalidity_date: None,
}],
key_identifier_method: rcgen::KeyIdMethod::Sha256,
}
.signed_by(&root)
.expect("signed CRL");
let resources = ResourcePolicy::default();
let mut inventory = KeyInventory::default();
inventory
.add_public_der("leaf".into(), leaf.der().to_vec(), &resources)
.expect("leaf imports");
inventory
.add_certificate_der(root.der().to_vec(), true, &resources)
.expect("root imports");
let policy = crate::policy::VerificationPolicy {
key_trust: crate::policy::KeyTrustPolicy {
verify_x509_chains: true,
check_crls: true,
verification_time: Some(std::time::SystemTime::now()),
..crate::policy::KeyTrustPolicy::default()
},
..crate::policy::VerificationPolicy::default()
};
let mut info = KeyInfo {
sources: vec![KeyInfoSource::KeyName("leaf".into())],
};
let resolver = inventory.verification_resolver();
assert!(
resolver
.resolve_with_policy_and_provider(
Some(&info),
SignatureAlgorithm::EcdsaSha256,
&policy,
crate::provider::default_provider()
)
.expect("unrevoked chain resolves")
.is_some()
);
info.sources.push(KeyInfoSource::X509Data(X509DataInfo {
crls: vec![crl.der().to_vec()],
..X509DataInfo::default()
}));
let mut candidate_budget = InspectedKeyCandidateBudget::new_with_utc_facts(
&policy,
crate::xmldsig::pkix::test_utc_facts(),
);
let error = resolver
.resolve_for_verification_with_budget(
Some(&info),
SignatureAlgorithm::EcdsaSha256,
&policy,
crate::provider::default_provider(),
&mut candidate_budget,
)
.err()
.expect("document CRL revokes leaf");
assert!(
error
.to_string()
.contains("certificate at chain position 0 is revoked"),
"{error}"
);
let mut bounded = policy.clone();
bounded.resources.max_external_resource_total_bytes =
leaf.der().len() + 2 * (root.der().len() + crl.der().len()) - 1;
assert!(matches!(
resolver.resolve_with_policy_and_provider(
Some(&info),
SignatureAlgorithm::EcdsaSha256,
&bounded,
crate::provider::default_provider()
),
Err(DsigError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
..
}
))
));
bounded.key_trust.check_crls = false;
assert!(
resolver
.resolve_with_policy_and_provider(
Some(&info),
SignatureAlgorithm::EcdsaSha256,
&bounded,
crate::provider::default_provider()
)
.expect("disabled CRL checks do not load CRLs")
.is_some()
);
}
#[test]
fn named_certificate_resolution_enforces_inventory_crl() {
fn cert(pem: &[u8]) -> Vec<u8> {
let text = std::str::from_utf8(pem).expect("fixture is UTF-8");
let start = text.find("-----BEGIN ").expect("fixture has PEM armor");
single_pem_block(
&text.as_bytes()[start..],
ResourcePolicy::default().max_external_resource_bytes,
)
.expect("certificate PEM parses")
.into_contents()
}
let resources = ResourcePolicy::default();
let mut inventory = KeyInventory::default();
inventory
.add_public_der(
"leaf".into(),
cert(include_bytes!(
"../tests/fixtures/keys/rsa/rsa-2048-cert.pem"
)),
&resources,
)
.expect("leaf imports");
for anchor in [
include_bytes!("../tests/fixtures/keys/ca2cert.pem").as_slice(),
include_bytes!("../tests/fixtures/keys/cacert.pem").as_slice(),
] {
inventory
.add_certificate_der(cert(anchor), true, &resources)
.expect("anchor imports");
}
inventory
.add_crl_der(
cert(include_bytes!(
"../tests/fixtures/keys/rsa/rsa-2048-cert-revoked-crl.pem"
)),
&resources,
)
.expect("CRL imports");
let policy = crate::policy::VerificationPolicy {
key_trust: crate::policy::KeyTrustPolicy {
verify_x509_chains: true,
check_crls: true,
max_x509_chain_depth: 3,
verification_time: Some(
std::time::SystemTime::UNIX_EPOCH
+ std::time::Duration::from_secs(1_773_964_800),
),
..crate::policy::KeyTrustPolicy::default()
},
..crate::policy::VerificationPolicy::default()
};
let key_info = KeyInfo {
sources: vec![KeyInfoSource::KeyName("leaf".into())],
..KeyInfo::default()
};
let error = inventory
.verification_resolver()
.resolve_with_policy_and_provider(
Some(&key_info),
SignatureAlgorithm::RsaSha256,
&policy,
crate::provider::default_provider(),
)
.err()
.expect("revocation evidence must reject this chain");
assert!(error.to_string().contains("cRLSign"), "{error}");
}
#[test]
fn hmac_resolution_does_not_load_unrelated_certificate_material() {
let mut inventory = KeyInventory::default();
inventory
.add_symmetric(
"hmac".into(),
SymmetricKeyKind::Hmac,
b"secret".to_vec(),
KeyUsages::VERIFY,
&ResourcePolicy::default(),
)
.expect("HMAC imports");
let pem = include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-cert.pem");
let certificate =
single_pem_block(pem, ResourcePolicy::default().max_external_resource_bytes)
.expect("certificate fixture");
inventory
.add_certificate_der(
certificate.contents().to_vec(),
true,
&ResourcePolicy::default(),
)
.expect("anchor imports");
let policy = crate::policy::VerificationPolicy {
resources: ResourcePolicy {
max_external_resource_bytes: 64,
max_external_resource_total_bytes: 64,
..ResourcePolicy::default()
},
..crate::policy::VerificationPolicy::default()
};
let info = KeyInfo {
sources: vec![KeyInfoSource::KeyName("hmac".into())],
..KeyInfo::default()
};
assert!(
inventory
.verification_resolver()
.resolve_with_policy_and_provider(
Some(&info),
SignatureAlgorithm::HmacSha256,
&policy,
crate::provider::default_provider(),
)
.expect("unrelated certificates cannot consume HMAC budget")
.is_some()
);
}
#[test]
fn named_key_resolution_obeys_source_policy() {
let resources = ResourcePolicy::default();
let mut inventory = KeyInventory::default();
inventory
.add_symmetric(
"blocked-name".into(),
SymmetricKeyKind::Hmac,
b"policy-guarded-hmac-secret".to_vec(),
KeyUsages::VERIFY,
&resources,
)
.expect("HMAC fixture imports");
let info = KeyInfo {
sources: vec![KeyInfoSource::KeyName("blocked-name".into())],
..KeyInfo::default()
};
let mut policy = crate::policy::VerificationPolicy::default();
policy.key_sources.key_name = false;
assert!(
inventory
.verification_resolver()
.resolve_with_policy_and_provider(
Some(&info),
SignatureAlgorithm::HmacSha256,
&policy,
crate::provider::default_provider(),
)
.is_err()
);
}
#[test]
fn named_lookup_and_fallback_share_one_candidate_budget() {
let mut inventory = KeyInventory::default();
inventory.public_keys.push(StoredPublicKey {
name: "named".into(),
key_info: KeyInfo {
sources: vec![
KeyInfoSource::KeyName("named".into()),
KeyInfoSource::KeyValue(KeyValueInfo::Rsa {
modulus: vec![3],
exponent: vec![3],
}),
],
},
usages: KeyUsages::VERIFY,
});
let info = KeyInfo {
sources: vec![KeyInfoSource::KeyName("named".into())],
};
let mut policy = crate::policy::VerificationPolicy::default();
policy.resources.max_key_candidates = 1;
let error = inventory
.verification_resolver()
.resolve_with_policy_and_provider(
Some(&info),
SignatureAlgorithm::RsaSha256,
&policy,
crate::provider::default_provider(),
)
.err()
.expect("delegation must not reset the candidate budget");
assert!(matches!(
error,
DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::KEY_CANDIDATES,
maximum: 1,
actual: 2,
})
));
}
#[test]
fn prefix_retry_spends_the_same_candidate_budget() {
let mut sources = vec![KeyInfoSource::KeyName("missing".into()); 3];
sources.push(KeyInfoSource::X509Data(X509DataInfo {
subject_names: vec!["missing subject".into()],
..X509DataInfo::default()
}));
let info = KeyInfo { sources };
let mut policy = crate::policy::VerificationPolicy::default();
policy.resources.max_key_candidates = 5;
let error = KeyInventory::default()
.verification_resolver()
.resolve_with_policy_and_provider(
Some(&info),
SignatureAlgorithm::RsaSha256,
&policy,
crate::provider::default_provider(),
)
.err()
.expect("the repeated prefix must exhaust the candidate limit");
assert!(matches!(
error,
DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::KEY_CANDIDATES,
maximum: 5,
actual: 6,
})
));
}
#[test]
fn selected_key_value_is_one_resource() {
for value in [
KeyValueInfo::Rsa {
modulus: vec![1; 256],
exponent: vec![1; 3],
},
KeyValueInfo::Dsa {
p: Some(vec![1; 64]),
q: Some(vec![1; 16]),
g: Some(vec![1; 64]),
y: vec![1; 64],
},
KeyValueInfo::Ec {
curve_oid: "1.2.840.10045.3.1.7".into(),
public_key: vec![1; 65],
},
] {
let info = KeyInfo {
sources: vec![KeyInfoSource::KeyValue(value)],
};
let size =
check_selected_public_material(&info, &ResourcePolicy::default()).expect("size");
let resources = ResourcePolicy {
max_external_resource_bytes: size,
..ResourcePolicy::default()
};
assert_eq!(
check_selected_public_material(&info, &resources).expect("exact limit"),
size
);
let resources = ResourcePolicy {
max_external_resource_bytes: size - 1,
..resources
};
assert!(matches!(check_selected_public_material(&info, &resources),
Err(DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES, actual, ..
})) if actual == size));
}
}
#[test]
fn named_verification_bounds_complete_key_value() {
use rsa::traits::PublicKeyParts as _;
let public = RsaPublicKey::from_public_key_pem(include_str!(
"../tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"
))
.expect("RSA fixture");
let modulus = public.n().to_be_bytes_trimmed_vartime();
let exponent = public.e().to_be_bytes_trimmed_vartime();
let size = modulus.len() + exponent.len();
let base64 = base64::engine::general_purpose::STANDARD;
let xml = format!(
"<Keys xmlns=\"{XMLSEC_NS}\"><KeyInfo xmlns=\"{XMLDSIG_NS}\"><KeyName>named</KeyName><KeyValue><RSAKeyValue><Modulus>{}</Modulus><Exponent>{}</Exponent></RSAKeyValue></KeyValue></KeyInfo></Keys>",
base64.encode(modulus),
base64.encode(exponent)
);
let inventory = KeyInventory::from_xml_bytes(
xml.as_bytes(),
&xml_policy(ResourcePolicy::default()),
XmlBackend::default(),
)
.expect("broad import");
let info = KeyInfo {
sources: vec![KeyInfoSource::KeyName("named".into())],
};
let mut policy = crate::policy::VerificationPolicy::default();
policy.resources.max_external_resource_bytes = size;
assert!(
inventory
.verification_resolver()
.resolve_with_policy_and_provider(
Some(&info),
SignatureAlgorithm::RsaSha256,
&policy,
crate::provider::default_provider()
)
.expect("exact complete-key limit")
.is_some()
);
policy.resources.max_external_resource_bytes = size - 1;
assert!(
matches!(inventory.verification_resolver().resolve_with_policy_and_provider(Some(&info), SignatureAlgorithm::RsaSha256,
&policy, crate::provider::default_provider()),
Err(DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES, actual, ..
})) if actual == size)
);
}
#[test]
fn configured_x509_fallback_budgets_live_copies() {
let resources = ResourcePolicy::default();
let certificate = single_pem_block(
include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-cert.pem"),
resources.max_external_resource_bytes,
)
.expect("certificate")
.into_contents();
for trusted in [false, true] {
for with_crl in [false, true] {
let mut inventory = KeyInventory::default();
inventory
.add_certificate_der(certificate.clone(), trusted, &resources)
.expect("certificate imports");
let document_crl = vec![0; 16];
if with_crl {
inventory.crls.push(vec![0; 32]);
}
let info = KeyInfo {
sources: vec![KeyInfoSource::X509Data(X509DataInfo {
subject_names: vec!["CN=absent".into()],
crls: if with_crl {
vec![document_crl]
} else {
Vec::new()
},
..X509DataInfo::default()
})],
};
let mut policy = crate::policy::VerificationPolicy::default();
policy.key_trust.verify_x509_chains = true;
policy.key_trust.check_crls = with_crl;
let configured = certificate.len() + if with_crl { 32 } else { 0 };
let peak = 2 * configured;
policy.resources.max_external_resource_total_bytes = peak - 1;
assert!(
matches!(
inventory
.verification_resolver()
.resolve_with_policy_and_provider(
Some(&info),
SignatureAlgorithm::RsaSha256,
&policy,
crate::provider::default_provider(),
),
Err(DsigError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource:
crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
..
}
))
),
"each owned fallback copy must fit before allocation"
);
policy.resources.max_external_resource_total_bytes = peak;
let resolver = inventory.verification_resolver();
let outcome = resolver.resolve_with_policy_and_provider(
Some(&info),
SignatureAlgorithm::RsaSha256,
&policy,
crate::provider::default_provider(),
);
assert!(
!matches!(outcome, Err(DsigError::Policy(_))),
"exact copy budget passes preflight"
);
}
}
}
#[test]
fn named_x509_fallback_budgets_document_crl_copies() {
let resources = ResourcePolicy::default();
let certificate = single_pem_block(
include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-cert.pem"),
resources.max_external_resource_bytes,
)
.expect("certificate")
.into_contents();
let mut inventory = KeyInventory::default();
inventory
.add_public_der("leaf".into(), certificate.clone(), &resources)
.expect("named certificate");
let info = KeyInfo {
sources: vec![
KeyInfoSource::KeyName("leaf".into()),
KeyInfoSource::X509Data(X509DataInfo {
crls: vec![vec![0; 32]],
..X509DataInfo::default()
}),
],
};
let mut policy = crate::policy::VerificationPolicy::default();
policy.key_trust.verify_x509_chains = true;
policy.key_trust.check_crls = true;
policy.resources.max_external_resource_total_bytes = certificate.len() + 64 - 1;
assert!(matches!(
inventory
.verification_resolver()
.resolve_with_policy_and_provider(
Some(&info),
SignatureAlgorithm::RsaSha256,
&policy,
crate::provider::default_provider(),
),
Err(DsigError::Policy(
crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
..
}
))
));
}
#[test]
fn selected_certificate_and_anchors_share_aggregate_budget() {
let certificate = single_pem_block(
include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-cert.pem"),
ResourcePolicy::default().max_external_resource_bytes,
)
.expect("leaf PEM")
.into_contents();
let anchor = single_pem_block(
include_bytes!("../tests/fixtures/keys/rsa/rsa-4096-cert.pem"),
ResourcePolicy::default().max_external_resource_bytes,
)
.expect("anchor PEM")
.into_contents();
let mut inventory = KeyInventory::default();
inventory
.add_public_der(
"leaf".into(),
certificate.clone(),
&ResourcePolicy::default(),
)
.expect("leaf imports");
inventory
.add_certificate_der(anchor.clone(), true, &ResourcePolicy::default())
.expect("anchor imports");
let mut policy = crate::policy::VerificationPolicy::default();
policy.key_trust.verify_x509_chains = true;
policy.resources.max_external_resource_total_bytes = certificate.len() + anchor.len() - 1;
let info = KeyInfo {
sources: vec![KeyInfoSource::KeyName("leaf".into())],
};
let error = inventory
.verification_resolver()
.resolve_with_policy_and_provider(
Some(&info),
SignatureAlgorithm::RsaSha256,
&policy,
crate::provider::default_provider(),
)
.err()
.expect("combined selected and configured material exceeds the budget");
assert!(matches!(
error,
DsigError::Policy(crate::policy::PolicyViolation::ResourceLimit {
resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
..
})
));
}
#[test]
fn named_key_does_not_bypass_other_source_permissions() {
let resources = ResourcePolicy::default();
let mut inventory = KeyInventory::default();
inventory
.add_symmetric(
"named".into(),
SymmetricKeyKind::Hmac,
b"verification-secret".to_vec(),
KeyUsages::VERIFY,
&resources,
)
.expect("HMAC fixture imports");
let info = KeyInfo {
sources: vec![
KeyInfoSource::KeyName("named".into()),
KeyInfoSource::KeyValue(KeyValueInfo::Rsa {
modulus: vec![3],
exponent: vec![3],
}),
],
..KeyInfo::default()
};
let mut policy = crate::policy::VerificationPolicy::default();
policy.key_sources.key_value = false;
assert!(
inventory
.verification_resolver()
.resolve_with_policy_and_provider(
Some(&info),
SignatureAlgorithm::HmacSha256,
&policy,
crate::provider::default_provider(),
)
.is_err()
);
}
#[test]
fn named_public_key_is_trusted_inventory_material() {
let mut inventory = KeyInventory::default();
inventory
.add_public_pem(
"named".into(),
include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"),
&ResourcePolicy::default(),
)
.expect("public key imports");
let info = KeyInfo {
sources: vec![KeyInfoSource::KeyName("named".into())],
..KeyInfo::default()
};
let mut policy = crate::policy::VerificationPolicy::default();
policy.key_sources.key_value = false;
policy.key_sources.der_encoded_key_value = false;
policy.key_sources.x509_data = false;
assert!(
inventory
.verification_resolver()
.resolve_with_policy_and_provider(
Some(&info),
SignatureAlgorithm::RsaSha256,
&policy,
crate::provider::default_provider(),
)
.expect("trusted inventory material resolves")
.is_some()
);
}
#[test]
fn named_public_key_obeys_current_verification_limits() {
let mut inventory = KeyInventory::default();
inventory
.add_public_pem(
"named".into(),
include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"),
&ResourcePolicy::default(),
)
.expect("public key imports");
let info = KeyInfo {
sources: vec![KeyInfoSource::KeyName("named".into())],
};
let mut policy = crate::policy::VerificationPolicy::default();
policy.resources.max_external_resource_bytes = 1;
assert!(
inventory
.verification_resolver()
.resolve_with_policy(Some(&info), SignatureAlgorithm::RsaSha256, &policy)
.is_err()
);
let certificate = pem::parse(include_bytes!(
"../tests/fixtures/keys/rsa/rsa-2048-cert.pem"
))
.expect("certificate PEM")
.into_contents();
let mut certificate_inventory = KeyInventory::default();
certificate_inventory
.add_public_der("named".into(), certificate, &ResourcePolicy::default())
.expect("certificate imports as a named public key");
assert!(
certificate_inventory
.verification_resolver()
.resolve_with_policy(Some(&info), SignatureAlgorithm::RsaSha256, &policy)
.is_err()
);
}
#[test]
fn unused_certificates_do_not_block_earlier_public_keys() {
let mut inventory = KeyInventory::default();
let certificate = pem::parse(include_bytes!(
"../tests/fixtures/keys/rsa/rsa-2048-cert.pem"
))
.expect("certificate PEM")
.into_contents();
inventory
.add_certificate_der(certificate, false, &ResourcePolicy::default())
.expect("certificate imports");
let public = pem::parse(include_bytes!(
"../tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"
))
.expect("public PEM")
.into_contents();
let info = KeyInfo {
sources: vec![
KeyInfoSource::DerEncodedKeyValue(public.clone()),
KeyInfoSource::X509Data(X509DataInfo {
subject_names: vec!["unused".into()],
..X509DataInfo::default()
}),
],
};
let mut policy = crate::policy::VerificationPolicy::default();
policy.resources.max_external_resource_bytes = public.len();
assert!(
inventory
.verification_resolver()
.resolve_with_policy(Some(&info), SignatureAlgorithm::RsaSha256, &policy)
.expect("earlier public key resolves")
.is_some()
);
}
#[test]
fn public_import_rejects_incompatible_usage() {
let resources = ResourcePolicy::default();
let public = include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-pubkey.pem");
let mut inventory = KeyInventory::default();
assert!(
inventory
.add_public_pem_with_usages("invalid".into(), public, KeyUsages::SIGN, &resources,)
.is_err()
);
assert_eq!(inventory.entry_count(), 0);
}
#[test]
fn public_certificate_import_accepts_supported_ed25519_key() {
let pair =
rcgen::KeyPair::generate_for(&rcgen::PKCS_ED25519).expect("Ed25519 key generation");
let params = rcgen::CertificateParams::new(vec!["example.test".into()])
.expect("certificate parameters");
let certificate = params.self_signed(&pair).expect("certificate generation");
let mut inventory = KeyInventory::default();
assert!(
inventory
.add_public_der(
"ed25519".into(),
certificate.der().to_vec(),
&ResourcePolicy::default()
)
.is_ok()
);
assert!(inventory.trusted_certificates.is_empty());
}
#[cfg(feature = "xmlenc")]
#[test]
fn imported_usage_restricts_operation_selection() {
let resources = ResourcePolicy::default();
let public = include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-pubkey.pem");
let bundle = include_bytes!("../tests/fixtures/xmlenc/01-phaos-xmlenc-3/rsa-priv-key.p12");
let mut inventory = KeyInventory::default();
inventory
.add_public_pem_with_usages("verify".into(), public, KeyUsages::VERIFY, &resources)
.expect("verification-only public key imports");
assert!(
inventory
.rsa_encryption_key("verify", &crate::policy::EncryptionPolicy::default())
.is_err()
);
inventory
.add_pkcs12_with_usages(
"decrypt".into(),
bundle,
"secret",
KeyUsages::DECRYPT,
&resources,
)
.expect("decryption-only private key imports");
assert!(
inventory
.signing_key(
"decrypt",
SignatureAlgorithm::RsaSha256,
&crate::policy::SigningPolicy::default(),
)
.is_err()
);
assert!(
inventory
.decryption_resolver("decrypt", &crate::policy::DecryptionPolicy::default())
.is_ok()
);
}
#[cfg(feature = "xmlenc")]
#[test]
fn selected_weak_rsa_decryptor_obeys_default_policy() {
let mut inventory = KeyInventory::default();
let weak = RsaPrivateKey::new(&mut ChaCha8Rng::seed_from_u64(0xA11C_E502), 1024)
.expect("legacy key generation")
.to_pkcs8_der()
.expect("legacy key encoding");
inventory
.add_private_der(
"weak".into(),
weak.as_bytes(),
None,
KeyUsages::DECRYPT,
&ResourcePolicy::default(),
)
.expect("legacy import");
let error = inventory
.decryption_resolver("weak", &crate::policy::DecryptionPolicy::default())
.err()
.expect("weak key is rejected");
assert!(
matches!(
error,
KeyStoreError::Policy(crate::policy::PolicyViolation::KeySize {
operation: "decryption",
..
})
),
"{error:?}"
);
let key = RsaPrivateKey::from_pkcs8_der(weak.as_bytes()).expect("legacy decode");
let mut encryption = crate::policy::EncryptionPolicy::default();
encryption.rsa_keys.minimum_modulus_bits = 1024;
let encrypted = crate::xmlenc::EncryptedDataBuilder::new(
crate::xmlenc::DataEncryptionAlgorithm::Aes128Gcm,
)
.policy(encryption)
.recipient_rsa_oaep(key.to_public_key())
.encrypt_binary(b"legacy consent")
.expect("explicit legacy encryption");
let mut decryption = crate::policy::DecryptionPolicy::default();
decryption.rsa_keys.minimum_modulus_bits = 1024;
let resolver = inventory
.decryption_resolver("weak", &decryption)
.expect("explicit legacy selection");
assert_eq!(
crate::xmlenc::DecryptContext::new(resolver.as_ref())
.policy(decryption)
.decrypt(&encrypted.encrypted_data_xml)
.expect("explicit legacy recovery"),
crate::xmlenc::DecryptedContent::Bytes(b"legacy consent".to_vec())
);
assert!(matches!(
crate::xmlenc::DecryptContext::new(resolver.as_ref())
.decrypt(&encrypted.encrypted_data_xml),
Err(crate::xmlenc::XmlEncError::Policy(
crate::policy::PolicyViolation::KeySize {
actual_bits: 1024,
..
}
))
));
}
#[cfg(feature = "xmlenc")]
#[test]
fn selected_rsa_recipient_obeys_operation_modulus_policy() {
let mut inventory = KeyInventory::default();
inventory
.add_public_pem(
"rsa".into(),
include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"),
&ResourcePolicy::default(),
)
.expect("RSA fixture imports");
let mut policy = crate::policy::EncryptionPolicy::default();
policy.rsa_keys.minimum_modulus_bits = 4096;
assert!(matches!(
inventory.rsa_encryption_key("rsa", &policy),
Err(KeyStoreError::Policy(
crate::policy::PolicyViolation::KeySize { .. }
))
));
assert!(matches!(
inventory.public_keys()[0].rsa_encryption_key(&policy),
Err(KeyStoreError::Policy(
crate::policy::PolicyViolation::KeySize { .. }
))
));
}
#[test]
fn inventory_merge_checks_names_and_budget_atomically() {
let resources = ResourcePolicy::default();
let mut first = KeyInventory::default();
first
.add_symmetric(
"one".into(),
SymmetricKeyKind::Hmac,
b"first-secret".to_vec(),
KeyUsages::SIGN,
&resources,
)
.expect("first key imports");
let mut duplicate = KeyInventory::default();
duplicate
.add_symmetric(
"one".into(),
SymmetricKeyKind::Hmac,
b"second-secret".to_vec(),
KeyUsages::VERIFY,
&resources,
)
.expect("independent duplicate imports");
assert!(matches!(
first.extend(duplicate, &resources),
Err(KeyStoreError::Selection("duplicate key name"))
));
assert_eq!(first.entry_count(), 1);
let mut second = KeyInventory::default();
second
.add_symmetric(
"two".into(),
SymmetricKeyKind::Hmac,
b"second-secret".to_vec(),
KeyUsages::VERIFY,
&resources,
)
.expect("second key imports");
let constrained = ResourcePolicy {
max_external_resource_total_bytes: b"first-secret".len(),
..ResourcePolicy::default()
};
assert!(matches!(
first.extend(second, &constrained),
Err(KeyStoreError::Policy(
crate::policy::PolicyViolation::ResourceLimitExceeded {
resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
..
}
))
));
assert_eq!(first.entry_count(), 1);
}
#[test]
fn public_dsa_store_entries_require_usable_parameters() {
for fields in [
"<Y>AQ==</Y>",
"<P/><Q/><G/><Y>AQ==</Y>",
"<P>AQ==</P><Q>AQ==</Q><G>AQ==</G><Y>AQ==</Y>",
] {
let xml = format!(
"<Keys xmlns=\"{XMLSEC_NS}\"><KeyInfo xmlns=\"{XMLDSIG_NS}\"><KeyName>dsa</KeyName><KeyValue><DSAKeyValue>{fields}</DSAKeyValue></KeyValue></KeyInfo></Keys>"
);
assert!(
KeyInventory::from_xml_bytes(
xml.as_bytes(),
&xml_policy(ResourcePolicy::default()),
XmlBackend::default()
)
.is_err(),
"accepted unusable DSA: {fields}"
);
}
}
#[test]
fn oversized_private_dsa_component_stops_before_big_integer_work() {
let oversized = base64::engine::general_purpose::STANDARD.encode(vec![1_u8; 513]);
let xml = format!(
"<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>"
);
assert!(matches!(
KeyInventory::from_xml_bytes(
xml.as_bytes(),
&xml_policy(ResourcePolicy::default()),
XmlBackend::default(),
),
Err(KeyStoreError::Invalid(message)) if message.contains("safety limit")
));
}
#[test]
fn oversized_pkcs8_dsa_parameters_stop_before_key_derivation() {
#[derive(der::Sequence)]
struct DsaParameters<'a> {
p: der::asn1::UintRef<'a>,
q: der::asn1::UintRef<'a>,
g: der::asn1::UintRef<'a>,
}
let oversized = vec![1_u8; crate::hard_limits::DSA_KEY_COMPONENT_BYTE_CEILING + 1];
let one = [1_u8];
let parameters = der::Encode::to_der(&DsaParameters {
p: der::asn1::UintRef::new(&oversized).expect("positive P"),
q: der::asn1::UintRef::new(&one).expect("positive Q"),
g: der::asn1::UintRef::new(&one).expect("positive G"),
})
.expect("DSA parameters encode");
let x = der::Encode::to_der(&der::asn1::UintRef::new(&one).expect("positive X"))
.expect("DSA X encodes");
let algorithm = pkcs8::AlgorithmIdentifierRef {
oid: dsa::OID,
parameters: Some(der::asn1::AnyRef::from_der(¶meters).expect("parameters")),
};
let private = der::asn1::OctetStringRef::new(&x).expect("private octets");
let pkcs8 = der::Encode::to_der(&PrivateKeyInfoRef::new(algorithm, private))
.expect("PKCS#8 encodes");
let mut inventory = KeyInventory::default();
let error = inventory
.add_private_der(
"oversized-dsa".into(),
&pkcs8,
None,
KeyUsages::SIGN,
&ResourcePolicy::default(),
)
.expect_err("oversized DSA parameter must fail preflight");
assert!(
matches!(
error,
KeyStoreError::Selection("invalid or oversized DSA private key")
),
"{error}"
);
}
#[test]
fn compressed_ec_spki_cannot_acquire_verify_usage() {
for pem in [
include_bytes!("../tests/fixtures/keys/ec/ec-prime256v1-pubkey.pem").as_slice(),
include_bytes!("../tests/fixtures/keys/ec/ec-prime384v1-pubkey.pem").as_slice(),
include_bytes!("../tests/fixtures/keys/ec/ec-prime521v1-pubkey.pem").as_slice(),
] {
let original =
single_pem_block(pem, ResourcePolicy::default().max_external_resource_bytes)
.expect("EC fixture")
.into_contents();
let spki = pkcs8::SubjectPublicKeyInfoRef::from_der(&original).expect("SPKI");
let point = spki
.subject_public_key
.as_bytes()
.expect("octet-aligned point");
let coordinate_len = (point.len() - 1) / 2;
let mut compressed = vec![2 | (point.last().expect("Y coordinate") & 1)];
compressed.extend_from_slice(&point[1..=coordinate_len]);
let encoded = der::Encode::to_der(&pkcs8::SubjectPublicKeyInfoRef {
algorithm: spki.algorithm,
subject_public_key: der::asn1::BitStringRef::from_bytes(&compressed)
.expect("point"),
})
.expect("compressed SPKI");
let mut inventory = KeyInventory::default();
assert!(
inventory
.add_public_der("compressed".into(), encoded, &ResourcePolicy::default())
.is_err()
);
assert!(inventory.public_keys().is_empty());
inventory
.add_public_der("uncompressed".into(), original, &ResourcePolicy::default())
.expect("supported uncompressed encoding remains usable");
}
struct CompressedPoint<'a>(&'a [u8], &'static rcgen::SignatureAlgorithm);
impl rcgen::PublicKeyData for CompressedPoint<'_> {
fn der_bytes(&self) -> &[u8] {
self.0
}
fn algorithm(&self) -> &'static rcgen::SignatureAlgorithm {
self.1
}
}
let mut root_params = rcgen::CertificateParams::new(Vec::new()).expect("root params");
root_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
let root = rcgen::CertifiedIssuer::self_signed(
root_params,
rcgen::KeyPair::generate().expect("root key"),
)
.expect("root certificate");
for (pem, algorithm) in [
(
include_bytes!("../tests/fixtures/keys/ec/ec-prime256v1-cert.pem").as_slice(),
&rcgen::PKCS_ECDSA_P256_SHA256,
),
(
include_bytes!("../tests/fixtures/keys/ec/ec-prime384v1-cert.pem").as_slice(),
&rcgen::PKCS_ECDSA_P384_SHA384,
),
] {
let original =
single_pem_block(pem, ResourcePolicy::default().max_external_resource_bytes)
.expect("certificate fixture")
.into_contents();
let (_, certificate) = X509Certificate::from_der(&original).expect("certificate");
let point = certificate.public_key().subject_public_key.data.as_ref();
let coordinate_len = (point.len() - 1) / 2;
let mut compressed = vec![2 | (point.last().expect("Y coordinate") & 1)];
compressed.extend_from_slice(&point[1..=coordinate_len]);
let leaf = rcgen::CertificateParams::new(Vec::new())
.expect("leaf params")
.signed_by(&CompressedPoint(&compressed, algorithm), &root)
.expect("signed compressed certificate");
let encoded = leaf.der().to_vec();
let mut inventory = KeyInventory::default();
assert!(
inventory
.add_public_der(
"compressed-cert".into(),
encoded,
&ResourcePolicy::default()
)
.is_err()
);
assert!(inventory.public_keys().is_empty());
inventory
.add_public_der(
"uncompressed-cert".into(),
original,
&ResourcePolicy::default(),
)
.expect("uncompressed certificate imports");
}
}
#[test]
fn malformed_ec_point_cannot_acquire_verify_usage() {
let block = single_pem_block(
include_bytes!("../tests/fixtures/keys/ec/ec-prime256v1-pubkey.pem"),
ResourcePolicy::default().max_external_resource_bytes,
)
.expect("EC SPKI fixture");
let mut der = block.into_contents();
let last = der.last_mut().expect("point bytes");
*last ^= 1;
let mut inventory = KeyInventory::default();
assert!(
inventory
.add_public_der("off-curve".into(), der, &ResourcePolicy::default())
.is_err()
);
assert!(inventory.public_keys().is_empty());
}
#[cfg(feature = "xmlenc")]
#[test]
fn decryption_policy_rejection_retains_its_type() {
let mut policy = crate::policy::DecryptionPolicy::default();
policy.resources.max_external_resource_bytes = usize::MAX;
assert!(matches!(
KeyInventory::default().decryption_resolver("missing", &policy),
Err(KeyStoreError::Policy(_))
));
}
#[cfg(feature = "xmlenc")]
#[test]
fn inventory_decryption_resolver_enforces_aes_usage_end_to_end() {
use crate::xmlenc::{
DataEncryptionAlgorithm, DecryptContext, DecryptedContent, EncryptedDataBuilder,
};
let key = b"0123456789abcdef";
let encrypted = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm)
.direct_key(*key)
.encrypt_binary(b"inventory decrypt payload")
.expect("AES fixture encrypts");
let resources = ResourcePolicy::default();
let mut keys = KeyInventory::default();
keys.add_symmetric(
"decrypt".into(),
SymmetricKeyKind::Aes,
key.to_vec(),
KeyUsages::DECRYPT,
&resources,
)
.expect("decrypt key imports");
let resolver = keys
.decryption_resolver("decrypt", &crate::policy::DecryptionPolicy::default())
.expect("decrypt use is allowed");
let content = DecryptContext::new(resolver.as_ref())
.decrypt(&encrypted.encrypted_data_xml)
.expect("inventory key decrypts");
assert!(
matches!(content, DecryptedContent::Bytes(bytes) if bytes == b"inventory decrypt payload")
);
let mut restricted = KeyInventory::default();
restricted
.add_symmetric(
"encrypt-only".into(),
SymmetricKeyKind::Aes,
key.to_vec(),
KeyUsages::ENCRYPT,
&resources,
)
.expect("encrypt-only key imports");
assert!(
restricted
.decryption_resolver("encrypt-only", &crate::policy::DecryptionPolicy::default())
.is_err()
);
}
#[cfg(feature = "xmlenc")]
#[test]
fn inventory_direct_aes_rechecks_execution_policy() {
use crate::xmlenc::{DataEncryptionAlgorithm, KeyCandidateBudget, XmlEncError};
let mut inventory = KeyInventory::default();
inventory
.add_symmetric(
"aes".into(),
SymmetricKeyKind::Aes,
vec![1; 16],
KeyUsages::DECRYPT,
&ResourcePolicy::default(),
)
.expect("AES imports");
let resolver = inventory
.decryption_resolver("aes", &crate::policy::DecryptionPolicy::default())
.expect("AES resolver");
let mut policy = crate::policy::DecryptionPolicy::default();
policy.resources.max_external_resource_bytes = 15;
let mut budget = KeyCandidateBudget::with_limit(1);
assert!(matches!(
resolver.resolve_key_candidates_with_policy(
&crate::provider::RustCryptoProvider,
DataEncryptionAlgorithm::Aes128Gcm,
None,
&policy,
&mut budget
),
Err(XmlEncError::Policy(_))
));
assert_eq!(budget.remaining(), 1);
policy.resources.max_external_resource_bytes = 16;
assert_eq!(
resolver
.resolve_key_candidates_with_policy(
&crate::provider::RustCryptoProvider,
DataEncryptionAlgorithm::Aes128Gcm,
None,
&policy,
&mut budget
)
.expect("exact execution allowance"),
vec![vec![1; 16]]
);
assert_eq!(budget.remaining(), 0);
}
#[cfg(feature = "xmlenc")]
#[test]
fn inventory_direct_aes_does_not_consume_recipient_candidates() {
use crate::xmlenc::{
CipherData, DataEncryptionAlgorithm, EncryptedKey, EncryptionMethod,
KeyCandidateBudget, KeyTransportAlgorithm, XmlEncError,
};
let mut keys = KeyInventory::default();
keys.add_symmetric(
"direct".into(),
SymmetricKeyKind::Aes,
vec![1; 16],
KeyUsages::DECRYPT,
&ResourcePolicy::default(),
)
.expect("AES imports");
let resolver = keys
.decryption_resolver("direct", &crate::policy::DecryptionPolicy::default())
.expect("AES resolver");
let recipient = EncryptedKey {
sources: Default::default(),
id: None,
recipient: None,
key_name: None,
encryption_method: EncryptionMethod {
chacha: None,
algorithm: KeyTransportAlgorithm::RsaOaep11.uri().into(),
key_size_bits: None,
oaep_digest: None,
mgf_algorithm: None,
oaep_params: None,
},
cipher_data: CipherData::Value {
value: String::new(),
},
reference_list: None,
carried_key_name: None,
};
let mut budget = KeyCandidateBudget::with_limit(1);
let provider = crate::provider::RustCryptoProvider;
for _ in 0..64 {
assert!(matches!(
resolver.resolve_key_candidates(
&provider,
DataEncryptionAlgorithm::Aes128Gcm,
Some(&recipient),
&mut budget
),
Err(XmlEncError::KeyNotFound)
));
assert_eq!(budget.remaining(), 1);
}
assert_eq!(
resolver
.resolve_key_candidates(
&provider,
DataEncryptionAlgorithm::Aes128Gcm,
None,
&mut budget
)
.expect("one direct candidate"),
vec![vec![1; 16]]
);
assert_eq!(budget.remaining(), 0);
}
#[cfg(feature = "xmlenc")]
#[test]
fn decryption_selection_checks_operation_limits_before_material_use() {
let resources = ResourcePolicy::default();
let mut keys = KeyInventory::default();
keys.add_symmetric(
"aes".into(),
SymmetricKeyKind::Aes,
vec![0x31; 16],
KeyUsages::DECRYPT,
&resources,
)
.expect("AES key imports");
keys.add_private_pem(
"rsa".into(),
include_bytes!("../tests/fixtures/keys/rsa/rsa-2048-key.pem"),
None,
KeyUsages::DECRYPT,
&resources,
)
.expect("RSA key imports");
let mut policy = crate::policy::DecryptionPolicy::default();
policy.resources.max_external_resource_bytes = 8;
policy.resources.max_external_resource_total_bytes = 8;
assert!(keys.decryption_resolver("aes", &policy).is_err());
assert!(keys.decryption_resolver("rsa", &policy).is_err());
}
}