quickcert 0.1.2

For generating test key/certificate pairs
Documentation
//! Key/certificate generation library.

mod err;

use std::{
  fs,
  io::{self, ErrorKind},
  net::IpAddr,
  path::Path
};

use openssl::{
  asn1::Asn1Time,
  bn::{BigNum, MsbOption},
  ec::{EcGroup, EcKey},
  error::ErrorStack,
  hash::MessageDigest,
  nid::Nid,
  pkey::{PKey, Private},
  rsa::Rsa,
  x509::{
    X509, X509Extension, X509NameBuilder, X509Req, X509ReqBuilder,
    extension::{
      AuthorityKeyIdentifier, BasicConstraints, KeyUsage,
      SubjectAlternativeName, SubjectKeyIdentifier
    }
  }
};

use rand::{
  Rng, RngExt,
  distr::{Distribution, StandardUniform}
};

use strum::EnumCount;

pub use err::Error;


#[derive(Debug, EnumCount)]
enum KeyType {
  Rsa2048,
  Rsa4096,
  EcP256
}

impl Distribution<KeyType> for StandardUniform {
  fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> KeyType {
    match rng.random_range(0..KeyType::COUNT) {
      0 => KeyType::Rsa2048,
      1 => KeyType::Rsa4096,
      2 => KeyType::EcP256,
      _ => unimplemented!()
    }
  }
}


/// Generate a random private key.
fn create_key() -> Result<PKey<Private>, ErrorStack> {
  let kt: KeyType = rand::rng().sample(StandardUniform);
  match kt {
    KeyType::Rsa2048 => {
      let rsa = Rsa::generate(2048)?;
      let privkey = PKey::from_rsa(rsa)?;
      Ok(privkey)
    }
    KeyType::Rsa4096 => {
      let rsa = Rsa::generate(4096)?;
      let privkey = PKey::from_rsa(rsa)?;
      Ok(privkey)
    }
    KeyType::EcP256 => {
      let nid = Nid::X9_62_PRIME256V1;
      let group = EcGroup::from_curve_name(nid)?;
      let ec = EcKey::generate(&group)?;
      let privkey = PKey::from_ec_key(ec)?;
      Ok(privkey)
    }
  }
}

/// Create a self-signed CA certificate
fn mk_ca_cert(privkey: &PKey<Private>) -> Result<X509, ErrorStack> {
  let mut x509_name = X509NameBuilder::new()?;
  x509_name.append_entry_by_text("C", "US")?;
  x509_name.append_entry_by_text("ST", "TX")?;
  x509_name.append_entry_by_text("O", "La Cosa Nostra")?;
  x509_name.append_entry_by_text("CN", "Not Suspicious")?;
  let x509_name = x509_name.build();

  let mut cert_builder = X509::builder()?;
  cert_builder.set_version(2)?;
  let serial_number = {
    let mut serial = BigNum::new()?;
    serial.rand(159, MsbOption::MAYBE_ZERO, false)?;
    serial.to_asn1_integer()?
  };
  cert_builder.set_serial_number(&serial_number)?;
  cert_builder.set_subject_name(&x509_name)?;
  cert_builder.set_issuer_name(&x509_name)?;
  cert_builder.set_pubkey(privkey)?;
  let not_before = Asn1Time::days_from_now(0)?;
  cert_builder.set_not_before(&not_before)?;
  let not_after = Asn1Time::days_from_now(365)?;
  cert_builder.set_not_after(&not_after)?;

  cert_builder
    .append_extension(BasicConstraints::new().critical().ca().build()?)?;
  cert_builder.append_extension(
    KeyUsage::new()
      .critical()
      .key_cert_sign()
      .crl_sign()
      .build()?
  )?;

  #[allow(deprecated)]
  let ext = X509Extension::new_nid(
    None,
    None,
    Nid::NETSCAPE_COMMENT,
    "Will sign anything for ice cream."
  )?;
  cert_builder.append_extension(ext)?;

  let subject_key_identifier = SubjectKeyIdentifier::new()
    .build(&cert_builder.x509v3_context(None, None))?;
  cert_builder.append_extension(subject_key_identifier)?;

  cert_builder.sign(privkey, MessageDigest::sha256())?;
  let cert = cert_builder.build();

  Ok(cert)
}


