Lines
93.18 %
Functions
58.06 %
Branches
100 %
//! Code to abstract over the notion of relays having one or more identities.
//!
//! Currently (2022), every Tor relay has exactly two identities: A legacy
//! identity that is based on the SHA-1 hash of an RSA-1024 public key, and a
//! modern identity that is an Ed25519 public key. This code lets us abstract
//! over those types, and over other new types that may exist in the future.
use std::fmt;
use derive_deftly::{Deftly, define_derive_deftly};
use derive_more::{Display, From};
use safelog::Redactable;
use tor_llcrypto::pk::{
ed25519::{ED25519_ID_LEN, Ed25519Identity},
rsa::{RSA_ID_LEN, RsaIdentity},
};
pub(crate) mod by_id;
pub(crate) mod set;
/// The type of a relay identity.
///
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd)] //
#[derive(Display, strum::EnumIter, strum::EnumCount, Deftly)]
#[derive_deftly_adhoc]
#[derive_deftly(RelayId)]
#[non_exhaustive]
pub enum RelayIdType {
/// An Ed25519 identity.
/// Every relay (currently) has one of these identities. It is the same
/// as the encoding of the relay's public Ed25519 identity key.
#[display("Ed25519")] // Display of this enum variant, ie of just the id type
#[deftly(display_id = "ed25519:{}")] // Display of a relay id value of this type
Ed25519,
/// An RSA identity.
/// Every relay (currently) has one of these identities. It is computed as
/// a SHA-1 digest of the DER encoding of the relay's public RSA 1024-bit
/// identity key. Because of short key length, this type of identity should
/// not be considered secure on its own.
#[display("RSA (legacy)")]
#[deftly(display_id = "{}")]
Rsa,
}
impl fmt::Display for RelayId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(&self.as_ref(), f)
define_derive_deftly! {
/// Derives `enum RelayId`, `enum RelayIdRef`, and many impls
RelayId expect items, beta_deftly:
${define IDENTITY $<$vname Identity>}
/// A single relay identity.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, From, Hash)]
pub enum RelayId {
$(
${vattrs doc}
$vname($IDENTITY),
)
/// A reference to a single relay identity.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] //
#[derive(Display, From, derive_more::TryInto)]
pub enum RelayIdRef<'a> {
#[display(${vmeta(display_id) as str}, _0)]
$vname(&'a $IDENTITY),
impl RelayIdType {
/// The number of distinct types currently implemented.
pub const COUNT: usize = <RelayIdType as strum::EnumCount>::COUNT;
/// Return an iterator over all
pub fn all_types() -> RelayIdTypeIter {
use strum::IntoEnumIterator;
Self::iter()
/// Return the length of this identity, in bytes.
pub fn id_len(&self) -> usize {
match self { $(
$vtype => ${shouty_snake_case $vname _ID_LEN},
) }
impl RelayId {
/// Return a [`RelayIdRef`] pointing to the contents of this identity.
pub fn as_ref(&self) -> RelayIdRef<'_> {
RelayId::$vname(key) => key.into(),
/// Try to construct a RelayId of a provided `id_type` from a byte-slice.
/// Return [`RelayIdError::BadLength`] if the slice is not the correct length for the key.
pub fn from_type_and_bytes(id_type: RelayIdType, id: &[u8]) -> Result<Self, RelayIdError> {
Ok(match id_type { $(
$vtype => $IDENTITY::from_bytes(id)
.ok_or(RelayIdError::BadLength)?
.into(),
) })
/// Return the type of this relay identity.
pub fn id_type(&self) -> RelayIdType {
self.as_ref().id_type()
/// Return a byte-slice corresponding to the contents of this identity.
/// The return value discards the type of the identity, and so should be
/// handled with care to make sure that it does not get confused with an
/// identity of some other type.
pub fn as_bytes(&self) -> &[u8] {
self.as_ref().as_bytes()
impl<'a> RelayIdRef<'a> {
/// Copy this reference into a new [`RelayId`] object.
//
// TODO(nickm): I wish I could make this a proper `ToOwned` implementation,
// but I see no way to do as long as RelayIdRef<'a> implements Clone too.
pub fn to_owned(&self) -> RelayId {
match *self { $(
RelayIdRef::$vname(key) => (*key).into(),
RelayIdRef::$vname(_) => $vtype,
pub fn as_bytes(&self) -> &'a [u8] {
RelayIdRef::$vname(key) => key.as_bytes(),
$/// Extract the `$IDENTITY` from a RelayIdRef that is known to hold one.
/// # Panics
/// Panics if this is not an `$vname` identity.
pub(crate) fn ${snake_case unwrap_ $vname}(self) -> &'a $IDENTITY {
match self {
RelayIdRef::$vname(key) => key,
_ => panic!($"Not an $vname identity."),
impl<'a> PartialEq<$IDENTITY> for RelayIdRef<'a> {
fn eq(&self, other: &$IDENTITY) -> bool {
matches!(self, RelayIdRef::$vname(this) if this == &other)
impl PartialEq<$IDENTITY> for RelayId {
self.as_ref() == *other
#[allow(clippy::single_component_path_imports)] // rust-clippy/issues/13419
use derive_deftly_template_RelayId; // allows putting the macro after RelayIdType
impl<'a> From<&'a RelayId> for RelayIdRef<'a> {
fn from(ident: &'a RelayId) -> Self {
ident.as_ref()
impl Redactable for RelayId {
fn display_redacted(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.as_ref().display_redacted(f)
fn debug_redacted(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.as_ref().debug_redacted(f)
impl<'a> Redactable for RelayIdRef<'a> {
RelayIdRef::Ed25519(k) => write!(f, "ed25519:{}", k.redacted()),
RelayIdRef::Rsa(k) => write!(f, "${}", k.redacted()),
use std::fmt::Debug;
RelayIdRef::Ed25519(k) => Debug::fmt(*k.redacted(), f),
RelayIdRef::Rsa(k) => Debug::fmt(*k.redacted(), f),
impl std::str::FromStr for RelayIdType {
type Err = RelayIdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.eq_ignore_ascii_case("rsa") {
Ok(RelayIdType::Rsa)
} else if s.eq_ignore_ascii_case("ed25519") {
Ok(RelayIdType::Ed25519)
} else {
Err(RelayIdError::UnrecognizedIdType)
impl std::str::FromStr for RelayId {
/// Try to parse `s` as a RelayId.
/// We use the following format, based on the one used by C tor.
/// * An optional `$` followed by a 40 byte hex string is always an RSA key.
/// * A 43 character un-padded base-64 string is always an Ed25519 key.
/// * The name of an algorithm ("rsa" or "ed25519"), followed by a colon and
/// and an un-padded base-64 string is a key of that type.
use base64ct::{Base64Unpadded, Encoding as _};
if let Some((alg, key)) = s.split_once(':') {
let alg: RelayIdType = alg.parse()?;
let len = alg.id_len();
let mut v = vec![0_u8; len];
let bytes = Base64Unpadded::decode(key, &mut v[..])?;
RelayId::from_type_and_bytes(alg, bytes)
} else if s.len() == RSA_ID_LEN * 2 || s.starts_with('$') {
let s = s.trim_start_matches('$');
let bytes = hex::decode(s).map_err(|_| RelayIdError::BadHex)?;
RelayId::from_type_and_bytes(RelayIdType::Rsa, &bytes)
let mut v = [0_u8; ED25519_ID_LEN];
let bytes = Base64Unpadded::decode(s, &mut v[..])?;
RelayId::from_type_and_bytes(RelayIdType::Ed25519, bytes)
impl serde::Serialize for RelayId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.as_ref().serialize(serializer)
impl<'a> serde::Serialize for RelayIdRef<'a> {
// TODO(nickm): maybe encode this as bytes when dealing with
// non-human-readable formats.
self.to_string().serialize(serializer)
impl<'de> serde::Deserialize<'de> for RelayId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
D: serde::Deserializer<'de>,
// TODO(nickm): maybe allow bytes when dealing with non-human-readable
// formats.
use serde::de::Error as _;
let s = <std::borrow::Cow<'_, str> as serde::Deserialize>::deserialize(deserializer)?;
s.parse()
.map_err(|e: RelayIdError| D::Error::custom(e.to_string()))
/// An error returned while trying to parse a RelayId.
#[derive(Clone, Debug, thiserror::Error)]
pub enum RelayIdError {
/// We didn't recognize the type of a relay identity.
/// This can happen when a type that we have never heard of is specified, or when a type
#[error("Unrecognized type for relay identity")]
UnrecognizedIdType,
/// We encountered base64 data that we couldn't parse.
#[error("Invalid base64 data")]
BadBase64,
/// We encountered hex data that we couldn't parse.
#[error("Invalid hexadecimal data")]
BadHex,
/// We got a key that was the wrong length.
#[error("Invalid length for relay identity")]
BadLength,
impl From<base64ct::Error> for RelayIdError {
fn from(err: base64ct::Error) -> Self {
match err {
base64ct::Error::InvalidEncoding => RelayIdError::BadBase64,
base64ct::Error::InvalidLength => RelayIdError::BadLength,
#[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)]
#![allow(clippy::string_slice)] // See arti#2571
//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
use hex_literal::hex;
use serde_test::{Token, assert_tokens};
use std::str::FromStr;
use super::*;
#[test]
fn parse_and_display() -> Result<(), RelayIdError> {
fn normalizes_to(s: &str, expected: &str) -> Result<(), RelayIdError> {
let k: RelayId = s.parse()?;
let s2 = k.to_string();
assert_eq!(s2, expected);
let k2: RelayId = s2.parse()?;
let s3 = k2.to_string();
assert_eq!(s3, s2);
let s4 = k2.as_ref().to_string();
assert_eq!(s4, s3);
Ok(())
fn check(s: &str) -> Result<(), RelayIdError> {
normalizes_to(s, s)
// Try a few RSA identities.
check("$1234567812345678123456781234567812345678")?;
normalizes_to(
"abcdefabcdefabcdefabcdefabcdef1234567890",
"$abcdefabcdefabcdefabcdefabcdef1234567890",
)?;
"abcdefabcdefABCDEFabcdefabcdef1234567890",
"rsa:q83vq83vq83vq83vq83vEjRWeJA",
// Try a few ed25519 identities
check("ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE")?;
"dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE",
"ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE",
fn parse_fail() {
let e = RelayId::from_str("tooshort").unwrap_err();
assert!(matches!(e, RelayIdError::BadLength));
let e = RelayId::from_str("this_string_is_40_bytes_but_it_isnt_hex!").unwrap_err();
assert!(matches!(e, RelayIdError::BadHex));
let e = RelayId::from_str("merkle-hellman:bestavoided").unwrap_err();
assert!(matches!(e, RelayIdError::UnrecognizedIdType));
let e = RelayId::from_str("ed25519:q83vq83vq83vq83vq83vEjRWeJA").unwrap_err();
let e = RelayId::from_str("ed25519:🤨🤨🤨🤨🤨").unwrap_err();
assert!(matches!(e, RelayIdError::BadBase64));
fn types() {
assert_eq!(
RelayId::from_str("$1234567812345678123456781234567812345678")
.unwrap()
.id_type(),
RelayIdType::Rsa,
);
.as_ref()
RelayId::from_str("ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE")
RelayIdType::Ed25519,
fn equals_other() {
let rsa1 = RsaIdentity::from(*b"You just have to kno");
let rsa2 = RsaIdentity::from(*b"w who you are and st");
let ed1 = Ed25519Identity::from(*b"ay true to that. So I'm going to");
let ed2 = Ed25519Identity::from(*b"keep fighting for people the onl");
assert_eq!(RelayId::from(rsa1), rsa1);
assert_ne!(RelayId::from(rsa1), rsa2);
assert_ne!(RelayId::from(rsa1), ed1);
assert_eq!(RelayId::from(ed1), ed1);
assert_ne!(RelayId::from(ed1), ed2);
assert_ne!(RelayId::from(ed1), rsa1);
assert_eq!(RelayIdRef::from(&rsa1), rsa1);
assert_ne!(RelayIdRef::from(&rsa1), rsa2);
assert_ne!(RelayIdRef::from(&rsa1), ed1);
assert_eq!(RelayIdRef::from(&ed1), ed1);
assert_ne!(RelayIdRef::from(&ed1), ed2);
assert_ne!(RelayIdRef::from(&ed1), rsa1);
fn as_bytes() {
.as_bytes(),
hex!("1234567812345678123456781234567812345678"),
b"this is incredibly silly!!!!!!!!"
fn unwrap_ok() {
let rsa = RelayId::from_str("$1234567812345678123456781234567812345678").unwrap();
rsa.as_ref().unwrap_rsa(),
&RsaIdentity::from_bytes(&hex!("1234567812345678123456781234567812345678")).unwrap()
let ed = RelayId::from_str("ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE").unwrap();
ed.as_ref().unwrap_ed25519(),
&Ed25519Identity::from_bytes(b"this is incredibly silly!!!!!!!!").unwrap()
#[should_panic]
fn unwrap_rsa_panic() {
if let Ok(ed) = RelayId::from_str("ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE") {
let _nope = RelayIdRef::from(&ed).unwrap_rsa();
fn unwrap_ed_panic() {
if let Ok(ed) = RelayId::from_str("$1234567812345678123456781234567812345678") {
let _nope = RelayIdRef::from(&ed).unwrap_ed25519();
fn serde_owned() {
let keys = vec![RelayId::from(rsa1), RelayId::from(ed1)];
assert_tokens(
&keys,
&[
Token::Seq { len: Some(2) },
Token::String("$596f75206a757374206861766520746f206b6e6f"),
Token::String("ed25519:YXkgdHJ1ZSB0byB0aGF0LiBTbyBJJ20gZ29pbmcgdG8"),
Token::SeqEnd,
],