use bherror::traits::{ErrorContext as _, ForeignError as _};
use openssl::{
base64,
error::ErrorStack,
pkey::{PKey, Public},
stack::Stack,
x509::{
store::{X509Store, X509StoreBuilder},
verify::X509VerifyFlags,
X509StoreContext, X509,
},
};
use crate::{Error, JwtX5Chain, Result};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct X5Chain {
leaf: X509,
intermediates: Vec<X509>,
}
impl X5Chain {
pub fn new(chain: Vec<X509>) -> Result<Self> {
validate_chain_order(&chain)?;
let mut chain = chain.into_iter();
let leaf = chain.next().expect("chain is empty");
let intermediates = chain.collect();
Ok(Self {
leaf,
intermediates,
})
}
pub fn from_raw_bytes(bytes: &[Vec<u8>]) -> Result<Self> {
let certs = bytes
.iter()
.enumerate()
.map(|(i, der)| X509::from_der(der).foreign_err(|| Error::X5Chain).ctx(|| i))
.collect::<Result<_>>()
.ctx(|| "invalid X509 certificate")?;
Self::new(certs)
}
pub fn from_pem<S: AsRef<str>>(pem: &[S]) -> Result<Self> {
let certs = pem
.iter()
.enumerate()
.map(|(i, pem)| {
X509::from_pem(pem.as_ref().as_bytes())
.foreign_err(|| Error::X5Chain)
.ctx(|| i)
})
.collect::<Result<_>>()
.ctx(|| "invalid X509 certificate")?;
Self::new(certs)
}
pub fn to_pem(&self) -> Result<Vec<String>> {
std::iter::once(&self.leaf)
.chain(&self.intermediates)
.map(x509_to_pem)
.collect()
}
pub fn from_pem_concat(pem: &str) -> Result<Self> {
Self::new(X509::stack_from_pem(pem.as_bytes()).foreign_err(|| Error::X5Chain)?)
}
pub fn to_pem_concat(&self) -> Result<String> {
Ok(self.to_pem()?.concat())
}
pub fn verify_against_trusted_roots(&self, trust: &X509Trust) -> Result<()> {
let intermediates = chain_to_stack(self.intermediates.clone())?;
let trust = certs_to_store(trust.0.clone())?;
let mut context = X509StoreContext::new().foreign_err(|| Error::X5Chain)?;
let is_valid = context
.init(&trust, &self.leaf, &intermediates, |ctx| {
clean_up_after_openssl(|| ctx.verify_cert())
})
.foreign_err(|| Error::X5Chain)?;
if !is_valid {
return Err(bherror::Error::root(Error::X5Chain)
.ctx("Chain validation against trusted root certificates failed")
.ctx(format!(
"OpenSSL error on depth {}: {}",
context.error_depth(),
context.error()
)));
};
Ok(())
}
pub fn as_bytes(&self) -> Result<Vec<Vec<u8>>> {
let mut bytes = Vec::new();
bytes.push(self.leaf.to_der().foreign_err(|| Error::X5Chain)?);
for intermediate in &self.intermediates {
bytes.push(intermediate.to_der().foreign_err(|| Error::X5Chain)?);
}
Ok(bytes)
}
pub fn leaf_certificate_key(&self) -> Result<PKey<Public>> {
self.leaf_certificate()
.public_key()
.foreign_err(|| Error::X5Chain)
.ctx(|| "Failed to access X509 public key")
}
pub fn leaf_certificate(&self) -> &X509 {
&self.leaf
}
#[cfg(any(feature = "test-utils", test))]
pub fn dummy() -> Self {
let cert = "-----BEGIN CERTIFICATE-----
MIICtzCCAl2gAwIBAgIUXT1Yn4YbUTz3wnpQC8RwexqCuDcwCgYIKoZIzj0EAwIw
ZTELMAkGA1UEBhMCSFIxFDASBgNVBAgMC0dyYWQgWmFncmViMQ8wDQYDVQQHDAZa
YWdyZWIxDTALBgNVBAoMBFRCVEwxETAPBgNVBAsMCFRlYW0gQmVlMQ0wCwYDVQQD
DARyb290MCAXDTI1MTIxMDEzMzMxN1oYDzIxMjUxMTE2MTMzMzE3WjBlMQswCQYD
VQQGEwJIUjEUMBIGA1UECAwLR3JhZCBaYWdyZWIxDzANBgNVBAcMBlphZ3JlYjEN
MAsGA1UECgwEVEJUTDERMA8GA1UECwwIVGVhbSBCZWUxDTALBgNVBAMMBHJvb3Qw
WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARkIF0Dd+xzZrIofEhY/Um7TlMLDfpE
og+Moq7unhBn9NT1W/iIVuxiZJR9FTPLctUKjkPiV9rvDvULosK/aRD6o4HoMIHl
MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHUFpeGAC0ZFRAQha7ykyHFmqekt
MIGiBgNVHSMEgZowgZeAFHUFpeGAC0ZFRAQha7ykyHFmqektoWmkZzBlMQswCQYD
VQQGEwJIUjEUMBIGA1UECAwLR3JhZCBaYWdyZWIxDzANBgNVBAcMBlphZ3JlYjEN
MAsGA1UECgwEVEJUTDERMA8GA1UECwwIVGVhbSBCZWUxDTALBgNVBAMMBHJvb3SC
FF09WJ+GG1E898J6UAvEcHsagrg3MA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQD
AgNIADBFAiBT9FLacHRRyUmLa8PdGEHbiJaE9p5rg9rzQSptcgfZKgIhAMVDZkEh
m0u5S+/UL3BnCba7Efw3lkBfTBkdKWgrdzEw
-----END CERTIFICATE-----";
let cert = X509::from_pem(cert.as_bytes()).unwrap();
let trust = X509Trust::new(vec![cert.clone()]);
let chain = X5Chain::new(vec![cert]).unwrap();
chain.verify_against_trusted_roots(&trust).unwrap();
chain
}
}
#[derive(Debug, Clone)]
pub struct X509Trust(Vec<X509>);
impl X509Trust {
pub fn new(trust: Vec<X509>) -> Self {
Self(trust)
}
}
fn x509_to_pem(x509: &X509) -> Result<String> {
let bytes = x509
.to_pem()
.foreign_err(|| Error::X5Chain)
.ctx(|| "Failed to convert x509 to pem")?;
String::from_utf8(bytes)
.foreign_err(|| Error::X5Chain)
.ctx(|| "Failed to convert pem bytes to utf-8 string")
}
fn chain_to_stack(chain: impl IntoIterator<Item = X509>) -> Result<Stack<X509>> {
let mut intermediates = Stack::new().foreign_err(|| Error::X5Chain)?;
for cert in chain {
intermediates.push(cert).foreign_err(|| Error::X5Chain)?;
}
Ok(intermediates)
}
fn certs_to_store(certificates: impl IntoIterator<Item = X509>) -> Result<X509Store> {
let mut builder = X509StoreBuilder::new().foreign_err(|| Error::X5Chain)?;
builder
.set_flags(X509VerifyFlags::X509_STRICT | X509VerifyFlags::CHECK_SS_SIGNATURE)
.foreign_err(|| Error::X5Chain)?;
for cert in certificates {
builder.add_cert(cert).foreign_err(|| Error::X5Chain)?;
}
Ok(builder.build())
}
fn validate_chain_order(chain: &[X509]) -> Result<()> {
if chain.is_empty() {
return Err(bherror::Error::root(Error::X5Chain).ctx("chain is empty"));
}
let is_ordered = chain
.windows(2)
.try_fold(true, |acc, cert_pair| {
let child = &cert_pair[0];
let parent = &cert_pair[1];
let is_child = clean_up_after_openssl(|| child.verify(parent.public_key()?.as_ref()))?;
Ok::<_, openssl::error::ErrorStack>(acc && is_child)
})
.foreign_err(|| Error::X5Chain)?;
if !is_ordered {
return Err(bherror::Error::root(Error::X5Chain).ctx("invalid chain order"));
}
Ok(())
}
impl TryFrom<JwtX5Chain> for X5Chain {
type Error = bherror::Error<Error>;
fn try_from(jwt_x5chain: JwtX5Chain) -> Result<Self> {
let der_certs: Vec<Vec<u8>> = jwt_x5chain
.into_base64_ders()
.iter()
.enumerate()
.map(|(i, base64_der)| {
base64::decode_block(base64_der)
.foreign_err(|| Error::X5Chain)
.ctx(|| i)
})
.collect::<Result<_>>()
.ctx(|| "invalid base64 string")?;
X5Chain::from_raw_bytes(&der_certs)
}
}
fn clean_up_after_openssl<T>(
f: impl FnOnce() -> std::result::Result<T, ErrorStack>,
) -> std::result::Result<T, ErrorStack> {
let return_value = f()?;
drop(ErrorStack::get());
Ok(return_value)
}
#[cfg(test)]
mod tests {
use super::*;
const CERTS: &str = "-----BEGIN CERTIFICATE-----
MIIB0DCCAXUCFA66hLaomVZPSG6NONR/+TborprgMAoGCCqGSM49BAMCMG0xCzAJ
BgNVBAYTAkhSMRQwEgYDVQQIDAtHcmFkIFphZ3JlYjEPMA0GA1UEBwwGWmFncmVi
MQ0wCwYDVQQKDARUQlRMMREwDwYDVQQLDAhUZWFtIEJlZTEVMBMGA1UEAwwMaW50
ZXJtZWRpYXJ5MCAXDTI1MTIxMDEzMzM1MFoYDzIxMjUxMTE2MTMzMzUwWjBlMQsw
CQYDVQQGEwJIUjEUMBIGA1UECAwLR3JhZCBaYWdyZWIxDzANBgNVBAcMBlphZ3Jl
YjENMAsGA1UECgwEVEJUTDERMA8GA1UECwwIVGVhbSBCZWUxDTALBgNVBAMMBGxl
YWYwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASbC272RjmKpednBkEvlk2CrEJe
iSgaviOkZ7yaRY83HpgIgG6QqJMyMYiaS8W6IS3jaf/ODTq2/np2a7Zna0u+MAoG
CCqGSM49BAMCA0kAMEYCIQClk1uafXqibcHx6kqbw0xiFcejZMQ1UMU03Xq51ftN
UwIhAJ1NXHQzD3LUehcb9eewIcv7R1SBKsb0R00gVYZ186ps
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICPDCCAeKgAwIBAgIUbxX7OZkOub1NGzC1D88yA6BDK4EwCgYIKoZIzj0EAwIw
ZTELMAkGA1UEBhMCSFIxFDASBgNVBAgMC0dyYWQgWmFncmViMQ8wDQYDVQQHDAZa
YWdyZWIxDTALBgNVBAoMBFRCVEwxETAPBgNVBAsMCFRlYW0gQmVlMQ0wCwYDVQQD
DARyb290MCAXDTI1MTIxMDEzMzMzMloYDzIxMjUxMTE2MTMzMzMyWjBtMQswCQYD
VQQGEwJIUjEUMBIGA1UECAwLR3JhZCBaYWdyZWIxDzANBgNVBAcMBlphZ3JlYjEN
MAsGA1UECgwEVEJUTDERMA8GA1UECwwIVGVhbSBCZWUxFTATBgNVBAMMDGludGVy
bWVkaWFyeTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEiANYWwYfYs8fnV2Jwk
UrKOoN3NqXk2DoJ86S7VzyIMkDtv2WEL+UX7zzVJzEI8SMfyMnfxQfw6qh0hzePP
qxajZjBkMB0GA1UdDgQWBBQnLZcuND2wi4LLgqEkCG/BTzl0TjAfBgNVHSMEGDAW
gBR1BaXhgAtGRUQEIWu8pMhxZqnpLTASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1Ud
DwEB/wQEAwIBhjAKBggqhkjOPQQDAgNIADBFAiEA20t9kGAyOgDv/m4/GO09OVV2
EOpMWMcM9L4uOaPcoZgCIAeoA48SV5eN5TukS8UF8Q5Iu6y1zla1/SLb5isn3WYr
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICtzCCAl2gAwIBAgIUXT1Yn4YbUTz3wnpQC8RwexqCuDcwCgYIKoZIzj0EAwIw
ZTELMAkGA1UEBhMCSFIxFDASBgNVBAgMC0dyYWQgWmFncmViMQ8wDQYDVQQHDAZa
YWdyZWIxDTALBgNVBAoMBFRCVEwxETAPBgNVBAsMCFRlYW0gQmVlMQ0wCwYDVQQD
DARyb290MCAXDTI1MTIxMDEzMzMxN1oYDzIxMjUxMTE2MTMzMzE3WjBlMQswCQYD
VQQGEwJIUjEUMBIGA1UECAwLR3JhZCBaYWdyZWIxDzANBgNVBAcMBlphZ3JlYjEN
MAsGA1UECgwEVEJUTDERMA8GA1UECwwIVGVhbSBCZWUxDTALBgNVBAMMBHJvb3Qw
WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARkIF0Dd+xzZrIofEhY/Um7TlMLDfpE
og+Moq7unhBn9NT1W/iIVuxiZJR9FTPLctUKjkPiV9rvDvULosK/aRD6o4HoMIHl
MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHUFpeGAC0ZFRAQha7ykyHFmqekt
MIGiBgNVHSMEgZowgZeAFHUFpeGAC0ZFRAQha7ykyHFmqektoWmkZzBlMQswCQYD
VQQGEwJIUjEUMBIGA1UECAwLR3JhZCBaYWdyZWIxDzANBgNVBAcMBlphZ3JlYjEN
MAsGA1UECgwEVEJUTDERMA8GA1UECwwIVGVhbSBCZWUxDTALBgNVBAMMBHJvb3SC
FF09WJ+GG1E898J6UAvEcHsagrg3MA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQD
AgNIADBFAiBT9FLacHRRyUmLa8PdGEHbiJaE9p5rg9rzQSptcgfZKgIhAMVDZkEh
m0u5S+/UL3BnCba7Efw3lkBfTBkdKWgrdzEw
-----END CERTIFICATE-----
";
fn get_certs() -> [X509; 3] {
X509::stack_from_pem(CERTS.as_bytes())
.unwrap()
.try_into()
.unwrap()
}
#[test]
fn test_validate_chain_order() {
let [leaf, intermediary, root] = get_certs();
validate_chain_order(&[leaf.clone(), intermediary.clone(), root.clone()]).unwrap();
let err = validate_chain_order(&[intermediary, leaf]).unwrap_err();
assert!(matches!(err.error, Error::X5Chain));
assert_empty_error_stack();
let err = validate_chain_order(&[]).unwrap_err();
assert!(matches!(err.error, Error::X5Chain));
assert_empty_error_stack();
validate_chain_order(&[root]).unwrap();
}
#[test]
fn test_from_raw_bytes() {
let [leaf, intermediary, root] = get_certs();
let leaf = leaf.to_der().unwrap();
let intermediary = intermediary.to_der().unwrap();
let root = root.to_der().unwrap();
X5Chain::from_raw_bytes(&[leaf, intermediary, root]).unwrap();
let err = X5Chain::from_raw_bytes(&[]).unwrap_err();
assert!(matches!(err.error, Error::X5Chain));
assert_empty_error_stack();
let err = X5Chain::from_raw_bytes(&[vec![0u8, 1u8], vec![2u8]]).unwrap_err();
assert!(matches!(err.error, Error::X5Chain));
assert_empty_error_stack();
}
#[test]
fn test_from_pem() {
let mut certs = CERTS.split_inclusive("-----END CERTIFICATE-----\n");
let leaf = certs.next().unwrap();
let intermediary = certs.next().unwrap();
let root = certs.next().unwrap();
X5Chain::from_pem(&[leaf, intermediary, root]).unwrap();
let err = X5Chain::from_pem::<&str>(&[]).unwrap_err();
assert!(matches!(err.error, Error::X5Chain));
assert_empty_error_stack();
let err = X5Chain::from_pem(&["babadeda", "bla"]).unwrap_err();
assert!(matches!(err.error, Error::X5Chain));
assert_empty_error_stack();
}
#[test]
fn test_to_pem() {
let mut certs = CERTS.split_inclusive("-----END CERTIFICATE-----\n");
let leaf = certs.next().unwrap();
let intermediary = certs.next().unwrap();
let root = certs.next().unwrap();
let x5chain = X5Chain::from_pem(&[leaf, intermediary, root]).unwrap();
let pem = x5chain.to_pem().unwrap();
assert_eq!(pem.concat(), CERTS);
let x5chain_round_trip = X5Chain::from_pem(&pem).unwrap();
assert_eq!(x5chain, x5chain_round_trip);
}
#[test]
fn test_from_pem_concat() {
let mut certs = CERTS.split_inclusive("-----END CERTIFICATE-----\n");
let leaf = certs.next().unwrap();
let intermediary = certs.next().unwrap();
let root = certs.next().unwrap();
let x5chain_from_pem = X5Chain::from_pem(&[leaf, intermediary, root]).unwrap();
let x5chain_from_pem_concat = X5Chain::from_pem_concat(CERTS).unwrap();
assert_eq!(x5chain_from_pem, x5chain_from_pem_concat);
let err = X5Chain::from_pem_concat("").unwrap_err();
assert!(matches!(err.error, Error::X5Chain));
assert_empty_error_stack();
let err = X5Chain::from_pem_concat("babadedabla").unwrap_err();
assert!(matches!(err.error, Error::X5Chain));
assert_empty_error_stack();
}
#[test]
fn test_to_pem_concat() {
let x5chain = X5Chain::from_pem_concat(CERTS).unwrap();
assert_eq!(x5chain.to_pem_concat().unwrap(), CERTS);
}
#[test]
fn check_x5chain_relationship() {
let [leaf, intermediary, root] = get_certs();
X5Chain::new(vec![leaf.clone()]).unwrap();
X5Chain::new(vec![leaf.clone(), intermediary.clone()]).unwrap();
X5Chain::new(vec![leaf.clone(), intermediary.clone(), root]).unwrap();
X5Chain::new(vec![intermediary, leaf]).unwrap_err();
assert_empty_error_stack();
X5Chain::new(Vec::new()).unwrap_err();
assert_empty_error_stack();
}
#[test]
fn test_verify_against_trusted_roots() {
let [leaf, intermediary, root] = get_certs();
let chain = X5Chain::new(vec![leaf.clone()]).unwrap();
let trusted = X509Trust::new(vec![intermediary.clone(), root.clone()]);
chain.verify_against_trusted_roots(&trusted).unwrap();
let trusted = X509Trust::new(vec![intermediary.clone()]);
chain.verify_against_trusted_roots(&trusted).unwrap_err();
assert_empty_error_stack();
let trusted = X509Trust::new(vec![root.clone()]);
chain.verify_against_trusted_roots(&trusted).unwrap_err();
assert_empty_error_stack();
let chain = X5Chain::new(vec![leaf, intermediary, root]).unwrap();
let trusted = X509Trust::new(Vec::new());
chain.verify_against_trusted_roots(&trusted).unwrap_err();
assert_empty_error_stack();
}
fn assert_empty_error_stack() {
let errors = openssl::error::ErrorStack::get();
assert!(
errors.errors().is_empty(),
"Error stack was non-empty: {:?}",
errors
);
}
#[test]
fn test_from_jwtx5chain_to_x5chain() {
let received: X5Chain = JwtX5Chain::dummy().try_into().unwrap();
let expected = X5Chain::dummy();
assert_eq!(received, expected);
}
}