Skip to content

Commit

Permalink
feat(bindings): enable application owned certs
Browse files Browse the repository at this point in the history
  • Loading branch information
jmayclin committed Nov 26, 2024
1 parent 9877437 commit 222a7c3
Show file tree
Hide file tree
Showing 5 changed files with 405 additions and 44 deletions.
296 changes: 273 additions & 23 deletions bindings/rust/s2n-tls/src/cert_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,85 @@ use s2n_tls_sys::*;
use std::{
marker::PhantomData,
ptr::{self, NonNull},
sync::Arc,
};

/// Internal wrapper type used for a convenient drop implementation.
///
/// [CertificateChain] is internally reference counted. The reference counted `T`
/// must have a drop implementation.
struct CertificateChainHandle(pub NonNull<s2n_cert_chain_and_key>);

impl Drop for CertificateChainHandle {
fn drop(&mut self) {
// ignore failures since there's not much we can do about it
unsafe {
let _ = s2n_cert_chain_and_key_free(self.0.as_ptr()).into_result();
}
}
}

/// A CertificateChain represents a chain of X.509 certificates.
///
/// Certificate chains are internally reference counted and are cheaply cloneable.
#[derive(Clone)]
pub struct CertificateChain<'a> {
ptr: NonNull<s2n_cert_chain_and_key>,
is_owned: bool,
ptr: Arc<CertificateChainHandle>,
_lifetime: PhantomData<&'a s2n_cert_chain_and_key>,
}

impl CertificateChain<'_> {
/// Create an internally reference counted cert chain.
///
/// This can be used with [crate::config::Builder::add_to_store] to share a
/// single cert across multiple configs.
pub fn load_pems(cert: &[u8], key: &[u8]) -> Result<CertificateChain<'static>, Error> {
let mut chain = Self::allocate_owned()?;
unsafe {
// SAFETY: manual audit of load_pem_bytes shows that `chain_pem` and
// `private_key_pem` are not modified.
// https://github.com/aws/s2n-tls/issues/4140
s2n_cert_chain_and_key_load_pem_bytes(
chain.as_mut_ptr(),
cert.as_ptr() as *mut _,
cert.len() as u32,
key.as_ptr() as *mut _,
key.len() as u32,
)
.into_result()
}?;

Ok(chain)
}

/// This allocates a new certificate chain from s2n.
pub(crate) fn new() -> Result<CertificateChain<'static>, Error> {
pub(crate) fn allocate_owned() -> Result<CertificateChain<'static>, Error> {
unsafe {
let ptr = s2n_cert_chain_and_key_new().into_result()?;
Ok(CertificateChain {
ptr,
is_owned: true,
ptr: Arc::new(CertificateChainHandle(ptr)),
_lifetime: PhantomData,
})
}
}

/// This is used to create a CertificateChain "reference" backed by memory
/// on some external struct, where the external struct has some lifetime `'a`.
pub(crate) unsafe fn from_ptr_reference<'a>(
ptr: NonNull<s2n_cert_chain_and_key>,
) -> CertificateChain<'a> {
let handle = Arc::new(CertificateChainHandle(ptr));

// When this CertificateChain goes out of scope, the data must not be
// freed. We manually increment the reference count to allow for the
// "reference" held by the external struct.
let clone_to_increment_refcount = Arc::clone(&handle);
std::mem::forget(clone_to_increment_refcount);
// `handle` + owning struct = 2
debug_assert_eq!(Arc::strong_count(&handle), 2);

