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:
parent
89ca965d2a
commit
a1074c0027
|
@ -4075,6 +4075,7 @@ dependencies = [
|
|||
"cipher",
|
||||
"ctr",
|
||||
"curve25519-dalek",
|
||||
"derive_more",
|
||||
"digest 0.10.6",
|
||||
"ed25519-dalek",
|
||||
"getrandom 0.2.8",
|
||||
|
@ -4146,6 +4147,7 @@ dependencies = [
|
|||
"base64ct",
|
||||
"bitflags 2.0.0",
|
||||
"cipher",
|
||||
"derive_builder_fork_arti",
|
||||
"derive_more",
|
||||
"digest 0.10.6",
|
||||
"educe",
|
||||
|
|
|
@ -64,7 +64,16 @@ pub struct Subcredential([u8; 32]);
|
|||
/// There can be gaps in this numbering. A descriptor with a higher-valued
|
||||
/// revision counter supersedes one with a lower revision counter.
|
||||
#[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);
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ base64ct = "1.5.1"
|
|||
cipher = { version = "0.4.3", optional = true, features = ["zeroize"] }
|
||||
ctr = { version = "0.9", features = ["zeroize"] }
|
||||
curve25519-dalek = "3.2"
|
||||
derive_more = "0.99.3"
|
||||
digest = "0.10.0"
|
||||
ed25519-dalek = { version = "1", features = ["batch"] }
|
||||
hex = "0.4"
|
||||
|
|
|
@ -15,7 +15,7 @@ use zeroize::Zeroize;
|
|||
/// (The decision to avoid implementing `Deref`/`DerefMut` is deliberate.)
|
||||
#[allow(renamed_and_removed_lints)] // TODO Remove @ 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]);
|
||||
|
||||
impl<const N: usize> ConstantTimeEq for CtByteArray<N> {
|
||||
|
|
|
@ -33,11 +33,11 @@ routerdesc = []
|
|||
# Enable the "ns consensus" document type, which some relays cache and serve.
|
||||
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.
|
||||
# TODO hs: mark these as part of "full" once they are done and stable.
|
||||
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"]
|
||||
# Testing only : expose code to parse inner layers of onion service descriptors.
|
||||
hsdesc-inner-docs = ["visibility"]
|
||||
|
@ -64,6 +64,7 @@ arrayref = "0.3"
|
|||
base64ct = { version = "1.5.1", features = ["alloc"] }
|
||||
bitflags = "2"
|
||||
cipher = { version = "0.4.1", features = ["zeroize"] }
|
||||
derive_builder = { version = "0.11.2", package = "derive_builder_fork_arti" }
|
||||
derive_more = "0.99.3"
|
||||
digest = "0.10.0"
|
||||
educe = "0.4.6"
|
||||
|
|
|
@ -27,7 +27,9 @@ use std::time::SystemTime;
|
|||
use base64ct::{Base64, Encoding};
|
||||
use humantime::format_rfc3339;
|
||||
use tor_bytes::EncodeError;
|
||||
use tor_cert::Ed25519Cert;
|
||||
use tor_error::{internal, into_internal, Bug};
|
||||
use tor_llcrypto::pk::ed25519;
|
||||
|
||||
use crate::parse::keyword::Keyword;
|
||||
use crate::parse::tokenize::tag_keywords_ok;
|
||||
|
@ -38,6 +40,7 @@ use crate::parse::tokenize::tag_keywords_ok;
|
|||
/// for clarity in function signatures etc.
|
||||
//
|
||||
// TODO hs: Is this type carrying its weight, or should we just replace it with String ?
|
||||
#[derive(Debug)]
|
||||
pub struct NetdocText<Builder> {
|
||||
/// The actual document
|
||||
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
|
||||
///
|
||||
/// # Example
|
||||
|
@ -210,7 +226,7 @@ impl NetdocEncoder {
|
|||
}
|
||||
|
||||
/// 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?;
|
||||
Ok(NetdocText {
|
||||
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)]
|
||||
mod test {
|
||||
// @@ begin test lint list maintained by maint/add_warning @@
|
||||
|
@ -428,7 +452,7 @@ qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE";
|
|||
.item(ACK::DIR_KEY_CERTIFICATION)
|
||||
.object("SIGNATURE", []);
|
||||
|
||||
let doc = encode.finish().unwrap();
|
||||
let doc = encode.finish::<NetdocEncoder>().unwrap();
|
||||
eprintln!("{}", &*doc);
|
||||
assert_eq!(
|
||||
&*doc,
|
||||
|
|
|
@ -11,34 +11,37 @@
|
|||
#![allow(dead_code, unused_variables, clippy::missing_panics_doc)] // TODO hs: remove.
|
||||
mod desc_enc;
|
||||
|
||||
mod build;
|
||||
mod inner;
|
||||
mod middle;
|
||||
mod outer;
|
||||
|
||||
use std::time::SystemTime;
|
||||
pub use desc_enc::DecryptionError;
|
||||
|
||||
use crate::{ParseErrorKind as EK, Result};
|
||||
pub use desc_enc::DecryptionError;
|
||||
use smallvec::SmallVec;
|
||||
use tor_checkable::{
|
||||
signed::{self, SignatureGated},
|
||||
timed::{self, TimerangeBound},
|
||||
};
|
||||
use tor_hscrypto::{
|
||||
pk::{
|
||||
HsBlindId, HsClientDescEncKey, HsClientDescEncSecretKey, HsIntroPtSessionIdKey,
|
||||
HsSvcNtorKey,
|
||||
},
|
||||
RevisionCounter, Subcredential,
|
||||
|
||||
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
|
||||
|
@ -113,9 +116,11 @@ pub struct HsDesc {
|
|||
|
||||
/// A type of authentication that is required when introducing to an onion
|
||||
/// service.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
enum IntroAuthType {
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, derive_more::Display)]
|
||||
pub enum IntroAuthType {
|
||||
/// Ed25519 authentication is required.
|
||||
#[display(fmt = "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)]
|
||||
mod test {
|
||||
// @@ begin test lint list maintained by maint/add_warning @@
|
||||
|
@ -316,7 +316,7 @@ mod test {
|
|||
#![allow(clippy::single_char_pattern)]
|
||||
#![allow(clippy::unwrap_used)]
|
||||
#![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 super::*;
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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-----
|
||||
"#
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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-----
|
||||
"#
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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==
|
||||
"#
|
||||
);
|
||||
}
|
||||
}
|
|
@ -42,13 +42,13 @@ pub(super) struct HsDescEncryption<'a> {
|
|||
}
|
||||
|
||||
/// 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.
|
||||
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").
|
||||
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
|
||||
/// 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
|
||||
/// "descriptor cookie".
|
||||
#[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.
|
||||
const SALT_LEN: usize = 16;
|
||||
|
|
|
@ -39,7 +39,7 @@ pub(super) struct HsDescInner {
|
|||
}
|
||||
|
||||
decl_keyword! {
|
||||
HsInnerKwd {
|
||||
pub(crate) HsInnerKwd {
|
||||
"create2-formats" => CREATE2_FORMATS,
|
||||
"intro-auth-required" => INTRO_AUTH_REQUIRED,
|
||||
"single-onion-service" => SINGLE_ONION_SERVICE,
|
||||
|
|
|
@ -13,11 +13,14 @@ use crate::types::misc::B64;
|
|||
use crate::{Pos, Result};
|
||||
|
||||
use super::desc_enc::{
|
||||
HsDescEncNonce, HsDescEncryption, HS_DESC_MIDDLE_CLIENT_ID_LEN, HS_DESC_MIDDLE_ENC_NONCE_LEN,
|
||||
HS_DESC_MIDDLE_IV_LEN,
|
||||
HsDescEncNonce, HsDescEncryption, HS_DESC_CLIENT_ID_LEN, HS_DESC_ENC_NONCE_LEN, HS_DESC_IV_LEN,
|
||||
};
|
||||
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
|
||||
/// service descriptor.
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -134,15 +137,15 @@ impl HsDescMiddle {
|
|||
/// Information that a single authorized client can use to decrypt the onion
|
||||
/// service descriptor.
|
||||
#[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.
|
||||
///
|
||||
/// 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`.
|
||||
///
|
||||
/// 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`,
|
||||
/// which in turn is
|
||||
/// 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
|
||||
/// reveals a `DescEncEncryptionCookie` (`N_hs_desc_enc`, not yet so named
|
||||
/// in the spec).
|
||||
encrypted_cookie: [u8; HS_DESC_MIDDLE_ENC_NONCE_LEN],
|
||||
pub(crate) encrypted_cookie: [u8; HS_DESC_ENC_NONCE_LEN],
|
||||
}
|
||||
|
||||
impl AuthClient {
|
||||
|
@ -173,7 +176,7 @@ impl AuthClient {
|
|||
}
|
||||
|
||||
decl_keyword! {
|
||||
HsMiddleKwd {
|
||||
pub(crate) HsMiddleKwd {
|
||||
"desc-auth-type" => DESC_AUTH_TYPE,
|
||||
"desc-auth-ephemeral-key" => DESC_AUTH_EPHEMERAL_KEY,
|
||||
"auth-client" => AUTH_CLIENT,
|
||||
|
|
|
@ -16,6 +16,12 @@ use crate::{Pos, Result};
|
|||
|
||||
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
|
||||
/// of an onion service descriptor.
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -99,7 +105,7 @@ impl HsDescOuter {
|
|||
pub(super) type UncheckedHsDescOuter = SignatureGated<TimerangeBound<HsDescOuter>>;
|
||||
|
||||
decl_keyword! {
|
||||
HsOuterKwd {
|
||||
pub(crate) HsOuterKwd {
|
||||
"hs-descriptor" => HS_DESCRIPTOR,
|
||||
"descriptor-lifetime" => DESCRIPTOR_LIFETIME,
|
||||
"descriptor-signing-key-cert" => DESCRIPTOR_SIGNING_KEY_CERT,
|
||||
|
@ -136,7 +142,7 @@ impl HsDescOuter {
|
|||
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.
|
||||
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
|
||||
// allocation. Someday we could make our signature-checking
|
||||
// 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(
|
||||
s.get(start_idx..end_idx)
|
||||
.expect("Somehow the first item came after the last‽")
|
||||
|
@ -188,7 +194,7 @@ impl HsDescOuter {
|
|||
// Check that the hs-descriptor version is 3.
|
||||
{
|
||||
let version = body.required(HS_DESCRIPTOR)?.required_arg(0)?;
|
||||
if version != "3" {
|
||||
if version != HS_DESC_VERSION_CURRENT {
|
||||
return Err(EK::BadDocumentVersion
|
||||
.with_msg(format!("Unexpected hsdesc version {}", version))
|
||||
.at_pos(Pos::at(version)));
|
||||
|
|
|
@ -56,7 +56,7 @@ pub use err::{BuildError, Error, ParseErrorKind, Pos};
|
|||
|
||||
#[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.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
|
Loading…
Reference in New Issue