Add builder for encoding hidden service descriptors.

This introduces the `NetdocBuilder` trait described in
`netdoc-builder.md` and a new `tor-netdoc::doc::hsdesc::build` module,
which exports the `HsDescBuilder`. Hidden services will use
`HsDescBuilder` to build and encode hidden service descriptors.

There are several TODOs in the code that I'm planning to address
separately.

Partially addresses #745.

Signed-off-by: Gabriela Moldovan <gabi@torproject.org>
This commit is contained in:
Gabi Moldovan 2023-03-16 13:51:29 +00:00 committed by Gabriela Moldovan
parent 89ca965d2a
commit a1074c0027
16 changed files with 1280 additions and 44 deletions

2
Cargo.lock generated
View File

@ -4075,6 +4075,7 @@ dependencies = [
"cipher", "cipher",
"ctr", "ctr",
"curve25519-dalek", "curve25519-dalek",
"derive_more",
"digest 0.10.6", "digest 0.10.6",
"ed25519-dalek", "ed25519-dalek",
"getrandom 0.2.8", "getrandom 0.2.8",
@ -4146,6 +4147,7 @@ dependencies = [
"base64ct", "base64ct",
"bitflags 2.0.0", "bitflags 2.0.0",
"cipher", "cipher",
"derive_builder_fork_arti",
"derive_more", "derive_more",
"digest 0.10.6", "digest 0.10.6",
"educe", "educe",

View File

@ -64,7 +64,16 @@ pub struct Subcredential([u8; 32]);
/// There can be gaps in this numbering. A descriptor with a higher-valued /// There can be gaps in this numbering. A descriptor with a higher-valued
/// revision counter supersedes one with a lower revision counter. /// revision counter supersedes one with a lower revision counter.
#[derive( #[derive(
Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, derive_more::From, derive_more::Into, Copy,
Clone,
Debug,
Ord,
PartialOrd,
Eq,
PartialEq,
derive_more::Deref,
derive_more::From,
derive_more::Into,
)] )]
pub struct RevisionCounter(u64); pub struct RevisionCounter(u64);

View File

@ -31,6 +31,7 @@ base64ct = "1.5.1"
cipher = { version = "0.4.3", optional = true, features = ["zeroize"] } cipher = { version = "0.4.3", optional = true, features = ["zeroize"] }
ctr = { version = "0.9", features = ["zeroize"] } ctr = { version = "0.9", features = ["zeroize"] }
curve25519-dalek = "3.2" curve25519-dalek = "3.2"
derive_more = "0.99.3"
digest = "0.10.0" digest = "0.10.0"
ed25519-dalek = { version = "1", features = ["batch"] } ed25519-dalek = { version = "1", features = ["batch"] }
hex = "0.4" hex = "0.4"

View File

