summaryrefslogtreecommitdiff
path: root/crates/tor-cert/src/rsa/encode.rs
blob: 264e66b8e0b5340584a8ffe8f4dec18c04d3232a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
//! RSA cross-cert generation

use std::time::SystemTime;

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

use crate::CertEncodeError;

/// 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 hours_since_epoch: u32 = expiration
            .duration_since(SystemTime::UNIX_EPOCH)
            .map_err(|_| CertEncodeError::InvalidExpiration)?
            .as_secs()
            .div_ceil(super::SECS_PER_HOUR)
            .try_into()
            .map_err(|_| CertEncodeError::InvalidExpiration)?;
        cert.write_u32(hours_since_epoch);
        {
            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))
    }
}

#[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 crate::rsa::{RsaCrosscert, SECS_PER_HOUR};

    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::now();
        let expiry = now + Duration::from_secs(24 * SECS_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));
    }
}