CertificateChain {
ptr,
is_owned: false,
ptr: handle,
_lifetime: PhantomData,
}
}
Expand All @@ -54,8 +105,7 @@ impl CertificateChain<'_> {
/// expensive API to call.
pub fn len(&self) -> usize {
let mut length: u32 = 0;
let res =
unsafe { s2n_cert_chain_get_length(self.ptr.as_ptr(), &mut length).into_result() };
let res = unsafe { s2n_cert_chain_get_length(self.as_ptr(), &mut length).into_result() };
if res.is_err() {
// Errors should only happen on empty chains (we guarantee that `ptr` is a valid chain).
return 0;
Expand All @@ -72,26 +122,20 @@ impl CertificateChain<'_> {
self.len() == 0
}

pub(crate) fn as_mut_ptr(&mut self) -> NonNull<s2n_cert_chain_and_key> {
self.ptr
pub(crate) fn as_mut_ptr(&mut self) -> *mut s2n_cert_chain_and_key {
self.ptr.0.as_ptr()
}

pub(crate) fn as_ptr(&self) -> *const s2n_cert_chain_and_key {
self.ptr.0.as_ptr() as *const _
}
}

// # Safety
//
// s2n_cert_chain_and_key objects can be sent across threads.
unsafe impl Send for CertificateChain<'_> {}

impl Drop for CertificateChain<'_> {
fn drop(&mut self) {
if self.is_owned {
// ignore failures since there's not much we can do about it
unsafe {
let _ = s2n_cert_chain_and_key_free(self.ptr.as_ptr()).into_result();
}
}
}
}
unsafe impl Sync for CertificateChain<'_> {}

