mirror of
https://github.com/ibm-s390-linux/s390-tools.git
synced 2026-08-05 02:14:52 +00:00
dcece3f980
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>
1599 lines
54 KiB
Rust
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(¶m)?;
|
|
|
|
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(¶m).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(¶m).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(¶m).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");
|
|
}
|
|
}
|
|
}
|