Merge branch 'hsdesc-encoder-client-auth' into 'main'
Stop requiring the caller to supply `AuthClient`s. See merge request tpo/core/arti!1087
This commit is contained in:
commit
c21b1d5dc1
|
@ -390,6 +390,8 @@ impl HsBlindIdSecretKey {
|
|||
}
|
||||
|
||||
/// A blinded Ed25519 keypair.
|
||||
///
|
||||
/// TODO hs: should we extend `define_pk_keypair` to also define a *Keypair struct?
|
||||
#[allow(clippy::exhaustive_structs)]
|
||||
#[derive(Debug)]
|
||||
pub struct HsBlindKeypair {
|
||||
|
@ -475,6 +477,19 @@ define_pk_keypair! {
|
|||
pub struct HsSvcDescEncKey(curve25519::PublicKey) / HsSvcDescEncSecretKey(curve25519::StaticSecret);
|
||||
}
|
||||
|
||||
/// An ephemeral x25519 keypair, generated by an onion service
|
||||
/// and used to for onion service encryption.
|
||||
///
|
||||
/// TODO hs: should we extend `define_pk_keypair` to also define a *Keypair struct?
|
||||
#[allow(clippy::exhaustive_structs)]
|
||||
#[derive(Debug)]
|
||||
pub struct HsSvcDescEncKeypair {
|
||||
/// The public part of the key.
|
||||
pub public: HsSvcDescEncKey,
|
||||
/// The secret part of the key.
|
||||
pub secret: HsSvcDescEncSecretKey,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
// @@ begin test lint list maintained by maint/add_warning @@
|
||||
|
|
|
@ -9,8 +9,9 @@ use crate::NetdocBuilder;
|
|||
use rand::{CryptoRng, RngCore};
|
||||
use tor_bytes::EncodeError;
|
||||
use tor_error::into_bad_api_usage;
|
||||
use tor_hscrypto::pk::{HsBlindKeypair, HsSvcDescEncKey};
|
||||
use tor_hscrypto::pk::{HsBlindKeypair, HsSvcDescEncKeypair};
|
||||
use tor_hscrypto::{RevisionCounter, Subcredential};
|
||||
use tor_llcrypto::pk::curve25519;
|
||||
use tor_llcrypto::pk::ed25519::{self, Ed25519PublicKey};
|
||||
use tor_units::IntegerMinutes;
|
||||
|
||||
|
@ -25,13 +26,12 @@ 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 = ""))]
|
||||
#[builder(public, derive(Debug, Clone), 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).
|
||||
|
@ -53,9 +53,13 @@ struct HsDesc<'a> {
|
|||
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 list of clients authorized to access the hidden service. If empty, client
|
||||
/// authentication is disabled.
|
||||
///
|
||||
/// If client authorization is disabled, the resulting middle document will contain a single
|
||||
/// `auth-client` line populated with random values.
|
||||
#[builder(default)]
|
||||
auth_clients: &'a [curve25519::PublicKey],
|
||||
/// The lifetime of this descriptor, in minutes.
|
||||
///
|
||||
/// This doesn't actually list the starting time or the end time for the
|
||||
|
@ -70,29 +74,49 @@ struct HsDesc<'a> {
|
|||
}
|
||||
|
||||
/// 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.
|
||||
#[derive(Debug)]
|
||||
pub(super) struct ClientAuth<'a> {
|
||||
/// An ephemeral x25519 keypair generated by the hidden service (`KP_hss_desc_enc`).
|
||||
///
|
||||
/// 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.
|
||||
/// A new keypair MUST be generated every time a descriptor is encoded, or the descriptor
|
||||
/// encryption will not be secure.
|
||||
ephemeral_key: HsSvcDescEncKeypair,
|
||||
/// One or more authorized clients.
|
||||
auth_clients: &'a [curve25519::PublicKey],
|
||||
/// The `N_hs_desc_enc` descriptor_cookie key generated by the hidden service.
|
||||
///
|
||||
/// 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.
|
||||
/// A new descriptor cookie is randomly generated for each descriptor.
|
||||
descriptor_cookie: [u8; HS_DESC_ENC_NONCE_LEN],
|
||||
}
|
||||
|
||||
impl<'a> ClientAuth<'a> {
|
||||
/// Create a new `ClientAuth` using the specified authorized clients.
|
||||
///
|
||||
/// 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],
|
||||
/// This returns `None` if the list of authorized clients is empty.
|
||||
fn new<R: RngCore + CryptoRng>(
|
||||
auth_clients: &'a [curve25519::PublicKey],
|
||||
rng: &mut R,
|
||||
) -> Option<ClientAuth<'a>> {
|
||||
// Client auth is disabled
|
||||
if auth_clients.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Generate a new `N_hs_desc_enc` descriptor_cookie key for this descriptor.
|
||||
let descriptor_cookie = rand::Rng::gen::<[u8; HS_DESC_ENC_NONCE_LEN]>(rng);
|
||||
|
||||
let secret = curve25519::StaticSecret::new(rng);
|
||||
let ephemeral_key = HsSvcDescEncKeypair {
|
||||
public: curve25519::PublicKey::from(&secret).into(),
|
||||
secret: secret.into(),
|
||||
};
|
||||
|
||||
Some(ClientAuth {
|
||||
ephemeral_key,
|
||||
auth_clients,
|
||||
descriptor_cookie,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> NetdocBuilder for HsDescBuilder<'a> {
|
||||
|
@ -106,6 +130,8 @@ impl<'a> NetdocBuilder for HsDescBuilder<'a> {
|
|||
.build()
|
||||
.map_err(into_bad_api_usage!("the HsDesc could not be built"))?;
|
||||
|
||||
let client_auth = ClientAuth::new(hs_desc.auth_clients, rng);
|
||||
|
||||
// Construct the inner (second layer) plaintext. This is the unencrypted value of the
|
||||
// "encrypted" field.
|
||||
let inner_plaintext = HsDescInner {
|
||||
|
@ -119,8 +145,7 @@ impl<'a> NetdocBuilder for HsDescBuilder<'a> {
|
|||
}
|
||||
.build_sign(rng)?;
|
||||
|
||||
let desc_enc_nonce = hs_desc
|
||||
.client_auth
|
||||
let desc_enc_nonce = client_auth
|
||||
.as_ref()
|
||||
.map(|client_auth| client_auth.descriptor_cookie.into());
|
||||
|
||||
|
@ -136,7 +161,8 @@ impl<'a> NetdocBuilder for HsDescBuilder<'a> {
|
|||
// 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,
|
||||
client_auth: client_auth.as_ref(),
|
||||
subcredential: hs_desc.subcredential,
|
||||
encrypted: inner_encrypted,
|
||||
}
|
||||
.build_sign(rng)?;
|
||||
|
@ -226,22 +252,18 @@ mod test {
|
|||
use std::time::Duration;
|
||||
|
||||
use super::*;
|
||||
use crate::doc::hsdesc::{EncryptedHsDesc, HsDesc as HsDescDecoder};
|
||||
use crate::doc::hsdesc::{EncryptedHsDesc, HsDesc as ParsedHsDesc};
|
||||
use tor_basic_utils::test_rng::Config;
|
||||
use tor_checkable::{SelfSigned, Timebound};
|
||||
use tor_hscrypto::pk::HsIdSecretKey;
|
||||
use tor_hscrypto::pk::{HsClientDescEncKey, HsClientDescEncSecretKey, HsIdSecretKey};
|
||||
use tor_hscrypto::time::TimePeriod;
|
||||
use tor_linkspec::LinkSpec;
|
||||
use tor_llcrypto::pk::curve25519;
|
||||
use tor_llcrypto::pk::keymanip::ExpandedSecretKey;
|
||||
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.
|
||||
|
||||
// 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];
|
||||
// TODO: move the test helpers to a separate module and make them more broadly available if
|
||||
// necessary.
|
||||
|
||||
/// Expect `err` to be a `Bug`, and return its string representation.
|
||||
///
|
||||
|
@ -278,8 +300,39 @@ mod test {
|
|||
(&ephemeral_key).into()
|
||||
}
|
||||
|
||||
/// Parse the specified hidden service descriptor.
|
||||
fn parse_hsdesc(
|
||||
unparsed_desc: &str,
|
||||
blinded_pk: ed25519::PublicKey,
|
||||
subcredential: &Subcredential,
|
||||
hsc_desc_enc: Option<(&HsClientDescEncKey, &HsClientDescEncSecretKey)>,
|
||||
) -> ParsedHsDesc {
|
||||
const TIMESTAMP: &str = "2023-01-23T15:00:00Z";
|
||||
|
||||
let id = ed25519::Ed25519Identity::from(blinded_pk);
|
||||
let enc_desc: EncryptedHsDesc = ParsedHsDesc::parse(unparsed_desc, &id.into())
|
||||
.unwrap()
|
||||
.check_signature()
|
||||
.unwrap()
|
||||
.check_valid_at(&humantime::parse_rfc3339(TIMESTAMP).unwrap())
|
||||
.unwrap();
|
||||
|
||||
enc_desc
|
||||
.decrypt(subcredential, hsc_desc_enc)
|
||||
.unwrap()
|
||||
.check_valid_at(&humantime::parse_rfc3339(TIMESTAMP).unwrap())
|
||||
.unwrap()
|
||||
.check_signature()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_decode() {
|
||||
const CREATE2_FORMATS: &[u32] = &[1, 2];
|
||||
const LIFETIME_MINS: u16 = 100;
|
||||
const REVISION_COUNT: u64 = 2;
|
||||
const CERT_EXPIRY_SECS: u64 = 60 * 60;
|
||||
|
||||
let mut rng = Config::Deterministic.into_rng().rng_compat();
|
||||
// The identity keypair of the hidden service.
|
||||
let hs_id = ed25519::Keypair::generate(&mut rng);
|
||||
|
@ -296,8 +349,8 @@ mod test {
|
|||
.unwrap();
|
||||
|
||||
let blinded_id = HsBlindKeypair { public, secret };
|
||||
let create2_formats = &[1, 2];
|
||||
let expiry = SystemTime::now() + Duration::from_secs(60 * 60);
|
||||
let id = ed25519::Ed25519Identity::from(blinded_id.public_key());
|
||||
let expiry = SystemTime::now() + Duration::from_secs(CERT_EXPIRY_SECS);
|
||||
let mut rng = Config::Deterministic.into_rng().rng_compat();
|
||||
let intro_points = vec![IntroPointDesc {
|
||||
link_specifiers: vec![LinkSpec::OrPort(Ipv4Addr::LOCALHOST.into(), 9999)],
|
||||
|
@ -306,55 +359,91 @@ mod test {
|
|||
svc_ntor_key: create_curve25519_pk(&mut rng).into(),
|
||||
}];
|
||||
|
||||
// Build and encode a new descriptor:
|
||||
let encoded_desc = HsDescBuilder::default()
|
||||
let builder = HsDescBuilder::default()
|
||||
.blinded_id(&blinded_id)
|
||||
.hs_desc_sign(&hs_desc_sign)
|
||||
.hs_desc_sign_cert_expiry(expiry)
|
||||
.create2_formats(create2_formats)
|
||||
.create2_formats(CREATE2_FORMATS)
|
||||
.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)
|
||||
.lifetime(LIFETIME_MINS.into())
|
||||
.revision_counter(REVISION_COUNT.into())
|
||||
.subcredential(subcredential);
|
||||
|
||||
// Build and encode a new descriptor (cloning `builder` because it's needed later, when we
|
||||
// test if client auth works):
|
||||
let encoded_desc = builder
|
||||
.clone()
|
||||
.build_sign(&mut Config::Deterministic.into_rng())
|
||||
.unwrap();
|
||||
|
||||
let id = ed25519::Ed25519Identity::from(blinded_id.public_key());
|
||||
// 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();
|
||||
// Now decode it...
|
||||
let desc = parse_hsdesc(
|
||||
encoded_desc.as_str(),
|
||||
*blinded_id.public,
|
||||
&subcredential,
|
||||
None, /* No client auth */
|
||||
);
|
||||
|
||||
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
|
||||
// ...and build a new descriptor using the information from the parsed descriptor,
|
||||
// asserting that the resulting descriptor 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(create2_formats)
|
||||
.create2_formats(CREATE2_FORMATS)
|
||||
.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(&mut Config::Deterministic.into_rng())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(&*encoded_desc, &*reencoded_desc);
|
||||
|
||||
// The same test, this time with client auth enabled (with a single authorized client):
|
||||
let client_skey: HsClientDescEncSecretKey = curve25519::StaticSecret::new(&mut rng).into();
|
||||
let client_pkey: HsClientDescEncKey =
|
||||
curve25519::PublicKey::from(client_skey.as_ref()).into();
|
||||
let auth_clients = vec![*client_pkey];
|
||||
|
||||
let encoded_desc = builder
|
||||
.auth_clients(&auth_clients)
|
||||
.build_sign(&mut Config::Deterministic.into_rng())
|
||||
.unwrap();
|
||||
|
||||
// Now decode it...
|
||||
let desc = parse_hsdesc(
|
||||
encoded_desc.as_str(),
|
||||
*blinded_id.public,
|
||||
&subcredential,
|
||||
Some((&client_pkey, &client_skey)), /* With client auth */
|
||||
);
|
||||
|
||||
// ...and build a new descriptor using the information from the parsed descriptor,
|
||||
// asserting that the resulting descriptor 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(CREATE2_FORMATS)
|
||||
.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)
|
||||
.auth_clients(&auth_clients)
|
||||
.lifetime(desc.idx_info.lifetime)
|
||||
.revision_counter(desc.idx_info.revision)
|
||||
.subcredential(subcredential)
|
||||
|
@ -363,6 +452,4 @@ mod test {
|
|||
|
||||
assert_eq!(&*encoded_desc, &*reencoded_desc);
|
||||
}
|
||||
|
||||
// TODO hs: encode a descriptor with client auth enabled
|
||||
}
|
||||
|
|
|
@ -4,15 +4,16 @@
|
|||
//! not meant to be used directly. Hidden services will use `HsDescBuilder` to build and encode
|
||||
//! hidden service descriptors.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
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::desc_enc::{
|
||||
build_descriptor_cookie_key, 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_hscrypto::Subcredential;
|
||||
use tor_llcrypto::pk::curve25519::{EphemeralSecret, PublicKey};
|
||||
use tor_llcrypto::util::ct::CtByteArray;
|
||||
|
||||
|
@ -26,7 +27,9 @@ use rand::{CryptoRng, Rng, RngCore};
|
|||
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>,
|
||||
pub(super) client_auth: Option<&'a ClientAuth<'a>>,
|
||||
/// The "subcredential" of the onion service.
|
||||
pub(super) subcredential: Subcredential,
|
||||
/// The (encrypted) inner document of the onion service descriptor.
|
||||
///
|
||||
/// The `encrypted` field is created by encrypting an
|
||||
|
@ -37,54 +40,77 @@ pub(super) struct HsDescMiddle<'a> {
|
|||
|
||||
impl<'a> NetdocBuilder for HsDescMiddle<'a> {
|
||||
fn build_sign<R: RngCore + CryptoRng>(self, rng: &mut R) -> Result<String, EncodeError> {
|
||||
use cipher::{KeyIvInit, StreamCipher};
|
||||
use tor_llcrypto::cipher::aes::Aes256Ctr as Cipher;
|
||||
use HsMiddleKwd::*;
|
||||
|
||||
let HsDescMiddle {
|
||||
client_auth,
|
||||
subcredential,
|
||||
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 => {
|
||||
// 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]>(),
|
||||
};
|
||||
let (ephemeral_key, auth_clients): (_, Box<dyn std::iter::Iterator<Item = AuthClient>>) =
|
||||
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.
|
||||
let auth_clients = client_auth.auth_clients.iter().map(|client| {
|
||||
let (client_id, cookie_key) = build_descriptor_cookie_key(
|
||||
client_auth.ephemeral_key.secret.as_ref(),
|
||||
client,
|
||||
&subcredential,
|
||||
);
|
||||
|
||||
// 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(rng);
|
||||
let dummy_ephemeral_key = PublicKey::from(&secret);
|
||||
// Encrypt the descriptor cookie with the public key of the client.
|
||||
let mut encrypted_cookie = client_auth.descriptor_cookie;
|
||||
let iv = rng.gen::<[u8; HS_DESC_IV_LEN]>();
|
||||
let mut cipher = Cipher::new(&cookie_key.into(), &iv.into());
|
||||
cipher.apply_keystream(&mut encrypted_cookie);
|
||||
|
||||
// TODO hs: Remove useless vec![] allocation.
|
||||
(dummy_ephemeral_key, Cow::Owned(vec![dummy_auth_client]))
|
||||
}
|
||||
};
|
||||
AuthClient {
|
||||
client_id,
|
||||
iv,
|
||||
encrypted_cookie,
|
||||
}
|
||||
});
|
||||
|
||||
(*client_auth.ephemeral_key.public, Box::new(auth_clients))
|
||||
}
|
||||
None => {
|
||||
// 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]>(),
|
||||
};
|
||||
|
||||
// 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(rng);
|
||||
let dummy_ephemeral_key = PublicKey::from(&secret);
|
||||
|
||||
(
|
||||
dummy_ephemeral_key,
|
||||
Box::new(std::iter::once(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 {
|
||||
for auth_client in auth_clients {
|
||||
encoder
|
||||
.item(AUTH_CLIENT)
|
||||
.arg(&Base64::encode_string(&*auth_client.client_id))
|
||||
|
@ -111,11 +137,13 @@ mod test {
|
|||
//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
|
||||
|
||||
use super::*;
|
||||
use crate::doc::hsdesc::build::test::{
|
||||
create_curve25519_pk, expect_bug, TEST_DESCRIPTOR_COOKIE,
|
||||
};
|
||||
use crate::doc::hsdesc::build::test::{create_curve25519_pk, expect_bug};
|
||||
use crate::doc::hsdesc::build::ClientAuth;
|
||||
use crate::doc::hsdesc::test::TEST_SUBCREDENTIAL;
|
||||
use tor_basic_utils::test_rng::Config;
|
||||
use tor_hscrypto::pk::HsSvcDescEncKeypair;
|
||||
use tor_llcrypto::pk::curve25519;
|
||||
use tor_llcrypto::util::rand_compat::RngCompatExt;
|
||||
|
||||
// Some dummy bytes, not actually encrypted.
|
||||
const TEST_ENCRYPTED_VALUE: &[u8] = &[1, 2, 3, 4];
|
||||
|
@ -124,6 +152,7 @@ mod test {
|
|||
fn middle_hsdesc_encoding_no_client_auth() {
|
||||
let hs_desc = HsDescMiddle {
|
||||
client_auth: None,
|
||||
subcredential: TEST_SUBCREDENTIAL.into(),
|
||||
encrypted: TEST_ENCRYPTED_VALUE.into(),
|
||||
}
|
||||
.build_sign(&mut Config::Deterministic.into_rng())
|
||||
|
@ -144,17 +173,25 @@ AQIDBA==
|
|||
|
||||
#[test]
|
||||
fn middle_hsdesc_encoding_with_bad_client_auth() {
|
||||
let mut rng = Config::Deterministic.into_rng().rng_compat();
|
||||
let secret = curve25519::StaticSecret::new(&mut rng);
|
||||
let public = curve25519::PublicKey::from(&secret).into();
|
||||
|
||||
let client_auth = ClientAuth {
|
||||
ephemeral_key: create_curve25519_pk(&mut Config::Deterministic.into_rng()).into(),
|
||||
auth_clients: vec![],
|
||||
descriptor_cookie: TEST_DESCRIPTOR_COOKIE,
|
||||
ephemeral_key: HsSvcDescEncKeypair {
|
||||
public,
|
||||
secret: secret.into(),
|
||||
},
|
||||
auth_clients: &[],
|
||||
descriptor_cookie: rand::Rng::gen::<[u8; HS_DESC_ENC_NONCE_LEN]>(&mut rng),
|
||||
};
|
||||
|
||||
let err = HsDescMiddle {
|
||||
client_auth: Some(&client_auth),
|
||||
subcredential: TEST_SUBCREDENTIAL.into(),
|
||||
encrypted: TEST_ENCRYPTED_VALUE.into(),
|
||||
}
|
||||
.build_sign(&mut Config::Deterministic.into_rng())
|
||||
.build_sign(&mut rng)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(expect_bug(err)
|
||||
|
@ -163,28 +200,28 @@ AQIDBA==
|
|||
|
||||
#[test]
|
||||
fn middle_hsdesc_encoding_client_auth() {
|
||||
let mut rng = Config::Deterministic.into_rng().rng_compat();
|
||||
// 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],
|
||||
},
|
||||
create_curve25519_pk(&mut rng),
|
||||
create_curve25519_pk(&mut rng),
|
||||
];
|
||||
|
||||
let secret = curve25519::StaticSecret::new(&mut rng);
|
||||
let public = curve25519::PublicKey::from(&secret).into();
|
||||
|
||||
let client_auth = ClientAuth {
|
||||
ephemeral_key: create_curve25519_pk(&mut Config::Deterministic.into_rng()).into(),
|
||||
auth_clients,
|
||||
descriptor_cookie: TEST_DESCRIPTOR_COOKIE,
|
||||
ephemeral_key: HsSvcDescEncKeypair {
|
||||
public,
|
||||
secret: secret.into(),
|
||||
},
|
||||
auth_clients: &auth_clients,
|
||||
descriptor_cookie: rand::Rng::gen::<[u8; HS_DESC_ENC_NONCE_LEN]>(&mut rng),
|
||||
};
|
||||
|
||||
let hs_desc = HsDescMiddle {
|
||||
client_auth: Some(&client_auth),
|
||||
subcredential: TEST_SUBCREDENTIAL.into(),
|
||||
encrypted: TEST_ENCRYPTED_VALUE.into(),
|
||||
}
|
||||
.build_sign(&mut Config::Deterministic.into_rng())
|
||||
|
@ -193,9 +230,9 @@ AQIDBA==
|
|||
assert_eq!(
|
||||
hs_desc,
|
||||
r#"desc-auth-type x25519
|
||||
desc-auth-ephemeral-key HWIigEAdcOgqgHPDFmzhhkeqvYP/GcMT2fKb5JY6ey8=
|
||||
auth-client AgICAgICAgI= AgICAgICAgICAgICAgICAg== AwMDAwMDAwMDAwMDAwMDAw==
|
||||
auth-client BAQEBAQEBAQ= BQUFBQUFBQUFBQUFBQUFBQ== BgYGBgYGBgYGBgYGBgYGBg==
|
||||
desc-auth-ephemeral-key 9Upi9XNWyqx3ZwHeQ5r3+Dh116k+C4yHeE9BcM68HDc=
|
||||
auth-client pxfSbhBMPw0= F+Z6EDfG7ofsQhdG2VKjNQ== fEursUD9Bj5Q9mFP8sIddA==
|
||||
auth-client DV7nt+CDOno= bRgLOvpjbo2k21IjKIJqFA== 2yVT+Lpm/WL4JAU64zlGpQ==
|
||||
encrypted
|
||||
-----BEGIN MESSAGE-----
|
||||
AQIDBA==
|
||||
|
|
|
@ -11,6 +11,8 @@ use arrayref::array_ref;
|
|||
use cipher::{KeyIvInit, StreamCipher};
|
||||
use digest::{ExtendableOutput, FixedOutput, Update, XofReader};
|
||||
use rand::{CryptoRng, Rng};
|
||||
use tor_llcrypto::pk::curve25519::PublicKey;
|
||||
use tor_llcrypto::pk::curve25519::StaticSecret;
|
||||
use tor_llcrypto::util::ct::CtByteArray;
|
||||
use zeroize::Zeroizing as Z;
|
||||
|
||||
|
@ -187,6 +189,41 @@ impl<'a> HsDescEncryption<'a> {
|
|||
#[error("Unable to decrypt onion service descriptor.")]
|
||||
pub struct DecryptionError {}
|
||||
|
||||
/// Create the CLIENT-ID and COOKIE-KEY required for hidden service client auth.
|
||||
///
|
||||
/// This is used by HS clients to decrypt the descriptor cookie from the onion service descriptor,
|
||||
/// and by HS services to build the client-auth sections of descriptors.
|
||||
///
|
||||
/// Section 2.5.1.2. of rend-spec-v3 says:
|
||||
/// ```text
|
||||
/// SECRET_SEED = x25519(hs_y, client_X)
|
||||
/// = x25519(client_y, hs_X)
|
||||
/// KEYS = KDF(N_hs_subcred | SECRET_SEED, 40)
|
||||
/// CLIENT-ID = first 8 bytes of KEYS
|
||||
/// COOKIE-KEY = last 32 bytes of KEYS
|
||||
///
|
||||
/// Where:
|
||||
/// hs_{X,y} = K{P,S}_hss_desc_enc
|
||||
/// client_{X,Y} = K{P,S}_hsc_desc_enc
|
||||
/// ```
|
||||
pub(crate) fn build_descriptor_cookie_key(
|
||||
our_secret_key: &StaticSecret,
|
||||
their_public_key: &PublicKey,
|
||||
subcredential: &Subcredential,
|
||||
) -> (CtByteArray<8>, [u8; 32]) {
|
||||
let secret_seed = our_secret_key.diffie_hellman(their_public_key);
|
||||
let mut kdf = KDF::default();
|
||||
kdf.update(subcredential.as_ref());
|
||||
kdf.update(secret_seed.as_bytes());
|
||||
let mut keys = kdf.finalize_xof();
|
||||
let mut client_id = CtByteArray::from([0_u8; 8]);
|
||||
let mut cookie_key = [0_u8; 32];
|
||||
keys.read(client_id.as_mut());
|
||||
keys.read(&mut cookie_key);
|
||||
|
||||
(client_id, cookie_key)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
// @@ begin test lint list maintained by maint/add_warning @@
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
//! Handle the middle document of an onion service descriptor.
|
||||
|
||||
use digest::XofReader;
|
||||
use once_cell::sync::Lazy;
|
||||
use tor_hscrypto::pk::{HsBlindId, HsClientDescEncSecretKey, HsSvcDescEncKey};
|
||||
use tor_hscrypto::{RevisionCounter, Subcredential};
|
||||
use tor_llcrypto::pk::curve25519;
|
||||
use tor_llcrypto::util::ct::CtByteArray;
|
||||
|
||||
use crate::doc::hsdesc::desc_enc::build_descriptor_cookie_key;
|
||||
use crate::parse::tokenize::{Item, NetDocReader};
|
||||
use crate::parse::{keyword::Keyword, parser::SectionRules};
|
||||
use crate::types::misc::B64;
|
||||
|
@ -88,36 +88,13 @@ impl HsDescMiddle {
|
|||
ks_hsc_desc_enc: &HsClientDescEncSecretKey,
|
||||
) -> Option<HsDescEncNonce> {
|
||||
use cipher::{KeyIvInit, StreamCipher};
|
||||
use digest::{ExtendableOutput, Update};
|
||||
use tor_llcrypto::cipher::aes::Aes256Ctr as Cipher;
|
||||
use tor_llcrypto::d::Shake256 as KDF;
|
||||
|
||||
// Perform a diffie hellman handshake using `KS_hsc_desc_enc` and `KP_hss_desc_enc`,
|
||||
// and use it to find our client_id and cookie_key.
|
||||
//
|
||||
// The spec says:
|
||||
//
|
||||
// SECRET_SEED = x25519(hs_y, client_X)
|
||||
// = x25519(client_y, hs_X)
|
||||
// KEYS = KDF(N_hs_subcred | SECRET_SEED, 40)
|
||||
// CLIENT-ID = fist 8 bytes of KEYS
|
||||
// COOKIE-KEY = last 32 bytes of KEYS
|
||||
//
|
||||
// Where:
|
||||
// hs_{X,y} = K{P,S}_hss_desc_enc
|
||||
// client_{X,Y} = K{P,S}_hsc_desc_enc
|
||||
let secret_seed = ks_hsc_desc_enc
|
||||
.as_ref()
|
||||
.diffie_hellman(&self.svc_desc_enc_key);
|
||||
let mut kdf = KDF::default();
|
||||
kdf.update(subcredential.as_ref());
|
||||
kdf.update(secret_seed.as_bytes());
|
||||
let mut keys = kdf.finalize_xof();
|
||||
let mut client_id = CtByteArray::from([0_u8; 8]);
|
||||
let mut cookie_key = [0_u8; 32];
|
||||
keys.read(client_id.as_mut());
|
||||
keys.read(&mut cookie_key);
|
||||
|
||||
let (client_id, cookie_key) = build_descriptor_cookie_key(
|
||||
ks_hsc_desc_enc.as_ref(),
|
||||
&self.svc_desc_enc_key,
|
||||
subcredential,
|
||||
);
|
||||
// See whether there is any matching client_id in self.auth_ids.
|
||||
// TODO HS: Perhaps we should use `tor_proto::util::ct::lookup`. We would
|
||||
// have to put it in a lower level module.
|
||||
|
@ -137,15 +114,15 @@ impl HsDescMiddle {
|
|||
/// Information that a single authorized client can use to decrypt the onion
|
||||
/// service descriptor.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AuthClient {
|
||||
pub(super) 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.
|
||||
pub(crate) client_id: CtByteArray<HS_DESC_CLIENT_ID_LEN>,
|
||||
pub(super) 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.
|
||||
pub(crate) iv: [u8; HS_DESC_IV_LEN],
|
||||
pub(super) 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`.
|
||||
|
@ -153,7 +130,7 @@ pub 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).
|
||||
pub(crate) encrypted_cookie: [u8; HS_DESC_ENC_NONCE_LEN],
|
||||
pub(super) encrypted_cookie: [u8; HS_DESC_ENC_NONCE_LEN],
|
||||
}
|
||||
|
||||
impl AuthClient {
|
||||
|
|
Loading…
Reference in New Issue