@ -15,7 +15,7 @@ use zeroize::Zeroize;
/// (The decision to avoid implementing `Deref`/`DerefMut` is deliberate.) /// (The decision to avoid implementing `Deref`/`DerefMut` is deliberate.)
#[allow(renamed_and_removed_lints)] // TODO Remove @ MSRV 1.68 #[allow(renamed_and_removed_lints)] // TODO Remove @ MSRV 1.68
#[allow(clippy::derive_hash_xor_eq)] // TODO Rename @ MSRV 1.68 #[allow(clippy::derive_hash_xor_eq)] // TODO Rename @ MSRV 1.68
#[derive(Clone, Copy, Debug, Hash, Zeroize)] #[derive(Clone, Copy, Debug, Hash, Zeroize, derive_more::Deref)]
pub struct CtByteArray<const N: usize>([u8; N]); pub struct CtByteArray<const N: usize>([u8; N]);
impl<const N: usize> ConstantTimeEq for CtByteArray<N> { impl<const N: usize> ConstantTimeEq for CtByteArray<N> {

View File

@ -33,11 +33,11 @@ routerdesc = []
# Enable the "ns consensus" document type, which some relays cache and serve. # Enable the "ns consensus" document type, which some relays cache and serve.
ns_consensus = [] ns_consensus = []
# Client-side and service-side support for onion services. # Client-side and service-side support for onion services.
# Experimental: not covered by semver guarantees. # Experimental: not covered by semver guarantees.
# TODO hs: mark these as part of "full" once they are done and stable. # TODO hs: mark these as part of "full" once they are done and stable.
hs-client = ["hs-common"] hs-client = ["hs-common"]
hs-service = ["hs-common", "rand"] hs-service = ["hs-common", "rand", "tor-cert/encode"]
hs-common = ["tor-hscrypto", "tor-linkspec", "tor-units"] hs-common = ["tor-hscrypto", "tor-linkspec", "tor-units"]
# Testing only : expose code to parse inner layers of onion service descriptors. # Testing only : expose code to parse inner layers of onion service descriptors.
hsdesc-inner-docs = ["visibility"] hsdesc-inner-docs = ["visibility"]
@ -64,6 +64,7 @@ arrayref = "0.3"
base64ct = { version = "1.5.1", features = ["alloc"] } base64ct = { version = "1.5.1", features = ["alloc"] }
bitflags = "2" bitflags = "2"
cipher = { version = "0.4.1", features = ["zeroize"] } cipher = { version = "0.4.1", features = ["zeroize"] }
derive_builder = { version = "0.11.2", package = "derive_builder_fork_arti" }
derive_more = "0.99.3" derive_more = "0.99.3"
digest = "0.10.0" digest = "0.10.0"
educe = "0.4.6" educe = "0.4.6"

View File

@ -27,7 +27,9 @@ use std::time::SystemTime;
use base64ct::{Base64, Encoding}; use base64ct::{Base64, Encoding};
use humantime::format_rfc3339; use humantime::format_rfc3339;
use tor_bytes::EncodeError; use tor_bytes::EncodeError;
use tor_cert::Ed25519Cert;
use tor_error::{internal, into_internal, Bug}; use tor_error::{internal, into_internal, Bug};
use tor_llcrypto::pk::ed25519;
use crate::parse::keyword::Keyword; use crate::parse::keyword::Keyword;
use crate::parse::tokenize::tag_keywords_ok; use crate::parse::tokenize::tag_keywords_ok;
@ -38,6 +40,7 @@ use crate::parse::tokenize::tag_keywords_ok;
/// for clarity in function signatures etc. /// for clarity in function signatures etc.
// //
// TODO hs: Is this type carrying its weight, or should we just replace it with String ? // TODO hs: Is this type carrying its weight, or should we just replace it with String ?
#[derive(Debug)]
pub struct NetdocText<Builder> { pub struct NetdocText<Builder> {
/// The actual document /// The actual document
text: String, text: String,
@ -52,6 +55,19 @@ impl<B> Deref for NetdocText<B> {
} }
} }
impl<B> NetdocText<B> {
/// Convert self into an identical `NetdocText` of a different kind.
///
/// Useful for marking the `NetdocText` as having been produced using a different builder (for
/// example, in cases where builders leverage other builders to create the final text).
pub(crate) fn into_kind<K>(self) -> NetdocText<K> {
NetdocText {
text: self.text,
kind: PhantomData,
}
}
}
/// Encoder, representing a partially-built document /// Encoder, representing a partially-built document
/// ///
/// # Example /// # Example
@ -210,7 +226,7 @@ impl NetdocEncoder {
} }
/// Build the document into textual form /// Build the document into textual form
pub(crate) fn finish(self) -> Result<NetdocText<Self>, Bug> { pub(crate) fn finish<B>(self) -> Result<NetdocText<B>, Bug> {
let text = self.built?; let text = self.built?;
Ok(NetdocText { Ok(NetdocText {
text, text,
@ -369,6 +385,14 @@ impl Drop for ItemEncoder<'_> {
} }
} }
/// A trait for building and signing netdocs.
pub trait NetdocBuilder {
/// Build the document into textual form.
fn build_sign(self) -> Result<NetdocText<Self>, EncodeError>
where
Self: Sized;
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
// @@ begin test lint list maintained by maint/add_warning @@ // @@ begin test lint list maintained by maint/add_warning @@
@ -428,7 +452,7 @@ qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE";
.item(ACK::DIR_KEY_CERTIFICATION) .item(ACK::DIR_KEY_CERTIFICATION)
.object("SIGNATURE", []); .object("SIGNATURE", []);
let doc = encode.finish().unwrap(); let doc = encode.finish::<NetdocEncoder>().unwrap();
eprintln!("{}", &*doc); eprintln!("{}", &*doc);
assert_eq!( assert_eq!(
&*doc, &*doc,

View File

@ -11,34 +11,37 @@
#![allow(dead_code, unused_variables, clippy::missing_panics_doc)] // TODO hs: remove. #![allow(dead_code, unused_variables, clippy::missing_panics_doc)] // TODO hs: remove.
mod desc_enc; mod desc_enc;
mod build;
mod inner; mod inner;
mod middle; mod middle;
mod outer; mod outer;
use std::time::SystemTime; pub use desc_enc::DecryptionError;
use crate::{ParseErrorKind as EK, Result}; use crate::{ParseErrorKind as EK, Result};
pub use desc_enc::DecryptionError;
use smallvec::SmallVec; use tor_checkable::signed::{self, SignatureGated};
use tor_checkable::{ use tor_checkable::timed::{self, TimerangeBound};
signed::{self, SignatureGated}, use tor_hscrypto::pk::{
timed::{self, TimerangeBound}, HsBlindId, HsClientDescEncKey, HsClientDescEncSecretKey, HsIntroPtSessionIdKey, HsSvcNtorKey,
};
use tor_hscrypto::{
pk::{
HsBlindId, HsClientDescEncKey, HsClientDescEncSecretKey, HsIntroPtSessionIdKey,
HsSvcNtorKey,
},
RevisionCounter, Subcredential,
}; };
use tor_hscrypto::{RevisionCounter, Subcredential};
use tor_linkspec::UnparsedLinkSpec; use tor_linkspec::UnparsedLinkSpec;
use tor_llcrypto::pk::curve25519; use tor_llcrypto::pk::curve25519;
use tor_units::IntegerMinutes; use tor_units::IntegerMinutes;
use smallvec::SmallVec;
use std::time::SystemTime;
#[cfg(feature = "hsdesc-inner-docs")] #[cfg(feature = "hsdesc-inner-docs")]
#[cfg_attr(docsrs, doc(cfg(feature = "hsdesc-inner-docs")))] #[cfg_attr(docsrs, doc(cfg(feature = "hsdesc-inner-docs")))]
pub use {inner::HsDescInner, middle::HsDescMiddle, outer::HsDescOuter}; 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. /// Metadata about an onion service descriptor, as stored at an HsDir.
/// ///
/// This object is parsed from the outermost document of an onion service /// This object is parsed from the outermost document of an onion service
@ -113,9 +116,11 @@ pub struct HsDesc {
/// A type of authentication that is required when introducing to an onion /// A type of authentication that is required when introducing to an onion
/// service. /// service.
#[derive(Debug, Clone, Copy, Eq, PartialEq)] #[non_exhaustive]
enum IntroAuthType { #[derive(Debug, Clone, Copy, Eq, PartialEq, derive_more::Display)]
pub enum IntroAuthType {
/// Ed25519 authentication is required. /// Ed25519 authentication is required.
#[display(fmt = "ed25519")]
Ed25519, Ed25519,
} }
@ -300,11 +305,6 @@ impl StoredHsDescMeta {
} }
} }
// TODO hs: Define a HsDescBuilder structure, but it should not create an HsDesc directly.
// Instead, it should make something that is _like_ an HsDesc but with extra client keys,
// full certificates and so on. Then, define a function taking the correct set of private
// keys and using them to encode, encrypt, and sign the built HsDesc.
#[cfg(test)] #[cfg(test)]
mod test { mod test {
// @@ begin test lint list maintained by maint/add_warning @@ // @@ begin test lint list maintained by maint/add_warning @@
@ -316,7 +316,7 @@ mod test {
#![allow(clippy::single_char_pattern)] #![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_duration_subtraction)] #![allow(clippy::unchecked_duration_subtraction)]
//! <!-- @@ end test lint list maintained by maint/add_warning @@ --> //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
use std::time::Duration; use std::time::Duration;
use super::*; use super::*;

View File

@ -0,0 +1,384 @@
//! Hidden service descriptor encoding.
mod inner;
mod middle;
mod outer;
use crate::doc::hsdesc::{IntroAuthType, IntroPointDesc};
use crate::{NetdocBuilder, NetdocText};
use tor_bytes::EncodeError;
use tor_error::into_bad_api_usage;
use tor_hscrypto::{pk::HsSvcDescEncKey, RevisionCounter, Subcredential};
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::HsDescInnerBuilder;
use self::middle::HsDescMiddleBuilder;
use self::outer::HsDescOuterBuilder;
use super::desc_enc::{HsDescEncNonce, HsDescEncryption, HS_DESC_ENC_NONCE_LEN};
use super::middle::AuthClient;
/// A builder for encoding hidden service descriptors.
///
/// TODO hs: a comprehensive usage example.
#[derive(Builder)]
#[builder(public, derive(Debug), pattern = "owned", build_fn(vis = ""))]
struct HsDesc<'a> {
/// The blinded hidden service signing keys used to sign descriptor signing keys
/// (KP_hs_blind_id, KS_hs_blind_id).
///
/// TODO hs: we might want to use a HsBlindIdKey/HsBlindIdSecretKey keypair instead. However,
/// it looks like that will require some work: Ed25519CertConstructor::encode_and_sign needs to
/// be changed to support types other than ed25519::Keypairs (i.e. skey will need to be of type
/// S: Signer + PublicKey, where PublicKey is a trait with a .pk() function that returns an
/// ed25519::PublicKey).
blinded_id: &'a ed25519::Keypair,
/// The short-term descriptor signing key (KP_hs_desc_sign, KS_hs_desc_sign).
hs_desc_sign: &'a ed25519::Keypair,
/// The expiration time of the descriptor signing key certificate.
hs_desc_sign_cert_expiry: SystemTime,
/// A list of recognized CREATE handshakes that this onion service supports.
// TODO hs: this should probably be a caret enum, not an integer
create2_formats: &'a [u32],
/// A list of authentication types that this onion service supports.
auth_required: Option<SmallVec<[IntroAuthType; 2]>>,
/// 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: &'a [IntroPointDesc],
/// The expiration time of an introduction point authentication key certificate.
intro_auth_key_cert_expiry: SystemTime,
/// The expiration time of an introduction point encryption key certificate.
intro_enc_key_cert_expiry: SystemTime,
/// Client authorization parameters, if client authentication is enabled. If set to `None`,
/// client authentication is disabled.
client_auth: Option<&'a ClientAuth>,
/// The lifetime of this descriptor, in minutes.
///
/// This doesn't actually list the starting time or the end time for the
/// descriptor: presumably, because we didn't want to leak the onion
/// service's view of the wallclock.
lifetime: IntegerMinutes<u16>,
/// A revision counter to tell whether this descriptor is more or less recent
/// than another one for the same blinded ID.
revision_counter: RevisionCounter,
/// The "subcredential" of the onion service.
subcredential: Subcredential,
}
/// Client authorization parameters.
// TODO HS this ought to go away from the public API (see TODO below), or use a builder?
#[derive(Clone, Debug)]
pub struct ClientAuth {
/// The ephemeral x25519 ephemeral public key generated by the hidden service
/// (`KP_hss_desc_enc`).
pub ephemeral_key: HsSvcDescEncKey,
/// One or more authorized clients, and the key exchange information that
/// they use to compute shared keys for decrypting the encryption layer.
///
/// If client authorization is disabled (i.e. this array is empty), the resulting middle
/// document will contain a single auth-client client populated with random values.
///
/// TODO hs: currently it is the responsibility of the hidden service to create an `AuthClient`
/// for each authorized client. Instead of using `Vec<AuthClient>` here, it would be better to
/// just have a list of public keys (one for each authorized client), and let
/// `HsDescMiddleBuilder` create the underlying `AuthClient`.
pub auth_clients: Vec<AuthClient>,
/// The value of `N_hs_desc_enc` descriptor_cookie key generated by the hidden service.
///
/// TODO hs: Do we even need this field? This is presumed to be randomly generated for each
/// descriptor by the hidden service, but since it's random, we might as well let the
/// descriptor builder generate it.
pub descriptor_cookie: [u8; HS_DESC_ENC_NONCE_LEN],
}
impl<'a> NetdocBuilder for HsDescBuilder<'a> {
fn build_sign(self) -> Result<NetdocText<Self>, EncodeError> {
/// The superencrypted field must be padded to the nearest multiple of 10k bytes
///
/// rend-spec-v3 2.5.1.1
const SUPERENCRYPTED_ALIGN: usize = 10 * (1 << 10);
let hs_desc = self
.build()
.map_err(into_bad_api_usage!("the HsDesc could not be built"))?;
// Construct the second layer plaintext. This is the unencrypted value of the "encrypted"
// field.
let encrypted_plaintext = HsDescInnerBuilder::default()
.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()?;
let desc_enc_nonce = hs_desc
.client_auth
.as_ref()
.map(|client_auth| client_auth.descriptor_cookie.into());
// Encrypt the second layer to obtain the "encrypted" field
let encrypted = hs_desc.encrypt_field(
encrypted_plaintext.as_bytes(),
desc_enc_nonce.as_ref(),
b"hsdir-encrypted-data",
);
// Construct the first layer plaintext. This is the unencrypted value of the
// "superencrypted" field.
let superencrypted_plaintext = HsDescMiddleBuilder::default()
.client_auth(hs_desc.client_auth)
.encrypted(encrypted)
.build_sign()?;
// Section 2.5.1.1. of rend-spec-v3: before encryption, pad the plaintext to the nearest
// multiple of 10k bytes
let superencrypted_plaintext =
pad_with_zero_to_align(superencrypted_plaintext.as_bytes(), SUPERENCRYPTED_ALIGN);
// Encrypt the first layer to obtain the "superencrypted" field.
let superencrypted = hs_desc.encrypt_field(
superencrypted_plaintext.borrow(),
// desc_enc_nonce is absent when handling the superencryption layer (2.5.1.1).
None,
b"hsdir-superencrypted-data",
);
// Finally, build the hidden service descriptor.
HsDescOuterBuilder::default()
.blinded_id(hs_desc.blinded_id)
.hs_desc_sign(hs_desc.hs_desc_sign)
.hs_desc_sign_cert_expiry(hs_desc.hs_desc_sign_cert_expiry)
.lifetime(hs_desc.lifetime)
.revision_counter(hs_desc.revision_counter)
.superencrypted(superencrypted)
.build_sign()
.map(NetdocText::into_kind)
}
}
impl<'a> HsDesc<'a> {
/// Encrypt the specified plaintext using the algorithm described in section
/// `[HS-DESC-ENCRYPTION-KEYS]` of rend-spec-v3.txt.
fn encrypt_field(
&self,
plaintext: &[u8],
desc_enc_nonce: Option<&HsDescEncNonce>,
string_const: &[u8],
) -> Vec<u8> {
let encrypt = HsDescEncryption {
blinded_id: &ed25519::Ed25519Identity::from(self.blinded_id.public).into(),
desc_enc_nonce,
subcredential: &self.subcredential,
revision: self.revision_counter,
string_const,
};
encrypt.encrypt(&mut rand::thread_rng(), plaintext)
}
}
/// Pad `v` with zeroes to the next multiple of `alignment`.
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 {
// No need to pad.
Cow::Borrowed(v)
}
}
#[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)]
//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
use std::time::Duration;
use super::*;
use crate::doc::hsdesc::test::TEST_SUBCREDENTIAL;
use crate::doc::hsdesc::{EncryptedHsDesc, HsDesc as HsDescDecoder};
use tor_basic_utils::test_rng::testing_rng;
use tor_checkable::{SelfSigned, Timebound};
use tor_hscrypto::time::TimePeriod;
use tor_linkspec::UnparsedLinkSpec;
use tor_llcrypto::pk::curve25519;
use tor_llcrypto::util::rand_compat::RngCompatExt;
// TODO: move the test helpers and constants to a separate module and make them more broadly
// available if necessary.
pub(super) const TEST_CURVE25519_PUBLIC1: [u8; 32] = [
182, 113, 33, 95, 205, 245, 236, 169, 54, 55, 168, 104, 105, 203, 2, 43, 72, 171, 252, 178,
132, 220, 55, 15, 129, 137, 67, 35, 147, 138, 122, 8,
];
pub(super) const TEST_CURVE25519_PUBLIC2: [u8; 32] = [
115, 163, 198, 37, 3, 64, 168, 156, 114, 124, 46, 142, 233, 91, 239, 29, 207, 240, 128,
202, 208, 112, 170, 247, 82, 46, 233, 6, 251, 246, 117, 113,
];
pub(super) const TEST_ED_KEYPAIR: [u8; 64] = [
164, 100, 212, 102, 173, 112, 229, 145, 212, 233, 189, 78, 124, 100, 245, 20, 102, 4, 108,
203, 245, 104, 234, 23, 9, 111, 238, 233, 53, 88, 41, 157, 236, 25, 168, 191, 85, 102, 73,
11, 12, 101, 80, 225, 230, 28, 9, 208, 127, 219, 229, 239, 42, 166, 147, 232, 55, 206, 57,
210, 10, 215, 54, 60,
];
// Not a real cookie, just a bunch of ones.
pub(super) const TEST_DESCRIPTOR_COOKIE: [u8; HS_DESC_ENC_NONCE_LEN] =
[1; HS_DESC_ENC_NONCE_LEN];
/// Expect `err` to be a `Bug`, and return its string representation.
///
/// # Panics
///
/// Panics if `err` is not a `Bug`.
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"),
}
}
/// Some tests require determinism, so always return the same keypair.
pub(super) fn test_ed25519_keypair() -> ed25519::Keypair {
ed25519::Keypair::from_bytes(&TEST_ED_KEYPAIR).unwrap()
}
/// Create a new ed25519 keypair.
pub(super) fn create_ed25519_keypair() -> ed25519::Keypair {
let mut rng = testing_rng().rng_compat();
ed25519::Keypair::generate(&mut rng)
}
/// Create a new curve25519 public key.
pub(super) fn create_curve25519_pk() -> curve25519::PublicKey {
let rng = testing_rng().rng_compat();
let ephemeral_key = curve25519::EphemeralSecret::new(rng);
(&ephemeral_key).into()
}
pub(super) fn test_intro_point_descriptor(
link_specifiers: Vec<UnparsedLinkSpec>,
) -> IntroPointDesc {
IntroPointDesc {
link_specifiers,
ipt_ntor_key: curve25519::PublicKey::from(TEST_CURVE25519_PUBLIC1),
ipt_sid_key: test_ed25519_keypair().public.into(),
svc_ntor_key: curve25519::PublicKey::from(TEST_CURVE25519_PUBLIC2).into(),
}
}
#[test]
fn encode_decode() {
let blinded_id = test_ed25519_keypair();
let hs_desc_sign = test_ed25519_keypair();
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();
// TODO: not a valid subcredential
let subcredential = TEST_SUBCREDENTIAL.into();
let expiry = SystemTime::now() + Duration::from_secs(60 * 60);
let intro_points = vec![IntroPointDesc {
link_specifiers: vec![UnparsedLinkSpec::new(0, vec![1, 2, 3, 4])],
ipt_ntor_key: create_curve25519_pk(),
ipt_sid_key: create_ed25519_keypair().public.into(),
svc_ntor_key: create_curve25519_pk().into(),
}];
// Build and encode a new descriptor:
let encoded_desc = HsDescBuilder::default()
.blinded_id(&blinded_id)
.hs_desc_sign(&hs_desc_sign)
.hs_desc_sign_cert_expiry(expiry)
.create2_formats(&[1, 2])
.auth_required(None)
.is_single_onion_service(true)
.intro_points(&intro_points)
.intro_auth_key_cert_expiry(expiry)
.intro_enc_key_cert_expiry(expiry)
.client_auth(None)
.lifetime(100.into())
.revision_counter(2.into())
.subcredential(subcredential)
.build_sign()
.unwrap();
let id = ed25519::Ed25519Identity::from(&blinded_id.public);
// Now decode it
let enc_desc: EncryptedHsDesc = HsDescDecoder::parse(&encoded_desc, &id.into())
.unwrap()
.check_signature()
.unwrap()
.check_valid_at(&humantime::parse_rfc3339("2023-01-23T15:00:00Z").unwrap())
.unwrap();
let desc = enc_desc
.decrypt(&subcredential, None)
.unwrap()
.check_valid_at(&humantime::parse_rfc3339("2023-01-23T15:00:00Z").unwrap())
.unwrap()
.check_signature()
.unwrap();
// Now encode it again and check the result is identical to the original
let reencoded_desc = HsDescBuilder::default()
.blinded_id(&blinded_id)
.hs_desc_sign(&hs_desc_sign)
.hs_desc_sign_cert_expiry(expiry)
// create2_formats is hard-coded rather than extracted from desc, because
// create2_formats is ignored while parsing
.create2_formats(&[1, 2])
.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)
.client_auth(None)
.lifetime(desc.idx_info.lifetime)
.revision_counter(desc.idx_info.revision)
.subcredential(subcredential)
.build_sign()
.unwrap();
// TODO: a more useful assertion. The two won't be identical unless client auth is enabled
// (if client auth is disabled, the builder generates a new desc-auth-ephemeral-key and a
// client-auth line filled with random values, which will be different for each descriptor).
//assert_eq!(&*encoded_desc, &*reencoded_desc);
}
// TODO hs: encode a descriptor with client auth enabled
}

