use anyhow::Context;
use std::{
sync::Arc,
time::{Duration, SystemTime},
};
use tor_basic_utils::rand_hostname;
use tor_cert::x509::TlsKeyAndCert;
use tor_chanmgr::ChanMgr;
use tor_error::internal;
use tor_key_forge::ToEncodableCert;
use tor_keymgr::{
CertSpecifierPattern, KeyCertificateSpecifier, KeyMgr, KeyPath, KeySpecifier,
KeySpecifierPattern, Keygen, KeystoreSelector, ToEncodableKey,
};
use tor_proto::RelayIdentities;
use tor_relay_crypto::{
RelaySigningKeyCert, gen_link_cert, gen_signing_cert, gen_tls_cert,
pk::{
RelayIdentityKeypair, RelayIdentityKeypairSpecifier, RelayIdentityRsaKeypair,
RelayIdentityRsaKeypairSpecifier, RelayLinkSigningKeypair,
RelayLinkSigningKeypairSpecifier, RelayLinkSigningKeypairSpecifierPattern,
RelaySigningKeyCertSpecifier, RelaySigningKeyCertSpecifierPattern, RelaySigningKeypair,
RelaySigningKeypairSpecifier, RelaySigningKeypairSpecifierPattern,
RelaySigningPublicKeySpecifier, Timestamp,
},
};
use tor_rtcompat::{Runtime, SleepProviderExt};
const KEY_ROTATION_EXPIRE_BUFFER: Duration = Duration::from_secs(3 * 60 * 60);
const KEY_DURATION_2DAYS: Duration = Duration::from_secs(2 * 24 * 60 * 60);
const KEY_DURATION_30DAYS: Duration = Duration::from_secs(30 * 24 * 60 * 60);
const KEY_DURATION_6MONTHS: Duration = Duration::from_secs(6 * 30 * 24 * 60 * 60);
fn build_proto_identities(now: SystemTime, keymgr: &KeyMgr) -> anyhow::Result<RelayIdentities> {
let mut rng = tor_llcrypto::rng::CautiousRng;
let rsa_id_kp: RelayIdentityRsaKeypair = keymgr
.get(&RelayIdentityRsaKeypairSpecifier::new())
.context("Failed to get RSA identity from key manager")?
.context("Missing RSA identity")?;
let ed_id_kp: RelayIdentityKeypair = keymgr
.get(&RelayIdentityKeypairSpecifier::new())
.context("Failed to get Ed25519 identity from key manager")?
.context("Missing Ed25519 identity")?;
let link_sign_kp: RelayLinkSigningKeypair = keymgr
.get_entry(
keymgr
.list_matching(&RelayLinkSigningKeypairSpecifierPattern::new_any().arti_pattern()?)?
.first()
.context("No store entry for link authentication key")?,
)
.context("Failed to get link authentication key from key manager")?
.context("Missing link authentication key")?;
let kp_relaysign_id: RelaySigningKeypair = keymgr
.get_entry(
keymgr
.list_matching(&RelaySigningKeypairSpecifierPattern::new_any().arti_pattern()?)?
.first()
.context("No store entry for signing key")?,
)
.context("Failed to get signing key from key manager")?
.context("Missing signing key")?;
let issuer_hostname = rand_hostname::random_hostname(&mut rng);
let subject_hostname = rand_hostname::random_hostname(&mut rng);
let tls_key_and_cert =
TlsKeyAndCert::create(&mut rng, now, &issuer_hostname, &subject_hostname)
.context("Failed to create TLS keys and certificates")?;
let cert_id_x509_rsa = tor_cert::x509::create_legacy_rsa_id_cert(
&mut rng,
now,
&issuer_hostname,
rsa_id_kp.keypair(),
)
.context("Failed to create legacy RSA identity certificate")?;
let cert_id_rsa = tor_cert::rsa::EncodedRsaCrosscert::encode_and_sign(
rsa_id_kp.keypair(),
&ed_id_kp.to_ed25519_id(),
now + KEY_DURATION_6MONTHS,
)?;
let cert_id_sign_ed = gen_signing_cert(&ed_id_kp, &kp_relaysign_id, now + KEY_DURATION_30DAYS)?;
let cert_sign_link_auth_ed =
gen_link_cert(&kp_relaysign_id, &link_sign_kp, now + KEY_DURATION_2DAYS)?;
let cert_sign_tls_ed = gen_tls_cert(
&kp_relaysign_id,
*tls_key_and_cert.link_cert_sha256(),
now + KEY_DURATION_2DAYS,
)?;
Ok(RelayIdentities::new(
&rsa_id_kp.public().into(),
ed_id_kp.to_ed25519_id(),
link_sign_kp,
cert_id_sign_ed.to_encodable_cert(),
cert_sign_tls_ed,
cert_sign_link_auth_ed.to_encodable_cert(),
cert_id_x509_rsa,
cert_id_rsa,
tls_key_and_cert,
))
}
fn generate_key<K>(keymgr: &KeyMgr, spec: &dyn KeySpecifier) -> Result<(), tor_keymgr::Error>
where
K: ToEncodableKey,
K::Key: Keygen,
{
let mut rng = tor_llcrypto::rng::CautiousRng;
match keymgr.generate::<K>(spec, KeystoreSelector::default(), &mut rng, false) {
Ok(_) => {}
Err(tor_keymgr::Error::KeyAlreadyExists) => (),
Err(e) => return Err(e),
};
Ok(())
}
fn remove_expired<F>(
now: SystemTime,
keymgr: &KeyMgr,
pattern: &tor_keymgr::KeyPathPattern,
label: &'static str,
expiry_from_keypath: F,
) -> anyhow::Result<(bool, Option<SystemTime>)>
where
F: Fn(&KeyPath) -> anyhow::Result<Timestamp>,
{
let entries = keymgr.list_matching(pattern)?;
let mut removed = false;
let mut min_valid_until: Option<Timestamp> = None;
for entry in entries {
let valid_until = expiry_from_keypath(entry.key_path())?;
if valid_until <= Timestamp::from(now + KEY_ROTATION_EXPIRE_BUFFER) {
tracing::debug!("Expired {} in keymgr. Removing it.", label);
keymgr.remove_entry(&entry)?;
removed = true;
} else {
min_valid_until =
Some(min_valid_until.map_or(valid_until, |current| current.min(valid_until)));
}
}
Ok((removed, min_valid_until.map(SystemTime::from)))
}
fn try_generate_key<K, P>(keymgr: &KeyMgr, spec: &dyn KeySpecifier) -> anyhow::Result<bool>
where
K: ToEncodableKey,
K::Key: Keygen,
P: KeySpecifierPattern,
{
let mut generated = false;
let mut rng = tor_llcrypto::rng::CautiousRng;
let entries = keymgr.list_matching(&P::new_any().arti_pattern()?)?;
if entries.is_empty() {
let _ = keymgr.get_or_generate::<K>(spec, KeystoreSelector::default(), &mut rng)?;
generated = true;
}
Ok(generated)
}
fn try_generate_key_cert<K, C, P>(
keymgr: &KeyMgr,
cert_spec: &dyn KeyCertificateSpecifier,
signing_key_spec: &dyn KeySpecifier,
make_certificate: impl FnOnce(&K, &<C as ToEncodableCert<K>>::SigningKey) -> C,
) -> anyhow::Result<bool>
where
K: ToEncodableKey,
K::Key: Keygen,
C: ToEncodableCert<K>,
P: CertSpecifierPattern,
{
let mut generated = false;
let mut rng = tor_llcrypto::rng::CautiousRng;
let entries = keymgr.list_matching(&P::new_any().arti_pattern()?)?;
if entries.is_empty() {
let _ = keymgr.get_or_generate_key_and_cert::<K, C>(
cert_spec,
signing_key_spec,
make_certificate,
KeystoreSelector::default(),
&mut rng,
)?;
generated = true;
}
Ok(generated)
}
fn try_generate_all(now: SystemTime, keymgr: &KeyMgr) -> anyhow::Result<Option<SystemTime>> {
let link_expiry = now + KEY_DURATION_2DAYS;
let link_spec = RelayLinkSigningKeypairSpecifier::new(Timestamp::from(link_expiry));
let link_generated = try_generate_key::<
RelayLinkSigningKeypair,
RelayLinkSigningKeypairSpecifierPattern,
>(keymgr, &link_spec)?;
let make_signing_cert = |subject_key: &RelaySigningKeypair,
signing_key: &RelayIdentityKeypair| {
gen_signing_cert(signing_key, subject_key, now + KEY_DURATION_30DAYS)
.expect("failed to generate relay signing cert")
};
let cert_expiry = now + KEY_DURATION_30DAYS;
let cert_spec = RelaySigningKeyCertSpecifier::new(RelaySigningPublicKeySpecifier::new(
Timestamp::from(cert_expiry),
));
let cert_generated = try_generate_key_cert::<
RelaySigningKeypair,
RelaySigningKeyCert,
RelaySigningKeyCertSpecifierPattern,
>(
keymgr,
&cert_spec,
&RelayIdentityKeypairSpecifier::new(),
make_signing_cert,
)?;
Ok([
link_generated.then_some(link_expiry),
cert_generated.then_some(cert_expiry),
]
.into_iter()
.flatten()
.min())
}
fn remove_expired_keys(
now: SystemTime,
keymgr: &KeyMgr,
) -> anyhow::Result<(bool, Option<SystemTime>)> {
let (relaysign_removed, relaysign_expiry) = remove_expired(
now,
keymgr,
&RelaySigningKeypairSpecifierPattern::new_any().arti_pattern()?,
"key KP_relaysign_ed",
|key_path| Ok(RelaySigningKeypairSpecifier::try_from(key_path)?.valid_until()),
)?;
let (link_removed, link_expiry) = remove_expired(
now,
keymgr,
&RelayLinkSigningKeypairSpecifierPattern::new_any().arti_pattern()?,
"key KP_link_ed",
|key_path| Ok(RelayLinkSigningKeypairSpecifier::try_from(key_path)?.valid_until()),
)?;
let (sign_cert_removed, sign_cert_expiry) = remove_expired(
now,
keymgr,
&RelaySigningKeyCertSpecifierPattern::new_any().arti_pattern()?,
"signing key cert",
|key_path| {
let spec: RelaySigningKeyCertSpecifier = key_path.try_into()?;
let subject_key_path = KeyPath::Arti(spec.subject_key_specifier().arti_path()?);
let subject_key_spec: RelaySigningPublicKeySpecifier =
(&subject_key_path).try_into()?;
Ok(subject_key_spec.valid_until())
},
)?;
let removed = relaysign_removed || link_removed || sign_cert_removed;
let next_expiry = [relaysign_expiry, link_expiry, sign_cert_expiry]
.into_iter()
.flatten()
.min();
Ok((removed, next_expiry))
}
fn try_rotate_keys(now: SystemTime, keymgr: &KeyMgr) -> anyhow::Result<(bool, SystemTime)> {
let (have_rotated, min_expiry) = remove_expired_keys(now, keymgr)?;
let gen_min_expiry = try_generate_all(now, keymgr)?;
let have_rotated = have_rotated || gen_min_expiry.is_some();
let next_expiry = [min_expiry, gen_min_expiry]
.into_iter()
.flatten()
.min()
.ok_or(internal!("No relay keys after rotation task loop"))?;
Ok((have_rotated, next_expiry))
}
pub(crate) fn try_generate_keys<R: Runtime>(
runtime: &R,
keymgr: &KeyMgr,
) -> anyhow::Result<RelayIdentities> {
let now = runtime.wallclock();
generate_key::<RelayIdentityKeypair>(keymgr, &RelayIdentityKeypairSpecifier::new())?;
generate_key::<RelayIdentityRsaKeypair>(keymgr, &RelayIdentityRsaKeypairSpecifier::new())?;
let _ = try_rotate_keys(now, keymgr)?;
build_proto_identities(now, keymgr)
}
pub(crate) async fn rotate_keys_task<R: Runtime>(
runtime: R,
keymgr: Arc<KeyMgr>,
chanmgr: Arc<ChanMgr<R>>,
) -> anyhow::Result<void::Void> {
loop {
let now = runtime.wallclock();
let (have_rotated, next_expiry) = try_rotate_keys(now, &keymgr)?;
if have_rotated {
let ids = build_proto_identities(now, &keymgr)?;
chanmgr
.set_relay_identities(Arc::new(ids))
.context("Failed to set relay identities on ChanMgr")?;
}
let next_wake = next_expiry
.checked_sub(KEY_ROTATION_EXPIRE_BUFFER)
.unwrap_or(now);
runtime.sleep_until_wallclock(next_wake).await;
}
}
#[cfg(test)]
mod test {
#![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)]
use super::*;
use tor_keymgr::{ArtiEphemeralKeystore, KeyMgrBuilder};
use tor_relay_crypto::pk::{
RelayLinkSigningKeypairSpecifierPattern, RelaySigningKeypairSpecifierPattern,
};
use tor_rtcompat::SleepProvider;
use tor_rtmock::MockRuntime;
fn setup_identity_keys(keymgr: &KeyMgr) {
use tor_relay_crypto::pk::{
RelayIdentityKeypair, RelayIdentityKeypairSpecifier, RelayIdentityRsaKeypair,
RelayIdentityRsaKeypairSpecifier,
};
generate_key::<RelayIdentityKeypair>(keymgr, &RelayIdentityKeypairSpecifier::new())
.unwrap();
generate_key::<RelayIdentityRsaKeypair>(keymgr, &RelayIdentityRsaKeypairSpecifier::new())
.unwrap();
}
fn new_keymgr() -> KeyMgr {
let store = Box::new(ArtiEphemeralKeystore::new("test".to_string()));
KeyMgrBuilder::default()
.primary_store(store)
.build()
.unwrap()
}
fn setup() -> KeyMgr {
let keymgr = new_keymgr();
setup_identity_keys(&keymgr);
keymgr
}
fn to_timestamp_in_secs(valid_until: SystemTime) -> Timestamp {
use std::time::UNIX_EPOCH;
let seconds = valid_until.duration_since(UNIX_EPOCH).unwrap().as_secs();
Timestamp::from(UNIX_EPOCH + Duration::from_secs(seconds))
}
fn count_link_keys(keymgr: &KeyMgr) -> usize {
keymgr
.list_matching(
&RelayLinkSigningKeypairSpecifierPattern::new_any()
.arti_pattern()
.unwrap(),
)
.unwrap()
.len()
}
fn count_signing_keys(keymgr: &KeyMgr) -> usize {
keymgr
.list_matching(
&RelaySigningKeypairSpecifierPattern::new_any()
.arti_pattern()
.unwrap(),
)
.unwrap()
.len()
}
#[test]
fn test_bootstrap() {
MockRuntime::test_with_various(|runtime| async move {
let keymgr = new_keymgr();
let _identities = match try_generate_keys(&runtime, &keymgr) {
Ok(ident) => ident,
Err(e) => {
panic!("Unable to bootstrap keys and generate RelayIdentities: {e}");
}
};
});
}
#[test]
fn test_initial_key_generation() {
MockRuntime::test_with_various(|runtime| async move {
let keymgr = setup();
let now = runtime.wallclock();
let (rotated, next_expiry) = try_rotate_keys(now, &keymgr).unwrap();
assert!(
rotated,
"keys should be reported as generated on first rotation"
);
assert_eq!(count_link_keys(&keymgr), 1, "expected one link key");
assert_eq!(count_signing_keys(&keymgr), 1, "expected one signing key");
let expected = runtime.wallclock() + KEY_DURATION_2DAYS;
assert_eq!(
next_expiry, expected,
"next expiry should be ~{KEY_DURATION_2DAYS:?} from now, got {next_expiry:?}"
);
});
}
#[test]
fn test_rotation_on_fresh_keys() {
MockRuntime::test_with_various(|runtime| async move {
let keymgr = setup();
let now = runtime.wallclock();
try_rotate_keys(now, &keymgr).unwrap();
runtime.advance_by(Duration::from_secs(60 * 60)).await;
let (rotated, _) = try_rotate_keys(now, &keymgr).unwrap();
assert!(!rotated, "fresh keys must not trigger a rotation");
assert_eq!(count_link_keys(&keymgr), 1, "expected one link key");
assert_eq!(count_signing_keys(&keymgr), 1, "expected one signing key");
});
}
#[test]
fn test_rotation_link_key() {
MockRuntime::test_with_various(|runtime| async move {
let keymgr = setup();
try_rotate_keys(runtime.wallclock(), &keymgr).unwrap();
let just_before =
KEY_DURATION_2DAYS - KEY_ROTATION_EXPIRE_BUFFER - Duration::from_secs(1);
runtime.advance_by(just_before).await;
let (rotated, _) = try_rotate_keys(runtime.wallclock(), &keymgr).unwrap();
assert!(
!rotated,
"link key MUST NOT rotate before the expiry buffer threshold"
);
assert_eq!(count_link_keys(&keymgr), 1, "expected one link key");
assert_eq!(count_signing_keys(&keymgr), 1, "expected one signing key");
let just_after =
KEY_DURATION_2DAYS - KEY_ROTATION_EXPIRE_BUFFER + Duration::from_secs(1);
runtime.advance_by(just_after).await;
let (rotated, _) = try_rotate_keys(runtime.wallclock(), &keymgr).unwrap();
assert!(
rotated,
"link key should rotate inside the expiry buffer threshold"
);
});
}
#[test]
fn test_rotation_signing_key() {
MockRuntime::test_with_various(|runtime| async move {
let keymgr = setup();
try_rotate_keys(runtime.wallclock(), &keymgr).unwrap();
let get_key_spec = || {
let entries = keymgr
.list_matching(
&RelaySigningKeypairSpecifierPattern::new_any()
.arti_pattern()
.unwrap(),
)
.unwrap();
let entry = entries.first().unwrap();
let spec: RelaySigningKeypairSpecifier = entry.key_path().try_into().unwrap();
spec
};
let just_before =
KEY_DURATION_30DAYS - KEY_ROTATION_EXPIRE_BUFFER - Duration::from_secs(1);
runtime.advance_by(just_before).await;
let (rotated, _) = try_rotate_keys(runtime.wallclock(), &keymgr).unwrap();
assert!(rotated, "Rotation must happen after 30 days");
let spec = get_key_spec();
assert_eq!(
spec.valid_until(),
to_timestamp_in_secs(
runtime.wallclock() + KEY_ROTATION_EXPIRE_BUFFER + Duration::from_secs(1)
),
"RelaySigningKeypairSpecifier should not have rotated"
);
assert_eq!(count_link_keys(&keymgr), 1, "expected one link key");
assert_eq!(count_signing_keys(&keymgr), 1, "expected one signing key");
let just_after =
KEY_DURATION_30DAYS - KEY_ROTATION_EXPIRE_BUFFER + Duration::from_secs(1);
runtime.advance_by(just_after).await;
let (rotated, _) = try_rotate_keys(runtime.wallclock(), &keymgr).unwrap();
assert!(rotated, "Rotation must happen after 30 days");
let spec = get_key_spec();
assert_eq!(
spec.valid_until(),
to_timestamp_in_secs(runtime.wallclock() + KEY_DURATION_30DAYS),
"RelaySigningKeypairSpecifier should have rotated"
);
});
}
}