#![cfg_attr(docsrs, feature(doc_cfg))]
#![doc = include_str!("../README.md")]
#![allow(renamed_and_removed_lints)] #![allow(unknown_lints)] #![warn(missing_docs)]
#![warn(noop_method_call)]
#![warn(unreachable_pub)]
#![warn(clippy::all)]
#![deny(clippy::await_holding_lock)]
#![deny(clippy::cargo_common_metadata)]
#![deny(clippy::cast_lossless)]
#![deny(clippy::checked_conversions)]
#![warn(clippy::cognitive_complexity)]
#![deny(clippy::debug_assert_with_mut_call)]
#![deny(clippy::exhaustive_enums)]
#![deny(clippy::exhaustive_structs)]
#![deny(clippy::expl_impl_clone_on_copy)]
#![deny(clippy::fallible_impl_from)]
#![deny(clippy::implicit_clone)]
#![deny(clippy::large_stack_arrays)]
#![warn(clippy::manual_ok_or)]
#![deny(clippy::missing_docs_in_private_items)]
#![warn(clippy::needless_borrow)]
#![warn(clippy::needless_pass_by_value)]
#![warn(clippy::option_option)]
#![deny(clippy::print_stderr)]
#![deny(clippy::print_stdout)]
#![warn(clippy::rc_buffer)]
#![deny(clippy::ref_option_ref)]
#![warn(clippy::semicolon_if_nothing_returned)]
#![warn(clippy::trait_duplication_in_bounds)]
#![deny(clippy::unchecked_time_subtraction)]
#![deny(clippy::unnecessary_wraps)]
#![warn(clippy::unseparated_literal_suffix)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::mod_module_files)]
#![allow(clippy::let_unit_value)] #![allow(clippy::uninlined_format_args)]
#![allow(clippy::significant_drop_in_scrutinee)] #![allow(clippy::result_large_err)] #![allow(clippy::needless_raw_string_hashes)] #![allow(clippy::needless_lifetimes)] #![allow(mismatched_lifetime_syntaxes)] #![allow(clippy::collapsible_if)] #![deny(clippy::unused_async)]
use std::{
sync::Arc,
time::{Duration, SystemTime},
};
use digest::Digest;
use rand::CryptoRng;
use rsa::pkcs8::{EncodePrivateKey as _, SubjectPublicKeyInfo};
use tor_error::into_internal;
use tor_llcrypto::{pk::rsa::KeyPair as RsaKeypair, util::rng::RngCompat};
use x509_cert::{
builder::{Builder, CertificateBuilder, Profile},
der::{DateTime, Encode, asn1::GeneralizedTime, zeroize::Zeroizing},
ext::pkix::{KeyUsage, KeyUsages},
serial_number::SerialNumber,
time::Validity,
};
const EXPECT_ID_BITS: usize = 1024;
const EXPECT_ID_EXPONENT: u32 = 65537;
const ID_CERT_LIFETIME_DAYS: u32 = 365;
pub fn create_legacy_rsa_id_cert<Rng: CryptoRng>(
rng: &mut Rng,
now: SystemTime,
hostname: &str,
keypair: &RsaKeypair,
) -> Result<Vec<u8>, X509CertError> {
use rsa::pkcs1v15::SigningKey;
use tor_llcrypto::d::Sha256;
let public = keypair.to_public_key();
if !public.exponent_is(EXPECT_ID_EXPONENT) {
return Err(X509CertError::InvalidSigningKey("Invalid exponent".into()));
}
if !public.bits() == EXPECT_ID_BITS {
return Err(X509CertError::InvalidSigningKey(
"Invalid key length".into(),
));
}
let self_signed_profile = Profile::Manual { issuer: None };
let serial_number = random_serial_number(rng)?;
let (validity, _) = cert_validity(now, ID_CERT_LIFETIME_DAYS)?;
let subject: x509_cert::name::Name = format!("CN={hostname}")
.parse()
.map_err(X509CertError::InvalidHostname)?;
let spki = SubjectPublicKeyInfo::from_key(keypair.to_public_key().as_key().clone())?;
let signer = SigningKey::<Sha256>::new(keypair.as_key().clone());
let mut builder = CertificateBuilder::new(
self_signed_profile,
serial_number,
validity,
subject,
spki,
&signer,
)?;
builder.add_extension(&KeyUsage(
KeyUsages::KeyCertSign | KeyUsages::DigitalSignature,
))?;
let cert = builder.build()?;
let mut output = Vec::new();
let _ignore_length: x509_cert::der::Length = cert
.encode_to_vec(&mut output)
.map_err(X509CertError::CouldNotEncode)?;
Ok(output)
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct TlsKeyAndCert {
certificates: Vec<Vec<u8>>,
private_key: rsa::RsaPrivateKey,
sha256_digest: [u8; 32],
expiration: SystemTime,
}
const TLS_CERT_LIFETIME_DAYS: u32 = 30;
impl TlsKeyAndCert {
pub fn certificates_der(&self) -> Vec<&[u8]> {
self.certificates.iter().map(|der| der.as_ref()).collect()
}
pub fn certificate_pem(&self) -> String {
let config = pem::EncodeConfig::new().set_line_ending(pem::LineEnding::LF);
self.certificates
.iter()
.map(|der| pem::encode_config(&pem::Pem::new("CERTIFICATE", &der[..]), config))
.collect()
}
pub fn private_key_pkcs8_der(&self) -> Result<Zeroizing<Vec<u8>>, X509CertError> {
Ok(self
.private_key
.to_pkcs8_der()
.map_err(X509CertError::CouldNotFormatPkcs8)?
.to_bytes())
}
pub fn private_key_pkcs8_pem(&self) -> Result<Zeroizing<String>, X509CertError> {
self.private_key
.to_pkcs8_pem(p256::pkcs8::LineEnding::LF)
.map_err(X509CertError::CouldNotFormatPkcs8)
}
pub fn expiration(&self) -> SystemTime {
self.expiration
}
pub fn link_cert_sha256(&self) -> &[u8; 32] {
&self.sha256_digest
}
pub fn create<Rng: CryptoRng>(
rng: &mut Rng,
now: SystemTime,
issuer_hostname: &str,
subject_hostname: &str,
) -> Result<Self, X509CertError> {
const RSA_KEY_BITS: usize = 2048;
let private_key = rsa::RsaPrivateKey::new(&mut RngCompat::new(&mut *rng), RSA_KEY_BITS)
.map_err(into_internal!("Unable to generate RSA key"))?;
let public_key = private_key.to_public_key();
let issuer_private_key = p256::ecdsa::SigningKey::random(&mut RngCompat::new(&mut *rng));
let issuer = format!("CN={issuer_hostname}")
.parse()
.map_err(X509CertError::InvalidHostname)?;
let subject: x509_cert::name::Name = format!("CN={subject_hostname}")
.parse()
.map_err(X509CertError::InvalidHostname)?;
let self_signed_profile = Profile::Leaf {
issuer,
enable_key_agreement: true,
enable_key_encipherment: true,
include_subject_key_identifier: true,
};
let serial_number = random_serial_number(rng)?;
let (validity, expiration) = cert_validity(now, TLS_CERT_LIFETIME_DAYS)?;
let spki = SubjectPublicKeyInfo::from_key(public_key)?;
let builder = CertificateBuilder::new(
self_signed_profile,
serial_number,
validity,
subject,
spki,
&issuer_private_key,
)?;
let cert = builder.build::<ecdsa::der::Signature<_>>()?;
let mut certificate_der = Vec::new();
let _ignore_length: x509_cert::der::Length = cert
.encode_to_vec(&mut certificate_der)
.map_err(X509CertError::CouldNotEncode)?;
let sha256_digest = tor_llcrypto::d::Sha256::digest(&certificate_der).into();
let certificates = vec![certificate_der];
Ok(TlsKeyAndCert {
certificates,
private_key,
sha256_digest,
expiration,
})
}
}
fn cert_validity(
now: SystemTime,
lifetime_days: u32,
) -> Result<(Validity, SystemTime), X509CertError> {
const ONE_DAY: Duration = Duration::new(86400, 0);
let start_of_day_containing = |when| -> Result<_, X509CertError> {
let dt = DateTime::from_system_time(when)
.map_err(into_internal!("Couldn't represent time as a DER DateTime"))?;
let dt = DateTime::new(dt.year(), dt.month(), dt.day(), 0, 0, 0)
.map_err(into_internal!("Couldn't construct DER DateTime"))?;
Ok(x509_cert::time::Time::GeneralTime(
GeneralizedTime::from_date_time(dt),
))
};
let start_on_day = now - ONE_DAY;
let end_on_day = start_on_day + ONE_DAY * lifetime_days;
let validity = Validity {
not_before: start_of_day_containing(start_on_day)?,
not_after: start_of_day_containing(end_on_day)?,
};
let expiration = validity.not_after.into();
Ok((validity, expiration))
}
fn random_serial_number<Rng: CryptoRng>(rng: &mut Rng) -> Result<SerialNumber, X509CertError> {
const SER_NUMBER_LEN: usize = 16;
let mut buf = [0; SER_NUMBER_LEN];
rng.fill_bytes(&mut buf[..]);
Ok(SerialNumber::new(&buf[..]).map_err(into_internal!("Couldn't construct serial number!"))?)
}
#[derive(Clone, Debug, thiserror::Error)]
#[non_exhaustive]
pub enum X509CertError {
#[error("Provided signing key not valid: {0}")]
InvalidSigningKey(String),
#[error("Couldn't use provided key as a subject")]
SubjectKeyError(#[from] x509_cert::spki::Error),
#[error("Unable to set hostname when creating certificate")]
InvalidHostname(#[source] x509_cert::der::Error),
#[error("Unable to build certificate")]
CouldNotBuild(#[source] Arc<x509_cert::builder::Error>),
#[error("Unable to encode certificate")]
CouldNotEncode(#[source] x509_cert::der::Error),
#[error("Unable to format key as PKCS8")]
CouldNotFormatPkcs8(#[source] p256::pkcs8::Error),
#[error("Internal error while creating certificate")]
Bug(#[from] tor_error::Bug),
}
impl From<x509_cert::builder::Error> for X509CertError {
fn from(value: x509_cert::builder::Error) -> Self {
X509CertError::CouldNotBuild(Arc::new(value))
}
}
#[cfg(test)]
mod test {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_time_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
use super::*;
use tor_basic_utils::test_rng::testing_rng;
use web_time_compat::SystemTimeExt;
#[test]
fn identity_cert_generation() {
let mut rng = testing_rng();
let keypair = RsaKeypair::generate(&mut rng).unwrap();
let cert = create_legacy_rsa_id_cert(
&mut rng,
SystemTime::get(),
"www.house-of-pancakes.example.com",
&keypair,
)
.unwrap();
let key_extracted = tor_llcrypto::util::x509_extract_rsa_subject_kludge(&cert[..]).unwrap();
assert_eq!(key_extracted, keypair.to_public_key());
}
#[test]
fn tls_cert_info() {
let mut rng = testing_rng();
let certified = TlsKeyAndCert::create(
&mut rng,
SystemTime::get(),
"foo.example.com",
"bar.example.com",
)
.unwrap();
dbg!(certified);
}
}