pub struct CertificateChainIter<'a> {
idx: u32,
Expand All @@ -112,7 +156,7 @@ impl<'a> Iterator for CertificateChainIter<'a> {
let mut out = ptr::null_mut();
unsafe {
if let Err(e) =
s2n_cert_chain_get_cert(self.chain.ptr.as_ptr(), &mut out, idx).into_result()
s2n_cert_chain_get_cert(self.chain.as_ptr(), &mut out, idx).into_result()
{
return Some(Err(e));
}
Expand Down Expand Up @@ -152,3 +196,209 @@ impl<'a> Certificate<'a> {
//
// Certificates just reference data in the chain, so share the Send-ness of the chain.
unsafe impl Send for Certificate<'_> {}

#[cfg(test)]
mod tests {
use crate::{
config,
error::ErrorType,
security::DEFAULT_TLS13,
testing::{InsecureAcceptAllCertificatesHandler, SniTestCerts, TestPair},
};

use super::*;

/// Create a test pair using SNI certs
/// * `certs`: takes references to already created cert chains. This is useful
/// to assert on expected reference counts.
/// * `types`: Used to find the CA paths for the client configs
fn sni_test_pair(
certs: Vec<CertificateChain<'static>>,
defaults: Option<Vec<CertificateChain<'static>>>,
types: &[SniTestCerts],
) -> Result<TestPair, crate::error::Error> {
let mut server_config = config::Builder::new();
server_config
.with_system_certs(false)?
.set_security_policy(&DEFAULT_TLS13)?;
for cert in certs.into_iter() {
server_config.add_to_store(cert)?;
}
if let Some(defaults) = defaults {
server_config.set_default_cert_chain_and_key(defaults)?;
}

let mut client_config = config::Builder::new();
client_config
.with_system_certs(false)?
.set_security_policy(&DEFAULT_TLS13)?
.set_verify_host_callback(InsecureAcceptAllCertificatesHandler {})?;
for tipe in types {
client_config.trust_pem(tipe.get().cert())?;
}
Ok(TestPair::from_configs(
&client_config.build()?,
&server_config.build()?,
))
}

/// This is a useful (but inefficient) test utility to check if CertificateChain
/// structs are equal. It does this by comparing the serialized `der` representation.
fn cert_chains_are_equal(
this: &CertificateChain<'_>,
that: &CertificateChain<'_>,
) -> bool {
let this: Vec<Vec<u8>> = this
.iter()
.map(|cert| cert.unwrap().der().unwrap().to_owned())
.collect();
let that: Vec<Vec<u8>> = that
.iter()
.map(|cert| cert.unwrap().der().unwrap().to_owned())
.collect();
this == that
}

#[test]
fn reference_count_increment() -> Result<(), crate::error::Error> {
let cert = SniTestCerts::AlligatorRsa.get().into_certificate_chain();
assert_eq!(Arc::strong_count(&cert.ptr), 1);

{
let mut server = config::Builder::new();
server.add_to_store(cert.clone())?;

// after being added, the reference count should have increased
assert_eq!(Arc::strong_count(&cert.ptr), 2);
}

// after the config goes out of scope and is dropped, the ref count should
// decrement
assert_eq!(Arc::strong_count(&cert.ptr), 1);
Ok(())
}

#[test]
fn cert_is_dropped() {
let weak_ref;
{
let cert = SniTestCerts::AlligatorEcdsa.get().into_certificate_chain();
weak_ref = Arc::downgrade(&cert.ptr);
assert_eq!(Arc::strong_count(&cert.ptr), 1);
}
assert_eq!(weak_ref.strong_count(), 0);
assert!(weak_ref.upgrade().is_none());
}

// a cert can be successfully shared across multiple configs
#[test]
fn shared_certs() -> Result<(), crate::error::Error> {
let test_key_pair = SniTestCerts::AlligatorRsa.get();
let cert = test_key_pair.into_certificate_chain();

let mut test_pair_1 =
sni_test_pair(vec![cert.clone()], None, &[SniTestCerts::AlligatorRsa])?;
let mut test_pair_2 =
sni_test_pair(vec![cert.clone()], None, &[SniTestCerts::AlligatorRsa])?;

assert_eq!(Arc::strong_count(&cert.ptr), 3);

assert!(test_pair_1.handshake().is_ok());
assert!(test_pair_2.handshake().is_ok());

assert_eq!(Arc::strong_count(&cert.ptr), 3);

drop(test_pair_1);
assert_eq!(Arc::strong_count(&cert.ptr), 2);
drop(test_pair_2);
assert_eq!(Arc::strong_count(&cert.ptr), 1);
Ok(())
}

#[test]
fn default_selection() -> Result<(), crate::error::Error> {
let alligator_cert = SniTestCerts::AlligatorRsa.get().into_certificate_chain();
let beaver_cert = SniTestCerts::BeaverRsa.get().into_certificate_chain();

// when no default is explicitly set, the first loaded cert is the default
{
let mut test_pair = sni_test_pair(
vec![alligator_cert.clone(), beaver_cert.clone()],
None,
&[SniTestCerts::AlligatorRsa, SniTestCerts::BeaverRsa],
)?;

assert!(test_pair.handshake().is_ok());

assert!(cert_chains_are_equal(
&alligator_cert,
&test_pair.client.peer_cert_chain().unwrap()
));

assert_eq!(Arc::strong_count(&alligator_cert.ptr), 2);
assert_eq!(Arc::strong_count(&beaver_cert.ptr), 2);
}

// set an explicit default
{
let mut test_pair = sni_test_pair(
vec![alligator_cert.clone(), beaver_cert.clone()],
Some(vec![beaver_cert.clone()]),
&[SniTestCerts::AlligatorRsa, SniTestCerts::BeaverRsa],
)?;

assert!(test_pair.handshake().is_ok());

assert!(cert_chains_are_equal(
&beaver_cert,
&test_pair.client.peer_cert_chain().unwrap()
));

assert_eq!(Arc::strong_count(&alligator_cert.ptr), 2);
// beaver has an additional reference because it was used in multiple
// calls
assert_eq!(Arc::strong_count(&beaver_cert.ptr), 3);
}

// set a default without adding it to the store
{
let mut test_pair = sni_test_pair(
vec![alligator_cert.clone()],
Some(vec![beaver_cert.clone()]),
&[SniTestCerts::AlligatorRsa, SniTestCerts::BeaverRsa],
)?;

assert!(test_pair.handshake().is_ok());

assert!(cert_chains_are_equal(
&beaver_cert,
&test_pair.client.peer_cert_chain().unwrap()
));

assert_eq!(Arc::strong_count(&alligator_cert.ptr), 2);
assert_eq!(Arc::strong_count(&beaver_cert.ptr), 2);
}

Ok(())
}

#[test]
fn cert_ownership_error() -> Result<(), crate::error::Error> {
let application_owned_cert = SniTestCerts::AlligatorRsa.get().into_certificate_chain();
let cert_for_lib = SniTestCerts::BeaverRsa.get();

let mut config = config::Builder::new();

// library owned certs can not be used with application owned certs
config.add_to_store(application_owned_cert)?;
let err = config
.load_pem(cert_for_lib.cert(), cert_for_lib.key())
.err()
.unwrap();

assert_eq!(err.kind(), ErrorType::UsageError);
assert_eq!(err.name(), "S2N_ERR_CERT_OWNERSHIP");

Ok(())
}
}
Loading

0 comments on commit 222a7c3

Please sign in to comment.