diff --git a/Cargo.lock b/Cargo.lock index 03934f8c1..ac32f6e26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3489,8 +3489,10 @@ version = "0.4.0" dependencies = [ "base64", "caret", + "derive_builder_fork_arti", "digest 0.10.3", "hex-literal", + "rand 0.8.5", "signature", "thiserror", "tor-bytes", diff --git a/crates/tor-cert/Cargo.toml b/crates/tor-cert/Cargo.toml index 75eb5a1ac..a66a400c7 100644 --- a/crates/tor-cert/Cargo.toml +++ b/crates/tor-cert/Cargo.toml @@ -11,8 +11,14 @@ keywords = ["tor", "arti", "certificate"] categories = ["parser-implementations"] repository = "https://gitlab.torproject.org/tpo/core/arti.git/" +[features] +default = [] +experimental = ["encode"] +encode = ["derive_builder"] + [dependencies] caret = { path = "../caret", version = "0.2.0" } +derive_builder = { version = "0.11.2", package = "derive_builder_fork_arti", optional = true } digest = "0.10.0" signature = "1" thiserror = "1" @@ -23,3 +29,4 @@ tor-llcrypto = { path = "../tor-llcrypto", version = "0.3.2" } [dev-dependencies] base64 = "0.13.0" hex-literal = "0.3" +rand = "0.8" diff --git a/crates/tor-cert/src/encode.rs b/crates/tor-cert/src/encode.rs new file mode 100644 index 000000000..45879b907 --- /dev/null +++ b/crates/tor-cert/src/encode.rs @@ -0,0 +1,200 @@ +//! Code for constructing and signing certificates. +//! +//! Only available when the crate is built with the `encode` feature. + +use crate::{ + CertExt, Ed25519Cert, Ed25519CertConstructor, EncodeError, ExtType, SignedWithEd25519Ext, + UnrecognizedExt, +}; +use std::time::{Duration, SystemTime}; +use tor_bytes::Writer; +use tor_llcrypto::pk::ed25519; + +impl Ed25519Cert { + /// Return a new `Ed25519CertConstructor` to create and return a new signed + /// `Ed25519Cert`. + pub fn constructor() -> Ed25519CertConstructor { + Default::default() + } +} + +impl CertExt { + /// As Writeable::WriteOnto, but may return an error. + /// + /// TODO: Migrate Writeable to provide this interface. + fn write_onto_fallible(&self, w: &mut B) -> Result<(), EncodeError> { + match self { + CertExt::SignedWithEd25519(pk) => pk.write_onto_fallible(w), + CertExt::Unrecognized(u) => u.write_onto_fallible(w), + } + } +} + +impl SignedWithEd25519Ext { + /// As Writeable::WriteOnto, but may return an error. + /// + /// TODO: Migrate Writeable to provide this interface. + #[allow(clippy::unnecessary_wraps)] + fn write_onto_fallible(&self, w: &mut B) -> Result<(), EncodeError> { + // body length + w.write_u16(32); + // Signed-with-ed25519-key-extension + w.write_u8(ExtType::SIGNED_WITH_ED25519_KEY.into()); + // flags = 0. + w.write_u8(0); + // body + w.write_all(self.pk.as_bytes()); + Ok(()) + } +} + +impl UnrecognizedExt { + /// As Writeable::WriteOnto, but may return an error. + /// + /// TODO: Migrate Writeable to provide this interface. + fn write_onto_fallible(&self, w: &mut B) -> Result<(), EncodeError> { + // We can't use Writer::write_nested_u16len here, since the length field + // doesn't include the type or the flags. + w.write_u16( + self.body + .len() + .try_into() + .map_err(|_| EncodeError::ExtensionTooLong)?, + ); + w.write_u8(self.ext_type.into()); + let flags = if self.affects_validation { 1 } else { 0 }; + w.write_u8(flags); + w.write_all(&self.body[..]); + Ok(()) + } +} + +impl Ed25519CertConstructor { + /// Set the approximate expiration time for this certificate. + /// + /// (The time will be rounded forward to the nearest hour after the epoch.) + pub fn expiration(&mut self, expiration: SystemTime) -> &mut Self { + /// The number of seconds in an hour. + const SEC_PER_HOUR: u64 = 3600; + let duration = expiration + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or(Duration::from_secs(0)); + let exp_hours = duration.as_secs().saturating_add(SEC_PER_HOUR - 1) / SEC_PER_HOUR; + self.exp_hours = Some(exp_hours.try_into().unwrap_or(u32::MAX)); + self + } + + /// Set the signing key to be included with this certificate. + /// + /// This is optional: you don't need to include the signing key at all. If + /// you do, it must match the key that you actually use to sign the + /// certificate. + pub fn signing_key(&mut self, key: ed25519::PublicKey) -> &mut Self { + self.clear_signing_key(); + self.signed_with = Some(Some(key)); + self.extensions + .get_or_insert_with(Vec::new) + .push(CertExt::SignedWithEd25519(SignedWithEd25519Ext { pk: key })); + + self + } + + /// Remove any signing key previously set on this Ed25519CertConstructor. + pub fn clear_signing_key(&mut self) -> &mut Self { + self.signed_with = None; + self.extensions + .get_or_insert_with(Vec::new) + .retain(|ext| !matches!(ext, CertExt::SignedWithEd25519(_))); + self + } + + /// Encode a certificate into a new vector, signing the result + /// with `keypair`. + /// + /// This function exists in lieu of a `build()` function, since we have a rule that + /// we don't produce an `Ed25519Cert` except if the certificate is known to be + /// valid. + pub fn encode_and_sign(&self, skey: &ed25519::Keypair) -> Result, EncodeError> { + use ed25519::Signer; + let Ed25519CertConstructor { + exp_hours, + cert_type, + cert_key, + extensions, + signed_with, + } = self; + + if let Some(Some(signer)) = &signed_with { + if signer != &skey.public { + return Err(EncodeError::KeyMismatch); + } + } + + let mut w = Vec::new(); + w.write_u8(1); // Version + w.write_u8( + cert_type + .ok_or(EncodeError::MissingField("cert_type"))? + .into(), + ); + w.write_u32(exp_hours.ok_or(EncodeError::MissingField("expiration"))?); + let cert_key = cert_key + .clone() + .ok_or(EncodeError::MissingField("cert_key"))?; + w.write_u8(cert_key.key_type().into()); + w.write_all(cert_key.as_bytes()); + let extensions = extensions.as_ref().map(Vec::as_slice).unwrap_or(&[]); + w.write_u8( + extensions + .len() + .try_into() + .map_err(|_| EncodeError::TooManyExtensions)?, + ); + + for e in extensions.iter() { + e.write_onto_fallible(&mut w)?; + } + + let signature = skey.sign(&w[..]); + w.write(&signature); + Ok(w) + } +} + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +mod test { + use super::*; + use crate::CertifiedKey; + use tor_checkable::{SelfSigned, Timebound}; + use tor_llcrypto::util::rand_compat::RngCompatExt; + + #[test] + fn signed_cert_without_key() { + let mut rng = rand::thread_rng().rng_compat(); + let keypair = ed25519::Keypair::generate(&mut rng); + let now = SystemTime::now(); + let day = Duration::from_secs(86400); + let encoded = Ed25519Cert::constructor() + .expiration(now + day * 30) + .cert_key(CertifiedKey::Ed25519(keypair.public)) + .cert_type(7.into()) + .encode_and_sign(&keypair) + .unwrap(); + + let decoded = Ed25519Cert::decode(&encoded).unwrap(); // Well-formed? + let validated = decoded + .check_key(&Some(keypair.public)) + .unwrap() + .check_signature() + .unwrap(); // Well-signed? + let cert = validated.check_valid_at(&(now + day * 20)).unwrap(); + assert_eq!(cert.cert_type(), 7.into()); + if let CertifiedKey::Ed25519(found) = cert.subject_key() { + assert_eq!(found, &keypair.public); + } else { + panic!("wrong key type"); + } + assert!(cert.signing_key() == Some(&keypair.public)); + } +} diff --git a/crates/tor-cert/src/err.rs b/crates/tor-cert/src/err.rs index 12e88d65f..7a9dd9354 100644 --- a/crates/tor-cert/src/err.rs +++ b/crates/tor-cert/src/err.rs @@ -22,3 +22,26 @@ pub enum CertError { #[error("Signature on certificate was invalid")] BadSignature, } + +/// An error related to signing or encoding a certificate +#[cfg(feature = "encode")] +#[derive(Clone, Debug, Error)] +#[non_exhaustive] +pub enum EncodeError { + /// This certificate contains the public key that it is supposed to + /// be signed by, and the provided signing private key isn't it. + #[error("Tried to sign with wrong key")] + KeyMismatch, + + /// The certificate contains more than 255 extensions. + #[error("Too many extensions")] + TooManyExtensions, + + /// Some extension had a length of over 2^16. + #[error("Extension too long")] + ExtensionTooLong, + + /// A mandatory field was not provided. + #[error("Missing field {0:?}")] + MissingField(&'static str), +} diff --git a/crates/tor-cert/src/lib.rs b/crates/tor-cert/src/lib.rs index 397c0be25..d9ace5759 100644 --- a/crates/tor-cert/src/lib.rs +++ b/crates/tor-cert/src/lib.rs @@ -99,6 +99,11 @@ use std::time; pub use err::CertError; +#[cfg(feature = "encode")] +mod encode; +#[cfg(feature = "encode")] +pub use err::EncodeError; + /// A Result defined to use CertError type CertResult = std::result::Result; @@ -179,8 +184,14 @@ caret_int! { /// Structure for an Ed25519-signed certificate as described in Tor's /// cert-spec.txt. #[derive(Debug, Clone)] +#[cfg_attr(feature = "encode", derive(derive_builder::Builder))] +#[cfg_attr( + feature = "encode", + builder(name = "Ed25519CertConstructor", build_fn(skip)) +)] pub struct Ed25519Cert { /// How many _hours_ after the epoch will this certificate expire? + #[cfg_attr(feature = "encode", builder(setter(custom)))] exp_hours: u32, /// Type of the certificate; recognized values are in certtype::* cert_type: CertType, @@ -188,11 +199,14 @@ pub struct Ed25519Cert { cert_key: CertifiedKey, /// A list of extensions. #[allow(unused)] + #[cfg_attr(feature = "encode", builder(setter(custom)))] extensions: Vec, /// The key that signed this cert. /// - /// Once the cert has been unwrapped from an KeyUnknownCert, this - /// field will be set. + /// Once the cert has been unwrapped from an KeyUnknownCert, this field will + /// be set. If there is a `SignedWithEd25519` extension in + /// `self.extensions`, this will match it. + #[cfg_attr(feature = "encode", builder(setter(custom)))] signed_with: Option, } @@ -295,17 +309,6 @@ impl CertExt { } } -/* -impl Writeable for CertExt { - fn write_onto(&self, w: &mut B) { - match self { - CertExt::SignedWithEd25519(pk) => pk.write_onto(w), - CertExt::Unrecognized(u) => u.write_onto(w), - } - } -} - */ - /// Extension indicating that a key that signed a given certificate. #[derive(Debug, Clone)] struct SignedWithEd25519Ext { @@ -313,44 +316,6 @@ struct SignedWithEd25519Ext { pk: ed25519::PublicKey, } -/* -impl Writeable for SignedWithEd25519Ext { - fn write_onto(&self, w: &mut B) { - // body length - w.write_u16(32); - // Signed-with-ed25519-key-extension - w.write_u8(ExtType::SIGNED_WITH_ED25519_KEY.into()); - // flags = 0. - w.write_u8(0); - // body - w.write_all(self.pk.as_bytes()); - } -} -*/ - -/* -impl UnrecognizedExt { - /// Assert that there is no problem with the internal representation - /// of this object. - fn assert_rep_ok(&self) { - assert!(self.body.len() <= std::u16::MAX as usize); - } -} -*/ - -/* -impl Writeable for UnrecognizedExt { - fn write_onto(&self, w: &mut B) { - self.assert_rep_ok(); - w.write_u16(self.body.len() as u16); - w.write_u8(self.ext_type.into()); - let flags = if self.affects_validation { 1 } else { 0 }; - w.write_u8(flags); - w.write_all(&self.body[..]); - } -} -*/ - impl Readable for CertExt { fn take_from(b: &mut Reader<'_>) -> BytesResult { let len = b.take_u16()?; @@ -385,34 +350,6 @@ impl Readable for CertExt { } impl Ed25519Cert { - /* - /// Helper: Assert that there is nothing wrong with the - /// internal structure of this certificate. - fn assert_rep_ok(&self) { - assert!(self.extensions.len() <= std::u8::MAX as usize); - } - - /// Encode a certificate into a new vector, signing the result - /// with `keypair`. - pub fn encode_and_sign(&self, skey: &ed25519::Keypair) -> Vec { - self.assert_rep_ok(); - let mut w = Vec::new(); - w.write_u8(1); // Version - w.write_u8(self.cert_type.into()); - w.write_u32(self.exp_hours); - w.write_u8(self.cert_key.key_type().into()); - w.write_all(self.cert_key.as_bytes()); - - for e in self.extensions.iter() { - w.write(e); - } - - let signature = skey.sign(&w[..]); - w.write(&signature); - w - } - */ - /// Try to decode a certificate from a byte slice. /// /// This function returns an error if the byte slice is not