View File

@ -0,0 +1,384 @@
//! Functionality for encoding the inner document of an onion service descriptor.
//!
//! NOTE: `HsDescInnerBuilder` is a private helper for building hidden service descriptors, and is
//! not meant to be used directly. Hidden services will use `HsDescBuilder` to build and encode
//! hidden service descriptors.
use crate::build::NetdocEncoder;
use crate::doc::hsdesc::inner::HsInnerKwd;
use crate::{NetdocBuilder, NetdocText};
use tor_bytes::{EncodeError, Writer};
use tor_cert::{CertType, CertifiedKey, Ed25519Cert};
use tor_error::{bad_api_usage, into_bad_api_usage};
use tor_llcrypto::pk::ed25519;
use tor_llcrypto::pk::keymanip::convert_curve25519_to_ed25519_public;
use base64ct::{Base64, Encoding};
mod private {
//! A private module that defines the `HsDescInner` structure.
//!
//! TODO: while you can use derive_builder to create a private "built" struct with a public
//! builder (by making the struct private and setting `build_fn(vis = "pub(super)")`), it does
//! not support finer grained visibility for the builder struct. This means we cannot make the
//! builder struct `pub(super)` while keeping the "built" struct private (which is what we want
//! here, as this builder only exists to serve as a helper for the top-level `HsDescBuilder`).
//!
//! This module was created as a workaround to this limitation.
use std::time::SystemTime;
use derive_builder::Builder;
use smallvec::SmallVec;
use tor_llcrypto::pk::ed25519;
use crate::doc::hsdesc::{IntroAuthType, IntroPointDesc};
/// The representation of the inner document of an onion service descriptor.
///
/// The plaintext format of this document is described in section 2.5.2.2. of rend-spec-v3.
#[derive(Builder)]
#[builder(public, derive(Debug), pattern = "owned", build_fn(vis = "pub(super)"))]
pub(super) struct HsDescInner<'a> {
/// The descriptor signing key.
pub(super) hs_desc_sign: &'a ed25519::Keypair,
/// A list of recognized CREATE handshakes that this onion service supports.
// TODO hs: this should probably be a caret enum, not an integer
pub(super) create2_formats: &'a [u32],
/// A list of authentication types that this onion service supports.
pub(super) auth_required: Option<&'a SmallVec<[IntroAuthType; 2]>>,
/// If true, this a "single onion service" and is not trying to keep its own location private.
pub(super) is_single_onion_service: bool,
/// One or more introduction points used to contact the onion service.
pub(super) intro_points: &'a [IntroPointDesc],
/// The expiration time of an introduction point authentication key certificate.
pub(super) intro_auth_key_cert_expiry: SystemTime,
/// The expiration time of an introduction point encryption key certificate.
pub(super) intro_enc_key_cert_expiry: SystemTime,
}
}
pub(super) use private::HsDescInnerBuilder;
use private::HsDescInner;
impl<'a> NetdocBuilder for HsDescInnerBuilder<'a> {
fn build_sign(self) -> Result<NetdocText<Self>, EncodeError> {
use HsInnerKwd::*;
let HsDescInner {
hs_desc_sign,
create2_formats,
auth_required,
is_single_onion_service,
intro_points,
intro_auth_key_cert_expiry,
intro_enc_key_cert_expiry,
} = self
.build()
.map_err(into_bad_api_usage!("the HsDescInner could not be built"))?;
let mut encoder = NetdocEncoder::new();
{
let mut create2_formats_enc = encoder.item(CREATE2_FORMATS);
for fmt in create2_formats {
create2_formats_enc = create2_formats_enc.arg(&fmt);
}
}
{
if let Some(auth_required) = auth_required {
let mut auth_required_enc = encoder.item(INTRO_AUTH_REQUIRED);
for auth in auth_required {
auth_required_enc = auth_required_enc.arg(&auth.to_string());
}
}
}
if is_single_onion_service {
encoder.item(SINGLE_ONION_SERVICE);
}
for intro_point in intro_points {
// rend-spec-v3 0.4. "Protocol building blocks [BUILDING-BLOCKS]": the number of link
// specifiers (NPSEC) must fit in a single byte.
let nspec: u8 = intro_point
.link_specifiers
.len()
.try_into()
.map_err(into_bad_api_usage!("Too many link specifiers."))?;
let mut link_specifiers = vec![];
link_specifiers.write_u8(nspec);
for link_spec in &intro_point.link_specifiers {
link_specifiers.write(link_spec)?;
}
encoder
.item(INTRODUCTION_POINT)
.arg(&Base64::encode_string(&link_specifiers));
encoder
.item(ONION_KEY)
.arg(&"ntor")
.arg(&Base64::encode_string(&intro_point.ipt_ntor_key.to_bytes()));
// For compatibility with c-tor, the introduction point authentication key is signed by
// the descriptor signing key.
let signed_auth_key = Ed25519Cert::constructor()
.cert_type(CertType::HS_IP_V_SIGNING)
.expiration(intro_auth_key_cert_expiry)
.signing_key(ed25519::Ed25519Identity::from(hs_desc_sign.public))
.cert_key(CertifiedKey::Ed25519((*intro_point.ipt_sid_key).into()))
.encode_and_sign(hs_desc_sign)
.map_err(into_bad_api_usage!("failed to sign the intro auth key"))?;
encoder
.item(AUTH_KEY)
.object("ED25519 CERT", signed_auth_key);
// "The key is a base64 encoded curve25519 public key used to encrypt the introduction
// request to service. (`KP_hss_ntor`)"
//
// TODO hs: The spec allows for multiple enc-key lines, but we currently only ever encode
// a single one.
encoder
.item(ENC_KEY)
.arg(&"ntor")
.arg(&Base64::encode_string(
&intro_point.svc_ntor_key.as_bytes()[..],
));
// The subject key is the the ed25519 equivalent of the svc_ntor_key curve25519 public
// encryption key.
// TODO hs: should the sign bit be 0 or 1?
let signbit = 0;
let ed_svc_ntor_key =
convert_curve25519_to_ed25519_public(&intro_point.svc_ntor_key, signbit)
.ok_or_else(|| {
bad_api_usage!("failed to convert curve25519 pk to ed25519 pk")
})?;
// For compatibility with c-tor, the encryption key is signed with the descriptor
// signing key.
let signed_enc_key = Ed25519Cert::constructor()
.cert_type(CertType::HS_IP_CC_SIGNING)
.expiration(intro_enc_key_cert_expiry)
.signing_key(ed25519::Ed25519Identity::from(hs_desc_sign.public))
.cert_key(CertifiedKey::Ed25519(ed25519::Ed25519Identity::from(
&ed_svc_ntor_key,
)))
.encode_and_sign(hs_desc_sign)
.map_err(into_bad_api_usage!(
"failed to sign the intro encryption key"
))?;
encoder
.item(ENC_KEY_CERT)
.object("ED25519 CERT", signed_enc_key);
}
encoder.finish().map_err(|e| e.into())
}
}
#[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)]
//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
use super::*;
use crate::doc::hsdesc::build::test::{
expect_bug, test_ed25519_keypair, test_intro_point_descriptor,
};
use crate::doc::hsdesc::IntroAuthType;
use smallvec::SmallVec;
use std::time::UNIX_EPOCH;
use tor_linkspec::UnparsedLinkSpec;
#[test]
fn inner_hsdesc_no_intro_auth() {
let hs_desc_sign = test_ed25519_keypair();
// A descriptor for a "single onion service"
let hs_desc = HsDescInnerBuilder::default()
.hs_desc_sign(&hs_desc_sign)
.create2_formats(&[1234])
.auth_required(None)
.is_single_onion_service(true)
.intro_points(&[]) // no introduction points
.intro_auth_key_cert_expiry(UNIX_EPOCH)
.intro_enc_key_cert_expiry(UNIX_EPOCH)
.build_sign()
.unwrap();
assert_eq!(&*hs_desc, "create2-formats 1234\nsingle-onion-service\n");
// A descriptor for a location-hidden service
let hs_desc = HsDescInnerBuilder::default()
.hs_desc_sign(&hs_desc_sign)
.create2_formats(&[1234])
.auth_required(None)
.is_single_onion_service(false)
.intro_points(&[]) // no introduction points
.intro_auth_key_cert_expiry(UNIX_EPOCH)
.intro_enc_key_cert_expiry(UNIX_EPOCH)
.build_sign()
.unwrap();
assert_eq!(&*hs_desc, "create2-formats 1234\n");
let link_specs1 = vec![UnparsedLinkSpec::new(0, vec![1, 2, 3, 4])];
let link_specs2 = vec![UnparsedLinkSpec::new(5, vec![6, 7, 8, 9])];
let link_specs3 = vec![UnparsedLinkSpec::new(10, vec![11, 12, 13, 14])];
let intros = vec![
test_intro_point_descriptor(link_specs1),
test_intro_point_descriptor(link_specs2),
test_intro_point_descriptor(link_specs3),
];
// A descriptor for a location-hidden service with 3 introduction points
let hs_desc = HsDescInnerBuilder::default()
.hs_desc_sign(&hs_desc_sign)
.create2_formats(&[1234, 32, 23])
.auth_required(None)
.is_single_onion_service(false)
.intro_points(&intros)
.intro_auth_key_cert_expiry(UNIX_EPOCH)
.intro_enc_key_cert_expiry(UNIX_EPOCH)
.build_sign()
.unwrap();
assert_eq!(
&*hs_desc,
r#"create2-formats 1234 32 23
introduction-point AQAEAQIDBA==
onion-key ntor tnEhX8317Kk2N6hoacsCK0ir/LKE3DcPgYlDI5OKegg=
auth-key
-----BEGIN ED25519 CERT-----
AQkAAAAAAewZqL9VZkkLDGVQ4eYcCdB/2+XvKqaT6DfOOdIK1zY8AQAgBADsGai/
VWZJCwxlUOHmHAnQf9vl7yqmk+g3zjnSCtc2PGJfiGR8BqiEDVPAUl0ToUn+72JW
zJUcnzAdNAysaR5UrK0i+/d4vOzNsNFQ2f7vkfFuwvQEeDlqw8lddvNwxg0=
-----END ED25519 CERT-----
enc-key ntor c6PGJQNAqJxyfC6O6VvvHc/wgMrQcKr3Ui7pBvv2dXE=
enc-key-cert
-----BEGIN ED25519 CERT-----
AQsAAAAAAR8cWg2fNApajIBOuKccs3OYfVopANyb9NDTpy7J86VTAQAgBADsGai/
VWZJCwxlUOHmHAnQf9vl7yqmk+g3zjnSCtc2PNm8uaG3bLDi06dUrHSEDBCIVsx5
CPFWD3sGHVPIZA5JnK7mbX8zhsX/MnV/0jj+YKLCQtOPwbqC3R7iXWjBsAE=
-----END ED25519 CERT-----
introduction-point AQUEBgcICQ==
onion-key ntor tnEhX8317Kk2N6hoacsCK0ir/LKE3DcPgYlDI5OKegg=
auth-key
-----BEGIN ED25519 CERT-----
AQkAAAAAAewZqL9VZkkLDGVQ4eYcCdB/2+XvKqaT6DfOOdIK1zY8AQAgBADsGai/
VWZJCwxlUOHmHAnQf9vl7yqmk+g3zjnSCtc2PGJfiGR8BqiEDVPAUl0ToUn+72JW
zJUcnzAdNAysaR5UrK0i+/d4vOzNsNFQ2f7vkfFuwvQEeDlqw8lddvNwxg0=
-----END ED25519 CERT-----
enc-key ntor c6PGJQNAqJxyfC6O6VvvHc/wgMrQcKr3Ui7pBvv2dXE=
enc-key-cert
-----BEGIN ED25519 CERT-----
AQsAAAAAAR8cWg2fNApajIBOuKccs3OYfVopANyb9NDTpy7J86VTAQAgBADsGai/
VWZJCwxlUOHmHAnQf9vl7yqmk+g3zjnSCtc2PNm8uaG3bLDi06dUrHSEDBCIVsx5
CPFWD3sGHVPIZA5JnK7mbX8zhsX/MnV/0jj+YKLCQtOPwbqC3R7iXWjBsAE=
-----END ED25519 CERT-----
introduction-point AQoECwwNDg==
onion-key ntor tnEhX8317Kk2N6hoacsCK0ir/LKE3DcPgYlDI5OKegg=
auth-key
-----BEGIN ED25519 CERT-----
AQkAAAAAAewZqL9VZkkLDGVQ4eYcCdB/2+XvKqaT6DfOOdIK1zY8AQAgBADsGai/
VWZJCwxlUOHmHAnQf9vl7yqmk+g3zjnSCtc2PGJfiGR8BqiEDVPAUl0ToUn+72JW
zJUcnzAdNAysaR5UrK0i+/d4vOzNsNFQ2f7vkfFuwvQEeDlqw8lddvNwxg0=
-----END ED25519 CERT-----
enc-key ntor c6PGJQNAqJxyfC6O6VvvHc/wgMrQcKr3Ui7pBvv2dXE=
enc-key-cert
-----BEGIN ED25519 CERT-----
AQsAAAAAAR8cWg2fNApajIBOuKccs3OYfVopANyb9NDTpy7J86VTAQAgBADsGai/
VWZJCwxlUOHmHAnQf9vl7yqmk+g3zjnSCtc2PNm8uaG3bLDi06dUrHSEDBCIVsx5
CPFWD3sGHVPIZA5JnK7mbX8zhsX/MnV/0jj+YKLCQtOPwbqC3R7iXWjBsAE=
-----END ED25519 CERT-----
"#
);
}
#[test]
fn inner_hsdesc_too_many_link_specifiers() {
let hs_desc_sign = test_ed25519_keypair();
let link_spec = UnparsedLinkSpec::new(0, vec![]);
let link_specifiers = std::iter::repeat(link_spec)
.take(u8::MAX as usize + 1)
.collect::<Vec<_>>();
let intros = vec![test_intro_point_descriptor(link_specifiers)];
// A descriptor for a location-hidden service with an introduction point with too many link
// specifiers
let err = HsDescInnerBuilder::default()
.hs_desc_sign(&hs_desc_sign)
.create2_formats(&[1234])
.auth_required(None)
.is_single_onion_service(false)
.intro_points(&intros)
.intro_auth_key_cert_expiry(UNIX_EPOCH)
.intro_enc_key_cert_expiry(UNIX_EPOCH)
.build_sign()
.unwrap_err();
assert!(expect_bug(err).contains("Too many link specifiers (max = 255)."));
}
#[test]
fn inner_hsdesc_intro_auth() {
let hs_desc_sign = test_ed25519_keypair();
let link_specs = vec![UnparsedLinkSpec::new(0, vec![1, 2, 3, 4])];
let intros = vec![test_intro_point_descriptor(link_specs)];
let auth = SmallVec::from([IntroAuthType::Ed25519, IntroAuthType::Ed25519]);
// A descriptor for a location-hidden service with 1 introduction points which requires
// auth.
let hs_desc = HsDescInnerBuilder::default()
.hs_desc_sign(&hs_desc_sign)
.create2_formats(&[1234])
.auth_required(Some(&auth))
.is_single_onion_service(false)
.intro_points(&intros)
.intro_auth_key_cert_expiry(UNIX_EPOCH)
.intro_enc_key_cert_expiry(UNIX_EPOCH)
.build_sign()
.unwrap();
assert_eq!(
&*hs_desc,
r#"create2-formats 1234
intro-auth-required ed25519 ed25519
introduction-point AQAEAQIDBA==
onion-key ntor tnEhX8317Kk2N6hoacsCK0ir/LKE3DcPgYlDI5OKegg=
auth-key
-----BEGIN ED25519 CERT-----
AQkAAAAAAewZqL9VZkkLDGVQ4eYcCdB/2+XvKqaT6DfOOdIK1zY8AQAgBADsGai/
VWZJCwxlUOHmHAnQf9vl7yqmk+g3zjnSCtc2PGJfiGR8BqiEDVPAUl0ToUn+72JW
zJUcnzAdNAysaR5UrK0i+/d4vOzNsNFQ2f7vkfFuwvQEeDlqw8lddvNwxg0=
-----END ED25519 CERT-----
enc-key ntor c6PGJQNAqJxyfC6O6VvvHc/wgMrQcKr3Ui7pBvv2dXE=
enc-key-cert
-----BEGIN ED25519 CERT-----
AQsAAAAAAR8cWg2fNApajIBOuKccs3OYfVopANyb9NDTpy7J86VTAQAgBADsGai/
VWZJCwxlUOHmHAnQf9vl7yqmk+g3zjnSCtc2PNm8uaG3bLDi06dUrHSEDBCIVsx5
CPFWD3sGHVPIZA5JnK7mbX8zhsX/MnV/0jj+YKLCQtOPwbqC3R7iXWjBsAE=
-----END ED25519 CERT-----
"#
);
}
}

