x509-cert 0.2.2

Pure Rust implementation of the X.509 Public Key Infrastructure Certificate format as described in RFC 5280
Documentation
#![cfg(all(feature = "builder", feature = "pem"))]

use der::{pem::LineEnding, Decode, Encode, EncodePem};
use p256::{ecdsa::DerSignature, pkcs8::DecodePrivateKey, NistP256};
use rsa::pkcs1::DecodeRsaPrivateKey;
use rsa::pkcs1v15::SigningKey;
use sha2::Sha256;
use spki::SubjectPublicKeyInfoOwned;
use std::{str::FromStr, time::Duration};
use x509_cert::{
    builder::{Builder, CertificateBuilder, Profile, RequestBuilder},
    ext::pkix::{name::GeneralName, SubjectAltName},
    name::Name,
    serial_number::SerialNumber,
    time::Validity,
};
use x509_cert_test_support::{openssl, zlint};

const RSA_2048_DER_EXAMPLE: &[u8] = include_bytes!("examples/rsa2048-pub.der");
const PKCS8_PUBLIC_KEY_DER: &[u8] = include_bytes!("examples/p256-pub.der");

#[test]
fn root_ca_certificate() {
    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()
        .to_der()
        .unwrap();
    let subject = Name::from_der(&subject).unwrap();
    let pub_key =
        SubjectPublicKeyInfoOwned::try_from(RSA_2048_DER_EXAMPLE).expect("get rsa pub key");

    let signer = rsa_signer();
    let builder =
        CertificateBuilder::new(profile, serial_number, validity, subject, pub_key, &signer)
            .expect("Create certificate");

    let certificate = builder.build().unwrap();

    let pem = certificate.to_pem(LineEnding::LF).expect("generate pem");
    println!("{}", openssl::check_certificate(pem.as_bytes()));

    let ignored = &[];
    zlint::check_certificate(pem.as_bytes(), ignored);
}

#[test]
fn root_ca_certificate_ecdsa() {
    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()
        .to_der()
        .unwrap();
    let subject = Name::from_der(&subject).unwrap();
    let pub_key =
        SubjectPublicKeyInfoOwned::try_from(PKCS8_PUBLIC_KEY_DER).expect("get ecdsa pub key");

    let signer = ecdsa_signer();
    let builder =
        CertificateBuilder::new(profile, serial_number, validity, subject, pub_key, &signer)
            .expect("Create certificate");

    let certificate = builder.build::<DerSignature>().unwrap();

    let pem = certificate.to_pem(LineEnding::LF).expect("generate pem");
    println!("{}", openssl::check_certificate(pem.as_bytes()));

    let ignored = &[];
    zlint::check_certificate(pem.as_bytes(), ignored);
}

#[test]
fn sub_ca_certificate() {
    let serial_number = SerialNumber::from(42u32);
    let validity = Validity::from_now(Duration::new(5, 0)).unwrap();

    let issuer =
        Name::from_str("CN=World domination corporation,O=World domination Inc,C=US").unwrap();
    let profile = Profile::SubCA {
        issuer,
        path_len_constraint: Some(0),
    };

    let subject =
        Name::from_str("CN=World domination task force,O=World domination Inc,C=US").unwrap();
    let pub_key =
        SubjectPublicKeyInfoOwned::try_from(RSA_2048_DER_EXAMPLE).expect("get rsa pub key");

    let signer = ecdsa_signer();
    let builder =
        CertificateBuilder::new(profile, serial_number, validity, subject, pub_key, &signer)
            .expect("Create certificate");

    let certificate = builder.build::<DerSignature>().unwrap();

    let pem = certificate.to_pem(LineEnding::LF).expect("generate pem");
    println!("{}", openssl::check_certificate(pem.as_bytes()));

    // TODO(baloo): not too sure we should tackle those in this API.
    let ignored = &[
        "w_sub_ca_aia_missing",
        "e_sub_ca_crl_distribution_points_missing",
        "e_sub_ca_certificate_policies_missing",
        "w_sub_ca_aia_does_not_contain_issuing_ca_url",
    ];

    zlint::check_certificate(pem.as_bytes(), ignored);
}

#[test]
fn leaf_certificate() {
    let serial_number = SerialNumber::from(42u32);
    let validity = Validity::from_now(Duration::new(5, 0)).unwrap();

    let issuer =
        Name::from_str("CN=World domination corporation,O=World domination Inc,C=US").unwrap();
    let profile = Profile::Leaf {
        issuer: issuer.clone(),
        enable_key_agreement: false,
        enable_key_encipherment: false,
        #[cfg(feature = "hazmat")]
        include_subject_key_identifier: true,
    };

    let subject = Name::from_str("CN=service.domination.world").unwrap();
    let pub_key =
        SubjectPublicKeyInfoOwned::try_from(RSA_2048_DER_EXAMPLE).expect("get rsa pub key");

    let signer = ecdsa_signer();
    let builder = CertificateBuilder::new(
        profile,
        serial_number.clone(),
        validity.clone(),
        subject.clone(),
        pub_key.clone(),
        &signer,
    )
    .expect("Create certificate");

    let certificate = builder.build::<DerSignature>().unwrap();

    let pem = certificate.to_pem(LineEnding::LF).expect("generate pem");
    println!("{}", openssl::check_certificate(pem.as_bytes()));

    // TODO(baloo): not too sure we should tackle those in this API.
    let ignored = vec![
        "e_sub_cert_aia_missing",
        "e_sub_cert_crl_distribution_points_missing",
        "w_sub_cert_aia_does_not_contain_issuing_ca_url",
        // Missing policies
        "e_sub_cert_certificate_policies_missing",
        "e_sub_cert_cert_policy_empty",
        // Needs to be added by the end-user
        "e_sub_cert_aia_does_not_contain_ocsp_url",
        // SAN needs to include DNS name (if used)
        "e_ext_san_missing",
        "e_subject_common_name_not_exactly_from_san",
        // Extended key usage needs to be added by end-user and is use-case dependent
        "e_sub_cert_eku_missing",
    ];

    zlint::check_certificate(pem.as_bytes(), &ignored);

    #[cfg(feature = "hazmat")]
    {
        let profile = Profile::Leaf {
            issuer,
            enable_key_agreement: false,
            enable_key_encipherment: false,
            include_subject_key_identifier: false,
        };
        let builder =
            CertificateBuilder::new(profile, serial_number, validity, subject, pub_key, &signer)
                .expect("Create certificate");

        let certificate = builder.build::<DerSignature>().unwrap();

        let pem = certificate.to_pem(LineEnding::LF).expect("generate pem");
        println!("{}", openssl::check_certificate(pem.as_bytes()));

        // Ignore warning about leaf not having SKI extension (this is a warning not a fail, as
        // denoted by the `w_` prefix.
        let mut ignored = ignored;
        ignored.push("w_ext_subject_key_identifier_missing_sub_cert");
        zlint::check_certificate(pem.as_bytes(), &ignored);
    }
}

