Skip to main content

tor_cert_x509/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![doc = include_str!("../README.md")]
3// @@ begin lint list maintained by maint/add_warning @@
4#![allow(renamed_and_removed_lints)] // @@REMOVE_WHEN(ci_arti_stable)
5#![allow(unknown_lints)] // @@REMOVE_WHEN(ci_arti_nightly)
6#![warn(missing_docs)]
7#![warn(noop_method_call)]
8#![warn(unreachable_pub)]
9#![warn(clippy::all)]
10#![deny(clippy::await_holding_lock)]
11#![deny(clippy::cargo_common_metadata)]
12#![deny(clippy::cast_lossless)]
13#![deny(clippy::checked_conversions)]
14#![warn(clippy::cognitive_complexity)]
15#![deny(clippy::debug_assert_with_mut_call)]
16#![deny(clippy::exhaustive_enums)]
17#![deny(clippy::exhaustive_structs)]
18#![deny(clippy::expl_impl_clone_on_copy)]
19#![deny(clippy::fallible_impl_from)]
20#![deny(clippy::implicit_clone)]
21#![deny(clippy::large_stack_arrays)]
22#![warn(clippy::manual_ok_or)]
23#![deny(clippy::missing_docs_in_private_items)]
24#![warn(clippy::needless_borrow)]
25#![warn(clippy::needless_pass_by_value)]
26#![warn(clippy::option_option)]
27#![deny(clippy::print_stderr)]
28#![deny(clippy::print_stdout)]
29#![warn(clippy::rc_buffer)]
30#![deny(clippy::ref_option_ref)]
31#![warn(clippy::semicolon_if_nothing_returned)]
32#![warn(clippy::trait_duplication_in_bounds)]
33#![deny(clippy::unchecked_time_subtraction)]
34#![deny(clippy::unnecessary_wraps)]
35#![warn(clippy::unseparated_literal_suffix)]
36#![deny(clippy::unwrap_used)]
37#![deny(clippy::mod_module_files)]
38#![allow(clippy::let_unit_value)] // This can reasonably be done for explicitness
39#![allow(clippy::uninlined_format_args)]
40#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945
41#![allow(clippy::result_large_err)] // temporary workaround for arti#587
42#![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best
43#![allow(clippy::needless_lifetimes)] // See arti#1765
44#![allow(mismatched_lifetime_syntaxes)] // temporary workaround for arti#2060
45#![allow(clippy::collapsible_if)] // See arti#2342
46#![deny(clippy::unused_async)]
47#![deny(clippy::string_slice)] // See arti#2571
48//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
49
50// This module uses the `x509-cert` crate to generate certificates;
51// if we decide to switch, `rcgen` and `x509-certificate`
52// seem like the likeliest options.
53
54use std::{
55    sync::Arc,
56    time::{Duration, SystemTime},
57};
58
59use digest::Digest;
60use rand::CryptoRng;
61use rsa::pkcs8::{EncodePrivateKey as _, SubjectPublicKeyInfo};
62use tor_error::into_internal;
63use tor_llcrypto::{pk::rsa::KeyPair as RsaKeypair, util::rng::RngCompat};
64use x509_cert::{
65    builder::{Builder, CertificateBuilder, Profile},
66    der::{DateTime, Encode, asn1::GeneralizedTime, zeroize::Zeroizing},
67    ext::pkix::{KeyUsage, KeyUsages},
68    serial_number::SerialNumber,
69    time::Validity,
70};
71
72/// Legacy identity keys are required to have this length.
73const EXPECT_ID_BITS: usize = 1024;
74/// Legacy identity keys are required to have this exponent.
75const EXPECT_ID_EXPONENT: u32 = 65537;
76/// Lifetime of generated id certs, in days.
77const ID_CERT_LIFETIME_DAYS: u32 = 365;
78
79/// Create an X.509 certificate, for use in a CERTS cell,
80/// self-certifying the provided RSA identity key.
81///
82/// The resulting certificate will be encoded in DER.
83/// Its cert_type field should be 02 when it is sent in a CERTS cell.
84///
85/// The resulting certificate is quite minimal, and has no unnecessary extensions.
86///
87/// Returns an error on failure, or if `keypair` is not a 1024-bit RSA key
88/// with exponent of 65537.
89pub fn create_legacy_rsa_id_cert<Rng: CryptoRng>(
90    rng: &mut Rng,
91    now: SystemTime,
92    hostname: &str,
93    keypair: &RsaKeypair,
94) -> Result<Vec<u8>, X509CertError> {
95    use rsa::pkcs1v15::SigningKey;
96    use tor_llcrypto::d::Sha256;
97    let public = keypair.to_public_key();
98    if !public.exponent_is(EXPECT_ID_EXPONENT) {
99        return Err(X509CertError::InvalidSigningKey("Invalid exponent".into()));
100    }
101    if !public.bits() == EXPECT_ID_BITS {
102        return Err(X509CertError::InvalidSigningKey(
103            "Invalid key length".into(),
104        ));
105    }
106
107    let self_signed_profile = Profile::Manual { issuer: None };
108    let serial_number = random_serial_number(rng)?;
109    let (validity, _) = cert_validity(now, ID_CERT_LIFETIME_DAYS)?;
110    // NOTE: This is how C Tor builds its DNs, but that doesn't mean it's a good idea.
111    let subject: x509_cert::name::Name = format!("CN={hostname}")
112        .parse()
113        .map_err(X509CertError::InvalidHostname)?;
114    let spki = SubjectPublicKeyInfo::from_key(keypair.to_public_key().as_key().clone())?;
115
116    let signer = SigningKey::<Sha256>::new(keypair.as_key().clone());
117
118    let mut builder = CertificateBuilder::new(
119        self_signed_profile,
120        serial_number,
121        validity,
122        subject,
123        spki,
124        &signer,
125    )?;
126
127    // We do not, strictly speaking, need this extension: Tor doesn't care that it's there.
128    // We do, however, need _some_ extension, or else we'll generate a v1 certificate,
129    // which we don't want to do.
130    builder.add_extension(&KeyUsage(
131        KeyUsages::KeyCertSign | KeyUsages::DigitalSignature,
132    ))?;
133
134    let cert = builder.build()?;
135
136    let mut output = Vec::new();
137    let _ignore_length: x509_cert::der::Length = cert
138        .encode_to_vec(&mut output)
139        .map_err(X509CertError::CouldNotEncode)?;
140    Ok(output)
141}
142
143/// A set of x.509 certificate information and keys for use with a TLS library.
144///
145/// Only relays need this: They should set these as the certificate(s) to be used
146/// for incoming TLS connections.
147///
148/// This is not necessarily the most convenient form to manipulate certificates in:
149/// rather, it is intended to provide the formats that TLS libraries generally
150/// expect to get.
151#[derive(Clone, Debug)]
152#[non_exhaustive]
153pub struct TlsKeyAndCert {
154    /// A list of certificates in DER form.
155    ///
156    /// (This may contain more than one certificate, but for now only one certificate is used.)
157    certificates: Vec<Vec<u8>>,
158
159    /// A private key for use in the TLS handshake.
160    //
161    // Disabled:
162    // private_key: ecdsa::SigningKey<p256::NistP256>,
163    private_key: rsa::RsaPrivateKey,
164
165    /// A SHA256 digest of the link certificate
166    /// (the one certifying the private key's public component).
167    ///
168    /// This digest is the one what will be certified by the relay's
169    /// `SIGNING_V_TLS_CERT`
170    /// certificate.
171    sha256_digest: [u8; 32],
172
173    /// A time after which this set of link information won't be valid,
174    /// and another should be generated.
175    expiration: SystemTime,
176}
177
178/// What lifetime do we pick for a TLS certificate, in days?
179const TLS_CERT_LIFETIME_DAYS: u32 = 30;
180
181impl TlsKeyAndCert {
182    /// Return the certificates as a list of DER-encoded values.
183    pub fn certificates_der(&self) -> Vec<&[u8]> {
184        self.certificates.iter().map(|der| der.as_ref()).collect()
185    }
186    /// Return the certificates as a concatenated list in PEM ("BEGIN CERTIFICATE") format.
187    pub fn certificate_pem(&self) -> String {
188        let config = pem::EncodeConfig::new().set_line_ending(pem::LineEnding::LF);
189        self.certificates
190            .iter()
191            .map(|der| pem::encode_config(&pem::Pem::new("CERTIFICATE", &der[..]), config))
192            .collect()
193    }
194    /// Return the private key in (unencrypted) PKCS8 DER format.
195    pub fn private_key_pkcs8_der(&self) -> Result<Zeroizing<Vec<u8>>, X509CertError> {
196        Ok(self
197            .private_key
198            .to_pkcs8_der()
199            .map_err(X509CertError::CouldNotFormatPkcs8)?
200            .to_bytes())
201    }
202    /// Return the private key in (unencrypted) PKCS8 PEM ("BEGIN PRIVATE KEY") format.
203    pub fn private_key_pkcs8_pem(&self) -> Result<Zeroizing<String>, X509CertError> {
204        self.private_key
205            .to_pkcs8_pem(p256::pkcs8::LineEnding::LF)
206            .map_err(X509CertError::CouldNotFormatPkcs8)
207    }
208    /// Return the earliest time at which any of these certificates will expire.
209    pub fn expiration(&self) -> SystemTime {
210        self.expiration
211    }
212
213    /// Return the SHA256 digest of the link certificate
214    ///
215    /// This digest is the one certified with the relay's
216    /// `SIGNING_V_TLS_CERT`
217    /// certificate.
218    pub fn link_cert_sha256(&self) -> &[u8; 32] {
219        &self.sha256_digest
220    }
221
222    /// Create a new TLS link key and associated certificate(s).
223    ///
224    /// The certificate will be valid at `now`, and for a while after.
225    ///
226    /// The certificate parameters and keys are chosen for reasonable security,
227    /// approximate conformance to RFC5280, and limited fingerprinting resistance.
228    ///
229    /// Note: The fingerprinting resistance is quite limited.
230    /// We will likely want to pursue these avenues for better fingerprinting resistance:
231    ///
232    /// - Encourage more use of TLS 1.3, where server certificates are encrypted.
233    ///   (This prevents passive fingerprinting only.)
234    /// - Adjust this function to make certificates look even more normal
235    /// - Integrate with ACME-supporting certificate issuers (Letsencrypt, etc)
236    ///   to get real certificates for Tor relays.
237    pub fn create<Rng: CryptoRng>(
238        rng: &mut Rng,
239        now: SystemTime,
240        issuer_hostname: &str,
241        subject_hostname: &str,
242    ) -> Result<Self, X509CertError> {
243        // We would prefer to use p256 here, since it is the most commonly used elliptic curve
244        // group for X.509 web certificate signing, as of this writing.
245        //
246        // We want to use an elliptic curve here for its higher security/performance ratio than RSA,
247        // and for its _much_ faster key generation time.
248        //
249        // But unfortunately, we can't: C tor has a bug where if the subject key is not RSA,
250        // the connection will be closed with an error:
251        // <https://gitlab.torproject.org/tpo/core/tor/-/issues/41226>.
252        // If this bug is fixed, then we will have to wait until all clients and servers upgrade
253        // before we can send p256 subject keys instead.
254        //
255        // DISABLED:
256        // let private_key = rsa::RsaPrivateKey::p256::ecdsa::SigningKey::random(&mut RngCompat::new(&mut *rng));
257        // let public_key = p256::ecdsa::VerifyingKey::from(&private_key);
258
259        const RSA_KEY_BITS: usize = 2048;
260        let private_key = rsa::RsaPrivateKey::new(&mut RngCompat::new(&mut *rng), RSA_KEY_BITS)
261            .map_err(into_internal!("Unable to generate RSA key"))?;
262        let public_key = private_key.to_public_key();
263
264        // Note that we'll discard this key after signing the certificate with it:
265        // The real certification for private_key is done in the SIGNING_V_TLS_CERT
266        // certificate.
267        let issuer_private_key = p256::ecdsa::SigningKey::random(&mut RngCompat::new(&mut *rng));
268
269        // NOTE: This is how C Tor builds its DNs, but that doesn't mean it's a good idea.
270        let issuer = format!("CN={issuer_hostname}")
271            .parse()
272            .map_err(X509CertError::InvalidHostname)?;
273        let subject: x509_cert::name::Name = format!("CN={subject_hostname}")
274            .parse()
275            .map_err(X509CertError::InvalidHostname)?;
276
277        let self_signed_profile = Profile::Leaf {
278            issuer,
279            enable_key_agreement: true,
280            enable_key_encipherment: true,
281            include_subject_key_identifier: true,
282        };
283        let serial_number = random_serial_number(rng)?;
284        let (validity, expiration) = cert_validity(now, TLS_CERT_LIFETIME_DAYS)?;
285        let spki = SubjectPublicKeyInfo::from_key(public_key)?;
286
287        let builder = CertificateBuilder::new(
288            self_signed_profile,
289            serial_number,
290            validity,
291            subject,
292            spki,
293            &issuer_private_key,
294        )?;
295
296        let cert = builder.build::<ecdsa::der::Signature<_>>()?;
297
298        let mut certificate_der = Vec::new();
299        let _ignore_length: x509_cert::der::Length = cert
300            .encode_to_vec(&mut certificate_der)
301            .map_err(X509CertError::CouldNotEncode)?;
302
303        let sha256_digest = tor_llcrypto::d::Sha256::digest(&certificate_der).into();
304        let certificates = vec![certificate_der];
305
306        Ok(TlsKeyAndCert {
307            certificates,
308            private_key,
309            sha256_digest,
310            expiration,
311        })
312    }
313}
314
315/// Return a Validity that includes `now`, and lasts for `lifetime_days` additionally.
316///
317/// Additionally, return the time at which the certificate expires.
318///
319/// We ensure that our cert is valid at least a day into the past.
320///
321/// We obfuscate our current time a little by rounding to the nearest midnight UTC.
322fn cert_validity(
323    now: SystemTime,
324    lifetime_days: u32,
325) -> Result<(Validity, SystemTime), X509CertError> {
326    const ONE_DAY: Duration = Duration::new(86400, 0);
327
328    let start_of_day_containing = |when| -> Result<_, X509CertError> {
329        let dt = DateTime::from_system_time(when)
330            .map_err(into_internal!("Couldn't represent time as a DER DateTime"))?;
331        let dt = DateTime::new(dt.year(), dt.month(), dt.day(), 0, 0, 0)
332            .map_err(into_internal!("Couldn't construct DER DateTime"))?;
333        Ok(x509_cert::time::Time::GeneralTime(
334            GeneralizedTime::from_date_time(dt),
335        ))
336    };
337
338    let start_on_day = now - ONE_DAY;
339    let end_on_day = start_on_day + ONE_DAY * lifetime_days;
340
341    let validity = Validity {
342        not_before: start_of_day_containing(start_on_day)?,
343        not_after: start_of_day_containing(end_on_day)?,
344    };
345    let expiration = validity.not_after.into();
346    Ok((validity, expiration))
347}
348
349/// Return a random serial number for use in a new certificate.
350fn random_serial_number<Rng: CryptoRng>(rng: &mut Rng) -> Result<SerialNumber, X509CertError> {
351    const SER_NUMBER_LEN: usize = 16;
352    let mut buf = [0; SER_NUMBER_LEN];
353    rng.fill_bytes(&mut buf[..]);
354    Ok(SerialNumber::new(&buf[..]).map_err(into_internal!("Couldn't construct serial number!"))?)
355}
356
357/// An error that has occurred while trying to create a certificate.
358#[derive(Clone, Debug, thiserror::Error)]
359#[non_exhaustive]
360pub enum X509CertError {
361    /// We received a signing key that we can't use.
362    #[error("Provided signing key not valid: {0}")]
363    InvalidSigningKey(String),
364
365    /// We received a subject key that we can't use.
366    #[error("Couldn't use provided key as a subject")]
367    SubjectKeyError(#[from] x509_cert::spki::Error),
368
369    /// We received a hostname that we couldn't use:
370    /// probably, it contained an equals sign or a comma.
371    #[error("Unable to set hostname when creating certificate")]
372    InvalidHostname(#[source] x509_cert::der::Error),
373
374    /// We couldn't construct the certificate.
375    #[error("Unable to build certificate")]
376    CouldNotBuild(#[source] Arc<x509_cert::builder::Error>),
377
378    /// We constructed the certificate, but couldn't encode it as DER.
379    #[error("Unable to encode certificate")]
380    CouldNotEncode(#[source] x509_cert::der::Error),
381
382    /// We constructed a key but couldn't format it as PKCS8.
383    #[error("Unable to format key as PKCS8")]
384    CouldNotFormatPkcs8(#[source] p256::pkcs8::Error),
385
386    /// We've encountered some kind of a bug.
387    #[error("Internal error while creating certificate")]
388    Bug(#[from] tor_error::Bug),
389}
390
391impl From<x509_cert::builder::Error> for X509CertError {
392    fn from(value: x509_cert::builder::Error) -> Self {
393        X509CertError::CouldNotBuild(Arc::new(value))
394    }
395}
396
397#[cfg(test)]
398mod test {
399    // @@ begin test lint list maintained by maint/add_warning @@
400    #![allow(clippy::bool_assert_comparison)]
401    #![allow(clippy::clone_on_copy)]
402    #![allow(clippy::dbg_macro)]
403    #![allow(clippy::mixed_attributes_style)]
404    #![allow(clippy::print_stderr)]
405    #![allow(clippy::print_stdout)]
406    #![allow(clippy::single_char_pattern)]
407    #![allow(clippy::unwrap_used)]
408    #![allow(clippy::unchecked_time_subtraction)]
409    #![allow(clippy::useless_vec)]
410    #![allow(clippy::needless_pass_by_value)]
411    #![allow(clippy::string_slice)] // See arti#2571
412    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
413
414    use super::*;
415    use tor_basic_utils::test_rng::testing_rng;
416    use web_time_compat::SystemTimeExt;
417
418    #[test]
419    fn identity_cert_generation() {
420        let mut rng = testing_rng();
421        let keypair = RsaKeypair::generate(&mut rng).unwrap();
422        let cert = create_legacy_rsa_id_cert(
423            &mut rng,
424            SystemTime::get(),
425            "www.house-of-pancakes.example.com",
426            &keypair,
427        )
428        .unwrap();
429
430        let key_extracted = tor_llcrypto::util::x509_extract_rsa_subject_kludge(&cert[..]).unwrap();
431        assert_eq!(key_extracted, keypair.to_public_key());
432
433        // TODO: It would be neat to validate this certificate with an independent x509 implementation,
434        // but afaict most of them sensibly refuse to handle RSA1024.
435        //
436        // I've checked the above-generated cert using `openssl verify`, but that's it.
437    }
438
439    #[test]
440    fn tls_cert_info() {
441        let mut rng = testing_rng();
442        let certified = TlsKeyAndCert::create(
443            &mut rng,
444            SystemTime::get(),
445            "foo.example.com",
446            "bar.example.com",
447        )
448        .unwrap();
449        dbg!(certified);
450    }
451}