Skip to content

Commit

Permalink
x509-cert: rework the profile of builder (#1306)
Browse files Browse the repository at this point in the history
This is now providing a trait to be implemented by the consumer.

A number of implementation are available, including ones trying to abide
by CABF Baseline Requirements.

Fixes #1281
  • Loading branch information
baloo committed May 13, 2024
1 parent 16fb786 commit d1cac63
Show file tree
Hide file tree
Showing 8 changed files with 958 additions and 236 deletions.
206 changes: 45 additions & 161 deletions x509-cert/src/builder.rs
Expand Up @@ -7,23 +7,22 @@ use der::{asn1::BitString, referenced::OwnedToRef, Encode};
use signature::{rand_core::CryptoRngCore, Keypair, RandomizedSigner, Signer};
use spki::{
AlgorithmIdentifier, DynSignatureAlgorithmIdentifier, EncodePublicKey, ObjectIdentifier,
SignatureBitStringEncoding, SubjectPublicKeyInfoOwned, SubjectPublicKeyInfoRef,
SignatureBitStringEncoding, SubjectPublicKeyInfoOwned,
};

use crate::{
certificate::{Certificate, TbsCertificate, Version},
ext::{
pkix::{
AuthorityKeyIdentifier, BasicConstraints, KeyUsage, KeyUsages, SubjectKeyIdentifier,
},
AsExtension, Extension, Extensions,
},
ext::{AsExtension, Extensions},
name::Name,
request::{attributes::AsAttribute, CertReq, CertReqInfo, ExtensionReq},
serial_number::SerialNumber,
time::Validity,
};

pub mod profile;

use self::profile::Profile;

const NULL_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("0.0.0");

/// Error type
Expand All @@ -38,6 +37,23 @@ pub enum Error {

/// Signing error propagated for the [`signature::Error`] type.
Signature(signature::Error),

/// Each RelativeDistinguishedName MUST contain exactly one AttributeTypeAndValue.
NonUniqueRdn,

/// Each Name MUST NOT contain more than one instance of a given
/// AttributeTypeAndValue across all RelativeDistinguishedNames unless explicitly
/// allowed in these Requirements
NonUniqueATV,

/// Non-ordered attribute or invalid attribute
InvalidAttribute {
/// Offending [`ObjectIdentifier`]
oid: ObjectIdentifier,
},

/// Not all required elements were specified
MissingAttributes,
}

#[cfg(feature = "std")]
Expand All @@ -49,6 +65,13 @@ impl fmt::Display for Error {
Error::Asn1(err) => write!(f, "ASN.1 error: {}", err),
Error::PublicKey(err) => write!(f, "public key error: {}", err),
Error::Signature(err) => write!(f, "signature error: {}", err),
Error::NonUniqueRdn => write!(
f,
"Each RelativeDistinguishedName MUST contain exactly one AttributeTypeAndValue."
),
Error::NonUniqueATV => write!(f, "Each Name MUST NOT contain more than one instance of a given AttributeTypeAndValue"),
Error::InvalidAttribute{oid} => write!(f, "Non-ordered attribute or invalid attribute found (oid={oid})"),
Error::MissingAttributes => write!(f, "Not all required elements were specified"),
}
}
}
Expand All @@ -74,156 +97,12 @@ impl From<signature::Error> for Error {
/// Result type
pub type Result<T> = core::result::Result<T, Error>;

/// The type of certificate to build
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Profile {
/// Build a root CA certificate
Root,
/// Build an intermediate sub CA certificate
SubCA {
/// issuer Name,
/// represents the name signing the certificate
issuer: Name,
/// pathLenConstraint INTEGER (0..MAX) OPTIONAL
/// BasicConstraints as defined in [RFC 5280 Section 4.2.1.9].
path_len_constraint: Option<u8>,
},
/// Build an end certificate
Leaf {
/// issuer Name,
/// represents the name signing the certificate
issuer: Name,
/// should the key agreement flag of KeyUsage be enabled
enable_key_agreement: bool,
/// should the key encipherment flag of KeyUsage be enabled
enable_key_encipherment: bool,
/// should the subject key identifier extension be included
///
/// From [RFC 5280 Section 4.2.1.2]:
/// For end entity certificates, subject key identifiers SHOULD be
/// derived from the public key. Two common methods for generating key
/// identifiers from the public key are identified above.
#[cfg(feature = "hazmat")]
include_subject_key_identifier: bool,
},
#[cfg(feature = "hazmat")]
/// Opt-out of the default extensions
Manual {
/// issuer Name,
/// represents the name signing the certificate
/// A `None` will make it a self-signed certificate
issuer: Option<Name>,
},
}

impl Profile {
fn get_issuer(&self, subject: &Name) -> Name {
match self {
Profile::Root => subject.clone(),
Profile::SubCA { issuer, .. } => issuer.clone(),
Profile::Leaf { issuer, .. } => issuer.clone(),
#[cfg(feature = "hazmat")]
Profile::Manual { issuer, .. } => issuer.as_ref().unwrap_or(subject).clone(),
}
}

fn build_extensions(
&self,
spk: SubjectPublicKeyInfoRef<'_>,
issuer_spk: SubjectPublicKeyInfoRef<'_>,
tbs: &TbsCertificate,
) -> Result<vec::Vec<Extension>> {
#[cfg(feature = "hazmat")]
// User opted out of default extensions set.
if let Profile::Manual { .. } = self {
return Ok(vec::Vec::default());
}

let mut extensions: vec::Vec<Extension> = vec::Vec::new();

match self {
#[cfg(feature = "hazmat")]
Profile::Leaf {
include_subject_key_identifier: false,
..
} => {}
_ => extensions.push(
SubjectKeyIdentifier::try_from(spk)?.to_extension(&tbs.subject, &extensions)?,
),
}

// Build Authority Key Identifier
match self {
Profile::Root => {}
_ => {
extensions.push(
AuthorityKeyIdentifier::try_from(issuer_spk.clone())?
.to_extension(&tbs.subject, &extensions)?,
);
}
}

// Build Basic Contraints extensions
extensions.push(match self {
Profile::Root => BasicConstraints {
ca: true,
path_len_constraint: None,
}
.to_extension(&tbs.subject, &extensions)?,
Profile::SubCA {
path_len_constraint,
..
} => BasicConstraints {
ca: true,
path_len_constraint: *path_len_constraint,
}
.to_extension(&tbs.subject, &extensions)?,
Profile::Leaf { .. } => BasicConstraints {
ca: false,
path_len_constraint: None,
}
.to_extension(&tbs.subject, &extensions)?,
#[cfg(feature = "hazmat")]
Profile::Manual { .. } => unreachable!(),
});

// Build Key Usage extension
match self {
Profile::Root | Profile::SubCA { .. } => {
extensions.push(
KeyUsage(KeyUsages::KeyCertSign | KeyUsages::CRLSign)
.to_extension(&tbs.subject, &extensions)?,
);
}
Profile::Leaf {
enable_key_agreement,
enable_key_encipherment,
..
} => {
let mut key_usage = KeyUsages::DigitalSignature | KeyUsages::NonRepudiation;
if *enable_key_encipherment {
key_usage |= KeyUsages::KeyEncipherment;
}
if *enable_key_agreement {
key_usage |= KeyUsages::KeyAgreement;
}

extensions.push(KeyUsage(key_usage).to_extension(&tbs.subject, &extensions)?);
}
#[cfg(feature = "hazmat")]
Profile::Manual { .. } => unreachable!(),
}

Ok(extensions)
}
}

/// X509 Certificate builder
///
/// ```
/// use der::Decode;
/// use x509_cert::spki::SubjectPublicKeyInfoOwned;
/// use x509_cert::builder::{CertificateBuilder, Profile, Builder};
/// use x509_cert::builder::{CertificateBuilder, Builder, profile};
/// use x509_cert::name::Name;
/// use x509_cert::serial_number::SerialNumber;
/// use x509_cert::time::Validity;
Expand All @@ -237,14 +116,14 @@ impl Profile {
/// # use der::referenced::RefToOwned;
/// # fn rsa_signer() -> SigningKey<Sha256> {
/// # let private_key = rsa::RsaPrivateKey::from_pkcs1_der(RSA_2048_PRIV_DER).unwrap();
/// # let signing_key = SigningKey::<Sha256>::new_with_prefix(private_key);
/// # let signing_key = SigningKey::<Sha256>::new(private_key);
/// # signing_key
/// # }
///
/// let serial_number = SerialNumber::from(42u32);
/// let validity = Validity::from_now(Duration::new(5, 0)).unwrap();
/// let profile = Profile::Root;
/// let subject = Name::from_str("CN=World domination corporation,O=World domination Inc,C=US").unwrap();
/// let profile = profile::cabf::Root::new(false,subject).expect("Create root profile");
///
/// let pub_key = SubjectPublicKeyInfoOwned::try_from(RSA_2048_DER).expect("get rsa pub key");
///
Expand All @@ -253,33 +132,35 @@ impl Profile {
/// profile,
/// serial_number,
/// validity,
/// subject,
/// pub_key,
/// )
/// .expect("Create certificate builder");
///
/// let cert = builder.build(&signer).expect("Create certificate");
/// ```
pub struct CertificateBuilder {
pub struct CertificateBuilder<P> {
tbs: TbsCertificate,
extensions: Extensions,
profile: Profile,
profile: P,
}

impl CertificateBuilder {
impl<P> CertificateBuilder<P>
where
P: Profile,
{
/// Creates a new certificate builder
pub fn new(
profile: Profile,
profile: P,
serial_number: SerialNumber,
mut validity: Validity,
subject: Name,
subject_public_key_info: SubjectPublicKeyInfoOwned,
) -> Result<Self> {
let signature_alg = AlgorithmIdentifier {
oid: NULL_OID,
parameters: None,
};

let subject = profile.get_subject();
let issuer = profile.get_issuer(&subject);

validity.not_before.rfc5280_adjust_utc_time()?;
Expand Down Expand Up @@ -455,7 +336,10 @@ pub trait Builder: Sized {
}
}

impl Builder for CertificateBuilder {
impl<P> Builder for CertificateBuilder<P>
where
P: Profile,
{
type Output = Certificate;

fn finalize<S>(&mut self, cert_signer: &S) -> Result<vec::Vec<u8>>
Expand Down
41 changes: 41 additions & 0 deletions x509-cert/src/builder/profile.rs
@@ -0,0 +1,41 @@
//! Certificate profiles
//!
//! Profiles need implement by the [`Profile`] trait.
//! They may then be consumed by a [`builder::CertificateBuilder`].
//!
//!
//! Multiple profiles are provided and you may select one depending on your use-case:
//! - [`cabf`] implements the Baseline Requirement from the CA Browser Forum as close as it can be
//! done.
//! - [`devid`] implements the specification for IEEE 802.1 AR. Certificates for Secure
//! Device Identity.
//!
//! Please follow each sub-module documentation and select a profile that may suit your needs, or
//! you may implement your own profile, if need be.

#[cfg(doc)]
use crate::builder;

use crate::{builder::Result, certificate::TbsCertificate, ext::Extension, name::Name};
use alloc::vec;
use spki::SubjectPublicKeyInfoRef;

pub mod cabf;
pub mod devid;

/// Profile for certificates
pub trait Profile {
/// Issuer to be used for issued certificates
fn get_issuer(&self, subject: &Name) -> Name;

/// Subject for the certificate to be used.
fn get_subject(&self) -> Name;

/// X509v3 extensions to be added in the certificates.
fn build_extensions(
&self,
spk: SubjectPublicKeyInfoRef<'_>,
issuer_spk: SubjectPublicKeyInfoRef<'_>,
tbs: &TbsCertificate,
) -> Result<vec::Vec<Extension>>;
}

0 comments on commit d1cac63

Please sign in to comment.