View File

@ -0,0 +1,239 @@
//! Functionality for encoding the middle document of an onion service descriptor.
//!
//! NOTE: `HsDescMiddleBuilder` is a private helper for building hidden service descriptors, and is
//! not meant to be used directly. Hidden services will use `HsDescBuilder` to build and encode
//! hidden service descriptors.
use crate::build::NetdocEncoder;
use crate::doc::hsdesc::desc_enc::{HS_DESC_CLIENT_ID_LEN, HS_DESC_ENC_NONCE_LEN, HS_DESC_IV_LEN};
use crate::doc::hsdesc::middle::{AuthClient, HsMiddleKwd, HS_DESC_AUTH_TYPE};
use crate::{NetdocBuilder, NetdocText};
use tor_bytes::EncodeError;
use tor_error::into_bad_api_usage;
use tor_llcrypto::pk::curve25519::{EphemeralSecret, PublicKey};
use tor_llcrypto::util::ct::CtByteArray;
use base64ct::{Base64, Encoding};
use rand::rngs::OsRng;
use rand::{thread_rng, Rng};
use std::borrow::Cow;
mod private {
//! A private module that defines the `HsDescMiddle` structure.
//!
//! TODO: while you can use derive_builder to create a private "built" struct with a public
//! builder (by making the struct private and setting `build_fn(vis = "pub(super)")`), it does
//! not support finer grained visibility for the builder struct. This means we cannot make the
//! builder struct `pub(super)` while keeping the "built" struct private (which is what we want
//! here, as this builder only exists to serve as a helper for the top-level `HsDescBuilder`).
//!
//! This module was created as a workaround to this limitation.
use crate::doc::hsdesc::build::ClientAuth;
use derive_builder::Builder;
/// The representation of the middle document of an onion service descriptor.
///
/// The plaintext format of this document is described in section 2.5.1.2. of rend-spec-v3.
#[derive(Builder)]
#[builder(public, derive(Debug), pattern = "owned", build_fn(vis = "pub(super)"))]
pub(super) struct HsDescMiddle<'a> {
/// Client authorization parameters, if client authentication is enabled. If set to `None`,
/// client authentication is disabled.
pub(super) client_auth: Option<&'a ClientAuth>,
/// The (encrypted) inner document of the onion service descriptor.
///
/// The `encrypted` field is generated by encrypting an inner document built using
/// [`crate::doc::hsdesc::build::inner::HsDescInnerBuilder`] as described in sections
/// 2.5.2.1. and 2.5.2.2. of rend-spec-v3.
pub(super) encrypted: Vec<u8>,
}
}
pub(super) use private::HsDescMiddleBuilder;
use private::HsDescMiddle;
impl<'a> NetdocBuilder for HsDescMiddleBuilder<'a> {
fn build_sign(self) -> Result<NetdocText<Self>, EncodeError> {
use HsMiddleKwd::*;
let HsDescMiddle {
client_auth,
encrypted,
} = self
.build()
.map_err(into_bad_api_usage!("the HsDescMiddle could not be built"))?;
let mut encoder = NetdocEncoder::new();
let (ephemeral_key, auth_clients): (_, Cow<Vec<_>>) = match client_auth {
Some(client_auth) if client_auth.auth_clients.is_empty() => {
return Err(tor_error::bad_api_usage!(
"client authentication is enabled, but there are no authorized clients"
)
.into());
}
Some(client_auth) => {
// Client auth is enabled.
(
*client_auth.ephemeral_key,
Cow::Borrowed(&client_auth.auth_clients),
)
}
None => {
// As per section 2.5.1.2. of rend-spec-v3, if client auth is disabled, we need to
// generate some fake data for the desc-auth-ephemeral-key and auth-client fields.
let secret = EphemeralSecret::new(OsRng);
let dummy_ephemeral_key = PublicKey::from(&secret);
let mut rng = thread_rng();
// Generate a single client-auth line filled with random values for client-id,
// iv, and encrypted-cookie.
let dummy_auth_client = AuthClient {
client_id: CtByteArray::from(rng.gen::<[u8; HS_DESC_CLIENT_ID_LEN]>()),
iv: rng.gen::<[u8; HS_DESC_IV_LEN]>(),
encrypted_cookie: rng.gen::<[u8; HS_DESC_ENC_NONCE_LEN]>(),
};
// TODO hs: Remove useless vec![] allocation.
(dummy_ephemeral_key, Cow::Owned(vec![dummy_auth_client]))
}
};
encoder.item(DESC_AUTH_TYPE).arg(&HS_DESC_AUTH_TYPE);
encoder
.item(DESC_AUTH_EPHEMERAL_KEY)
.arg(&Base64::encode_string(ephemeral_key.as_bytes()));
for auth_client in &*auth_clients {
encoder
.item(AUTH_CLIENT)
.arg(&Base64::encode_string(&*auth_client.client_id))
.arg(&Base64::encode_string(&auth_client.iv))
.arg(&Base64::encode_string(&auth_client.encrypted_cookie));
}
encoder.item(ENCRYPTED).object("MESSAGE", encrypted);
encoder.finish().map_err(|e| e.into())
}
}
#[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)]
//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
use super::*;
use crate::doc::hsdesc::build::test::{
expect_bug, TEST_CURVE25519_PUBLIC1, TEST_DESCRIPTOR_COOKIE,
};
use crate::doc::hsdesc::build::ClientAuth;
use tor_llcrypto::pk::curve25519;
// Some dummy bytes, not actually encrypted.
const TEST_ENCRYPTED_VALUE: &[u8] = &[1, 2, 3, 4];
#[test]
fn middle_hsdesc_encoding_no_client_auth() {
let hs_desc = HsDescMiddleBuilder::default()
.client_auth(None)
.encrypted(TEST_ENCRYPTED_VALUE.into())
.build_sign()
.unwrap();
let mut lines = hs_desc.splitn(4, '\n');
assert_eq!(lines.next().unwrap(), "desc-auth-type x25519");
// If client auth is disabled, HsDescMiddleBuilder will generate a dummy ephemeral key (a
// different one each time), so this test checks it is present in the built document. It
// does _not_ actually validate its value.
assert!(lines
.next()
.unwrap()
.starts_with("desc-auth-ephemeral-key "));
// The above also applies to the auth-client line (since client auth is disabled, it's
// going to have a different value on each run).
assert!(lines.next().unwrap().starts_with("auth-client "));
assert_eq!(
lines.next().unwrap(),
r#"encrypted
-----BEGIN MESSAGE-----
AQIDBA==
-----END MESSAGE-----
"#
);
assert!(lines.next().is_none());
}
#[test]
fn middle_hsdesc_encoding_with_bad_client_auth() {
let client_auth = ClientAuth {
ephemeral_key: curve25519::PublicKey::from(TEST_CURVE25519_PUBLIC1).into(),
auth_clients: vec![],
descriptor_cookie: TEST_DESCRIPTOR_COOKIE,
};
let err = HsDescMiddleBuilder::default()
.client_auth(Some(&client_auth))
.encrypted(TEST_ENCRYPTED_VALUE.into())
.build_sign()
.unwrap_err();
assert!(expect_bug(err)
.contains("client authentication is enabled, but there are no authorized clients"));
}
#[test]
fn middle_hsdesc_encoding_client_auth() {
// 2 authorized clients
let auth_clients = vec![
AuthClient {
client_id: [2; 8].into(),
iv: [2; 16],
encrypted_cookie: [3; 16],
},
AuthClient {
client_id: [4; 8].into(),
iv: [5; 16],
encrypted_cookie: [6; 16],
},
];
let client_auth = ClientAuth {
ephemeral_key: curve25519::PublicKey::from(TEST_CURVE25519_PUBLIC1).into(),
auth_clients,
descriptor_cookie: TEST_DESCRIPTOR_COOKIE,
};
let hs_desc = HsDescMiddleBuilder::default()
.client_auth(Some(&client_auth))
.encrypted(TEST_ENCRYPTED_VALUE.into())
.build_sign()
.unwrap();
assert_eq!(
&*hs_desc,
r#"desc-auth-type x25519
desc-auth-ephemeral-key tnEhX8317Kk2N6hoacsCK0ir/LKE3DcPgYlDI5OKegg=
auth-client AgICAgICAgI= AgICAgICAgICAgICAgICAg== AwMDAwMDAwMDAwMDAwMDAw==
auth-client BAQEBAQEBAQ= BQUFBQUFBQUFBQUFBQUFBQ== BgYGBgYGBgYGBgYGBgYGBg==
encrypted
-----BEGIN MESSAGE-----
AQIDBA==
-----END MESSAGE-----
"#
);
}
}