/// Create a CA private key and self-signed certificate.
///
/// # Errors
/// [`Error::OpenSSL`] means there was a problem generating the key or
/// certificate.
pub fn mk_ca() -> Result<PkiIdent, Error> {
  let key = create_key()?;
  let cert = mk_ca_cert(&key)?;
  Ok(PkiIdent { key, cert })
}


#[derive(Debug, Clone)]
pub struct PkiIdent {
  pub key: PKey<Private>,
  pub cert: X509
}

impl PkiIdent {
  /// Return PEM-encoded key and certificate buffers.
  ///
  /// On success a tuple will be returned where the first field is the
  /// PEM-encoded private key, and the second field is the PEM-encoded
  /// certificate.
  ///
  /// # Errors
  /// [`Error::OpenSSL`] means there was a problem serializing either the key
  /// or the certificate.
  pub fn to_pem(&self) -> Result<(Vec<u8>, Vec<u8>), Error> {
    let keyder = self.key.private_key_to_pem_pkcs8()?;
    let certder = self.cert.to_pem()?;
    Ok((keyder, certder))
  }

  /// Return DER-encoded key and certificate.
  ///
  /// On success a tuple will be returned where the first field is the
  /// DER-encoded private key, and the second field is the DER-encoded
  /// certificate.
  ///
  /// # Errors
  /// [`Error::OpenSSL`] means there was a problem serializing either the key
  /// or the certificate.
  pub fn to_der(&self) -> Result<(Vec<u8>, Vec<u8>), Error> {
    let keyder = self.key.private_key_to_der()?;
    let certder = self.cert.to_der()?;
    Ok((keyder, certder))
  }

  /// Load key/certificate pair.
  ///
  /// # Errors
  /// [`Error::OpenSSL`] means there was a problem serializing either the key
  /// or the certificate.  [`Error::IO`] means file I/O occurred.
  pub fn load_pem(name: &str) -> Result<Self, Error> {
    let fname = format!("{name}.key.pem");
    let keybuf = fs::read(fname)?;
    let key = PKey::private_key_from_pem(&keybuf)?;

    let fname = format!("{name}.cert.pem");
    let certbuf = fs::read(fname)?;
    let cert = X509::from_pem(&certbuf)?;

    let ident = Self { key, cert };

    Ok(ident)
  }

  /// Load CA key/certificate pair.
  ///
  /// # Errors
  /// [`Error::OpenSSL`] means there was a problem serializing either the key
  /// or the certificate.  [`Error::IO`] means file I/O occurred.
  pub fn load_ca() -> Result<Self, Error> {
    Self::load_pem("ca")
  }

  /// Load CA key/certificate pair.
  ///
  /// # Errors
  /// [`Error::OpenSSL`] means there was a problem serializing either the key
  /// or the certificate.  [`Error::IO`] means file I/O occurred.
  pub fn save_pem(&self, name: &str, force: bool) -> Result<(), Error> {
    let fname = format!("{name}.key.pem");
    let keyfile = Path::new(&fname);
    if keyfile.exists() && !force {
      Err(io::Error::from(ErrorKind::AlreadyExists))?;
    }

    let fname = format!("{name}.cert.pem");
    let certfile = Path::new(&fname);
    if certfile.exists() && !force {
      Err(io::Error::from(ErrorKind::AlreadyExists))?;
    }

    let key_pem = self.key.private_key_to_pem_pkcs8()?;
    fs::write(keyfile, key_pem)?;

    let cert_pem = self.cert.to_pem()?;
    fs::write(certfile, cert_pem)?;

    Ok(())
  }
}


