Merge branch 'encode_hsdesc' into 'main'
Add builder for encoding hidden service descriptors. See merge request tpo/core/arti!1070
This commit is contained in:
commit
aba78a3eda
|
@ -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,31 +27,13 @@ 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;
|
||||
|
||||
/// Network document text according to dir-spec.txt s1.2 and maybe s1.3
|
||||
///
|
||||
/// Contains just the text, but marked with the type of the builder
|
||||
/// for clarity in function signatures etc.
|
||||
//
|
||||
// TODO hs: Is this type carrying its weight, or should we just replace it with String ?
|
||||
pub struct NetdocText<Builder> {
|
||||
/// The actual document
|
||||
text: String,
|
||||
/// Marker. Variance: this somehow came from a T (not that we expect this to matter)
|
||||
kind: PhantomData<Builder>,
|
||||
}
|
||||
|
||||
impl<B> Deref for NetdocText<B> {
|
||||
type Target = str;
|
||||
fn deref(&self) -> &str {
|
||||
&self.text
|
||||
}
|
||||
}
|
||||
|
||||
/// Encoder, representing a partially-built document
|
||||
///
|
||||
/// # Example
|
||||
|
@ -198,9 +180,6 @@ impl NetdocEncoder {
|
|||
/// Obtain the text of a section of the document
|
||||
///
|
||||
/// Useful for making a signature.
|
||||
//
|
||||
// Q. Should this return `&str` or `NetdocText<'self>` ?
|
||||
// (`NetdocText would have to then contain `Cow`, which is fine.)
|
||||
pub(crate) fn slice(&self, begin: Cursor, end: Cursor) -> Result<&str, Bug> {
|
||||
self.built
|
||||
.as_ref()
|
||||
|
@ -210,12 +189,8 @@ impl NetdocEncoder {
|
|||
}
|
||||
|
||||
/// Build the document into textual form
|
||||
pub(crate) fn finish(self) -> Result<NetdocText<Self>, Bug> {
|
||||
let text = self.built?;
|
||||
Ok(NetdocText {
|
||||
text,
|
||||
kind: PhantomData,
|
||||
})
|
||||
pub(crate) fn finish(self) -> Result<String, Bug> {
|
||||
self.built
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -231,6 +206,12 @@ impl ItemArgument for str {
|
|||
}
|
||||
}
|
||||
|
||||
impl ItemArgument for String {
|
||||
fn write_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
|
||||
ItemArgument::write_onto(&self.as_str(), out)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ItemArgument + ?Sized> ItemArgument for &'_ T {
|
||||
fn write_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
|
||||
<T as ItemArgument>::write_onto(self, out)
|
||||
|
@ -363,6 +344,12 @@ 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<String, EncodeError>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
// @@ begin test lint list maintained by maint/add_warning @@
|
||||
|
@ -423,9 +410,9 @@ qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE";
|
|||
.object("SIGNATURE", []);
|
||||
|
||||
let doc = encode.finish().unwrap();
|
||||
eprintln!("{}", &*doc);
|
||||
eprintln!("{}", doc);
|
||||
assert_eq!(
|
||||
&*doc,
|
||||
doc,
|
||||
r"dir-key-certificate-version 3
|
||||
fingerprint 9367f9781da8eabbf96b691175f0e701b43c602e
|
||||
dir-key-published 2020-04-18 08:36:57
|
||||
|
|
|
@ -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,389 @@
|
|||
//! Hidden service descriptor encoding.
|
||||
|
||||
mod inner;
|
||||
mod middle;
|
||||
mod outer;
|
||||
|
||||
use crate::doc::hsdesc::IntroAuthType;
|
||||
use crate::NetdocBuilder;
|
||||
use tor_bytes::EncodeError;
|
||||
use tor_error::into_bad_api_usage;
|
||||
use tor_hscrypto::pk::HsSvcDescEncKey;
|
||||
use tor_hscrypto::{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::{HsDescInner, IntroPointDesc};
|
||||
use self::middle::HsDescMiddle;
|
||||
use self::outer::HsDescOuter;
|
||||
|
||||
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
|
||||
/// `HsDescMiddle` 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<String, 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 inner (second layer) plaintext. This is the unencrypted value of the
|
||||
// "encrypted" field.
|
||||
let inner_plaintext = HsDescInner {
|
||||
hs_desc_sign: hs_desc.hs_desc_sign,
|
||||
create2_formats: hs_desc.create2_formats,
|
||||
auth_required: hs_desc.auth_required.as_ref(),
|
||||
is_single_onion_service: hs_desc.is_single_onion_service,
|
||||
intro_points: hs_desc.intro_points,
|
||||
intro_auth_key_cert_expiry: hs_desc.intro_auth_key_cert_expiry,
|
||||
intro_enc_key_cert_expiry: hs_desc.intro_enc_key_cert_expiry,
|
||||
}
|
||||
.build_sign()?;
|
||||
|
||||
let desc_enc_nonce = hs_desc
|
||||
.client_auth
|
||||
.as_ref()
|
||||
.map(|client_auth| client_auth.descriptor_cookie.into());
|
||||
|
||||
// Encrypt the inner document. The encrypted blob is the ciphertext contained in the
|
||||
// "encrypted" field described in section 2.5.1.2. of rend-spec-v3.
|
||||
let inner_encrypted = hs_desc.encrypt_field(
|
||||
inner_plaintext.as_bytes(),
|
||||
desc_enc_nonce.as_ref(),
|
||||
b"hsdir-encrypted-data",
|
||||
);
|
||||
|
||||
// Construct the middle (first player) plaintext. This is the unencrypted value of the
|
||||
// "superencrypted" field.
|
||||
let middle_plaintext = HsDescMiddle {
|
||||
client_auth: hs_desc.client_auth,
|
||||
encrypted: inner_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 middle_plaintext =
|
||||
pad_with_zero_to_align(middle_plaintext.as_bytes(), SUPERENCRYPTED_ALIGN);
|
||||
|
||||
// Encrypt the middle document. The encrypted blob is the ciphertext contained in the
|
||||
// "superencrypted" field described in section 2.5.1.1. of rend-spec-v3.
|
||||
let middle_encrypted = hs_desc.encrypt_field(
|
||||
middle_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.
|
||||
HsDescOuter {
|
||||
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: middle_encrypted,
|
||||
}
|
||||
.build_sign()
|
||||
}
|
||||
}
|
||||
|
||||
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::net::Ipv4Addr;
|
||||
use std::time::Duration;
|
||||
|
||||
use super::*;
|
||||
use crate::doc::hsdesc::desc_enc::HS_DESC_ENC_NONCE_LEN;
|
||||
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::LinkSpec;
|
||||
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<LinkSpec>) -> 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![LinkSpec::OrPort(Ipv4Addr::LOCALHOST.into(), 9999)],
|
||||
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,396 @@
|
|||
//! Functionality for encoding the inner document of an onion service descriptor.
|
||||
//!
|
||||
//! NOTE: `HsDescInner` 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::doc::hsdesc::IntroAuthType;
|
||||
use crate::NetdocBuilder;
|
||||
|
||||
use tor_bytes::{EncodeError, Writer};
|
||||
use tor_cert::{CertType, CertifiedKey, Ed25519Cert};
|
||||
use tor_error::{bad_api_usage, into_bad_api_usage};
|
||||
use tor_hscrypto::pk::HsIntroPtSessionIdKey;
|
||||
use tor_hscrypto::pk::HsSvcNtorKey;
|
||||
use tor_linkspec::LinkSpec;
|
||||
use tor_llcrypto::pk::keymanip::convert_curve25519_to_ed25519_public;
|
||||
use tor_llcrypto::pk::{curve25519, ed25519};
|
||||
|
||||
use base64ct::{Base64, Encoding};
|
||||
|
||||
use std::time::SystemTime;
|
||||
|
||||
use smallvec::SmallVec;
|
||||
|
||||
/// 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(Debug)]
|
||||
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,
|
||||
}
|
||||
|
||||
/// Information in an onion service descriptor about a single introduction point.
|
||||
///
|
||||
/// TODO hs: Move out of tor-netdoc: this is a general-purpose representation of an introduction
|
||||
/// point, not merely an intermediate representation for decoding/encoding. There may be other
|
||||
/// types that need to be factored out tor-netdoc for the same reason.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IntroPointDesc {
|
||||
/// A list of link specifiers needed to extend a circuit to the introduction point.
|
||||
///
|
||||
/// These can include public keys and network addresses.
|
||||
pub(crate) link_specifiers: Vec<LinkSpec>,
|
||||
/// The key used to extend a circuit _to the introduction point_, using the
|
||||
/// ntor or ntor3 handshakes. (`KP_ntor`)
|
||||
pub(crate) ipt_ntor_key: curve25519::PublicKey,
|
||||
/// A key used to identify the onion service at this introduction point.
|
||||
/// (`KP_hs_ipt_sid`)
|
||||
pub(crate) ipt_sid_key: HsIntroPtSessionIdKey,
|
||||
/// `KP_hss_ntor`, the key used to encrypt a handshake _to the onion
|
||||
/// service_ when using this introduction point.
|
||||
///
|
||||
/// The onion service uses a separate key of this type with each
|
||||
/// introduction point as part of its strategy for preventing replay
|
||||
/// attacks.
|
||||
pub(crate) svc_ntor_key: HsSvcNtorKey,
|
||||
}
|
||||
|
||||
impl<'a> NetdocBuilder for HsDescInner<'a> {
|
||||
fn build_sign(self) -> Result<String, 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;
|
||||
|
||||
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::net::Ipv4Addr;
|
||||
use std::time::UNIX_EPOCH;
|
||||
use tor_linkspec::LinkSpec;
|
||||
|
||||
/// Build an inner document using the specified parameters.
|
||||
fn create_inner_desc(
|
||||
create2_formats: &[u32],
|
||||
auth_required: Option<&SmallVec<[IntroAuthType; 2]>>,
|
||||
is_single_onion_service: bool,
|
||||
intro_points: &[IntroPointDesc],
|
||||
) -> Result<String, EncodeError> {
|
||||
let hs_desc_sign = test_ed25519_keypair();
|
||||
|
||||
HsDescInner {
|
||||
hs_desc_sign: &hs_desc_sign,
|
||||
create2_formats,
|
||||
auth_required,
|
||||
is_single_onion_service,
|
||||
intro_points,
|
||||
intro_auth_key_cert_expiry: UNIX_EPOCH,
|
||||
intro_enc_key_cert_expiry: UNIX_EPOCH,
|
||||
}
|
||||
.build_sign()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inner_hsdesc_no_intro_auth() {
|
||||
// A descriptor for a "single onion service"
|
||||
let hs_desc = create_inner_desc(
|
||||
&[1234], /* create2_formats */
|
||||
None, /* auth_required */
|
||||
true, /* is_single_onion_service */
|
||||
&[], /* intro_points */
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(hs_desc, "create2-formats 1234\nsingle-onion-service\n");
|
||||
|
||||
// A descriptor for a location-hidden service
|
||||
let hs_desc = create_inner_desc(
|
||||
&[1234], /* create2_formats */
|
||||
None, /* auth_required */
|
||||
false, /* is_single_onion_service */
|
||||
&[], /* intro_points */
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(hs_desc, "create2-formats 1234\n");
|
||||
|
||||
let link_specs1 = vec![LinkSpec::OrPort(Ipv4Addr::LOCALHOST.into(), 1234)];
|
||||
let link_specs2 = vec![LinkSpec::OrPort(Ipv4Addr::LOCALHOST.into(), 5679)];
|
||||
let link_specs3 = vec![LinkSpec::OrPort(Ipv4Addr::LOCALHOST.into(), 8901)];
|
||||
|
||||
let intros = &[
|
||||
test_intro_point_descriptor(link_specs1),
|
||||
test_intro_point_descriptor(link_specs2),
|
||||
test_intro_point_descriptor(link_specs3),
|
||||
];
|
||||
|
||||
let hs_desc = create_inner_desc(
|
||||
&[1234, 32, 23], /* create2_formats */
|
||||
None, /* auth_required */
|
||||
false, /* is_single_onion_service */
|
||||
intros, /* intro_points */
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
hs_desc,
|
||||
r#"create2-formats 1234 32 23
|
||||
introduction-point AQAGfwAAAQTS
|
||||
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 AQAGfwAAARYv
|
||||
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 AQAGfwAAASLF
|
||||
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 = LinkSpec::OrPort(Ipv4Addr::LOCALHOST.into(), 9999);
|
||||
let link_specifiers = std::iter::repeat(link_spec)
|
||||
.take(u8::MAX as usize + 1)
|
||||
.collect::<Vec<_>>();
|
||||
let intros = &[test_intro_point_descriptor(link_specifiers)];
|
||||
|
||||
// A descriptor for a location-hidden service with an introduction point with too many link
|
||||
// specifiers
|
||||
let err = create_inner_desc(
|
||||
&[1234], /* create2_formats */
|
||||
None, /* auth_required */
|
||||
false, /* is_single_onion_service */
|
||||
intros, /* intro_points */
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(expect_bug(err).contains("Too many link specifiers."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inner_hsdesc_intro_auth() {
|
||||
let hs_desc_sign = test_ed25519_keypair();
|
||||
let link_specs = vec![LinkSpec::OrPort(Ipv4Addr::LOCALHOST.into(), 8080)];
|
||||
let intros = &[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 = create_inner_desc(
|
||||
&[1234], /* create2_formats */
|
||||
Some(&auth), /* auth_required */
|
||||
false, /* is_single_onion_service */
|
||||
intros, /* intro_points */
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
hs_desc,
|
||||
r#"create2-formats 1234
|
||||
intro-auth-required ed25519 ed25519
|
||||
introduction-point AQAGfwAAAR+Q
|
||||
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,219 @@
|
|||
//! Functionality for encoding the middle document of an onion service descriptor.
|
||||
//!
|
||||
//! NOTE: `HsDescMiddle` 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::build::ClientAuth;
|
||||
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;
|
||||
|
||||
use tor_bytes::EncodeError;
|
||||
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;
|
||||
|
||||
/// 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(Debug)]
|
||||
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 created 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>,
|
||||
}
|
||||
|
||||
impl<'a> NetdocBuilder for HsDescMiddle<'a> {
|
||||
fn build_sign(self) -> Result<String, EncodeError> {
|
||||
use HsMiddleKwd::*;
|
||||
|
||||
let HsDescMiddle {
|
||||
client_auth,
|
||||
encrypted,
|
||||
} = self;
|
||||
|
||||
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 = HsDescMiddle {
|
||||
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, HsDescMiddle 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 = HsDescMiddle {
|
||||
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 = HsDescMiddle {
|
||||
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,161 @@
|
|||
//! Functionality for encoding the outer document of an onion service descriptor.
|
||||
//!
|
||||
//! NOTE: `HsDescOuter` 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::{NetdocBuilder, NetdocEncoder};
|
||||
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_hscrypto::RevisionCounter;
|
||||
use tor_llcrypto::pk::ed25519;
|
||||
use tor_units::IntegerMinutes;
|
||||
|
||||
use base64ct::{Base64, Encoding};
|
||||
|
||||
use std::time::SystemTime;
|
||||
|
||||
/// 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(Debug)]
|
||||
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 created by encrypting a middle document built using
|
||||
/// [`crate::doc::hsdesc::build::middle::HsDescMiddle`] as described in sections
|
||||
/// 2.5.1.1. and 2.5.1.2. of rend-spec-v3.
|
||||
pub(super) superencrypted: Vec<u8>,
|
||||
}
|
||||
|
||||
impl<'a> NetdocBuilder for HsDescOuter<'a> {
|
||||
fn build_sign(self) -> Result<String, EncodeError> {
|
||||
use HsOuterKwd::*;
|
||||
|
||||
let HsDescOuter {
|
||||
blinded_id,
|
||||
hs_desc_sign,
|
||||
hs_desc_sign_cert_expiry,
|
||||
lifetime,
|
||||
revision_counter,
|
||||
superencrypted,
|
||||
} = self;
|
||||
|
||||
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 = HsDescOuter {
|
||||
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==
|
||||
"#
|
||||
);
|
||||
}
|
||||
}
|
|
@ -41,13 +41,22 @@ pub(super) struct HsDescEncryption<'a> {
|
|||
pub(super) string_const: &'a [u8],
|
||||
}
|
||||
|
||||
/// The length of a client ID.
|
||||
pub(crate) const HS_DESC_CLIENT_ID_LEN: usize = 8;
|
||||
|
||||
/// The length of the the `AuthClient` IV.
|
||||
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_ENC_NONCE_LEN: usize = 16;
|
||||
|
||||
/// A value used in deriving the encryption key for the inner (encryption) layer
|
||||
/// of onion service encryption.
|
||||
///
|
||||
/// 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; 16]);
|
||||
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,
|
||||
|
|
|
@ -12,9 +12,15 @@ use crate::parse::{keyword::Keyword, parser::SectionRules};
|
|||
use crate::types::misc::B64;
|
||||
use crate::{Pos, Result};
|
||||
|
||||
use super::desc_enc::{HsDescEncNonce, HsDescEncryption};
|
||||
use super::desc_enc::{
|
||||
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)]
|
||||
|
@ -67,7 +73,7 @@ impl HsDescMiddle {
|
|||
}
|
||||
|
||||
/// Use a `ClientDescAuthSecretKey` (`KS_hsc_desc_enc`) to see if there is any `auth-client`
|
||||
/// entry for us (a client who holds that secret key) in this descriptor.
|
||||
/// entry for us (a client who holds that secret key) in this descriptor.
|
||||
/// If so, decrypt it and return its
|
||||
/// corresponding "Descriptor Cookie" (`N_hs_desc_enc`)
|
||||
///
|
||||
|
@ -131,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<8>,
|
||||
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; 16],
|
||||
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`.
|
||||
|
@ -147,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; 16],
|
||||
pub(crate) encrypted_cookie: [u8; HS_DESC_ENC_NONCE_LEN],
|
||||
}
|
||||
|
||||
impl AuthClient {
|
||||
|
@ -170,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,
|
||||
|
@ -203,7 +209,7 @@ impl HsDescMiddle {
|
|||
Ok(result)
|
||||
}
|
||||
|
||||
/// Extract an HsDescMiddle from a reader.
|
||||
/// Extract an HsDescMiddle from a reader.
|
||||
///
|
||||
/// The reader must contain a single HsDescOuter; we return an error if not.
|
||||
fn take_from_reader(reader: &mut NetDocReader<'_, HsMiddleKwd>) -> Result<HsDescMiddle> {
|
||||
|
|
|
@ -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;
|
||||
|
||||
/// 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