Merge branch 'generate_cert' into 'main'
Implement functionality to construct signed Ed25519 certs. Closes #511 See merge request tpo/core/arti!611
This commit is contained in:
commit
033a05a9af
|
@ -3478,8 +3478,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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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")]
|
#[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),
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
MODIFIED: Signer trait now exposed.
|
|
@ -13,7 +13,7 @@ use arrayref::array_ref;
|
||||||
use std::fmt::{self, Debug, Display, Formatter};
|
use std::fmt::{self, Debug, Display, Formatter};
|
||||||
use subtle::{Choice, ConstantTimeEq};
|
use subtle::{Choice, ConstantTimeEq};
|
||||||
|
|
||||||
pub use ed25519_dalek::{ExpandedSecretKey, Keypair, PublicKey, SecretKey, Signature};
|
pub use ed25519_dalek::{ExpandedSecretKey, Keypair, PublicKey, SecretKey, Signature, Signer};
|
||||||
|
|
||||||
/// A relay's identity, as an unchecked, unvalidated Ed25519 key.
|
/// A relay's identity, as an unchecked, unvalidated Ed25519 key.
|
||||||
///
|
///
|
||||||
|
|
Loading…
Reference in New Issue