mod inner;
mod middle;
mod outer;
use crate::doc::hsdesc::{IntroAuthType, IntroPointDesc};
use crate::NetdocBuilder;
use rand::{CryptoRng, RngCore};
use tor_bytes::EncodeError;
use tor_cell::chancell::msg::HandshakeType;
use tor_cert::{CertEncodeError, CertType, CertifiedKey, Ed25519Cert, EncodedEd25519Cert};
use tor_error::into_bad_api_usage;
use tor_hscrypto::pk::{HsBlindIdKey, HsBlindIdKeypair, HsSvcDescEncKeypair};
use tor_hscrypto::{RevisionCounter, Subcredential};
use tor_llcrypto::pk::curve25519;
use tor_llcrypto::pk::ed25519;
use tor_units::IntegerMinutes;
use derive_builder::Builder;
use smallvec::SmallVec;
use std::borrow::{Borrow, Cow};
use std::time::SystemTime;
use self::inner::HsDescInner;
use self::middle::HsDescMiddle;
use self::outer::HsDescOuter;
use super::desc_enc::{HsDescEncNonce, HsDescEncryption, HS_DESC_ENC_NONCE_LEN};
#[derive(Builder)]
#[builder(public, derive(Debug, Clone), pattern = "owned", build_fn(vis = ""))]
struct HsDesc<'a> {
blinded_id: &'a HsBlindIdKey,
hs_desc_sign: &'a ed25519::Keypair,
hs_desc_sign_cert: EncodedEd25519Cert,
create2_formats: &'a [HandshakeType],
auth_required: Option<SmallVec<[IntroAuthType; 2]>>,
is_single_onion_service: bool,
intro_points: &'a [IntroPointDesc],
intro_auth_key_cert_expiry: SystemTime,
intro_enc_key_cert_expiry: SystemTime,
#[builder(default)]
auth_clients: Option<&'a [curve25519::PublicKey]>,
lifetime: IntegerMinutes<u16>,
revision_counter: RevisionCounter,
subcredential: Subcredential,
}
#[derive(Debug)]
pub(super) struct ClientAuth<'a> {
ephemeral_key: HsSvcDescEncKeypair,
auth_clients: &'a [curve25519::PublicKey],
descriptor_cookie: [u8; HS_DESC_ENC_NONCE_LEN],
}
impl<'a> ClientAuth<'a> {
fn new<R: RngCore + CryptoRng>(
auth_clients: Option<&'a [curve25519::PublicKey]>,
rng: &mut R,
) -> Option<ClientAuth<'a>> {
let Some(auth_clients) = auth_clients else {
return None;
};
let descriptor_cookie = rand::Rng::gen::<[u8; HS_DESC_ENC_NONCE_LEN]>(rng);
let secret = curve25519::StaticSecret::random_from_rng(rng);
let ephemeral_key = HsSvcDescEncKeypair {
public: curve25519::PublicKey::from(&secret).into(),
secret: secret.into(),
};
Some(ClientAuth {
ephemeral_key,
auth_clients,
descriptor_cookie,
})
}
}
impl<'a> NetdocBuilder for HsDescBuilder<'a> {
fn build_sign<R: RngCore + CryptoRng>(self, rng: &mut R) -> Result<String, EncodeError> {
const SUPERENCRYPTED_ALIGN: usize = 10 * (1 << 10);
let hs_desc = self
.build()
.map_err(into_bad_api_usage!("the HsDesc could not be built"))?;
let client_auth = ClientAuth::new(hs_desc.auth_clients, rng);
let inner_plaintext = HsDescInner {
hs_desc_sign: hs_desc.hs_desc_sign,
create2_formats: hs_desc.create2_formats,
auth_required: hs_desc.auth_required.as_ref(),
is_single_onion_service: hs_desc.is_single_onion_service,
intro_points: hs_desc.intro_points,
intro_auth_key_cert_expiry: hs_desc.intro_auth_key_cert_expiry,
intro_enc_key_cert_expiry: hs_desc.intro_enc_key_cert_expiry,
}
.build_sign(rng)?;
let desc_enc_nonce = client_auth
.as_ref()
.map(|client_auth| client_auth.descriptor_cookie.into());
let inner_encrypted = hs_desc.encrypt_field(
rng,
inner_plaintext.as_bytes(),
desc_enc_nonce.as_ref(),
b"hsdir-encrypted-data",
);
let middle_plaintext = HsDescMiddle {
client_auth: client_auth.as_ref(),
subcredential: hs_desc.subcredential,
encrypted: inner_encrypted,
}
.build_sign(rng)?;
let middle_plaintext =
pad_with_zero_to_align(middle_plaintext.as_bytes(), SUPERENCRYPTED_ALIGN);
let middle_encrypted = hs_desc.encrypt_field(
rng,
middle_plaintext.borrow(),
None,
b"hsdir-superencrypted-data",
);
HsDescOuter {
hs_desc_sign: hs_desc.hs_desc_sign,
hs_desc_sign_cert: hs_desc.hs_desc_sign_cert,
lifetime: hs_desc.lifetime,
revision_counter: hs_desc.revision_counter,
superencrypted: middle_encrypted,
}
.build_sign(rng)
}
}
pub fn create_desc_sign_key_cert(
hs_desc_sign: &ed25519::PublicKey,
blind_id: &HsBlindIdKeypair,
expiry: SystemTime,
) -> Result<EncodedEd25519Cert, CertEncodeError> {
Ed25519Cert::constructor()
.cert_type(CertType::HS_BLINDED_ID_V_SIGNING)
.expiration(expiry)
.signing_key(ed25519::Ed25519Identity::from(blind_id.as_ref().public()))
.cert_key(CertifiedKey::Ed25519(hs_desc_sign.into()))
.encode_and_sign(blind_id)
}
impl<'a> HsDesc<'a> {
fn encrypt_field<R: RngCore + CryptoRng>(
&self,
rng: &mut R,
plaintext: &[u8],
desc_enc_nonce: Option<&HsDescEncNonce>,
string_const: &[u8],
) -> Vec<u8> {
let encrypt = HsDescEncryption {
blinded_id: &ed25519::Ed25519Identity::from(self.blinded_id.as_ref()).into(),
desc_enc_nonce,
subcredential: &self.subcredential,
revision: self.revision_counter,
string_const,
};
encrypt.encrypt(rng, plaintext)
}
}
fn pad_with_zero_to_align(v: &[u8], alignment: usize) -> Cow<[u8]> {
let padding = (alignment - (v.len() % alignment)) % alignment;
if padding > 0 {
let padded = v
.iter()
.copied()
.chain(std::iter::repeat(0).take(padding))
.collect::<Vec<_>>();
Cow::Owned(padded)
} else {
Cow::Borrowed(v)
}
}
#[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_duration_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
use std::net::Ipv4Addr;
use std::time::Duration;
use super::*;
use crate::doc::hsdesc::{EncryptedHsDesc, HsDesc as ParsedHsDesc};
use tor_basic_utils::test_rng::Config;
use tor_checkable::{SelfSigned, Timebound};
use tor_hscrypto::pk::{HsClientDescEncKeypair, HsIdKeypair};
use tor_hscrypto::time::TimePeriod;
use tor_linkspec::LinkSpec;
use tor_llcrypto::pk::{curve25519, ed25519::ExpandedKeypair};
pub(super) fn expect_bug(err: EncodeError) -> String {
match err {
EncodeError::Bug(b) => b.to_string(),
EncodeError::BadLengthValue => panic!("expected Bug, got BadLengthValue"),
_ => panic!("expected Bug, got unknown error"),
}
}
pub(super) fn create_intro_point_descriptor<R: RngCore + CryptoRng>(
rng: &mut R,
link_specifiers: &[LinkSpec],
) -> IntroPointDesc {
let link_specifiers = link_specifiers
.iter()
.map(|link_spec| link_spec.encode())
.collect::<Result<Vec<_>, _>>()
.unwrap();
IntroPointDesc {
link_specifiers,
ipt_ntor_key: create_curve25519_pk(rng),
ipt_sid_key: ed25519::Keypair::generate(rng).verifying_key().into(),
svc_ntor_key: create_curve25519_pk(rng).into(),
}
}
pub(super) fn create_curve25519_pk<R: RngCore + CryptoRng>(
rng: &mut R,
) -> curve25519::PublicKey {
let ephemeral_key = curve25519::EphemeralSecret::random_from_rng(rng);
(&ephemeral_key).into()
}
fn parse_hsdesc(
unparsed_desc: &str,
blinded_pk: ed25519::PublicKey,
subcredential: &Subcredential,
hsc_desc_enc: Option<&HsClientDescEncKeypair>,
) -> ParsedHsDesc {
const TIMESTAMP: &str = "2023-01-23T15:00:00Z";
let id = ed25519::Ed25519Identity::from(blinded_pk);
let enc_desc: EncryptedHsDesc = ParsedHsDesc::parse(unparsed_desc, &id.into())
.unwrap()
.check_signature()
.unwrap()
.check_valid_at(&humantime::parse_rfc3339(TIMESTAMP).unwrap())
.unwrap();
enc_desc
.decrypt(subcredential, hsc_desc_enc)
.unwrap()
.check_valid_at(&humantime::parse_rfc3339(TIMESTAMP).unwrap())
.unwrap()
.check_signature()
.unwrap()
}
#[test]
fn encode_decode() {
const CREATE2_FORMATS: &[HandshakeType] = &[HandshakeType::TAP, HandshakeType::NTOR];
const LIFETIME_MINS: u16 = 100;
const REVISION_COUNT: u64 = 2;
const CERT_EXPIRY_SECS: u64 = 60 * 60;
let mut rng = Config::Deterministic.into_rng();
let hs_id = ed25519::Keypair::generate(&mut rng);
let hs_desc_sign = ed25519::Keypair::generate(&mut rng);
let period = TimePeriod::new(
humantime::parse_duration("24 hours").unwrap(),
humantime::parse_rfc3339("2023-02-09T12:00:00Z").unwrap(),
humantime::parse_duration("12 hours").unwrap(),
)
.unwrap();
let (_, blinded_id, subcredential) = HsIdKeypair::from(ExpandedKeypair::from(&hs_id))
.compute_blinded_key(period)
.unwrap();
let expiry = SystemTime::now() + Duration::from_secs(CERT_EXPIRY_SECS);
let mut rng = Config::Deterministic.into_rng();
let intro_points = vec![IntroPointDesc {
link_specifiers: vec![LinkSpec::OrPort(Ipv4Addr::LOCALHOST.into(), 9999)
.encode()
.unwrap()],
ipt_ntor_key: create_curve25519_pk(&mut rng),
ipt_sid_key: ed25519::Keypair::generate(&mut rng).verifying_key().into(),
svc_ntor_key: create_curve25519_pk(&mut rng).into(),
}];
let hs_desc_sign_cert =
create_desc_sign_key_cert(&hs_desc_sign.verifying_key(), &blinded_id, expiry).unwrap();
let blinded_pk = (&blinded_id).into();
let builder = HsDescBuilder::default()
.blinded_id(&blinded_pk)
.hs_desc_sign(&hs_desc_sign)
.hs_desc_sign_cert(hs_desc_sign_cert)
.create2_formats(CREATE2_FORMATS)
.auth_required(None)
.is_single_onion_service(true)
.intro_points(&intro_points)
.intro_auth_key_cert_expiry(expiry)
.intro_enc_key_cert_expiry(expiry)
.lifetime(LIFETIME_MINS.into())
.revision_counter(REVISION_COUNT.into())
.subcredential(subcredential);
let encoded_desc = builder
.clone()
.build_sign(&mut Config::Deterministic.into_rng())
.unwrap();
let desc = parse_hsdesc(
encoded_desc.as_str(),
*blinded_id.as_ref().public(),
&subcredential,
None, );
let hs_desc_sign_cert =
create_desc_sign_key_cert(&hs_desc_sign.verifying_key(), &blinded_id, expiry).unwrap();
let reencoded_desc = HsDescBuilder::default()
.blinded_id(&(&blinded_id).into())
.hs_desc_sign(&hs_desc_sign)
.hs_desc_sign_cert(hs_desc_sign_cert)
.create2_formats(CREATE2_FORMATS)
.auth_required(None)
.is_single_onion_service(desc.is_single_onion_service)
.intro_points(&intro_points)
.intro_auth_key_cert_expiry(expiry)
.intro_enc_key_cert_expiry(expiry)
.lifetime(desc.idx_info.lifetime)
.revision_counter(desc.idx_info.revision)
.subcredential(subcredential)
.build_sign(&mut Config::Deterministic.into_rng())
.unwrap();
assert_eq!(&*encoded_desc, &*reencoded_desc);
let client_kp: HsClientDescEncKeypair = HsClientDescEncKeypair::generate(&mut rng);
let client_pkey = client_kp.public().as_ref();
let auth_clients = vec![*client_pkey];
let encoded_desc = builder
.auth_clients(Some(&auth_clients[..]))
.build_sign(&mut Config::Deterministic.into_rng())
.unwrap();
let desc = parse_hsdesc(
encoded_desc.as_str(),
*blinded_id.as_ref().public(),
&subcredential,
Some(&client_kp), );
let hs_desc_sign_cert =
create_desc_sign_key_cert(&hs_desc_sign.verifying_key(), &blinded_id, expiry).unwrap();
let reencoded_desc = HsDescBuilder::default()
.blinded_id(&(&blinded_id).into())
.hs_desc_sign(&hs_desc_sign)
.hs_desc_sign_cert(hs_desc_sign_cert)
.create2_formats(CREATE2_FORMATS)
.auth_required(None)
.is_single_onion_service(desc.is_single_onion_service)
.intro_points(&intro_points)
.intro_auth_key_cert_expiry(expiry)
.intro_enc_key_cert_expiry(expiry)
.auth_clients(Some(&auth_clients))
.lifetime(desc.idx_info.lifetime)
.revision_counter(desc.idx_info.revision)
.subcredential(subcredential)
.build_sign(&mut Config::Deterministic.into_rng())
.unwrap();
assert_eq!(&*encoded_desc, &*reencoded_desc);
}
}