Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

x509-cert: rework certificate builder profiles #1306

Merged
merged 1 commit into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
206 changes: 45 additions & 161 deletions x509-cert/src/builder.rs
Original file line number Diff line number Diff line change
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,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those new error entries are mostly tied to the checks run for compliance with cabf, I could probably make a subtype?


/// 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,
baloo marked this conversation as resolved.
Show resolved Hide resolved
},
#[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};
tarcieri marked this conversation as resolved.
Show resolved Hide resolved
/// 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
Original file line number Diff line number Diff line change
@@ -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>>;
}