#[test]
fn pss_certificate() {
    let serial_number = SerialNumber::from(42u32);
    let validity = Validity::from_now(Duration::new(5, 0)).unwrap();

    let issuer =
        Name::from_str("CN=World domination corporation,O=World domination Inc,C=US").unwrap();
    let profile = Profile::Leaf {
        issuer,
        enable_key_agreement: false,
        enable_key_encipherment: false,
        #[cfg(feature = "hazmat")]
        include_subject_key_identifier: true,
    };

    let subject = Name::from_str("CN=service.domination.world").unwrap();
    let pub_key =
        SubjectPublicKeyInfoOwned::try_from(RSA_2048_DER_EXAMPLE).expect("get rsa pub key");

    let signer = rsa_pss_signer();
    let builder =
        CertificateBuilder::new(profile, serial_number, validity, subject, pub_key, &signer)
            .expect("Create certificate");

    let certificate = builder
        .build_with_rng::<rsa::pss::Signature>(&mut rand::thread_rng())
        .unwrap();

    let pem = certificate.to_pem(LineEnding::LF).expect("generate pem");
    println!("{}", openssl::check_certificate(pem.as_bytes()));

    // TODO(baloo): not too sure we should tackle those in this API.
    let ignored = &[
        "e_sub_cert_aia_missing",
        "e_sub_cert_crl_distribution_points_missing",
        "w_sub_cert_aia_does_not_contain_issuing_ca_url",
        // Missing policies
        "e_sub_cert_certificate_policies_missing",
        "e_sub_cert_cert_policy_empty",
        // Needs to be added by the end-user
        "e_sub_cert_aia_does_not_contain_ocsp_url",
        // SAN needs to include DNS name (if used)
        "e_ext_san_missing",
        "e_subject_common_name_not_exactly_from_san",
        // Extended key usage needs to be added by end-user and is use-case dependent
        "e_sub_cert_eku_missing",
        // zlint warns on RSAPSS signature algorithms
        "e_signature_algorithm_not_supported",
    ];

    zlint::check_certificate(pem.as_bytes(), ignored);
}

const RSA_2048_PRIV_DER_EXAMPLE: &[u8] = include_bytes!("examples/rsa2048-priv.der");

fn rsa_signer() -> SigningKey<Sha256> {
    let private_key = rsa::RsaPrivateKey::from_pkcs1_der(RSA_2048_PRIV_DER_EXAMPLE).unwrap();
    let signing_key = SigningKey::<Sha256>::new(private_key);
    signing_key
}

fn rsa_pss_signer() -> rsa::pss::SigningKey<Sha256> {
    let private_key = rsa::RsaPrivateKey::from_pkcs1_der(RSA_2048_PRIV_DER_EXAMPLE).unwrap();
    let signing_key = rsa::pss::SigningKey::<Sha256>::new(private_key);
    signing_key
}

const PKCS8_PRIVATE_KEY_DER: &[u8] = include_bytes!("examples/p256-priv.der");

fn ecdsa_signer() -> ecdsa::SigningKey<NistP256> {
    let secret_key = p256::SecretKey::from_pkcs8_der(PKCS8_PRIVATE_KEY_DER).unwrap();
    ecdsa::SigningKey::from(secret_key)
}

#[test]
fn certificate_request() {
    use std::net::{IpAddr, Ipv4Addr};
    let subject = Name::from_str("CN=service.domination.world").unwrap();

    let signer = ecdsa_signer();
    let mut builder = RequestBuilder::new(subject, &signer).expect("Create certificate request");
    builder
        .add_extension(&SubjectAltName(vec![GeneralName::from(IpAddr::V4(
            Ipv4Addr::new(192, 0, 2, 0),
        ))]))
        .unwrap();

    let cert_req = builder.build::<DerSignature>().unwrap();
    let pem = cert_req.to_pem(LineEnding::LF).expect("generate pem");
    use std::fs::File;
    use std::io::Write;
    let mut file = File::create("/tmp/ecdsa.csr").expect("create pem file");
    file.write_all(pem.as_bytes()).expect("Create pem file");
    println!("{}", openssl::check_request(pem.as_bytes()));
}