Implement functionality to construct signed Ed25519 certs.

This is behind a feature flag, since it isn't needed for pure
clients: only onion services and relays need this.

I've named the object that constructs these certs
`Ed25519CertConstructor` because it doesn't follow the builder
pattern exactly: mainly because you can't get an Ed25519Cert out of
it.  _That_ part is necessary because we require that an Ed25519Cert
should only exist if the certificate was found to be well-signed
with the right public key.

Closes #511.
This commit is contained in:
Nick Mathewson 2022-06-29 12:48:22 -04:00
parent 56fefd7adf
commit 48e44b0140
5 changed files with 248 additions and 79 deletions

2
Cargo.lock generated
View File

@ -3489,8 +3489,10 @@ version = "0.4.0"
dependencies = [ dependencies = [
"base64", "base64",
"caret", "caret",
"derive_builder_fork_arti",
"digest 0.10.3", "digest 0.10.3",
"hex-literal", "hex-literal",
"rand 0.8.5",
"signature", "signature",
"thiserror", "thiserror",
"tor-bytes", "tor-bytes",

View File

@ -11,8 +11,14 @@ keywords = ["tor", "arti", "certificate"]
categories = ["parser-implementations"] categories = ["parser-implementations"]
repository = "https://gitlab.torproject.org/tpo/core/arti.git/" repository = "https://gitlab.torproject.org/tpo/core/arti.git/"
[features]
default = []
experimental = ["encode"]
encode = ["derive_builder"]
[dependencies] [dependencies]
caret = { path = "../caret", version = "0.2.0" } caret = { path = "../caret", version = "0.2.0" }
derive_builder = { version = "0.11.2", package = "derive_builder_fork_arti", optional = true }
digest = "0.10.0" digest = "0.10.0"
signature = "1" signature = "1"
thiserror = "1" thiserror = "1"
@ -23,3 +29,4 @@ tor-llcrypto = { path = "../tor-llcrypto", version = "0.3.2" }
[dev-dependencies] [dev-dependencies]
base64 = "0.13.0" base64 = "0.13.0"
hex-literal = "0.3" hex-literal = "0.3"
rand = "0.8"

View File

@ -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<B: Writer + ?Sized>(&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<B: Writer + ?Sized>(&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<B: Writer + ?Sized>(&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<Vec<u8>, 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));
}
}

View File

@ -22,3 +22,26 @@ pub enum CertError {
#[error("Signature on certificate was invalid")] #[error("Signature on certificate was invalid")]
BadSignature, 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),
}

View File

@ -99,6 +99,11 @@ use std::time;
pub use err::CertError; pub use err::CertError;
#[cfg(feature = "encode")]
mod encode;
#[cfg(feature = "encode")]
pub use err::EncodeError;
/// A Result defined to use CertError /// A Result defined to use CertError
type CertResult<T> = std::result::Result<T, CertError>; type CertResult<T> = std::result::Result<T, CertError>;
@ -179,8 +184,14 @@ caret_int! {
/// Structure for an Ed25519-signed certificate as described in Tor's /// Structure for an Ed25519-signed certificate as described in Tor's
/// cert-spec.txt. /// cert-spec.txt.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[cfg_attr(feature = "encode", derive(derive_builder::Builder))]
#[cfg_attr(
feature = "encode",
builder(name = "Ed25519CertConstructor", build_fn(skip))
)]
pub struct Ed25519Cert { pub struct Ed25519Cert {
/// How many _hours_ after the epoch will this certificate expire? /// How many _hours_ after the epoch will this certificate expire?
#[cfg_attr(feature = "encode", builder(setter(custom)))]
exp_hours: u32, exp_hours: u32,
/// Type of the certificate; recognized values are in certtype::* /// Type of the certificate; recognized values are in certtype::*
cert_type: CertType, cert_type: CertType,
@ -188,11 +199,14 @@ pub struct Ed25519Cert {
cert_key: CertifiedKey, cert_key: CertifiedKey,
/// A list of extensions. /// A list of extensions.
#[allow(unused)] #[allow(unused)]
#[cfg_attr(feature = "encode", builder(setter(custom)))]
extensions: Vec<CertExt>, extensions: Vec<CertExt>,
/// The key that signed this cert. /// The key that signed this cert.
/// ///
/// Once the cert has been unwrapped from an KeyUnknownCert, this /// Once the cert has been unwrapped from an KeyUnknownCert, this field will
/// field will be set. /// 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<ed25519::PublicKey>, signed_with: Option<ed25519::PublicKey>,
} }
@ -295,17 +309,6 @@ impl CertExt {
} }
} }
/*
impl Writeable for CertExt {
fn write_onto<B: Writer + ?Sized>(&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. /// Extension indicating that a key that signed a given certificate.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct SignedWithEd25519Ext { struct SignedWithEd25519Ext {
@ -313,44 +316,6 @@ struct SignedWithEd25519Ext {
pk: ed25519::PublicKey, pk: ed25519::PublicKey,
} }
/*
impl Writeable for SignedWithEd25519Ext {
fn write_onto<B: Writer + ?Sized>(&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<B: Writer + ?Sized>(&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 { impl Readable for CertExt {
fn take_from(b: &mut Reader<'_>) -> BytesResult<Self> { fn take_from(b: &mut Reader<'_>) -> BytesResult<Self> {
let len = b.take_u16()?; let len = b.take_u16()?;
@ -385,34 +350,6 @@ impl Readable for CertExt {
} }
impl Ed25519Cert { 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<u8> {
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. /// Try to decode a certificate from a byte slice.
/// ///
/// This function returns an error if the byte slice is not /// This function returns an error if the byte slice is not