//! Implementation for onion service descriptors. //! //! An onion service descriptor is a document generated by an onion service and //! uploaded to one or more HsDir nodes for clients to later download. It tells //! the onion service client where to find the current introduction points for //! the onion service, and how to connect to them. //! //! An onion service descriptor is more complicated than most other //! documentation types, because it is partially encrypted. #![allow(dead_code, unused_variables, clippy::missing_panics_doc)] // TODO hs: remove. mod desc_enc; mod build; mod inner; mod middle; mod outer; pub use desc_enc::DecryptionError; use crate::{ParseErrorKind as EK, Result}; use tor_checkable::signed::{self, SignatureGated}; use tor_checkable::timed::{self, TimerangeBound}; use tor_hscrypto::pk::{ HsBlindId, HsClientDescEncKey, HsClientDescEncSecretKey, HsIntroPtSessionIdKey, HsSvcNtorKey, }; use tor_hscrypto::{RevisionCounter, Subcredential}; use tor_linkspec::UnparsedLinkSpec; use tor_llcrypto::pk::curve25519; use tor_units::IntegerMinutes; use smallvec::SmallVec; use std::time::SystemTime; #[cfg(feature = "hsdesc-inner-docs")] #[cfg_attr(docsrs, doc(cfg(feature = "hsdesc-inner-docs")))] pub use {inner::HsDescInner, middle::HsDescMiddle, outer::HsDescOuter}; #[cfg(feature = "hs-service")] #[cfg_attr(docsrs, doc(cfg(feature = "hs-service")))] pub use build::HsDescBuilder; /// Metadata about an onion service descriptor, as stored at an HsDir. /// /// This object is parsed from the outermost document of an onion service /// descriptor, and used on the HsDir to maintain its index. It does not /// include the inner documents' information about introduction points, since the /// HsDir cannot decrypt those without knowing the onion service's un-blinded /// identity. /// /// The HsDir caches this value, along with the original text of the descriptor. pub struct StoredHsDescMeta { /// The blinded onion identity for this descriptor. (This is the only /// identity that the HsDir knows.) blinded_id: HsBlindId, /// Information about the expiration and revision counter for this /// descriptor. idx_info: IndexInfo, } /// An unchecked StoredHsDescMeta: parsed, but not checked for liveness or validity. pub type UncheckedStoredHsDescMeta = signed::SignatureGated>; /// Information about how long to hold a given onion service descriptor, and /// when to replace it. #[derive(Debug, Clone)] struct IndexInfo { /// The lifetime in minutes that this descriptor should be held after it is /// received. lifetime: IntegerMinutes, /// The expiration time on the `descriptor-signing-key-cert` included in this /// descriptor. signing_cert_expires: SystemTime, /// The revision counter on this descriptor: higher values should replace /// older ones. revision: RevisionCounter, } /// A decrypted, decoded onion service descriptor. /// /// This object includes information from both the outer (plaintext) document of /// the descriptor, and the inner (encrypted) documents. It tells the client the /// information it needs to contact the onion service, including necessary /// introduction points and public keys. #[derive(Debug, Clone)] pub struct HsDesc { /// Information about the expiration and revision counter for this /// descriptor. idx_info: IndexInfo, /// `KP_hsc_desc_enc`, the public key corresponding to the private key that /// we used to decrypt this descriptor. /// /// This is set to None if we did not have to use a private key to decrypt /// the descriptor. decrypted_with_id: Option, /// A list of recognized CREATE handshakes that this onion service supports. // TODO hs: this should probably be a caret enum, not an integer // TODO hs: Add this if we actually need it. // create2_formats: Vec, /// A list of authentication types that this onion service supports. auth_required: Option>, /// If true, this a "single onion service" and is not trying to keep its own location private. is_single_onion_service: bool, /// One or more introduction points used to contact the onion service. intro_points: Vec, } /// A type of authentication that is required when introducing to an onion /// service. #[non_exhaustive] #[derive(Debug, Clone, Copy, Eq, PartialEq, derive_more::Display)] pub enum IntroAuthType { /// Ed25519 authentication is required. #[display(fmt = "ed25519")] Ed25519, } /// Information in an onion service descriptor about a single /// introduction point. #[derive(Debug, Clone)] pub struct IntroPointDesc { /// A list of link specifiers needed to extend a circuit to the introduction point. /// /// These can include public keys and network addresses. // // TODO hs: perhaps we should make certain link specifiers mandatory? That // would make it possible for IntroPointDesc to implement CircTarget. link_specifiers: Vec, /// The key used to extend a circuit _to the introduction point_, using the /// ntor or ntor3 handshakes. (`KP_ntor`) ipt_ntor_key: curve25519::PublicKey, /// A key used to identify the onion service at this introduction point. /// (`KP_hs_ipt_sid`) ipt_sid_key: HsIntroPtSessionIdKey, /// `KP_hss_ntor`, the key used to encrypt a handshake _to the onion /// service_ when using this introduction point. /// /// The onion service uses a separate key of this type with each /// introduction point as part of its strategy for preventing replay /// attacks. svc_ntor_key: HsSvcNtorKey, } /// An onion service after it has been parsed by the client, but not yet decrypted. pub struct EncryptedHsDesc { /// The un-decoded outer document of our onion service descriptor. outer_doc: outer::HsDescOuter, } /// An unchecked HsDesc: parsed, but not checked for liveness or validity. pub type UncheckedEncryptedHsDesc = signed::SignatureGated>; impl StoredHsDescMeta { // TODO hs: needs accessor functions too. (Let's not use public fields; we // are likely to want to mess with the repr of these types.) /// Parse the outermost layer of the descriptor in `input`, and return the /// resulting metadata (if possible). pub fn parse(input: &str) -> Result { let outer = outer::HsDescOuter::parse(input)?; Ok(outer.dangerously_map(|timebound| { timebound.dangerously_map(|outer| StoredHsDescMeta::from_outer_doc(&outer)) })) } } impl HsDesc { // TODO hs: needs accessor functions too. (Let's not use public fields; we // are likely to want to mess with the repr of these types.) /// Parse the outermost document of the descriptor in `input`, and validate /// that its identity is consistent with `blinded_onion_id`. /// /// On success, the caller will get a wrapped object which they must /// validate and then decrypt. pub fn parse( input: &str, // We don't actually need this to parse the HsDesc, but we _do_ need it to prevent // a nasty pattern where we forget to check that we got the right one. blinded_onion_id: &HsBlindId, ) -> Result { 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 { // TODO hs: This errorkind is not quite right. return Err( EK::BadObjectVal.with_msg("onion service descriptor did not have the expected ID") ); } Ok(result) } } impl EncryptedHsDesc { /// Attempt to decrypt both layers of encryption in this onion service /// descriptor. /// /// If `hsc_desc_enc` is provided, we use it to decrypt the inner encryption layer; /// otherwise, we require that the inner document is encrypted using the "no /// client authorization" method. /// /// Note that `hsc_desc_enc` must be a key *pair* - ie, a KP_hsc_desc_enc /// and corresponding KS_hsc_desc_enc. This function **does not check** /// this. // // TODO hs: I'm not sure that taking `hsc_desc_enc` as an argument is correct. Instead, maybe // we should take a set of keys? pub fn decrypt( self, subcredential: &Subcredential, hsc_desc_enc: Option<(&HsClientDescEncKey, &HsClientDescEncSecretKey)>, ) -> Result>> { 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(); // Decrypt the superencryption layer; parse the middle document. let middle = self.outer_doc.decrypt_body(subcredential).map_err(|e| { EK::BadObjectVal.with_msg("onion service descriptor superencryption failed.") })?; let middle = std::str::from_utf8(&middle[..]) .map_err(|e| EK::BadObjectVal.with_msg("Bad utf-8 in middle document"))?; let middle = middle::HsDescMiddle::parse(middle)?; // Decrypt the encryption layer and parse the inner document. let inner = middle .decrypt_inner( &blinded_id, revision_counter, subcredential, hsc_desc_enc.map(|keys| keys.1), ) .map_err(|e| { EK::BadObjectVal.with_msg("onion service descriptor encryption failed.") })?; let inner = std::str::from_utf8(&inner[..]) .map_err(|e| EK::BadObjectVal.with_msg("Bad utf-8 in inner document"))?; let (cert_signing_key, time_bound) = inner::HsDescInner::parse(inner)?; if cert_signing_key.as_ref() != Some(kp_desc_sign) { return Err(EK::BadObjectVal .with_msg("Signing keys in inner document did not match those in outer document")); } // Construct the HsDesc! let time_bound = time_bound.dangerously_map(|sig_bound| { sig_bound.dangerously_map(|inner| HsDesc { idx_info: IndexInfo::from_outer_doc(&self.outer_doc), decrypted_with_id: hsc_desc_enc.map(|keys| keys.0.clone()), auth_required: inner.intro_auth_types, is_single_onion_service: inner.single_onion_service, intro_points: inner.intro_points, }) }); Ok(time_bound) } /// Create a new `IndexInfo` from the outer part of an onion service descriptor. fn from_outer_doc(outer_layer: outer::HsDescOuter) -> Self { EncryptedHsDesc { outer_doc: outer_layer, } } } impl IndexInfo { /// Create a new `IndexInfo` from the outer part of an onion service descriptor. 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, } } } impl StoredHsDescMeta { /// Create a new `StoredHsDescMeta` from the outer part of an onion service descriptor. 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(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::print_stderr)] #![allow(clippy::print_stdout)] #![allow(clippy::single_char_pattern)] #![allow(clippy::unwrap_used)] #![allow(clippy::unchecked_duration_subtraction)] //! use std::time::Duration; use super::*; use hex_literal::hex; use tor_checkable::{SelfSigned, Timebound}; use tor_hscrypto::{pk::HsIdKey, time::TimePeriod}; use tor_llcrypto::pk::ed25519; pub(super) const TEST_DATA: &str = include_str!("../../testdata/hsdesc1.txt"); pub(super) const TEST_SUBCREDENTIAL: [u8; 32] = hex!("78210A0D2C72BB7A0CAF606BCD938B9A3696894FDDDBC3B87D424753A7E3DF37"); // This HsDesc uses DescEnc authentication. pub(super) const TEST_DATA_2: &str = include_str!("../../testdata/hsdesc2.txt"); pub(super) const TEST_DATA_TIMEPERIOD_2: u64 = 19397; // paozpdhgz2okvc6kgbxvh2bnfsmt4xergrtcl4obkhopyvwxkpjzvoad.onion pub(super) const TEST_HSID_2: [u8; 32] = hex!("781D978CE6CE9CAA8BCA306F53E82D2C993E5C91346625F1C151DCFC56D753D3"); pub(super) const TEST_SUBCREDENTIAL_2: [u8; 32] = hex!("24A133E905102BDA9A6AFE57F901366A1B8281865A91F1FE0853E4B50CC8B070"); // SACGOAEODFGCYY22NYZV45ZESFPFLDGLMBWFACKEO34XGHASSAMQ (base32) pub(super) const TEST_PUBKEY_2: [u8; 32] = hex!("900467008E194C2C635A6E335E7724915E558CCB606C50094476F9731C129019"); // SDZNMD4RP4SCH4EYTTUZPFRZINNFWAOPPKZ6BINZAC7LREV24RBQ (base32) pub(super) const TEST_SECKEY_2: [u8; 32] = hex!("90F2D60F917F2423F0989CE9979639435A5B01CF7AB3E0A1B900BEB892BAE443"); #[test] 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(), &hex!("43cc0d62fc6252f578705ca645a46109e265290343b1137e90189744b20b3f2d") ); 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 blinded_id = hex!("43cc0d62fc6252f578705ca645a46109e265290343b1137e90189744b20b3f2d").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)?; let desc = desc .check_valid_at(&humantime::parse_rfc3339("2023-01-24T03:00:00Z").unwrap()) .unwrap(); let desc = desc.check_signature().unwrap(); 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.decrypted_with_id.is_none()); assert!(desc.auth_required.is_none()); assert_eq!(desc.is_single_onion_service, false); assert_eq!(desc.intro_points.len(), 3); // TODO hs: add checks that the intro point fields are as expected. Ok(()) } /// Get an EncryptedHsDesc corresponding to `TEST_DATA_2`. 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() { // If we try to decrypt TEST_DATA_2 with no ClientDescEncKey, we get a // failure. 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() { // But if we try to decrypt TEST_DATA_2 with the correct ClientDescEncKey, we get a // the data inside! 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((&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); } }