View File

@ -0,0 +1,183 @@
//! Functionality for encoding the outer document of an onion service descriptor.
//!
//! NOTE: `HsDescOuterBuilder` is a private helper for building hidden service descriptors, and is
//! not meant to be used directly. Hidden services will use `HsDescBuilder` to build and encode
//! hidden service descriptors.
use crate::build::NetdocEncoder;
use crate::build::{NetdocBuilder, NetdocText};
use crate::doc::hsdesc::outer::{HsOuterKwd, HS_DESC_SIGNATURE_PREFIX, HS_DESC_VERSION_CURRENT};
use tor_bytes::EncodeError;
use tor_cert::{CertType, CertifiedKey, Ed25519Cert};
use tor_error::into_bad_api_usage;
use tor_llcrypto::pk::ed25519;
use base64ct::{Base64, Encoding};
mod private {
//! A private module that defines the `HsDescOuter` structure.
//!
//! TODO: while you can use derive_builder to create a private "built" struct with a public
//! builder (by making the struct private and setting `build_fn(vis = "pub(super)")`), it does
//! not support finer grained visibility for the builder struct. This means we cannot make the
//! builder struct `pub(super)` while keeping the "built" struct private (which is what we want
//! here, as this builder only exists to serve as a helper for the top-level `HsDescBuilder`).
//!
//! This module was created as a workaround to this limitation.
use std::time::SystemTime;
use derive_builder::Builder;
use tor_hscrypto::RevisionCounter;
use tor_llcrypto::pk::ed25519;
use tor_units::IntegerMinutes;
/// The representation of the outer wrapper of an onion service descriptor.
///
/// The format of this document is described in section 2.4. of rend-spec-v3.
#[derive(Builder)]
#[builder(public, derive(Debug), pattern = "owned", build_fn(vis = "pub(super)"))]
pub(super) struct HsDescOuter<'a> {
/// The blinded hidden service signing keys used to sign descriptor signing keys
/// (KP_hs_blind_id, KS_hs_blind_id).
pub(super) blinded_id: &'a ed25519::Keypair,
/// The short-term descriptor signing key.
pub(super) hs_desc_sign: &'a ed25519::Keypair,
/// The expiration time of the descriptor signing key certificate.
pub(super) hs_desc_sign_cert_expiry: SystemTime,
/// The lifetime of this descriptor, in minutes.
///
/// This doesn't actually list the starting time or the end time for the
/// descriptor: presumably, because we didn't want to leak the onion
/// service's view of the wallclock.
pub(super) lifetime: IntegerMinutes<u16>,
/// A revision counter to tell whether this descriptor is more or less recent
/// than another one for the same blinded ID.
pub(super) revision_counter: RevisionCounter,
/// The (superencrypted) middle document of the onion service descriptor.
///
/// The superencrypted field is generated by encrypting a middle document built using
/// [`crate::doc::hsdesc::build::middle::HsDescMiddleBuilder`] as described in sections
/// 2.5.1.1. and 2.5.1.2. of rend-spec-v3.
pub(super) superencrypted: Vec<u8>,
}
}
pub(super) use private::HsDescOuterBuilder;
use private::HsDescOuter;
impl<'a> NetdocBuilder for HsDescOuterBuilder<'a> {
fn build_sign(self) -> Result<NetdocText<Self>, EncodeError> {
use HsOuterKwd::*;
let HsDescOuter {
blinded_id,
hs_desc_sign,
hs_desc_sign_cert_expiry,
lifetime,
revision_counter,
superencrypted,
} = self
.build()
.map_err(into_bad_api_usage!("failed to build HsDescOuter"))?;
let mut encoder = NetdocEncoder::new();
let beginning = encoder.cursor();
encoder.item(HS_DESCRIPTOR).arg(&HS_DESC_VERSION_CURRENT);
encoder.item(DESCRIPTOR_LIFETIME).arg(&lifetime.to_string());
// "The certificate cross-certifies the short-term descriptor signing key with the blinded
// public key. The certificate type must be [08], and the blinded public key must be
// present as the signing-key extension."
let desc_signing_key_cert = Ed25519Cert::constructor()
.cert_type(CertType::HS_BLINDED_ID_V_SIGNING)
.expiration(hs_desc_sign_cert_expiry)
.signing_key(ed25519::Ed25519Identity::from(&blinded_id.public))
.cert_key(CertifiedKey::Ed25519(hs_desc_sign.public.into()))
.encode_and_sign(blinded_id)
.map_err(into_bad_api_usage!(
"failed to sign the descriptor signing key"
))?;
encoder
.item(DESCRIPTOR_SIGNING_KEY_CERT)
.object("ED25519 CERT", desc_signing_key_cert);
encoder.item(REVISION_COUNTER).arg(&*revision_counter);
// TODO: According to section 2.4. of rend-spec-v3 this blob should _not_ end with a
// newline character. We need to update the encoder to support this.
encoder
.item(SUPERENCRYPTED)
.object("MESSAGE", superencrypted);
let end = encoder.cursor();
let mut text = HS_DESC_SIGNATURE_PREFIX.to_vec();
text.extend_from_slice(encoder.slice(beginning, end)?.as_bytes());
let signature = ed25519::ExpandedSecretKey::from(&hs_desc_sign.secret)
.sign(&text, &hs_desc_sign.public);
encoder
.item(SIGNATURE)
.arg(&Base64::encode_string(&signature.to_bytes()));
encoder.finish().map_err(|e| e.into())
}
}
#[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)]
//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
use std::time::UNIX_EPOCH;
use super::*;
use crate::doc::hsdesc::build::test::test_ed25519_keypair;
use tor_units::IntegerMinutes;
// Some dummy bytes, not actually encrypted.
const TEST_SUPERENCRYPTED_VALUE: &[u8] = &[1, 2, 3, 4];
#[test]
fn outer_hsdesc() {
let blinded_id = test_ed25519_keypair();
let hs_desc_sign = test_ed25519_keypair();
let hs_desc = HsDescOuterBuilder::default()
.blinded_id(&blinded_id)
.hs_desc_sign(&hs_desc_sign)
.hs_desc_sign_cert_expiry(UNIX_EPOCH)
.lifetime(IntegerMinutes::new(20))
.revision_counter(9001.into())
.superencrypted(TEST_SUPERENCRYPTED_VALUE.into())
.build_sign()
.unwrap();
assert_eq!(
&*hs_desc,
r#"hs-descriptor 3
descriptor-lifetime 20
descriptor-signing-key-cert
-----BEGIN ED25519 CERT-----
AQgAAAAAAewZqL9VZkkLDGVQ4eYcCdB/2+XvKqaT6DfOOdIK1zY8AQAgBADsGai/
VWZJCwxlUOHmHAnQf9vl7yqmk+g3zjnSCtc2PFnSUwwD3tSQYaF4DKBVemqZwhm+
6jWFAvdK+7LsP/HaE0r6JcT7pOchkyczbBns2gCdGULRvejzNoq3gegDIQ0=
-----END ED25519 CERT-----
revision-counter 9001
superencrypted
-----BEGIN MESSAGE-----
AQIDBA==
-----END MESSAGE-----
signature +Y3v0te3Cq4HdfliGTpl3kIKwcMBaRWAclxXRxOcVb1hI+3UNvT59JfJjz8JrBEc1N8TjxlxtTCvJKU4Xy9jBQ==
"#
);
}
}

