rustls/rustls/examples/internal/test_ca.rs

322 lines
12 KiB
Rust

use std::collections::HashMap;
use std::env;
use std::fs::{self, File};
use std::io::Write;
use std::net::IpAddr;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use rcgen::{
BasicConstraints, CertificateParams, CertificateRevocationListParams, CertifiedKey,
DistinguishedName, DnType, ExtendedKeyUsagePurpose, Ia5String, IsCa, KeyIdMethod, KeyPair,
KeyUsagePurpose, RevocationReason, RevokedCertParams, RsaKeySize, SanType, SerialNumber,
SignatureAlgorithm, PKCS_ECDSA_P256_SHA256, PKCS_ECDSA_P384_SHA384, PKCS_ECDSA_P521_SHA512,
PKCS_ED25519, PKCS_RSA_SHA256, PKCS_RSA_SHA384, PKCS_RSA_SHA512,
};
use time::OffsetDateTime;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut certified_keys = HashMap::with_capacity(ROLES.len() * SIG_ALGS.len());
for role in ROLES {
for alg in SIG_ALGS {
// Generate a key pair and serialize it to a PEM encoded file.
let key_pair = alg.key_pair();
let mut key_pair_file = File::create(role.key_file_path(alg))?;
key_pair_file.write_all(key_pair.serialize_pem().as_bytes())?;
// Issue a certificate for the key pair. For trust anchors, this will be self-signed.
// Otherwise we dig out the issuer and issuer_key for the issuer, which should have
// been produced in earlier iterations based on the careful ordering of roles.
let cert = match role {
Role::TrustAnchor => role
.params(alg)
.self_signed(&key_pair)?,
Role::Intermediate => {
let issuer: &CertifiedKey = certified_keys
.get(&(Role::TrustAnchor, alg.inner))
.unwrap();
role.params(alg)
.signed_by(&key_pair, &issuer.cert, &issuer.key_pair)?
}
Role::EndEntity | Role::Client => {
let issuer = certified_keys
.get(&(Role::Intermediate, alg.inner))
.unwrap();
role.params(alg)
.signed_by(&key_pair, &issuer.cert, &issuer.key_pair)?
}
};
// Serialize the issued certificate to a PEM encoded file.
let mut cert_file = File::create(role.cert_pem_file_path(alg))?;
cert_file.write_all(cert.pem().as_bytes())?;
// And to a DER encoded file.
let mut cert_file = File::create(role.cert_der_file_path(alg))?;
cert_file.write_all(cert.der())?;
// If we're not a trust anchor, generate a CRL for the certificate we just issued.
if role != Role::TrustAnchor {
// The CRL will be signed by the issuer of the certificate being revoked. For
// intermediates this will be the trust anchor, and for client/EE certs this will
// be the intermediate.
let issuer = match role {
Role::Intermediate => certified_keys
.get(&(Role::TrustAnchor, alg.inner))
.unwrap(),
Role::EndEntity | Role::Client => certified_keys
.get(&(Role::Intermediate, alg.inner))
.unwrap(),
_ => panic!("unexpected role for CRL generation: {role:?}"),
};
let crl = crl_for_serial(
cert.params()
.serial_number
.clone()
.unwrap(),
)
.signed_by(&issuer.cert, &issuer.key_pair)?;
let mut crl_file = File::create(
alg.output_directory()
.join(format!("{}.revoked.crl.pem", role.label())),
)?;
crl_file.write_all(crl.pem().unwrap().as_bytes())?;
}
// When we're issuing end entity or client certs we have a bit of extra work to do
// now that we have full chains in hand.
if matches!(role, Role::EndEntity | Role::Client) {
let root = &certified_keys
.get(&(Role::TrustAnchor, alg.inner))
.unwrap()
.cert;
let intermediate = &certified_keys
.get(&(Role::Intermediate, alg.inner))
.unwrap()
.cert;
// Write the PEM chain and full chain files for the end entity and client certs.
// Chain files include the intermediate and root certs, while full chain files include
// the end entity or client cert as well.
for f in [
("chain", &[intermediate, root][..]),
("fullchain", &[&cert, intermediate, root][..]),
] {
let mut chain_file = File::create(alg.output_directory().join(format!(
"{}.{}",
role.label(),
f.0
)))?;
for cert in f.1 {
chain_file.write_all(cert.pem().as_bytes())?;
}
}
}
certified_keys.insert((role, alg.inner), CertifiedKey { cert, key_pair });
}
}
Ok(())
}
fn crl_for_serial(serial_number: SerialNumber) -> CertificateRevocationListParams {
let now = OffsetDateTime::now_utc();
CertificateRevocationListParams {
this_update: now,
next_update: now + Duration::from_secs(60 * 60 * 24 * 5),
crl_number: SerialNumber::from(1234),
issuing_distribution_point: None,
revoked_certs: vec![RevokedCertParams {
serial_number,
revocation_time: now,
reason_code: Some(RevocationReason::KeyCompromise),
invalidity_date: None,
}],
key_identifier_method: KeyIdMethod::Sha256,
}
}
// Note: these are ordered such that the data dependencies for issuance are satisfied.
const ROLES: [Role; 4] = [
Role::TrustAnchor,
Role::Intermediate,
Role::EndEntity,
Role::Client,
];
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
enum Role {
Client,
EndEntity,
Intermediate,
TrustAnchor,
}
impl Role {
fn params(&self, alg: &'static SigAlgContext) -> CertificateParams {
let mut params = CertificateParams::default();
params.distinguished_name = self.common_name(alg);
params.use_authority_key_identifier_extension = true;
let serial = SERIAL_NUMBER.fetch_add(1, Ordering::SeqCst);
params.serial_number = Some(SerialNumber::from_slice(&serial.to_be_bytes()[..]));
match self {
Self::TrustAnchor | Self::Intermediate => {
params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
params.key_usages = ISSUER_KEY_USAGES.to_vec();
params.extended_key_usages = ISSUER_EXTENDED_KEY_USAGES.to_vec();
}
Self::EndEntity | Self::Client => {
params.is_ca = IsCa::NoCa;
params.key_usages = EE_KEY_USAGES.to_vec();
params.subject_alt_names = vec![
SanType::DnsName(Ia5String::try_from("testserver.com".to_string()).unwrap()),
SanType::DnsName(
Ia5String::try_from("second.testserver.com".to_string()).unwrap(),
),
SanType::DnsName(Ia5String::try_from("localhost".to_string()).unwrap()),
SanType::IpAddress(IpAddr::from_str("198.51.100.1").unwrap()),
SanType::IpAddress(IpAddr::from_str("2001:db8::1").unwrap()),
];
}
}
// Client certificates additionally get the client auth EKU.
if *self == Self::Client {
params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth];
}
params
}
fn common_name(&self, alg: &'static SigAlgContext) -> DistinguishedName {
let mut distinguished_name = DistinguishedName::new();
distinguished_name.push(
DnType::CommonName,
match self {
Self::Client => "ponytown client".to_owned(),
Self::EndEntity => "testserver.com".to_owned(),
Self::Intermediate => {
format!("ponytown {} level 2 intermediate", alg.issuer_cn)
}
Self::TrustAnchor => format!("ponytown {} CA", alg.issuer_cn),
},
);
distinguished_name
}
fn key_file_path(&self, alg: &'static SigAlgContext) -> PathBuf {
alg.output_directory()
.join(format!("{}.key", self.label()))
}
fn cert_pem_file_path(&self, alg: &'static SigAlgContext) -> PathBuf {
alg.output_directory()
.join(format!("{}.cert", self.label()))
}
fn cert_der_file_path(&self, alg: &'static SigAlgContext) -> PathBuf {
alg.output_directory()
.join(format!("{}.der", self.label()))
}
fn label(&self) -> &'static str {
match self {
Self::Client => "client",
Self::EndEntity => "end",
Self::Intermediate => "inter",
Self::TrustAnchor => "ca",
}
}
}
// Note: for convenience we use the RSA sigalg digest algorithm to inform the RSA modulus
// size, mapping SHA256 to RSA 2048, SHA384 to RSA 3072, and SHA512 to RSA 4096.
static SIG_ALGS: &[SigAlgContext] = &[
SigAlgContext {
inner: &PKCS_RSA_SHA256,
issuer_cn: "RSA 2048",
},
SigAlgContext {
inner: &PKCS_RSA_SHA384,
issuer_cn: "RSA 3072",
},
SigAlgContext {
inner: &PKCS_RSA_SHA512,
issuer_cn: "RSA 4096",
},
SigAlgContext {
inner: &PKCS_ECDSA_P256_SHA256,
issuer_cn: "ECDSA p256",
},
SigAlgContext {
inner: &PKCS_ECDSA_P384_SHA384,
issuer_cn: "ECDSA p384",
},
SigAlgContext {
inner: &PKCS_ECDSA_P521_SHA512,
issuer_cn: "ECDSA p521",
},
SigAlgContext {
inner: &PKCS_ED25519,
issuer_cn: "EdDSA",
},
];
struct SigAlgContext {
pub(crate) inner: &'static SignatureAlgorithm,
pub(crate) issuer_cn: &'static str,
}
impl SigAlgContext {
fn output_directory(&self) -> PathBuf {
let output_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap())
.join("../")
.join("test-ca")
.join(
self.issuer_cn
.to_lowercase()
.replace(' ', "-"),
);
fs::create_dir_all(&output_dir).unwrap();
output_dir
}
fn key_pair(&self) -> KeyPair {
if *self.inner == PKCS_RSA_SHA256 {
KeyPair::generate_rsa_for(&PKCS_RSA_SHA256, RsaKeySize::_2048)
} else if *self.inner == PKCS_RSA_SHA384 {
KeyPair::generate_rsa_for(&PKCS_RSA_SHA384, RsaKeySize::_3072)
} else if *self.inner == PKCS_RSA_SHA512 {
KeyPair::generate_rsa_for(&PKCS_RSA_SHA512, RsaKeySize::_4096)
} else {
KeyPair::generate_for(self.inner)
}
.unwrap()
}
}
const ISSUER_KEY_USAGES: &[KeyUsagePurpose; 7] = &[
KeyUsagePurpose::CrlSign,
KeyUsagePurpose::KeyCertSign,
KeyUsagePurpose::DigitalSignature,
KeyUsagePurpose::ContentCommitment,
KeyUsagePurpose::KeyEncipherment,
KeyUsagePurpose::DataEncipherment,
KeyUsagePurpose::KeyAgreement,
];
const ISSUER_EXTENDED_KEY_USAGES: &[ExtendedKeyUsagePurpose; 2] = &[
ExtendedKeyUsagePurpose::ServerAuth,
ExtendedKeyUsagePurpose::ClientAuth,
];
const EE_KEY_USAGES: &[KeyUsagePurpose; 2] = &[
KeyUsagePurpose::DigitalSignature,
KeyUsagePurpose::ContentCommitment,
];
static SERIAL_NUMBER: AtomicU64 = AtomicU64::new(1);