mod desc_enc;
#[cfg(feature = "hs-service")]
mod build;
mod inner;
mod middle;
mod outer;
pub mod pow;
pub use desc_enc::DecryptionError;
use tor_basic_utils::rangebounds::RangeBoundsExt;
use tor_error::internal;
use crate::{NetdocErrorKind as EK, Result};
use tor_checkable::signed::{self, SignatureGated};
use tor_checkable::timed::{self, TimerangeBound};
use tor_checkable::{SelfSigned, Timebound};
use tor_hscrypto::pk::{HsBlindId, HsClientDescEncKeypair, HsIntroPtSessionIdKey, HsSvcNtorKey};
use tor_hscrypto::{RevisionCounter, Subcredential};
use tor_linkspec::EncodedLinkSpec;
use tor_llcrypto::pk::curve25519;
use tor_units::IntegerMinutes;
use derive_builder::Builder;
use smallvec::SmallVec;
use std::result::Result as StdResult;
use std::time::SystemTime;
#[cfg(feature = "hsdesc-inner-docs")]
pub use {inner::HsDescInner, middle::HsDescMiddle, outer::HsDescOuter};
#[cfg(feature = "hs-service")]
pub use build::{HsDescBuilder, create_desc_sign_key_cert};
#[cfg(feature = "hs-dir")]
#[allow(dead_code)] pub struct StoredHsDescMeta {
blinded_id: HsBlindId,
idx_info: IndexInfo,
}
#[cfg(feature = "hs-dir")]
pub type UncheckedStoredHsDescMeta =
signed::SignatureGated<timed::TimerangeBound<StoredHsDescMeta>>;
#[derive(Debug, Clone)]
#[allow(dead_code)] struct IndexInfo {
lifetime: IntegerMinutes<u16>,
signing_cert_expires: SystemTime,
revision: RevisionCounter,
}
#[derive(Debug, Clone)]
pub struct HsDesc {
#[allow(dead_code)] idx_info: IndexInfo,
auth_required: Option<SmallVec<[IntroAuthType; 2]>>,
is_single_onion_service: bool,
intro_points: Vec<IntroPointDesc>,
pow_params: pow::PowParamSet,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Eq, PartialEq, derive_more::Display)]
pub enum IntroAuthType {
#[display("ed25519")]
Ed25519,
}
#[derive(Debug, Clone, amplify::Getters, Builder)]
#[builder(pattern = "owned")] pub struct IntroPointDesc {
#[getter(skip)]
link_specifiers: Vec<EncodedLinkSpec>,
#[builder(setter(name = "ipt_kp_ntor"))] ipt_ntor_key: curve25519::PublicKey,
#[builder(setter(name = "kp_hs_ipt_sid"))] ipt_sid_key: HsIntroPtSessionIdKey,
#[builder(setter(name = "kp_hss_ntor"))] svc_ntor_key: HsSvcNtorKey,
}
pub struct EncryptedHsDesc {
outer_doc: outer::HsDescOuter,
}
pub type UncheckedEncryptedHsDesc = signed::SignatureGated<timed::TimerangeBound<EncryptedHsDesc>>;
#[cfg(feature = "hs-dir")]
impl StoredHsDescMeta {
pub fn parse(input: &str) -> Result<UncheckedStoredHsDescMeta> {
let outer = outer::HsDescOuter::parse(input)?;
Ok(outer.dangerously_map(|timebound| {
timebound.dangerously_map(|outer| StoredHsDescMeta::from_outer_doc(&outer))
}))
}
}
impl HsDesc {
pub fn parse(
input: &str,
blinded_onion_id: &HsBlindId,
) -> Result<UncheckedEncryptedHsDesc> {
let outer = outer::HsDescOuter::parse(input)?;
let mut id_matches = false;
let result = outer.dangerously_map(|timebound| {
timebound.dangerously_map(|outer| {
id_matches = blinded_onion_id == &outer.blinded_id();
EncryptedHsDesc::from_outer_doc(outer)
})
});
if !id_matches {
return Err(
EK::BadObjectVal.with_msg("onion service descriptor did not have the expected ID")
);
}
Ok(result)
}
pub fn parse_decrypt_validate(
input: &str,
blinded_onion_id: &HsBlindId,
valid_at: SystemTime,
subcredential: &Subcredential,
hsc_desc_enc: Option<&HsClientDescEncKeypair>,
) -> StdResult<TimerangeBound<Self>, HsDescError> {
use HsDescError as E;
let unchecked_desc = Self::parse(input, blinded_onion_id)
.map_err(E::OuterParsing)?
.check_signature()
.map_err(|e| E::OuterValidation(e.into()))?;
let (inner_desc, new_bounds) = {
unchecked_desc
.is_valid_at(&valid_at)
.map_err(|e| E::OuterValidation(e.into()))?;
let inner_timerangebound = unchecked_desc
.dangerously_peek()
.decrypt(subcredential, hsc_desc_enc)?;
let new_bounds = unchecked_desc
.intersect(&inner_timerangebound)
.map(|(b1, b2)| (b1.cloned(), b2.cloned()));
(inner_timerangebound, new_bounds)
};
let hsdesc = inner_desc
.check_valid_at(&valid_at)
.map_err(|e| E::InnerValidation(e.into()))?
.check_signature()
.map_err(|e| E::InnerValidation(e.into()))?;
let new_bounds = new_bounds
.ok_or_else(|| internal!("failed to compute TimerangeBounds for a valid descriptor"))?;
Ok(TimerangeBound::new(hsdesc, new_bounds))
}
pub fn intro_points(&self) -> &[IntroPointDesc] {
&self.intro_points
}
pub fn is_single_onion_service(&self) -> bool {
self.is_single_onion_service
}
pub fn requires_intro_authentication(&self) -> bool {
self.auth_required.is_some()
}
pub fn pow_params(&self) -> &[pow::PowParams] {
self.pow_params.slice()
}
}
#[derive(Clone, Debug, thiserror::Error)]
#[non_exhaustive]
pub enum HsDescError {
#[error("Parsing failure on outer layer of an onion service descriptor.")]
OuterParsing(#[source] crate::Error),
#[error("Validation failure on outer layer of an onion service descriptor.")]
OuterValidation(#[source] crate::Error),
#[error("Decryption failure on onion service descriptor: missing decryption key")]
MissingDecryptionKey,
#[error("Decryption failure on onion service descriptor: incorrect decryption key")]
WrongDecryptionKey,
#[error("Decryption failure on onion service descriptor: could not decrypt")]
DecryptionFailed,
#[error("Parsing failure on inner layer of an onion service descriptor")]
InnerParsing(#[source] crate::Error),
#[error("Validation failure on inner layer of an onion service descriptor")]
InnerValidation(#[source] crate::Error),
#[error("Internal error: {0}")]
Bug(#[from] tor_error::Bug),
}
impl tor_error::HasKind for HsDescError {
fn kind(&self) -> tor_error::ErrorKind {
use HsDescError as E;
use tor_error::ErrorKind as EK;
match self {
E::OuterParsing(_) | E::OuterValidation(_) => EK::TorProtocolViolation,
E::MissingDecryptionKey => EK::OnionServiceMissingClientAuth,
E::WrongDecryptionKey => EK::OnionServiceWrongClientAuth,
E::DecryptionFailed | E::InnerParsing(_) | E::InnerValidation(_) => {
EK::OnionServiceProtocolViolation
}
E::Bug(e) => e.kind(),
}
}
}
impl HsDescError {
pub fn should_report_as_suspicious(&self) -> bool {
use crate::NetdocErrorKind as EK;
use HsDescError as E;
#[allow(clippy::match_like_matches_macro)]
match self {
E::OuterParsing(e) => match e.netdoc_error_kind() {
EK::ExtraneousSpace => true,
EK::WrongEndingToken => true,
EK::MissingKeyword => true,
_ => false,
},
E::OuterValidation(e) => match e.netdoc_error_kind() {
EK::BadSignature => true,
_ => false,
},
E::MissingDecryptionKey => false,
E::WrongDecryptionKey => false,
E::DecryptionFailed => false,
E::InnerParsing(_) => false,
E::InnerValidation(_) => false,
E::Bug(_) => false,
}
}
}
impl IntroPointDesc {
pub fn builder() -> IntroPointDescBuilder {
IntroPointDescBuilder::default()
}
pub fn link_specifiers(&self) -> &[EncodedLinkSpec] {
&self.link_specifiers
}
}
impl EncryptedHsDesc {
pub fn decrypt(
&self,
subcredential: &Subcredential,
hsc_desc_enc: Option<&HsClientDescEncKeypair>,
) -> StdResult<TimerangeBound<SignatureGated<HsDesc>>, HsDescError> {
use HsDescError as E;
let blinded_id = self.outer_doc.blinded_id();
let revision_counter = self.outer_doc.revision_counter();
let kp_desc_sign = self.outer_doc.desc_sign_key_id();
let middle = self
.outer_doc
.decrypt_body(subcredential)
.map_err(|_| E::DecryptionFailed)?;
let middle = std::str::from_utf8(&middle[..]).map_err(|_| {
E::InnerParsing(EK::BadObjectVal.with_msg("Bad utf-8 in middle document"))
})?;
let middle = middle::HsDescMiddle::parse(middle).map_err(E::InnerParsing)?;
let inner = middle.decrypt_inner(
&blinded_id,
revision_counter,
subcredential,
hsc_desc_enc.map(|keys| keys.secret()),
)?;
let inner = std::str::from_utf8(&inner[..]).map_err(|_| {
E::InnerParsing(EK::BadObjectVal.with_msg("Bad utf-8 in inner document"))
})?;
let (cert_signing_key, time_bound) =
inner::HsDescInner::parse(inner).map_err(E::InnerParsing)?;
if cert_signing_key.as_ref() != Some(kp_desc_sign) {
return Err(E::InnerValidation(EK::BadObjectVal.with_msg(
"Signing keys in inner document did not match those in outer document",
)));
}
let time_bound = time_bound.dangerously_map(|sig_bound| {
sig_bound.dangerously_map(|inner| HsDesc {
idx_info: IndexInfo::from_outer_doc(&self.outer_doc),
auth_required: inner.intro_auth_types,
is_single_onion_service: inner.single_onion_service,
intro_points: inner.intro_points,
pow_params: inner.pow_params,
})
});
Ok(time_bound)
}
fn from_outer_doc(outer_layer: outer::HsDescOuter) -> Self {
EncryptedHsDesc {
outer_doc: outer_layer,
}
}
}
impl IndexInfo {
fn from_outer_doc(outer: &outer::HsDescOuter) -> Self {
IndexInfo {
lifetime: outer.lifetime,
signing_cert_expires: outer.desc_signing_key_cert.expiry(),
revision: outer.revision_counter(),
}
}
}
#[cfg(feature = "hs-dir")]
impl StoredHsDescMeta {
fn from_outer_doc(outer: &outer::HsDescOuter) -> Self {
let blinded_id = outer.blinded_id();
let idx_info = IndexInfo::from_outer_doc(outer);
StoredHsDescMeta {
blinded_id,
idx_info,
}
}
}
#[cfg(any(test, feature = "testing"))]
#[allow(missing_docs)]
#[allow(clippy::missing_docs_in_private_items)]
#[allow(clippy::unwrap_used)]
pub mod test_data {
use super::*;
use hex_literal::hex;
pub const TEST_DATA: &str = include_str!("../../testdata/hsdesc1.txt");
pub const TEST_SUBCREDENTIAL: [u8; 32] =
hex!("78210A0D2C72BB7A0CAF606BCD938B9A3696894FDDDBC3B87D424753A7E3DF37");
pub const TEST_DATA_2: &str = include_str!("../../testdata/hsdesc2.txt");
pub const TEST_DATA_TIMEPERIOD_2: u64 = 19397;
pub const TEST_HSID_2: [u8; 32] =
hex!("781D978CE6CE9CAA8BCA306F53E82D2C993E5C91346625F1C151DCFC56D753D3");
pub const TEST_SUBCREDENTIAL_2: [u8; 32] =
hex!("24A133E905102BDA9A6AFE57F901366A1B8281865A91F1FE0853E4B50CC8B070");
pub const TEST_PUBKEY_2: [u8; 32] =
hex!("900467008E194C2C635A6E335E7724915E558CCB606C50094476F9731C129019");
pub const TEST_SECKEY_2: [u8; 32] =
hex!("90F2D60F917F2423F0989CE9979639435A5B01CF7AB3E0A1B900BEB892BAE443");
pub(crate) const TEST_DATA_HS_BLIND_ID: [u8; 32] =
hex!("43cc0d62fc6252f578705ca645a46109e265290343b1137e90189744b20b3f2d");
pub fn test_parsed_hsdesc() -> Result<HsDesc> {
let blinded_id = TEST_DATA_HS_BLIND_ID.into();
let desc = HsDesc::parse(TEST_DATA, &blinded_id)?
.check_signature()?
.check_valid_at(&humantime::parse_rfc3339("2023-01-23T15:00:00Z").unwrap())
.unwrap()
.decrypt(&TEST_SUBCREDENTIAL.into(), None)
.unwrap();
let desc = desc
.check_valid_at(&humantime::parse_rfc3339("2023-01-24T03:00:00Z").unwrap())
.unwrap();
let desc = desc.check_signature().unwrap();
Ok(desc)
}
}
#[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 std::time::Duration;
use super::test_data::*;
use super::*;
use hex_literal::hex;
use tor_hscrypto::{pk::HsIdKey, time::TimePeriod};
use tor_llcrypto::pk::ed25519;
#[test]
#[cfg(feature = "hs-dir")]
fn parse_meta_good() -> Result<()> {
let meta = StoredHsDescMeta::parse(TEST_DATA)?
.check_signature()?
.check_valid_at(&humantime::parse_rfc3339("2023-01-23T15:00:00Z").unwrap())
.unwrap();
assert_eq!(meta.blinded_id.as_ref(), &TEST_DATA_HS_BLIND_ID);
assert_eq!(
Duration::try_from(meta.idx_info.lifetime).unwrap(),
Duration::from_secs(60 * 180)
);
assert_eq!(
meta.idx_info.signing_cert_expires,
humantime::parse_rfc3339("2023-01-26T03:00:00Z").unwrap()
);
assert_eq!(meta.idx_info.revision, RevisionCounter::from(19655750));
Ok(())
}
#[test]
fn parse_desc_good() -> Result<()> {
let wrong_blinded_id = [12; 32].into();
let desc = HsDesc::parse(TEST_DATA, &wrong_blinded_id);
assert!(desc.is_err());
let desc = test_parsed_hsdesc()?;
assert_eq!(
Duration::try_from(desc.idx_info.lifetime).unwrap(),
Duration::from_secs(60 * 180)
);
assert_eq!(
desc.idx_info.signing_cert_expires,
humantime::parse_rfc3339("2023-01-26T03:00:00Z").unwrap()
);
assert_eq!(desc.idx_info.revision, RevisionCounter::from(19655750));
assert!(desc.auth_required.is_none());
assert_eq!(desc.is_single_onion_service, false);
assert_eq!(desc.intro_points.len(), 3);
let ipt0 = &desc.intro_points()[0];
assert_eq!(
ipt0.ipt_ntor_key().as_bytes(),
&hex!("553BF9F9E1979D6F5D5D7D20BB3FE7272E32E22B6E86E35C76A7CA8A377E402F")
);
Ok(())
}
fn get_test2_encrypted() -> EncryptedHsDesc {
let id: HsIdKey = ed25519::PublicKey::from_bytes(&TEST_HSID_2).unwrap().into();
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();
assert_eq!(period.interval_num(), TEST_DATA_TIMEPERIOD_2);
let (blind_id, subcredential) = id.compute_blinded_key(period).unwrap();
assert_eq!(
blind_id.as_bytes(),
&hex!("706628758208395D461AA0F460A5E76E7B828C66B5E794768592B451302E961D")
);
assert_eq!(subcredential.as_ref(), &TEST_SUBCREDENTIAL_2);
HsDesc::parse(TEST_DATA_2, &blind_id.into())
.unwrap()
.check_signature()
.unwrap()
.check_valid_at(&humantime::parse_rfc3339("2023-02-09T12:00:00Z").unwrap())
.unwrap()
}
#[test]
fn parse_desc_auth_missing() {
let encrypted = get_test2_encrypted();
let subcredential = TEST_SUBCREDENTIAL_2.into();
let with_no_auth = encrypted.decrypt(&subcredential, None);
assert!(with_no_auth.is_err());
}
#[test]
fn parse_desc_auth_good() {
let encrypted = get_test2_encrypted();
let subcredential = TEST_SUBCREDENTIAL_2.into();
let pk = curve25519::PublicKey::from(TEST_PUBKEY_2).into();
let sk = curve25519::StaticSecret::from(TEST_SECKEY_2).into();
let desc = encrypted
.decrypt(&subcredential, Some(&HsClientDescEncKeypair::new(pk, sk)))
.unwrap();
let desc = desc
.check_valid_at(&humantime::parse_rfc3339("2023-01-24T03:00:00Z").unwrap())
.unwrap();
let desc = desc.check_signature().unwrap();
assert_eq!(desc.intro_points.len(), 3);
}
}