View File

@ -42,13 +42,13 @@ pub(super) struct HsDescEncryption<'a> {
} }
/// The length of a client ID. /// The length of a client ID.
pub(crate) const HS_DESC_MIDDLE_CLIENT_ID_LEN: usize = 8; pub(crate) const HS_DESC_CLIENT_ID_LEN: usize = 8;
/// The length of the the `AuthClient` IV. /// The length of the the `AuthClient` IV.
pub(crate) const HS_DESC_MIDDLE_IV_LEN: usize = 16; pub(crate) const HS_DESC_IV_LEN: usize = 16;
/// The length of an `N_hs_desc_enc` nonce (also known as a "descriptor cookie"). /// The length of an `N_hs_desc_enc` nonce (also known as a "descriptor cookie").
pub(crate) const HS_DESC_MIDDLE_ENC_NONCE_LEN: usize = 16; pub(crate) const HS_DESC_ENC_NONCE_LEN: usize = 16;
/// A value used in deriving the encryption key for the inner (encryption) layer /// A value used in deriving the encryption key for the inner (encryption) layer
/// of onion service encryption. /// of onion service encryption.
@ -56,7 +56,7 @@ pub(crate) const HS_DESC_MIDDLE_ENC_NONCE_LEN: usize = 16;
/// This is `N_hs_desc_enc` in the spec, where sometimes we also call it a /// This is `N_hs_desc_enc` in the spec, where sometimes we also call it a
/// "descriptor cookie". /// "descriptor cookie".
#[derive(derive_more::AsRef, derive_more::From)] #[derive(derive_more::AsRef, derive_more::From)]
pub(super) struct HsDescEncNonce([u8; HS_DESC_MIDDLE_ENC_NONCE_LEN]); pub(super) struct HsDescEncNonce([u8; HS_DESC_ENC_NONCE_LEN]);
/// Length of our cryptographic salt. /// Length of our cryptographic salt.
const SALT_LEN: usize = 16; const SALT_LEN: usize = 16;

View File

@ -39,7 +39,7 @@ pub(super) struct HsDescInner {
} }
decl_keyword! { decl_keyword! {
HsInnerKwd { pub(crate) HsInnerKwd {
"create2-formats" => CREATE2_FORMATS, "create2-formats" => CREATE2_FORMATS,
"intro-auth-required" => INTRO_AUTH_REQUIRED, "intro-auth-required" => INTRO_AUTH_REQUIRED,
"single-onion-service" => SINGLE_ONION_SERVICE, "single-onion-service" => SINGLE_ONION_SERVICE,

View File

@ -13,11 +13,14 @@ use crate::types::misc::B64;
use crate::{Pos, Result}; use crate::{Pos, Result};
use super::desc_enc::{ use super::desc_enc::{
HsDescEncNonce, HsDescEncryption, HS_DESC_MIDDLE_CLIENT_ID_LEN, HS_DESC_MIDDLE_ENC_NONCE_LEN, HsDescEncNonce, HsDescEncryption, HS_DESC_CLIENT_ID_LEN, HS_DESC_ENC_NONCE_LEN, HS_DESC_IV_LEN,
HS_DESC_MIDDLE_IV_LEN,
}; };
use super::DecryptionError; use super::DecryptionError;
/// TODO hs: This should be an enum.
/// The only currently recognized `desc-auth-type`.
pub(super) const HS_DESC_AUTH_TYPE: &str = "x25519";
/// A more-or-less verbatim representation of the middle document of an onion /// A more-or-less verbatim representation of the middle document of an onion
/// service descriptor. /// service descriptor.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -134,15 +137,15 @@ impl HsDescMiddle {
/// Information that a single authorized client can use to decrypt the onion /// Information that a single authorized client can use to decrypt the onion
/// service descriptor. /// service descriptor.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct AuthClient { pub struct AuthClient {
/// A check field that clients can use to see if this [`AuthClient`] entry corresponds to a key they hold. /// A check field that clients can use to see if this [`AuthClient`] entry corresponds to a key they hold.
/// ///
/// This is the first part of the `auth-client` line. /// This is the first part of the `auth-client` line.
client_id: CtByteArray<HS_DESC_MIDDLE_CLIENT_ID_LEN>, pub(crate) client_id: CtByteArray<HS_DESC_CLIENT_ID_LEN>,
/// An IV used to decrypt `encrypted_cookie`. /// An IV used to decrypt `encrypted_cookie`.
/// ///
/// This is the second item on the `auth-client` line. /// This is the second item on the `auth-client` line.
iv: [u8; HS_DESC_MIDDLE_IV_LEN], pub(crate) iv: [u8; HS_DESC_IV_LEN],
/// An encrypted value used to find the descriptor cookie `N_hs_desc_enc`, /// An encrypted value used to find the descriptor cookie `N_hs_desc_enc`,
/// which in turn is /// which in turn is
/// needed to decrypt the [HsDescMiddle]'s `encrypted_body`. /// needed to decrypt the [HsDescMiddle]'s `encrypted_body`.
@ -150,7 +153,7 @@ struct AuthClient {
/// This is the third item on the `auth-client` line. When decrypted, it /// This is the third item on the `auth-client` line. When decrypted, it
/// reveals a `DescEncEncryptionCookie` (`N_hs_desc_enc`, not yet so named /// reveals a `DescEncEncryptionCookie` (`N_hs_desc_enc`, not yet so named
/// in the spec). /// in the spec).
encrypted_cookie: [u8; HS_DESC_MIDDLE_ENC_NONCE_LEN], pub(crate) encrypted_cookie: [u8; HS_DESC_ENC_NONCE_LEN],
} }
impl AuthClient { impl AuthClient {
@ -173,7 +176,7 @@ impl AuthClient {
} }
decl_keyword! { decl_keyword! {
HsMiddleKwd { pub(crate) HsMiddleKwd {
"desc-auth-type" => DESC_AUTH_TYPE, "desc-auth-type" => DESC_AUTH_TYPE,
"desc-auth-ephemeral-key" => DESC_AUTH_EPHEMERAL_KEY, "desc-auth-ephemeral-key" => DESC_AUTH_EPHEMERAL_KEY,
"auth-client" => AUTH_CLIENT, "auth-client" => AUTH_CLIENT,

View File

@ -16,6 +16,12 @@ use crate::{Pos, Result};
use super::desc_enc; use super::desc_enc;
/// The current version-number.
pub(super) const HS_DESC_VERSION_CURRENT: &str = "3";
/// The text the outer document signature is prefixed with.
pub(super) const HS_DESC_SIGNATURE_PREFIX: &[u8] = b"Tor onion service descriptor sig v3";
/// A more-or-less verbatim representation of the outermost plaintext document /// A more-or-less verbatim representation of the outermost plaintext document
/// of an onion service descriptor. /// of an onion service descriptor.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -99,7 +105,7 @@ impl HsDescOuter {
pub(super) type UncheckedHsDescOuter = SignatureGated<TimerangeBound<HsDescOuter>>; pub(super) type UncheckedHsDescOuter = SignatureGated<TimerangeBound<HsDescOuter>>;
decl_keyword! { decl_keyword! {
HsOuterKwd { pub(crate) HsOuterKwd {
"hs-descriptor" => HS_DESCRIPTOR, "hs-descriptor" => HS_DESCRIPTOR,
"descriptor-lifetime" => DESCRIPTOR_LIFETIME, "descriptor-lifetime" => DESCRIPTOR_LIFETIME,
"descriptor-signing-key-cert" => DESCRIPTOR_SIGNING_KEY_CERT, "descriptor-signing-key-cert" => DESCRIPTOR_SIGNING_KEY_CERT,
@ -136,7 +142,7 @@ impl HsDescOuter {
Ok(result) Ok(result)
} }
/// Extract an HsDescOuter from a reader. /// Extract an HsDescOuter from a reader.
/// ///
/// The reader must contain a single HsDescOuter; we return an error if not. /// The reader must contain a single HsDescOuter; we return an error if not.
fn take_from_reader(reader: &mut NetDocReader<'_, HsOuterKwd>) -> Result<UncheckedHsDescOuter> { fn take_from_reader(reader: &mut NetDocReader<'_, HsOuterKwd>) -> Result<UncheckedHsDescOuter> {
@ -176,7 +182,7 @@ impl HsDescOuter {
// TODO: This way of handling prefixes does a needless // TODO: This way of handling prefixes does a needless
// allocation. Someday we could make our signature-checking // allocation. Someday we could make our signature-checking
// logic even smarter. // logic even smarter.
let mut signed_text = b"Tor onion service descriptor sig v3".to_vec(); let mut signed_text = HS_DESC_SIGNATURE_PREFIX.to_vec();
signed_text.extend_from_slice( signed_text.extend_from_slice(
s.get(start_idx..end_idx) s.get(start_idx..end_idx)
.expect("Somehow the first item came after the last‽") .expect("Somehow the first item came after the last‽")
@ -188,7 +194,7 @@ impl HsDescOuter {
// Check that the hs-descriptor version is 3. // Check that the hs-descriptor version is 3.
{ {
let version = body.required(HS_DESCRIPTOR)?.required_arg(0)?; let version = body.required(HS_DESCRIPTOR)?.required_arg(0)?;
if version != "3" { if version != HS_DESC_VERSION_CURRENT {
return Err(EK::BadDocumentVersion return Err(EK::BadDocumentVersion
.with_msg(format!("Unexpected hsdesc version {}", version)) .with_msg(format!("Unexpected hsdesc version {}", version))
.at_pos(Pos::at(version))); .at_pos(Pos::at(version)));

View File

@ -56,7 +56,7 @@ pub use err::{BuildError, Error, ParseErrorKind, Pos};
#[cfg(feature = "hs-service")] #[cfg(feature = "hs-service")]
#[cfg_attr(docsrs, doc(cfg(feature = "hs-service")))] #[cfg_attr(docsrs, doc(cfg(feature = "hs-service")))]
pub use build::NetdocText; pub use build::{NetdocBuilder, NetdocText};
/// Alias for the Result type returned by most objects in this module. /// Alias for the Result type returned by most objects in this module.
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;