/// Create a server certificate.
///
/// `name` will be used as common name.  `names` is a list of domains/ips that
/// will be added as subject alt names.
///
/// # Errors
/// [`Error::OpenSSL`] means there was a problem generating the key or
/// certificate.
pub fn mk_server<I, T>(
  ca: &PkiIdent,
  name: &str,
  names: I
) -> Result<PkiIdent, Error>
where
  I: IntoIterator<Item = T>,
  T: AsRef<str>
{
  let privkey = create_key()?;

  let req = mk_request(&privkey, name)?;

  let mut cert_builder = X509::builder()?;
  cert_builder.set_version(2)?;
  let serial_number = {
    let mut serial = BigNum::new()?;
    serial.rand(159, MsbOption::MAYBE_ZERO, false)?;
    serial.to_asn1_integer()?
  };
  cert_builder.set_serial_number(&serial_number)?;
  cert_builder.set_subject_name(req.subject_name())?;
  cert_builder.set_issuer_name(ca.cert.subject_name())?;
  cert_builder.set_pubkey(&privkey)?;
  let not_before = Asn1Time::days_from_now(0)?;
  cert_builder.set_not_before(&not_before)?;
  let not_after = Asn1Time::days_from_now(365)?;
  cert_builder.set_not_after(&not_after)?;

  cert_builder.append_extension(BasicConstraints::new().build()?)?;

  cert_builder.append_extension(
    KeyUsage::new()
      .critical()
      .non_repudiation()
      .digital_signature()
      .key_encipherment()
      .build()?
  )?;

  let subject_key_identifier = SubjectKeyIdentifier::new()
    .build(&cert_builder.x509v3_context(Some(&ca.cert), None))?;
  cert_builder.append_extension(subject_key_identifier)?;

  let auth_key_identifier = AuthorityKeyIdentifier::new()
    .keyid(false)
    .issuer(false)
    .build(&cert_builder.x509v3_context(Some(&ca.cert), None))?;
  cert_builder.append_extension(auth_key_identifier)?;

  let mut subject_alt_name = SubjectAlternativeName::new();
  for nm in names {
    // Attempt to parse name as an ip address.  If successful, add IP entry.
    // Otherwise assume dns name.
    let nm = nm.as_ref();
    if nm.parse::<IpAddr>().is_ok() {
      subject_alt_name.ip(nm);
    } else {
      subject_alt_name.dns(nm);
    }
  }
  let subject_alt_name = subject_alt_name
    .build(&cert_builder.x509v3_context(Some(&ca.cert), None))?;
  cert_builder.append_extension(subject_alt_name)?;

  cert_builder.sign(&ca.key, MessageDigest::sha256())?;
  let cert = cert_builder.build();

  /*
  // Verify that this cert was issued by this ca
  match ca.cert.issued(&cert) {
    X509VerifyResult::OK => println!("Certificate verified!"),
    ver_err => println!("Failed to verify certificate: {}", ver_err)
  }
  */

  Ok(PkiIdent { key: privkey, cert })
}


/// Make a X509 request with the given private key
fn mk_request(
  privkey: &PKey<Private>,
  cn: &str
) -> Result<X509Req, ErrorStack> {
  let mut req_builder = X509ReqBuilder::new()?;
  req_builder.set_pubkey(privkey)?;

  let mut x509_name = X509NameBuilder::new()?;
  x509_name.append_entry_by_text("C", "US")?;
  x509_name.append_entry_by_text("ST", "TX")?;
  x509_name.append_entry_by_text("O", "La Cosa Nostra")?;
  x509_name.append_entry_by_text("CN", cn)?;
  let x509_name = x509_name.build();
  req_builder.set_subject_name(&x509_name)?;

  req_builder.sign(privkey, MessageDigest::sha256())?;
  let req = req_builder.build();
  Ok(req)
}

// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :