Files
s390-tools/rust/pv/src/verify/helper.rs
T
Marc Hartmayer dcece3f980 pv: Fix typo
Signed-off-by: Marc Hartmayer <marc@linux.ibm.com>
Reviewed-by: Steffen Eiden <seiden@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
2026-07-22 17:44:10 +02:00

1599 lines
54 KiB
Rust

// SPDX-License-Identifier: MIT
//
// Copyright IBM Corp. 2023
use std::cmp::Ordering;
use std::ffi::c_int;
use std::path::Path;
use std::str::from_utf8;
use std::time::Duration;
use log::{debug, error, warn};
use openssl::asn1::{Asn1Time, Asn1TimeRef};
use openssl::error::ErrorStack;
use openssl::nid::Nid;
use openssl::ssl::SslFiletype;
use openssl::stack::Stack;
use openssl::x509::store::{File, X509Lookup, X509StoreBuilder, X509StoreRef};
use openssl::x509::verify::{X509VerifyFlags, X509VerifyParam};
use openssl::x509::{
X509CrlRef, X509Name, X509NameRef, X509PurposeId, X509Ref, X509StoreContext,
X509StoreContextRef, X509VerifyResult, X509,
};
#[cfg(not(test))]
pub(crate) use prod_client::download_first_crl_from_x509;
#[cfg(test)]
pub(crate) use tests::download_first_crl_from_x509;
use crate::error::bail_hkd_verify;
use crate::openssl_extensions::{AkidCheckResult, AkidExtension};
use crate::HkdVerifyErrorType::*;
use crate::{Error, Result};
/// Minimum security level for the keys/certificates used to establish a chain of
/// trust (see <https://www.openssl.org/docs/man1.1.1/man3/X509_VERIFY_PARAM_set_auth_level.html>
/// for details).
const SECURITY_LEVEL: usize = 2;
const SECURITY_BITS_ARRAY: [u32; 6] = [0, 80, 112, 128, 192, 256];
const SECURITY_BITS: u32 = SECURITY_BITS_ARRAY[SECURITY_LEVEL];
const SECURITY_CHAIN_MAX_LEN: c_int = 2;
/// Maximum number of HTTP(S) redirections allowed when downloading CRLs.
///
/// This limit prevents infinite redirect loops and potential denial-of-service
/// attacks through redirect chains. When downloading a CRL from a distribution
/// point, if the number of redirects exceeds this limit, the download fails
/// with [`HkdVerifyErrorType::TooManyRedirectionsCrlDownload`].
///
/// # Security
///
/// Each redirect URL is validated to ensure it uses HTTP or HTTPS protocol only,
/// preventing protocol downgrade attacks or redirects to unsafe protocols.
///
/// # Value
///
/// Set to 10 redirections, which should be sufficient for legitimate CDN setups
/// while preventing abuse.
const CRL_MAX_REDIRECTIONS: usize = 10;
/// Maximum timeout duration for CRL (Certificate Revocation List) downloads.
///
/// This timeout applies to individual CRL download operations to prevent
/// indefinite blocking when fetching revocation information from remote servers.
///
/// # Value
///
/// Set to 10 seconds, which provides a reasonable balance between:
/// - Allowing sufficient time for legitimate CRL downloads over slow networks
/// - Preventing excessive delays in certificate verification workflows
/// - Protecting against unresponsive or malicious CRL distribution points
const CRL_TIMEOUT_MAX: Duration = Duration::from_secs(10);
/// Verifies that the HKD
/// * has enough security bits
/// * is inside its validity period
/// * the Authority Key ID matches the Signing Key ID of the [`sign_key`]
pub fn verify_hkd_options(hkd: &X509Ref, sign_key: &X509Ref) -> Result<()> {
let hk_pkey = hkd.public_key()?;
let security_bits = hk_pkey.security_bits();
if SECURITY_BITS > 0 && SECURITY_BITS > security_bits {
return Err(Error::HkdVerify(SecurityBits(security_bits, SECURITY_BITS)));
}
// TODO rust-openssl fix X509::not.after/before() impl to return Option& not panic on nullptr
// from C? try_... rust-openssl
// verify that the HKD is still valid
check_validity_period(hkd.not_before(), hkd.not_after())?;
// verify that the AKID of the hkd matches the SKID of the issuer
if let Some(akid) = hkd.akid() {
if akid.check(sign_key) != AkidCheckResult::OK {
bail_hkd_verify!(Akid);
}
}
Ok(())
}
pub fn verify_crl(crl: &X509CrlRef, issuer: &X509Ref) -> Option<()> {
let last = crl.last_update();
let next = crl.next_update()?;
check_validity_period(last, next).ok()?;
if let Some(akid) = crl.akid() {
if akid.check(issuer) != AkidCheckResult::OK {
return None;
}
}
match crl.verify(issuer.public_key().ok()?.as_ref()).ok()? {
true => Some(()),
false => None,
}
}
pub enum StoreSetupMode {
WithCrlCheck,
WithoutCrlCheck,
}
/// Setup the x509Store such that it can be used it for verifying certificates
pub fn store_setup<P: AsRef<Path>, Q: AsRef<Path>, R: AsRef<Path>>(
root_ca_path: Option<P>,
crl_paths: &[Q],
cert_w_crl_paths: &[R],
mode: StoreSetupMode,
) -> Result<X509StoreBuilder> {
let mut x509store_builder = X509StoreBuilder::new()?;
match root_ca_path {
None => x509store_builder.set_default_paths()?,
Some(p) => load_root_ca(p, &mut x509store_builder)?,
}
for crl in crl_paths {
load_crl_to_store(&mut x509store_builder, crl, true).map_err(|source| Error::X509Load {
path: crl.as_ref().into(),
ty: Error::CRL,
source,
})?;
}
for crl in cert_w_crl_paths {
load_crl_to_store(&mut x509store_builder, crl, false).map_err(|source| {
Error::X509Load {
path: crl.as_ref().into(),
ty: Error::CRL,
source,
}
})?;
}
let mut param = X509VerifyParam::new()?;
let mut flags = X509VerifyFlags::X509_STRICT
| X509VerifyFlags::TRUSTED_FIRST
| X509VerifyFlags::CHECK_SS_SIGNATURE
| X509VerifyFlags::POLICY_CHECK;
match mode {
StoreSetupMode::WithCrlCheck => {
flags |= X509VerifyFlags::CRL_CHECK | X509VerifyFlags::CRL_CHECK_ALL
}
StoreSetupMode::WithoutCrlCheck => {
// nothing to do
}
}
param.set_depth(SECURITY_CHAIN_MAX_LEN);
param.set_auth_level(SECURITY_LEVEL as i32);
param.set_purpose(X509PurposeId::ANY)?;
param.set_flags(flags)?;
x509store_builder.set_param(&param)?;
Ok(x509store_builder)
}
/// Root CA verification mode
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RootCaVerification {
/// Enforce specific root CA organization name requirement
RootCaOrganizationPinning(&'static str),
/// Skip root CA verification (when custom root CA is provided)
SkipPinning,
}
fn x509_subject_orgs(cert: &X509Ref) -> Vec<String> {
cert.subject_name()
.entries_by_nid(Nid::ORGANIZATIONNAME)
.filter_map(|entry| entry.data().as_utf8().ok())
.map(|s| s.to_string())
.collect()
}
/// Checks if the certificate subject matches the expected organization name
///
/// # Arguments
/// * `cert` - The certificate chain to verify
/// * `expected_org` - The expected organization name to match
///
/// # Returns
/// * `true` if the certificate organization name equals the expected string
/// * `false` otherwise
fn check_x509_org_name(cert: &X509Ref, expected_org: &str) -> bool {
x509_subject_orgs(cert).iter().any(|s| s == expected_org)
}
/// Verify that the given IBM signing keys can be trusted
/// -> check the chain: `IBMsignKey`<-InterCA(s)<-`RootCA`
///
/// # Arguments
/// * `store` - The X509 store containing trusted certificates
/// * `untrusted_certs` - Stack of untrusted certificates to build the chain
/// * `sign_keys` - IBM Z signing keys to verify
/// * `root_ca_verification` - Root CA verification mode
pub fn verify_chain(
store: &X509StoreRef,
untrusted_certs: &Stack<X509>,
sign_keys: &[X509],
root_ca_verification: &RootCaVerification,
) -> Result<()> {
fn verify_fun(
ctx: &mut X509StoreContextRef,
root_ca_verification: &RootCaVerification,
) -> std::result::Result<bool, ErrorStack> {
// verify certificate
let res = ctx.verify_cert()?;
if !res {
debug!("Failed to verify the signing key with the chain of trust");
return Ok(res);
}
// verify that the chain is as expected
let chain = match ctx.chain() {
Some(c) => c,
None => {
debug!("No verification chain in verify-context. (openssl BUG)");
ctx.set_error(X509VerifyResult::APPLICATION_VERIFICATION);
return Ok(false);
}
};
if chain.len() < SECURITY_CHAIN_MAX_LEN as usize {
debug!("Verification expects one root and at least one intermediate certificate",);
ctx.set_error(X509VerifyResult::APPLICATION_VERIFICATION);
return Ok(false);
}
if let RootCaVerification::RootCaOrganizationPinning(expected_org) = root_ca_verification {
// The root CA is the last certificate in the chain
let root_ca = match chain.get(chain.len() - 1) {
Some(cert) => cert,
None => {
ctx.set_error(X509VerifyResult::APPLICATION_VERIFICATION);
return Ok(false);
}
};
if !check_x509_org_name(root_ca, expected_org) {
error!(
"error: Root CA pinning failed as organization mismatch
expected: {}
found: {}
To trust a different root CA, use '--root-ca <FILE>'.",
expected_org,
x509_subject_orgs(root_ca).join(", "),
);
ctx.set_error(X509VerifyResult::APPLICATION_VERIFICATION);
return Ok(false);
}
}
Ok(true)
}
let mut store_ctx = X509StoreContext::new()?;
for sign_key in sign_keys {
// (rust)OpenSSL should not error out on `X509_verify_cert`\
// (Internal (probably unrecoverable) error like OOM)
if !store_ctx
.init(store, sign_key, untrusted_certs, |ctx| {
verify_fun(ctx, root_ca_verification)
})
.map_err(|e| Error::InternalSsl("The IBM Z signing key could not be verified.", e))?
{
return Err(Error::HkdVerify(IbmSignInvalid(
store_ctx.error(),
store_ctx.error_depth(),
)));
}
}
Ok(())
}
/// Consumes and splits the given vector into a single IBM Z signing key and other certificates
///
/// Error if not exactly one IBM Z signing key available
pub fn extract_ibm_sign_key(certs: Vec<X509>) -> Result<(X509, Stack<X509>)> {
let ibm_z_sign_key = get_ibm_z_sign_key(&certs)?;
let mut chain = Stack::<X509>::new()?;
for x in certs.into_iter().filter(|x| !is_ibm_signing_cert(x)) {
chain.push(x)?;
}
Ok((ibm_z_sign_key, chain))
}
// Name Entry values of an IBM Z key signing cert
// Asn1StringRef::as_slice aka ASN1_STRING_get0_data gives a string without \0 delimiter
const IBM_Z_COMMON_NAME: &[u8; 43usize] = b"International Business Machines Corporation";
const IBM_Z_COUNTRY_NAME: &[u8; 2usize] = b"US";
const IBM_Z_LOCALITY_NAME_POUGHKEEPSIE: &[u8; 12usize] = b"Poughkeepsie";
const IBM_Z_LOCALITY_NAME_ARMONK: &[u8; 6usize] = b"Armonk";
const IBM_Z_ORGANIZATIONAL_UNIT_NAME_SUFFIX: &str = "Key Signing Service";
const IBM_Z_ORGANIZATION_NAME: &[u8; 43usize] = b"International Business Machines Corporation";
const IBM_Z_STATE: &[u8; 8usize] = b"New York";
const IMB_Z_ENTRY_COUNT: usize = 6;
fn name_data_eq(entries: &X509NameRef, nid: Nid, rhs: &[u8]) -> bool {
let mut it = entries.entries_by_nid(nid);
match it.next() {
None => false,
Some(entry) => entry.data().as_slice() == rhs,
}
}
fn is_ibm_signing_cert(cert: &X509) -> bool {
let subj = cert.subject_name();
if subj.entries().count() != IMB_Z_ENTRY_COUNT
|| !name_data_eq(subj, Nid::COUNTRYNAME, IBM_Z_COUNTRY_NAME)
|| !name_data_eq(subj, Nid::STATEORPROVINCENAME, IBM_Z_STATE)
|| !(name_data_eq(subj, Nid::LOCALITYNAME, IBM_Z_LOCALITY_NAME_POUGHKEEPSIE)
|| name_data_eq(subj, Nid::LOCALITYNAME, IBM_Z_LOCALITY_NAME_ARMONK))
|| !name_data_eq(subj, Nid::ORGANIZATIONNAME, IBM_Z_ORGANIZATION_NAME)
|| !name_data_eq(subj, Nid::COMMONNAME, IBM_Z_COMMON_NAME)
{
return false;
}
match subj.entries_by_nid(Nid::ORGANIZATIONALUNITNAME).next() {
None => false,
Some(entry) => match entry.data().as_utf8() {
Err(_) => false,
Ok(s) => s
.as_bytes()
.ends_with(IBM_Z_ORGANIZATIONAL_UNIT_NAME_SUFFIX.as_bytes()),
},
}
}
fn get_ibm_z_sign_key(certs: &[X509]) -> Result<X509> {
let mut ibm_sign_keys = certs.iter().filter(|x| is_ibm_signing_cert(x)).cloned();
match ibm_sign_keys.next() {
None => bail_hkd_verify!(NoIbmSignKey),
Some(k) => match ibm_sign_keys.next() {
None => Ok(k),
Some(_) => bail_hkd_verify!(ManyIbmSignKeys),
},
}
}
fn load_root_ca<P: AsRef<Path>>(path: P, x509_store: &mut X509StoreBuilder) -> Result<()> {
let lu = x509_store.add_lookup(X509Lookup::<File>::file())?;
// Try to load cert as PEM file
match lu.load_cert_file(&path, SslFiletype::PEM) {
Ok(_) => lu
.load_crl_file(&path, SslFiletype::PEM)
.map(|_| ())
.or(Ok(())),
// Not a PEM file? try ASN1
Err(_) => lu
.load_cert_file(&path, SslFiletype::ASN1)
.map(|_| ())
.map_err(|source| Error::X509Load {
path: path.as_ref().into(),
ty: Error::CERT,
source,
}),
}
}
fn load_crl_to_store<P: AsRef<Path>>(
x509_store: &mut X509StoreBuilder,
path: P,
err_out_empty_crl: bool,
) -> std::result::Result<(), ErrorStack> {
let lu = x509_store.add_lookup(X509Lookup::<File>::file())?;
// Try to load cert as PEM file
if lu.load_crl_file(&path, SslFiletype::PEM).is_err() {
// Not a PEM file? try read as ASN1
let res = lu.load_crl_file(path, SslFiletype::ASN1);
if err_out_empty_crl {
res?;
}
}
Ok(())
}
/// Run through the forest of the distribution points and find them
pub fn x509_dist_points(cert: &X509Ref) -> Vec<String> {
let mut res = Vec::<String>::with_capacity(1);
let dps = match cert.crl_distribution_points() {
Some(d) => d,
None => return res,
};
for dp in dps {
let dp_nm = match dp.distpoint() {
Some(nm) => nm,
None => continue,
};
let dp_gns = match dp_nm.fullname() {
Some(gns) => gns,
None => continue,
};
for dp_gn in dp_gns {
match dp_gn.uri() {
Some(uri) => res.push(uri.to_string()),
None => continue,
};
}
}
res
}
/// Validates that a URL uses HTTP or HTTPS protocol
///
/// # Arguments
/// * `url` - The URL to validate
///
/// # Returns
/// * `Ok(())` if protocol is HTTP or HTTPS
/// * `Err(Error::HkdVerify(InvalidCrlProtocol))` otherwise
///
/// # Security
/// This function enforces protocol restrictions to prevent CRL downloads from
/// potentially unsafe protocols like FTP, file://, or custom schemes.
fn validate_crl_url_protocol(url: &str) -> Result<()> {
let url_lower = url.to_lowercase();
if url_lower.starts_with("http://") || url_lower.starts_with("https://") {
Ok(())
} else {
bail_hkd_verify!(InvalidCrlProtocol)
}
}
/// Trait for HTTP client operations to enable testing
trait HttpClient {
fn url(&mut self, url: &str) -> Result<()>;
fn timeout(&mut self, timeout: Duration) -> Result<()>;
fn useragent(&mut self, agent: &str) -> Result<()>;
fn perform(&mut self) -> Result<()>;
fn get(&mut self, enable: bool) -> Result<()>;
fn follow_location(&mut self, enable: bool) -> Result<()>;
fn redirect_url(&self) -> Result<Option<String>>;
fn get_ref(&self) -> &[u8];
fn max_filesize(&mut self, size: u64) -> Result<()>;
}
/// Production HTTP client implementation
#[cfg(not(test))]
mod prod_client {
use curl::easy::{Easy2, Handler, WriteError};
use super::*;
/// Production HTTP client using curl
pub(super) struct CurlHttpClient<H: Handler> {
handle: Easy2<H>,
}
pub(crate) struct Buf(Vec<u8>);
impl Handler for Buf {
fn write(&mut self, data: &[u8]) -> std::result::Result<usize, WriteError> {
self.0.extend_from_slice(data);
Ok(data.len())
}
}
impl CurlHttpClient<Buf> {
pub(super) fn new(capacity: usize) -> Self {
Self {
handle: Easy2::new(Buf(Vec::with_capacity(capacity))),
}
}
}
impl HttpClient for CurlHttpClient<Buf> {
fn get_ref(&self) -> &[u8] {
&self.handle.get_ref().0
}
fn perform(&mut self) -> Result<()> {
match self.handle.perform() {
Ok(()) => Ok(()),
Err(err) if err.is_filesize_exceeded() => {
Err(Error::HkdVerify(CrlDownloadTooLarge(CRL_FILE_SIZE_MAX)))
}
Err(err) => Err(err.into()),
}
}
fn get(&mut self, enable: bool) -> Result<(), Error> {
self.handle.get(enable)?;
Ok(())
}
fn follow_location(&mut self, enable: bool) -> Result<()> {
self.handle.follow_location(enable)?;
Ok(())
}
fn timeout(&mut self, timeout: Duration) -> Result<()> {
self.handle.timeout(timeout)?;
Ok(())
}
fn url(&mut self, url: &str) -> Result<()> {
self.handle.url(url)?;
Ok(())
}
fn useragent(&mut self, agent: &str) -> Result<()> {
self.handle.useragent(agent)?;
Ok(())
}
fn redirect_url(&self) -> Result<Option<String>> {
Ok(self.handle.redirect_url()?.map(|s| s.to_string()))
}
fn max_filesize(&mut self, size: u64) -> Result<()> {
self.handle.max_filesize(size)?;
Ok(())
}
}
/// Searches for CRL Distribution points and downloads the CRL. Stops after the first successful
/// download.
///
/// # Security
///
/// This function enforces the following security restrictions:
/// 1. **Protocol Restriction**: Only HTTP and HTTPS protocols are allowed for
/// CRL downloads. This prevents potential security issues with other protocols
/// like FTP, file://, or custom schemes.
/// 2. **Redirect Protocol Restriction**: Redirects are only allowed to HTTP/HTTPS destinations,
/// preventing protocol downgrade attacks.
///
/// Error if something bad(=unexpected) happens
/// CRL not available at all URIs and unexpected format at all URIs are mapped to Ok(None)
pub(crate) fn download_first_crl_from_x509(
cert: &X509Ref,
) -> Result<Option<Vec<openssl::x509::X509Crl>>> {
// A typical CRL is about 1200 bytes long
download_first_crl_from_x509_impl(cert, CurlHttpClient::new(1200))
}
}
const CRL_FILE_SIZE_MAX: u64 = 10 * 1024 * 1024; // 10 MiB
/// Searches for CRL Distribution points and downloads the CRL. Stops after the first successful
/// download.
///
/// Error if something bad(=unexpected) happens
/// CRL not available at all URIs and unexpected format at all URIs are mapped to Ok(None)
/// Internal implementation that accepts an HttpClient
fn download_first_crl_from_x509_impl<H: HttpClient>(
cert: &X509Ref,
mut client: H,
) -> Result<Option<Vec<openssl::x509::X509Crl>>> {
use crate::utils::read_crls;
'outer: for dist_point_url in x509_dist_points(cert) {
// Validate protocol BEFORE attempting download
if validate_crl_url_protocol(&dist_point_url).is_err() {
warn!("Invalid CRL URL protocol, skipping: {}", dist_point_url);
continue;
}
client.url(&dist_point_url)?;
client.get(true)?;
// Disable automatic redirects - we handle them manually to validate the
// protocol of each redirect URL and to limit the amount of
// redirections.
client.follow_location(false)?;
client.timeout(CRL_TIMEOUT_MAX)?;
client.max_filesize(CRL_FILE_SIZE_MAX)?;
client.useragent("s390-tools-pv-crl")?;
for i in 0..CRL_MAX_REDIRECTIONS {
if let Err(err) = client.perform() {
debug!("Failed to download CRL: {}", err);
continue 'outer;
}
if let Some(url) = client.redirect_url()? {
if i == CRL_MAX_REDIRECTIONS - 1 {
bail_hkd_verify!(TooManyRedirectionsCrlDownload);
}
if validate_crl_url_protocol(&url).is_err() {
continue 'outer;
}
debug!("Redirection to: {url}");
client.url(&url)?;
} else {
// No redirection, therefore let's stop and read the data
break;
}
}
match read_crls(client.get_ref()) {
Err(_) => continue,
Ok(crl) if crl.is_empty() => continue,
Ok(crl) => return Ok(Some(crl)),
}
}
Ok(None)
}
fn check_validity_period(not_before: &Asn1TimeRef, not_after: &Asn1TimeRef) -> Result<()> {
let now = Asn1Time::days_from_now(0)?;
if let Ordering::Less = now.compare(not_before)? {
bail_hkd_verify!(BeforeValidity);
}
match now.compare(not_after)? {
Ordering::Less => Ok(()),
_ => bail_hkd_verify!(AfterValidity),
}
}
const NIDS_CORRECT_ORDER: [Nid; 6] = [
Nid::COUNTRYNAME,
Nid::ORGANIZATIONNAME,
Nid::ORGANIZATIONALUNITNAME,
Nid::LOCALITYNAME,
Nid::STATEORPROVINCENAME,
Nid::COMMONNAME,
];
/// Workaround to fix the mismatch between issuer name of the
/// IBM Z signing CRLs and the IBM Z signing key subject name.
pub fn reorder_x509_names(subject: &X509NameRef) -> std::result::Result<X509Name, ErrorStack> {
let mut correct_subj = X509Name::builder()?;
for nid in NIDS_CORRECT_ORDER {
if let Some(name) = subject.entries_by_nid(nid).next() {
correct_subj.append_entry(name)?;
}
}
Ok(correct_subj.build())
}
/// Workaround for potential locality mismatches between CRLs and Certs
/// # Return
/// fixed subject or none if locality was not Armonk or any OpenSSL error
pub fn armonk_locality_fixup(subject: &X509NameRef) -> Option<X509Name> {
if !name_data_eq(subject, Nid::LOCALITYNAME, IBM_Z_LOCALITY_NAME_ARMONK) {
return None;
}
let mut ret = X509Name::builder().ok()?;
for entry in subject.entries() {
match entry.object().nid() {
nid @ Nid::LOCALITYNAME => ret
.append_entry_by_nid(nid, from_utf8(IBM_Z_LOCALITY_NAME_POUGHKEEPSIE).ok()?)
.ok()?,
_ => {
ret.append_entry(entry).ok()?;
}
}
}
Some(ret.build())
}
#[cfg(test)]
mod tests {
use std::time::{Duration, SystemTime};
use super::*;
use crate::test_utils::*;
fn sys_to_asn1_time(syst: SystemTime) -> Asn1Time {
let secs = syst
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
Asn1Time::from_unix(secs as i64).unwrap()
}
#[test]
fn check_validity_period() {
let day = Duration::from_secs(60 * 60 * 24);
let yesterday = sys_to_asn1_time(SystemTime::now() - day);
let tomorrow = sys_to_asn1_time(SystemTime::now() + day);
assert!(super::check_validity_period(&yesterday, &tomorrow).is_ok());
assert!(matches!(
super::check_validity_period(&tomorrow, &tomorrow),
Err(Error::HkdVerify(BeforeValidity))
));
assert!(matches!(
super::check_validity_period(&yesterday, &yesterday),
Err(Error::HkdVerify(AfterValidity))
));
}
#[test]
fn is_ibm_z_sign_key() {
let ibm_crt = load_gen_cert("ibm.crt");
let no_ibm_crt = load_gen_cert("inter_ca.crt");
let ibm_wrong_subj = load_gen_cert("ibm_wrong_subject.crt");
assert!(is_ibm_signing_cert(&ibm_crt));
assert!(!is_ibm_signing_cert(&no_ibm_crt));
assert!(!is_ibm_signing_cert(&ibm_wrong_subj));
}
#[test]
fn get_ibm_z_sign_key() {
let ibm_crt = load_gen_cert("ibm.crt");
let ibm_wrong_subj = load_gen_cert("ibm_wrong_subject.crt");
let no_sign_crt = load_gen_cert("inter_ca.crt");
assert!(super::get_ibm_z_sign_key(std::slice::from_ref(&ibm_crt)).is_ok());
assert!(matches!(
super::get_ibm_z_sign_key(&[ibm_crt.clone(), ibm_crt.clone()]),
Err(Error::HkdVerify(ManyIbmSignKeys))
));
assert!(matches!(
super::get_ibm_z_sign_key(std::slice::from_ref(&ibm_wrong_subj)),
Err(Error::HkdVerify(NoIbmSignKey))
));
assert!(matches!(
super::get_ibm_z_sign_key(std::slice::from_ref(&no_sign_crt)),
Err(Error::HkdVerify(NoIbmSignKey))
));
assert!(super::get_ibm_z_sign_key(&[ibm_crt, no_sign_crt]).is_ok(),);
}
use std::collections::HashMap;
use openssl::bn::{BigNum, MsbOption};
use openssl::hash::MessageDigest;
use openssl::pkey::PKey;
use openssl::rsa::Rsa;
use openssl::x509::{X509Builder, X509Crl, X509Extension, X509NameBuilder};
use crate::test_utils::get_cert_asset_path;
/// Mock HTTP response for testing
pub struct MockResponse {
pub data: Vec<u8>,
pub redirect_to: Option<String>,
pub should_fail: bool,
}
/// Mock HTTP client for testing
pub struct MockHttpClient {
url: String,
responses: HashMap<String, MockResponse>,
perform_count: usize,
max_filesize: Option<u64>,
}
impl MockHttpClient {
pub fn new(responses: HashMap<String, MockResponse>) -> Self {
Self {
url: String::new(),
responses,
perform_count: 0,
max_filesize: None,
}
}
}
impl HttpClient for MockHttpClient {
fn url(&mut self, url: &str) -> Result<()> {
self.url = url.to_string();
Ok(())
}
fn get(&mut self, _enable: bool) -> Result<()> {
Ok(())
}
fn follow_location(&mut self, enable: bool) -> Result<()> {
// The Rust curl crate does not support to limit the protocols,
// therefore expect that our workaround is used.
assert!(
!enable,
"Unexpected curl crate redirection instead of pv redirection used "
);
Ok(())
}
fn timeout(&mut self, _timeout: Duration) -> Result<()> {
Ok(())
}
fn useragent(&mut self, _agent: &str) -> Result<()> {
Ok(())
}
fn perform(&mut self) -> Result<()> {
self.perform_count += 1;
if let Some(response) = self.responses.get(&self.url) {
if self
.max_filesize
.is_some_and(|max_filesize| response.data.len() as u64 > max_filesize)
{
return Err(Error::HkdVerify(CrlDownloadTooLarge(CRL_FILE_SIZE_MAX)));
}
if response.should_fail {
bail_hkd_verify!(CrlDownloadFailed);
}
Ok(())
} else {
bail_hkd_verify!(CrlDownloadFailed);
}
}
fn redirect_url(&self) -> Result<Option<String>> {
if let Some(response) = self.responses.get(&self.url) {
Ok(response.redirect_to.clone())
} else {
Ok(None)
}
}
fn get_ref(&self) -> &[u8] {
if let Some(response) = self.responses.get(&self.url) {
&response.data
} else {
&[]
}
}
fn max_filesize(&mut self, size: u64) -> Result<()> {
self.max_filesize = Some(size);
Ok(())
}
}
/// Helper to create mock response with data
fn mock_response(data: Vec<u8>) -> MockResponse {
MockResponse {
data,
redirect_to: None,
should_fail: false,
}
}
/// Helper to create mock redirect response
fn mock_redirect(redirect_to: &str) -> MockResponse {
MockResponse {
data: vec![],
redirect_to: Some(redirect_to.to_string()),
should_fail: false,
}
}
/// Helper to create mock failure response
fn mock_failure() -> MockResponse {
MockResponse {
data: vec![],
redirect_to: None,
should_fail: true,
}
}
/// Helper to create test certificate with CRL distribution points
fn create_cert_with_crl_dps(crl_uris: &[&str]) -> X509 {
// Generate a key pair
let rsa = Rsa::generate(2048).unwrap();
let key_pair = PKey::from_rsa(rsa).unwrap();
let mut builder = X509Builder::new().unwrap();
builder.set_version(2).unwrap();
// Set serial number
let serial = {
let mut num = BigNum::new().unwrap();
num.rand(159, MsbOption::MAYBE_ZERO, false).unwrap();
num.to_asn1_integer().unwrap()
};
builder.set_serial_number(&serial).unwrap();
// Set subject name
let mut name_builder = X509NameBuilder::new().unwrap();
name_builder.append_entry_by_text("C", "US").unwrap();
name_builder
.append_entry_by_text("O", "Test Organization")
.unwrap();
name_builder
.append_entry_by_text("CN", "Test Certificate")
.unwrap();
let name = name_builder.build();
builder.set_subject_name(&name).unwrap();
builder.set_issuer_name(&name).unwrap();
builder.set_pubkey(&key_pair).unwrap();
// Set validity
builder
.set_not_before(&Asn1Time::days_from_now(0).unwrap())
.unwrap();
builder
.set_not_after(&Asn1Time::days_from_now(365).unwrap())
.unwrap();
// Add CRL Distribution Points if provided
if !crl_uris.is_empty() {
// Create a single extension with all URIs
let crl_dp_value = crl_uris
.iter()
.map(|uri| format!("URI:{}", uri))
.collect::<Vec<_>>()
.join(",");
#[allow(deprecated)]
let crl_ext =
X509Extension::new_nid(None, None, Nid::CRL_DISTRIBUTION_POINTS, &crl_dp_value)
.unwrap();
builder.append_extension(crl_ext).unwrap();
}
// Sign the certificate
builder.sign(&key_pair, MessageDigest::sha256()).unwrap();
builder.build()
}
/// Helper to test with mock client
fn download_with_mock(
cert: &X509Ref,
responses: HashMap<String, MockResponse>,
) -> Result<Option<Vec<X509Crl>>> {
download_first_crl_from_x509_impl(cert, MockHttpClient::new(responses))
}
// Mock function
pub(crate) fn download_first_crl_from_x509(cert: &X509Ref) -> Result<Option<Vec<X509Crl>>> {
use std::collections::HashMap;
let dist_points = x509_dist_points(cert);
// Build mock responses for each distribution point
let mut responses = HashMap::new();
for dist_point in dist_points {
// Treat distribution point as filename, remove the "http://" prefix
let dist_point_path = dist_point
.strip_prefix("http://")
.unwrap_or(&dist_point)
.to_string();
let path = get_cert_asset_path(&dist_point_path);
if let Ok(crl_data) = std::fs::read(&path) {
responses.insert(dist_point, mock_response(crl_data));
}
// If file doesn't exist, skip this distribution point
}
download_with_mock(cert, responses)
}
mod protocol_validation {
use super::*;
#[test]
fn validate_protocol_http() {
assert!(validate_crl_url_protocol("http://example.com/test.crl").is_ok());
}
#[test]
fn validate_protocol_https() {
assert!(validate_crl_url_protocol("https://example.com/test.crl").is_ok());
}
#[test]
fn validate_protocol_ftp_rejected() {
assert!(matches!(
validate_crl_url_protocol("ftp://example.com/test.crl"),
Err(Error::HkdVerify(InvalidCrlProtocol))
));
}
#[test]
fn validate_protocol_file_rejected() {
assert!(matches!(
validate_crl_url_protocol("file:///tmp/test.crl"),
Err(Error::HkdVerify(InvalidCrlProtocol))
));
}
#[test]
fn validate_protocol_custom_rejected() {
assert!(matches!(
validate_crl_url_protocol("custom://example.com/test.crl"),
Err(Error::HkdVerify(InvalidCrlProtocol))
));
}
#[test]
fn validate_protocol_case_insensitive() {
assert!(validate_crl_url_protocol("HTTP://example.com/test.crl").is_ok());
assert!(validate_crl_url_protocol("HTTPS://example.com/test.crl").is_ok());
assert!(validate_crl_url_protocol("HtTpS://example.com/test.crl").is_ok());
}
}
mod distribution_points {
use super::*;
#[test]
fn extract_single_distribution_point() {
let cert = create_cert_with_crl_dps(&["http://example.com/test.crl"]);
let dps = x509_dist_points(&cert);
assert_eq!(dps.len(), 1);
assert_eq!(dps[0], "http://example.com/test.crl");
}
#[test]
fn extract_multiple_distribution_points() {
let cert = create_cert_with_crl_dps(&[
"http://primary.example.com/test.crl",
"http://backup.example.com/test.crl",
]);
let dps = x509_dist_points(&cert);
assert_eq!(dps.len(), 2);
assert_eq!(dps[0], "http://primary.example.com/test.crl");
assert_eq!(dps[1], "http://backup.example.com/test.crl");
}
#[test]
fn extract_no_distribution_points() {
let cert = create_cert_with_crl_dps(&[]);
let dps = x509_dist_points(&cert);
assert!(dps.is_empty());
}
}
mod download_success {
use super::*;
#[test]
fn download_success_single_dp() {
let cert = create_cert_with_crl_dps(&["http://example.com/test.crl"]);
let crl_data = std::fs::read(get_cert_asset_path("inter_ca.crl")).unwrap();
let mut responses = HashMap::new();
responses.insert(
"http://example.com/test.crl".to_string(),
mock_response(crl_data),
);
let result = download_with_mock(&cert, responses);
assert!(result.is_ok());
let crls = result.unwrap();
assert!(crls.is_some());
assert_eq!(crls.unwrap().len(), 1);
}
#[test]
fn download_no_crl_available() {
let cert = create_cert_with_crl_dps(&["http://example.com/test.crl"]);
let mut responses = HashMap::new();
responses.insert("http://example.com/test.crl".to_string(), mock_failure());
let result = download_with_mock(&cert, responses);
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[test]
fn download_first_dp_succeeds() {
let cert = create_cert_with_crl_dps(&[
"http://primary.example.com/test.crl",
"http://backup.example.com/test.crl",
]);
let crl_data = std::fs::read(get_cert_asset_path("inter_ca.crl")).unwrap();
let mut responses = HashMap::new();
responses.insert(
"http://primary.example.com/test.crl".to_string(),
mock_response(crl_data),
);
let result = download_with_mock(&cert, responses);
assert!(result.is_ok());
assert!(result.unwrap().is_some());
}
#[test]
fn download_fallback_to_second_dp() {
let cert = create_cert_with_crl_dps(&[
"http://primary.example.com/test.crl",
"http://backup.example.com/test.crl",
]);
let crl_data = std::fs::read(get_cert_asset_path("inter_ca.crl")).unwrap();
let mut responses = HashMap::new();
responses.insert(
"http://primary.example.com/test.crl".to_string(),
mock_failure(),
);
responses.insert(
"http://backup.example.com/test.crl".to_string(),
mock_response(crl_data),
);
let result = download_with_mock(&cert, responses);
assert!(result.is_ok());
assert!(result.unwrap().is_some());
}
#[test]
fn download_skip_invalid_protocol() {
let cert = create_cert_with_crl_dps(&[
"ftp://example.com/test.crl",
"http://example.com/test.crl",
]);
let crl_data = std::fs::read(get_cert_asset_path("inter_ca.crl")).unwrap();
let mut responses = HashMap::new();
responses.insert(
"http://example.com/test.crl".to_string(),
mock_response(crl_data),
);
let result = download_with_mock(&cert, responses);
assert!(result.is_ok());
assert!(result.unwrap().is_some());
}
#[test]
fn download_invalid_crl_data() {
let cert = create_cert_with_crl_dps(&["http://example.com/test.crl"]);
let mut responses = HashMap::new();
responses.insert(
"http://example.com/test.crl".to_string(),
mock_response(vec![0x00, 0x01, 0x02, 0x03]),
);
let result = download_with_mock(&cert, responses);
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[test]
fn download_empty_response() {
let cert = create_cert_with_crl_dps(&["http://example.com/test.crl"]);
let mut responses = HashMap::new();
responses.insert(
"http://example.com/test.crl".to_string(),
mock_response(vec![]),
);
let result = download_with_mock(&cert, responses);
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
}
mod redirect_handling {
use super::*;
#[test]
fn redirect_single_valid() {
let cert = create_cert_with_crl_dps(&["http://example.com/test.crl"]);
let crl_data = std::fs::read(get_cert_asset_path("inter_ca.crl")).unwrap();
let mut responses = HashMap::new();
responses.insert(
"http://example.com/test.crl".to_string(),
mock_redirect("http://cdn.example.com/test.crl"),
);
responses.insert(
"http://cdn.example.com/test.crl".to_string(),
mock_response(crl_data),
);
let result = download_with_mock(&cert, responses);
assert!(result.is_ok());
assert!(result.unwrap().is_some());
}
#[test]
fn redirect_to_invalid_protocol() {
let cert = create_cert_with_crl_dps(&[
"http://example.com/test.crl",
"http://backup.example.com/test.crl",
]);
let crl_data = std::fs::read(get_cert_asset_path("inter_ca.crl")).unwrap();
let mut responses = HashMap::new();
responses.insert(
"http://example.com/test.crl".to_string(),
mock_redirect("ftp://example.com/test.crl"),
);
responses.insert(
"http://backup.example.com/test.crl".to_string(),
mock_response(crl_data),
);
let result = download_with_mock(&cert, responses);
assert!(result.is_ok());
assert!(result.unwrap().is_some());
}
#[test]
fn redirect_too_many() {
let cert = create_cert_with_crl_dps(&["http://example.com/test.crl"]);
let mut responses = HashMap::new();
responses.insert(
"http://example.com/test.crl".to_string(),
mock_redirect("http://redirect1.example.com/test.crl"),
);
responses.insert(
"http://redirect1.example.com/test.crl".to_string(),
mock_redirect("http://redirect2.example.com/test.crl"),
);
responses.insert(
"http://redirect2.example.com/test.crl".to_string(),
mock_redirect("http://redirect3.example.com/test.crl"),
);
responses.insert(
"http://redirect3.example.com/test.crl".to_string(),
mock_redirect("http://redirect4.example.com/test.crl"),
);
responses.insert(
"http://redirect4.example.com/test.crl".to_string(),
mock_redirect("http://redirect5.example.com/test.crl"),
);
responses.insert(
"http://redirect5.example.com/test.crl".to_string(),
mock_redirect("http://redirect6.example.com/test.crl"),
);
responses.insert(
"http://redirect6.example.com/test.crl".to_string(),
mock_redirect("http://redirect7.example.com/test.crl"),
);
responses.insert(
"http://redirect7.example.com/test.crl".to_string(),
mock_redirect("http://redirect8.example.com/test.crl"),
);
responses.insert(
"http://redirect8.example.com/test.crl".to_string(),
mock_redirect("http://redirect9.example.com/test.crl"),
);
responses.insert(
"http://redirect9.example.com/test.crl".to_string(),
mock_redirect("http://redirect10.example.com/test.crl"),
);
let result = download_with_mock(&cert, responses);
assert!(matches!(
result,
Err(Error::HkdVerify(TooManyRedirectionsCrlDownload))
));
}
#[test]
fn redirect_chain_valid() {
let cert = create_cert_with_crl_dps(&["http://example.com/test.crl"]);
let crl_data = std::fs::read(get_cert_asset_path("inter_ca.crl")).unwrap();
let mut responses = HashMap::new();
responses.insert(
"http://example.com/test.crl".to_string(),
mock_redirect("http://redirect1.example.com/test.crl"),
);
responses.insert(
"http://redirect1.example.com/test.crl".to_string(),
mock_redirect("http://redirect2.example.com/test.crl"),
);
responses.insert(
"http://redirect2.example.com/test.crl".to_string(),
mock_response(crl_data),
);
let result = download_with_mock(&cert, responses);
assert!(result.is_ok());
assert!(result.unwrap().is_some());
}
#[test]
fn redirect_endless_loop() {
let cert = create_cert_with_crl_dps(&["http://example.com/test.crl"]);
let mut responses = HashMap::new();
// Create a circular redirect: A -> B -> A
responses.insert(
"http://example.com/test.crl".to_string(),
mock_redirect("http://redirect.example.com/test.crl"),
);
responses.insert(
"http://redirect.example.com/test.crl".to_string(),
mock_redirect("http://example.com/test.crl"),
);
let result = download_with_mock(&cert, responses);
assert!(matches!(
result,
Err(Error::HkdVerify(TooManyRedirectionsCrlDownload))
));
}
}
mod max_filesize {
use super::*;
use crate::test_utils::get_cert_asset_path;
#[test]
fn download_just_over_max_filesize() {
testing_logger::setup();
let cert = create_cert_with_crl_dps(&["http://example.com/test.crl"]);
// Create data just over the 10 MiB limit
let over_limit_data = vec![0u8; 10 * 1024 * 1024 + 1];
let mut responses = HashMap::new();
responses.insert(
"http://example.com/test.crl".to_string(),
mock_response(over_limit_data),
);
let result = download_with_mock(&cert, responses);
// When file size exceeds limit, the download fails and function tries next URL
// Since there's only one URL, it returns Ok(None) after exhausting all options
assert!(result.is_ok());
assert!(result.unwrap().is_none());
// Verify that error was logged about file size exceeding limit
testing_logger::validate(|captured_logs| {
assert!(
captured_logs
.iter()
.any(|log| { log.body.contains("CRL download exceeds maximum file size") }),
"Expected log message about CRL file size exceeding maximum limit"
);
});
}
#[test]
fn download_fallback_after_size_exceeded() {
let cert = create_cert_with_crl_dps(&[
"http://primary.example.com/test.crl",
"http://backup.example.com/test.crl",
]);
let crl_data = std::fs::read(get_cert_asset_path("inter_ca.crl")).unwrap();
let mut responses = HashMap::new();
// First URL has file that's too large
responses.insert(
"http://primary.example.com/test.crl".to_string(),
mock_response(vec![0u8; (CRL_FILE_SIZE_MAX + 1).try_into().unwrap()]),
);
// Second URL has valid CRL
responses.insert(
"http://backup.example.com/test.crl".to_string(),
mock_response(crl_data),
);
let result = download_with_mock(&cert, responses);
// Should fall back to second URL and succeed
assert!(result.is_ok());
assert!(result.unwrap().is_some());
}
}
mod organization_name_validation {
use openssl::bn::{BigNum, MsbOption};
use openssl::hash::MessageDigest;
use openssl::pkey::PKey;
use openssl::rsa::Rsa;
use openssl::x509::{X509Builder, X509NameBuilder};
use super::*;
/// Helper to create a certificate with custom organization names
fn create_cert_with_orgs(org_names: &[&str]) -> X509 {
let rsa = Rsa::generate(2048).unwrap();
let key_pair = PKey::from_rsa(rsa).unwrap();
let mut builder = X509Builder::new().unwrap();
builder.set_version(2).unwrap();
let serial = {
let mut num = BigNum::new().unwrap();
num.rand(159, MsbOption::MAYBE_ZERO, false).unwrap();
num.to_asn1_integer().unwrap()
};
builder.set_serial_number(&serial).unwrap();
let mut name_builder = X509NameBuilder::new().unwrap();
name_builder.append_entry_by_text("C", "US").unwrap();
// Add all organization names
for org in org_names {
name_builder.append_entry_by_text("O", org).unwrap();
}
name_builder
.append_entry_by_text("CN", "Test Certificate")
.unwrap();
let name = name_builder.build();
builder.set_subject_name(&name).unwrap();
builder.set_issuer_name(&name).unwrap();
builder.set_pubkey(&key_pair).unwrap();
builder
.set_not_before(&Asn1Time::days_from_now(0).unwrap())
.unwrap();
builder
.set_not_after(&Asn1Time::days_from_now(365).unwrap())
.unwrap();
builder.sign(&key_pair, MessageDigest::sha256()).unwrap();
builder.build()
}
#[test]
fn organization_name_edge_cases() {
// Test X.509 length limits: minimum (1 char) and maximum (64 chars)
let min_org = "A";
let max_org = "B".repeat(64);
let special_org = "Test & Co.";
let unicode_org = "日本電気株式会社";
let cert = create_cert_with_orgs(&[min_org, &max_org, special_org, unicode_org]);
assert!(check_x509_org_name(&cert, min_org));
assert!(check_x509_org_name(&cert, &max_org));
assert!(check_x509_org_name(&cert, special_org));
assert!(check_x509_org_name(&cert, unicode_org));
assert!(!check_x509_org_name(&cert, "AB"));
assert!(!check_x509_org_name(&cert, &"B".repeat(63)));
assert!(!check_x509_org_name(&cert, "Test"));
assert!(!check_x509_org_name(&cert, ""));
}
#[test]
fn multiple_organization_values() {
let cert = create_cert_with_orgs(&["IBM", "Red Hat"]);
assert!(check_x509_org_name(&cert, "IBM"));
assert!(check_x509_org_name(&cert, "Red Hat"));
assert!(!check_x509_org_name(&cert, "Microsoft"));
}
#[test]
fn organization_name_exact_matching() {
// Test case sensitivity and whitespace handling
let cert = create_cert_with_orgs(&["IBM Corporation", "IBM Corp"]);
assert!(check_x509_org_name(&cert, "IBM Corporation"));
assert!(!check_x509_org_name(&cert, "ibm corporation"));
assert!(!check_x509_org_name(&cert, "IBM CORPORATION"));
assert!(check_x509_org_name(&cert, "IBM Corp"));
assert!(!check_x509_org_name(&cert, "IBM Corp"));
}
}
mod custom_root_ca_validation {
use openssl::x509::verify::{X509VerifyFlags, X509VerifyParam};
use openssl::x509::X509Crl;
use super::*;
use crate::verify::X509StoreExtension;
#[test]
fn custom_root_ca_does_not_bypass_chain_validation() {
// This test verifies that providing a custom root CA (--root-ca flag)
// only skips organization pinning, but does NOT bypass other validation
let root_ca = load_gen_cert("root_ca.crt");
let fake_inter = load_gen_cert("fake_inter_ca.crt");
let ibm_cert = load_gen_cert("ibm.crt");
let mut store_builder = X509StoreBuilder::new().unwrap();
store_builder.add_cert(root_ca).unwrap();
let crl =
X509Crl::from_pem(&std::fs::read(get_cert_asset_path("inter_ca.crl")).unwrap())
.unwrap();
store_builder.add_crl(&crl).unwrap();
let mut param = X509VerifyParam::new().unwrap();
param.set_flags(X509VerifyFlags::CRL_CHECK).unwrap();
store_builder.set_param(&param).unwrap();
let store = store_builder.build();
// Create untrusted chain with FAKE intermediate (invalid chain)
let mut untrusted = Stack::<X509>::new().unwrap();
untrusted.push(fake_inter).unwrap();
let result = verify_chain(
&store,
&untrusted,
&[ibm_cert],
&RootCaVerification::SkipPinning,
);
assert!(
result.is_err(),
"Custom root CA should not bypass chain validation"
);
}
#[test]
fn custom_root_ca_skips_organization_pinning() {
// This test verifies that SkipPinning mode allows any root CA organization
let root_ca = load_gen_cert("root_ca.crt");
let inter_ca = load_gen_cert("inter_ca.crt");
let ibm_cert = load_gen_cert("ibm.crt");
let mut store_builder = X509StoreBuilder::new().unwrap();
store_builder.add_cert(root_ca).unwrap();
let crl =
X509Crl::from_pem(&std::fs::read(get_cert_asset_path("inter_ca.crl")).unwrap())
.unwrap();
store_builder.add_crl(&crl).unwrap();
let mut param = X509VerifyParam::new().unwrap();
param.set_flags(X509VerifyFlags::CRL_CHECK).unwrap();
store_builder.set_param(&param).unwrap();
let store = store_builder.build();
let mut untrusted = Stack::<X509>::new().unwrap();
untrusted.push(inter_ca).unwrap();
let result = verify_chain(
&store,
&untrusted,
&[ibm_cert],
&RootCaVerification::SkipPinning,
);
assert!(
result.is_ok(),
"Valid chain should succeed with SkipPinning"
);
}
#[test]
fn organization_pinning_enforces_expected_org() {
// This test verifies that organization pinning rejects wrong organizations
let root_ca = load_gen_cert("root_ca.crt");
let inter_ca = load_gen_cert("inter_ca.crt");
let ibm_cert = load_gen_cert("ibm.crt");
let mut store_builder = X509StoreBuilder::new().unwrap();
store_builder.add_cert(root_ca.clone()).unwrap();
let crl =
X509Crl::from_pem(&std::fs::read(get_cert_asset_path("inter_ca.crl")).unwrap())
.unwrap();
store_builder.add_crl(&crl).unwrap();
let mut param = X509VerifyParam::new().unwrap();
param.set_flags(X509VerifyFlags::CRL_CHECK).unwrap();
store_builder.set_param(&param).unwrap();
let store = store_builder.build();
let mut untrusted = Stack::<X509>::new().unwrap();
untrusted.push(inter_ca).unwrap();
// With wrong organization pinning, should fail
let wrong_org = "Wrong Organization Name";
let result = verify_chain(
&store,
&untrusted,
&[ibm_cert.clone()],
&RootCaVerification::RootCaOrganizationPinning(wrong_org),
);
assert!(result.is_err(), "Should fail with wrong organization");
// With correct organization pinning, should succeed
let result = verify_chain(
&store,
&untrusted,
&[ibm_cert],
&RootCaVerification::RootCaOrganizationPinning(
"International Business Machines Corporation",
),
);
assert!(result.is_ok(), "Should succeed with correct organization");
}
}
}