mirror of
https://github.com/ibm-s390-linux/s390-tools.git
synced 2026-08-05 02:14:52 +00:00
pv: Add root CA organization pinning to certificate verification
Enforce root CA organization name requirements during certificate chain validation to prevent accepting certificates from unexpected root CAs. Add RootCaVerification enum with RootCaOrganizationPinning and SkipPinning variants. Extend verify_chain() to validate root CA organization. Default to pinning "DigiCert" for standard trust chains, skip pinning when custom root CA is provided. Assisted-by: IBM Bob:1.0.6 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>
This commit is contained in:
committed by
Jan Höppner
parent
42eba67268
commit
ff19180f03
@@ -187,6 +187,10 @@ impl CertVerifier {
|
||||
untr_certs.append(&mut crt);
|
||||
}
|
||||
let (ibm_z_sign_key, chain) = helper::extract_ibm_sign_key(untr_certs.clone())?;
|
||||
let root_ca_verification = match root_ca_path {
|
||||
Some(_) => helper::RootCaVerification::SkipPinning,
|
||||
None => helper::RootCaVerification::RootCaOrganizationPinning("DigiCert"),
|
||||
};
|
||||
|
||||
// Two-round verification:
|
||||
//
|
||||
@@ -202,6 +206,7 @@ impl CertVerifier {
|
||||
&store_builder.build(),
|
||||
&chain,
|
||||
slice::from_ref(&ibm_z_sign_key),
|
||||
&root_ca_verification,
|
||||
)?;
|
||||
|
||||
// Round 2: Download CRLs and verify again, but this time with CRL checks
|
||||
@@ -220,7 +225,12 @@ impl CertVerifier {
|
||||
}
|
||||
|
||||
let store = store_builder.build();
|
||||
helper::verify_chain(&store, &chain, slice::from_ref(&ibm_z_sign_key))?;
|
||||
helper::verify_chain(
|
||||
&store,
|
||||
&chain,
|
||||
slice::from_ref(&ibm_z_sign_key),
|
||||
&root_ca_verification,
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
store,
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::path::Path;
|
||||
use std::str::from_utf8;
|
||||
use std::time::Duration;
|
||||
|
||||
use log::{debug, warn};
|
||||
use log::{debug, error, warn};
|
||||
use openssl::asn1::{Asn1Time, Asn1TimeRef};
|
||||
use openssl::error::ErrorStack;
|
||||
use openssl::nid::Nid;
|
||||
@@ -169,14 +169,54 @@ pub fn store_setup<P: AsRef<Path>, Q: AsRef<Path>, R: AsRef<Path>>(
|
||||
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) -> std::result::Result<bool, ErrorStack> {
|
||||
fn verify_fun(
|
||||
ctx: &mut X509StoreContextRef,
|
||||
root_ca_verification: &RootCaVerification,
|
||||
) -> std::result::Result<bool, ErrorStack> {
|
||||
// verify certificate
|
||||
let res = ctx.verify_cert()?;
|
||||
if !res {
|
||||
@@ -195,10 +235,35 @@ pub fn verify_chain(
|
||||
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);
|
||||
Ok(false)
|
||||
} else {
|
||||
Ok(true)
|
||||
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()?;
|
||||
@@ -207,7 +272,9 @@ pub fn verify_chain(
|
||||
// (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, verify_fun)
|
||||
.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(
|
||||
@@ -1299,4 +1366,233 @@ mod tests {
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,33 @@ fn verify_chain_offline() {
|
||||
|
||||
let mut sk = Stack::<X509>::new().unwrap();
|
||||
sk.push(inter_crt).unwrap();
|
||||
assert!(verify_chain(&store, &sk, &[ibm_crt]).is_ok());
|
||||
assert!(verify_chain(
|
||||
&store,
|
||||
&sk,
|
||||
&[ibm_crt.clone()],
|
||||
&RootCaVerification::RootCaOrganizationPinning(
|
||||
"International Business Machines Corporationn"
|
||||
)
|
||||
)
|
||||
.is_err());
|
||||
assert!(verify_chain(
|
||||
&store,
|
||||
&sk,
|
||||
&[ibm_crt.clone()],
|
||||
&RootCaVerification::RootCaOrganizationPinning("International")
|
||||
)
|
||||
.is_err());
|
||||
assert!(verify_chain(
|
||||
&store,
|
||||
&sk,
|
||||
&[ibm_crt.clone()],
|
||||
&RootCaVerification::RootCaOrganizationPinning(
|
||||
"International Business Machines Corporation"
|
||||
)
|
||||
)
|
||||
.is_ok());
|
||||
|
||||
assert!(verify_chain(&store, &sk, &[ibm_crt], &RootCaVerification::SkipPinning).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user