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:
parent
56fefd7adf
commit
48e44b0140
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -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<T> = std::result::Result<T, CertError>;
|
||||
|
||||
|
@ -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<CertExt>,
|
||||
/// 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<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.
|
||||
#[derive(Debug, Clone)]
|
||||
struct SignedWithEd25519Ext {
|
||||
|
@ -313,44 +316,6 @@ struct SignedWithEd25519Ext {
|
|||
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 {
|
||||
fn take_from(b: &mut Reader<'_>) -> BytesResult<Self> {
|
||||
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<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.
|
||||
///
|
||||
/// This function returns an error if the byte slice is not
|
||||
|
|
Loading…
Reference in New Issue