tor-cert 0.41.0

Non-standard certificate formats used by Tor
Documentation
//! RSA cross-cert generation

use web_time_compat::SystemTime;

use derive_more::{AsRef, Deref, Into};
use tor_bytes::Writer as _;
use tor_llcrypto::pk::{ed25519, rsa};

use crate::{CertEncodeError, EncodedCert, ExpiryHours};

/// An RSA cross certificate certificate,
/// created using [`EncodedRsaCrosscert::encode_and_sign`].
///
/// It corresponds to the type of certificate parsed with
/// [`RsaCrosscert`](super::RsaCrosscert).
/// It is used to prove that an Ed25519 identity speaks
/// on behalf of an RSA identity.
///
/// The certificate is encoded in the format specified
/// in Tor's [certificate specification](https://spec.torproject.org/cert-spec.html#rsa-cross-cert)
///
/// This certificate has already been validated.
#[derive(Clone, Debug, PartialEq, Into, AsRef, Deref)]
pub struct EncodedRsaCrosscert(Vec<u8>);

impl EncodedRsaCrosscert {
    /// Create a new [`EncodedRsaCrosscert`] certifying `ed_identity` as
    /// speaking on behalf of `rsa_identity`.
    ///
    /// The certificate will expire no earlier than `expiration`,
    /// and no more than one hour later.
    /// (Expiration times in these certificates have a one-hour granularity.)
    pub fn encode_and_sign(
        rsa_identity: &rsa::KeyPair,
        ed_identity: &ed25519::Ed25519Identity,
        expiration: SystemTime,
    ) -> Result<Self, CertEncodeError> {
        let mut cert = Vec::new();
        cert.write(ed_identity)?;
        let exp_hours = ExpiryHours::try_from_systemtime_ceil(expiration)?;
        cert.write(&exp_hours)?;
        {
            let signature = rsa_identity
                .sign(&super::compute_digest(&cert))
                .map_err(|_| CertEncodeError::RsaSignatureFailed)?;
            let mut inner = cert.write_nested_u8len();
            inner.write_and_consume(signature)?;
            inner.finish()?;
        }

        Ok(EncodedRsaCrosscert(cert))
    }
}

impl EncodedCert for EncodedRsaCrosscert {
    fn cert_type(&self) -> crate::CertType {
        crate::CertType::RSA_ID_V_IDENTITY
    }

    fn encoded(&self) -> &[u8] {
        &self.0
    }
}

#[cfg(test)]
mod test {
    // @@ begin test lint list maintained by maint/add_warning @@
    #![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)]
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
    use std::time::Duration;

    use tor_basic_utils::test_rng::testing_rng;
    use tor_checkable::{ExternallySigned, Timebound};
    use web_time_compat::SystemTimeExt;

    use crate::SEC_PER_HOUR;
    use crate::rsa::RsaCrosscert;

    use super::*;

    #[test]
    fn generate() {
        let mut rng = testing_rng();
        let keypair = rsa::KeyPair::generate(&mut rng).unwrap();
        let ed_id =
            ed25519::Ed25519Identity::from_base64("dGhhdW1hdHVyZ3kgaXMgc3RvcmVkIGluIHRoZSBvcmI")
                .unwrap();

        let now = SystemTime::get();
        let expiry = now + Duration::from_secs(24 * SEC_PER_HOUR);

        let cert = EncodedRsaCrosscert::encode_and_sign(&keypair, &ed_id, expiry).unwrap();

        let parsed = RsaCrosscert::decode(cert.as_ref()).unwrap();
        let parsed = parsed
            .check_signature(&keypair.to_public_key())
            .unwrap()
            .check_valid_at(&now)
            .unwrap();

        assert!(parsed.subject_key_matches(&ed_id));
        assert_eq!(parsed.subject_key, ed_id);
        let parsed_expiry = parsed.expiry();
        assert!(parsed_expiry >= expiry);
        assert!(parsed_expiry < expiry + Duration::new(3600, 0));
    }
}