netdoc: implement onion service descryptor encryption
This is tested via a round-trip check, and via a successful decryption of our example descriptor's outer layer.
This commit is contained in:
parent
6c4e9c8f1d
commit
02fa682bc0
|
@ -4068,8 +4068,10 @@ dependencies = [
|
||||||
name = "tor-netdoc"
|
name = "tor-netdoc"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"arrayref",
|
||||||
"base64ct",
|
"base64ct",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
|
"cipher",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"digest 0.10.6",
|
"digest 0.10.6",
|
||||||
"educe",
|
"educe",
|
||||||
|
@ -4087,6 +4089,7 @@ dependencies = [
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"time",
|
"time",
|
||||||
"tinystr",
|
"tinystr",
|
||||||
|
"tor-basic-utils",
|
||||||
"tor-bytes",
|
"tor-bytes",
|
||||||
"tor-cert",
|
"tor-cert",
|
||||||
"tor-checkable",
|
"tor-checkable",
|
||||||
|
@ -4098,6 +4101,7 @@ dependencies = [
|
||||||
"visibility",
|
"visibility",
|
||||||
"visible",
|
"visible",
|
||||||
"weak-table",
|
"weak-table",
|
||||||
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -52,8 +52,10 @@ experimental-api = []
|
||||||
dangerous-expose-struct-fields = ["visible", "visibility"]
|
dangerous-expose-struct-fields = ["visible", "visibility"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
arrayref = "0.3"
|
||||||
base64ct = { version = "1.5.1", features = ["alloc"] }
|
base64ct = { version = "1.5.1", features = ["alloc"] }
|
||||||
bitflags = "1"
|
bitflags = "1"
|
||||||
|
cipher = { version = "0.4.1", features = ["zeroize"] }
|
||||||
derive_more = "0.99.3"
|
derive_more = "0.99.3"
|
||||||
digest = "0.10.0"
|
digest = "0.10.0"
|
||||||
educe = "0.4.6"
|
educe = "0.4.6"
|
||||||
|
@ -72,18 +74,21 @@ tor-bytes = { path = "../tor-bytes", version = "0.6.1" }
|
||||||
tor-cert = { path = "../tor-cert", version = "0.6.1" }
|
tor-cert = { path = "../tor-cert", version = "0.6.1" }
|
||||||
tor-checkable = { path = "../tor-checkable", version = "0.4.0" }
|
tor-checkable = { path = "../tor-checkable", version = "0.4.0" }
|
||||||
tor-error = { path = "../tor-error", version = "0.4.1" }
|
tor-error = { path = "../tor-error", version = "0.4.1" }
|
||||||
tor-hscrypto = { path = "../tor-hscrypto", version = "0.1.0", optional = true }
|
tor-hscrypto = { path = "../tor-hscrypto", version = "0.1.0", optional = true }
|
||||||
tor-linkspec = { path = "../tor-linkspec", version = "0.6.0", optional = true }
|
tor-linkspec = { path = "../tor-linkspec", version = "0.6.0", optional = true }
|
||||||
tor-llcrypto = { path = "../tor-llcrypto", version = "0.4.1" }
|
tor-llcrypto = { path = "../tor-llcrypto", version = "0.4.1" }
|
||||||
tor-protover = { path = "../tor-protover", version = "0.4.0" }
|
tor-protover = { path = "../tor-protover", version = "0.4.0" }
|
||||||
visibility = { version = "0.0.1", optional = true }
|
visibility = { version = "0.0.1", optional = true }
|
||||||
visible = { version = "0.0.1", optional = true }
|
visible = { version = "0.0.1", optional = true }
|
||||||
weak-table = "0.3.0"
|
weak-table = "0.3.0"
|
||||||
|
zeroize = "1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
hex-literal = "0.3"
|
hex-literal = "0.3"
|
||||||
itertools = "0.10.1"
|
itertools = "0.10.1"
|
||||||
serde_json = "1.0.50"
|
serde_json = "1.0.50"
|
||||||
|
tor-basic-utils = { version = "0.5.0", path = "../tor-basic-utils" }
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
|
@ -2,8 +2,17 @@
|
||||||
//!
|
//!
|
||||||
//! TODO hs: It's possible that this should move to tor-netdoc.
|
//! TODO hs: It's possible that this should move to tor-netdoc.
|
||||||
|
|
||||||
use rand::CryptoRng;
|
|
||||||
use tor_hscrypto::{pk::BlindedOnionId, RevisionCounter, Subcredential};
|
use tor_hscrypto::{pk::BlindedOnionId, RevisionCounter, Subcredential};
|
||||||
|
use tor_llcrypto::cipher::aes::Aes256Ctr as Cipher;
|
||||||
|
use tor_llcrypto::d::Sha3_256 as Hash;
|
||||||
|
use tor_llcrypto::d::Shake256 as KDF;
|
||||||
|
|
||||||
|
use arrayref::array_ref;
|
||||||
|
use cipher::{KeyIvInit, StreamCipher};
|
||||||
|
use digest::{ExtendableOutput, FixedOutput, Update, XofReader};
|
||||||
|
use rand::{CryptoRng, Rng};
|
||||||
|
use tor_llcrypto::util::ct::CtByteArray;
|
||||||
|
use zeroize::Zeroizing as Z;
|
||||||
|
|
||||||
/// Parameters for encrypting or decrypting part of an onion service descriptor.
|
/// Parameters for encrypting or decrypting part of an onion service descriptor.
|
||||||
///
|
///
|
||||||
|
@ -24,17 +33,113 @@ pub(super) struct HsDescEncryption<'a> {
|
||||||
|
|
||||||
/// A value used in deriving the encryption key for the inner layer of onion
|
/// A value used in deriving the encryption key for the inner layer of onion
|
||||||
/// service encryption.
|
/// service encryption.
|
||||||
|
#[derive(derive_more::AsRef)]
|
||||||
pub(super) struct DescEncryptionCookie([u8; 32]);
|
pub(super) struct DescEncryptionCookie([u8; 32]);
|
||||||
|
|
||||||
|
/// Length of our cryptographic salt.
|
||||||
|
const SALT_LEN: usize = 16;
|
||||||
|
/// Length of our ersatz MAC.
|
||||||
|
const MAC_LEN: usize = 32;
|
||||||
|
/// An instance of our cryptographic salt.
|
||||||
|
type Salt = [u8; SALT_LEN];
|
||||||
|
|
||||||
impl<'a> HsDescEncryption<'a> {
|
impl<'a> HsDescEncryption<'a> {
|
||||||
/// Encrypt a given bytestring using these encryption parameters.
|
/// Encrypt a given bytestring using these encryption parameters.
|
||||||
pub(super) fn encrypt<R: CryptoRng>(&self, rng: &mut R, data: &[u8]) -> Vec<u8> {
|
pub(super) fn encrypt<R: Rng + CryptoRng>(&self, rng: &mut R, data: &[u8]) -> Vec<u8> {
|
||||||
todo!() // TODO hs
|
let output_len = data.len() + SALT_LEN + MAC_LEN;
|
||||||
|
let mut output = Vec::with_capacity(output_len);
|
||||||
|
let salt: [u8; SALT_LEN] = rng.gen();
|
||||||
|
|
||||||
|
let (mut cipher, mut mac) = self.init(&salt);
|
||||||
|
|
||||||
|
output.extend_from_slice(&salt[..]);
|
||||||
|
output.extend_from_slice(data);
|
||||||
|
cipher.apply_keystream(&mut output[SALT_LEN..]);
|
||||||
|
mac.update(&output[SALT_LEN..]);
|
||||||
|
let mut mac_val = Default::default();
|
||||||
|
let mac = mac.finalize_into(&mut mac_val);
|
||||||
|
output.extend_from_slice(&mac_val);
|
||||||
|
debug_assert_eq!(output.len(), output_len);
|
||||||
|
|
||||||
|
output
|
||||||
}
|
}
|
||||||
/// Decrypt a given bytestring that was first encrypted using these
|
/// Decrypt a given bytestring that was first encrypted using these
|
||||||
/// encryption parameters.
|
/// encryption parameters.
|
||||||
pub(super) fn decrypt(&self, data: &[u8]) -> Result<Vec<u8>, DecryptionError> {
|
pub(super) fn decrypt(&self, data: &[u8]) -> Result<Vec<u8>, DecryptionError> {
|
||||||
todo!() // TODO hs
|
if data.len() < SALT_LEN + MAC_LEN {
|
||||||
|
return Err(DecryptionError::default());
|
||||||
|
}
|
||||||
|
let msg_len = data.len() - SALT_LEN - MAC_LEN;
|
||||||
|
|
||||||
|
let salt = *array_ref![data, 0, SALT_LEN];
|
||||||
|
let ciphertext = &data[SALT_LEN..(SALT_LEN + msg_len)];
|
||||||
|
|
||||||
|
let expected_mac = CtByteArray::from(*array_ref![data, SALT_LEN + msg_len, MAC_LEN]);
|
||||||
|
let (mut cipher, mut mac) = self.init(&salt);
|
||||||
|
|
||||||
|
// check mac.
|
||||||
|
mac.update(ciphertext);
|
||||||
|
let mut received_mac = CtByteArray::from([0_u8; MAC_LEN]);
|
||||||
|
mac.finalize_into(received_mac.as_mut().into());
|
||||||
|
if received_mac != expected_mac {
|
||||||
|
return Err(DecryptionError::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut decrypted = ciphertext.to_vec();
|
||||||
|
cipher.apply_keystream(&mut decrypted[..]);
|
||||||
|
|
||||||
|
Ok(decrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the cryptographic objects that are used for en/decrypting and
|
||||||
|
/// authenticating a HsDesc layer, given these parameters and a provided
|
||||||
|
/// salt.
|
||||||
|
fn init(&self, salt: &[u8; 16]) -> (Cipher, Hash) {
|
||||||
|
let mut key_stream = self.get_kdf(salt).finalize_xof();
|
||||||
|
|
||||||
|
/// Length of our MAC key.
|
||||||
|
const MAC_KEY_LEN: usize = 32;
|
||||||
|
/// Length of the cipher key that we use.
|
||||||
|
const CIPHER_KEY_LEN: usize = 32;
|
||||||
|
/// Length of our cipher's IV.
|
||||||
|
const IV_LEN: usize = 16;
|
||||||
|
|
||||||
|
let mut key = Z::new([0_u8; CIPHER_KEY_LEN]);
|
||||||
|
let mut iv = Z::new([0_u8; IV_LEN]);
|
||||||
|
let mut mac_key = Z::new([0_u8; MAC_KEY_LEN]); // XXXX conjectural!
|
||||||
|
key_stream.read(&mut key[..]);
|
||||||
|
key_stream.read(&mut iv[..]);
|
||||||
|
key_stream.read(&mut mac_key[..]);
|
||||||
|
|
||||||
|
let cipher = Cipher::new(key.as_ref().into(), iv.as_ref().into());
|
||||||
|
|
||||||
|
let mut mac = Hash::default();
|
||||||
|
mac.update(&(MAC_KEY_LEN as u64).to_be_bytes());
|
||||||
|
mac.update(&mac_key[..]);
|
||||||
|
mac.update(&(salt.len() as u64).to_be_bytes());
|
||||||
|
mac.update(&salt[..]);
|
||||||
|
|
||||||
|
(cipher, mac)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a KDF that can yield the keys to be used for encryption with
|
||||||
|
/// these key parameters.
|
||||||
|
fn get_kdf(&self, salt: &[u8; 16]) -> KDF {
|
||||||
|
let mut kdf = KDF::default();
|
||||||
|
|
||||||
|
// secret_input = SECRET_DATA | N_hs_subcred | INT_8(revision_counter)
|
||||||
|
kdf.update(self.blinded_id.as_ref());
|
||||||
|
if let Some(cookie) = self.encryption_cookie {
|
||||||
|
kdf.update(cookie.as_ref());
|
||||||
|
}
|
||||||
|
kdf.update(self.subcredential.as_ref());
|
||||||
|
kdf.update(&u64::from(self.revision).to_be_bytes());
|
||||||
|
|
||||||
|
// keys = KDF(secret_input | salt | STRING_CONSTANT, S_KEY_LEN + S_IV_LEN + MAC_KEY_LEN)
|
||||||
|
kdf.update(salt);
|
||||||
|
kdf.update(self.string_const);
|
||||||
|
|
||||||
|
kdf
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,6 +147,74 @@ impl<'a> HsDescEncryption<'a> {
|
||||||
///
|
///
|
||||||
/// This error is deliberately uninformative, to avoid side channels.
|
/// This error is deliberately uninformative, to avoid side channels.
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
#[derive(Clone, Debug, thiserror::Error)]
|
#[derive(Clone, Debug, Default, thiserror::Error)]
|
||||||
#[error("Unable to decrypt onion service decryptor.")]
|
#[error("Unable to decrypt onion service decryptor.")]
|
||||||
pub struct DecryptionError {}
|
pub struct DecryptionError {}
|
||||||
|
|
||||||
|
#[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 tor_basic_utils::test_rng::testing_rng;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn roundtrip_basics() {
|
||||||
|
let blinded_id = [7; 32].into();
|
||||||
|
let subcredential = [11; 32].into();
|
||||||
|
let revision = 13.into();
|
||||||
|
let string_const = "greetings puny humans";
|
||||||
|
let params = HsDescEncryption {
|
||||||
|
blinded_id: &blinded_id,
|
||||||
|
encryption_cookie: None,
|
||||||
|
subcredential: &subcredential,
|
||||||
|
revision,
|
||||||
|
string_const: string_const.as_bytes(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut rng = testing_rng();
|
||||||
|
|
||||||
|
let bigmsg: Vec<u8> = (1..123).cycle().take(1021).collect();
|
||||||
|
for message in [&b""[..], &b"hello world"[..], &bigmsg[..]] {
|
||||||
|
let mut encrypted = params.encrypt(&mut rng, message);
|
||||||
|
assert_eq!(encrypted.len(), message.len() + 48);
|
||||||
|
let decrypted = params.decrypt(&encrypted[..]).unwrap();
|
||||||
|
assert_eq!(message, &decrypted);
|
||||||
|
|
||||||
|
// Make sure we can't decrypt a partial input.
|
||||||
|
let decryption_err = params.decrypt(&encrypted[..encrypted.len() - 1]);
|
||||||
|
assert!(decryption_err.is_err());
|
||||||
|
// Frob a point in the encrypted form and ensure we won't decrypt.
|
||||||
|
encrypted[7] ^= 3;
|
||||||
|
let decryption_err = params.decrypt(&encrypted[..]);
|
||||||
|
assert!(decryption_err.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn too_short() {
|
||||||
|
let blinded_id = [7; 32].into();
|
||||||
|
let subcredential = [11; 32].into();
|
||||||
|
let revision = 13.into();
|
||||||
|
let string_const = "greetings puny humans";
|
||||||
|
let params = HsDescEncryption {
|
||||||
|
blinded_id: &blinded_id,
|
||||||
|
encryption_cookie: None,
|
||||||
|
subcredential: &subcredential,
|
||||||
|
revision,
|
||||||
|
string_const: string_const.as_bytes(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(params.decrypt(b"").is_err());
|
||||||
|
assert!(params.decrypt(&[0_u8; 47]).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -5,12 +5,16 @@ use tor_cert::Ed25519Cert;
|
||||||
use tor_checkable::signed::SignatureGated;
|
use tor_checkable::signed::SignatureGated;
|
||||||
use tor_checkable::timed::TimerangeBound;
|
use tor_checkable::timed::TimerangeBound;
|
||||||
use tor_checkable::Timebound;
|
use tor_checkable::Timebound;
|
||||||
|
use tor_hscrypto::pk::BlindedOnionId;
|
||||||
|
use tor_hscrypto::{RevisionCounter, Subcredential};
|
||||||
use tor_llcrypto::pk::ed25519::{self, ValidatableEd25519Signature};
|
use tor_llcrypto::pk::ed25519::{self, ValidatableEd25519Signature};
|
||||||
|
|
||||||
use crate::parse::{keyword::Keyword, parser::SectionRules, tokenize::NetDocReader};
|
use crate::parse::{keyword::Keyword, parser::SectionRules, tokenize::NetDocReader};
|
||||||
use crate::types::misc::{UnvalidatedEdCert, B64};
|
use crate::types::misc::{UnvalidatedEdCert, B64};
|
||||||
use crate::{Pos, Result};
|
use crate::{Pos, Result};
|
||||||
|
|
||||||
|
use super::desc_enc;
|
||||||
|
|
||||||
/// A more-or-less verbatim representation of the outermost layer of an onion
|
/// A more-or-less verbatim representation of the outermost layer of an onion
|
||||||
/// service descriptor.
|
/// service descriptor.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
@ -37,10 +41,36 @@ pub(super) struct HsDescOuter {
|
||||||
pub(super) encrypted_body: Vec<u8>,
|
pub(super) encrypted_body: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An `HsDescOuter` whose signatures have all been verified, but which has not
|
impl HsDescOuter {
|
||||||
/// been checked for timeliness.
|
/// Return the blinded Id for this onion service descriptor.
|
||||||
#[derive(Clone, Debug)]
|
pub(super) fn blinded_id(&self) -> BlindedOnionId {
|
||||||
pub(super) struct HsDescOuterSigChecked(HsDescOuter);
|
let ident = self
|
||||||
|
.desc_signing_key_cert
|
||||||
|
.signing_key()
|
||||||
|
.expect("signing key was absent!?");
|
||||||
|
(*ident).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypt and return the encrypted (middle-layer) body of this onion
|
||||||
|
/// service descriptor.
|
||||||
|
pub(super) fn decrypt_body(
|
||||||
|
&self,
|
||||||
|
subcredential: &Subcredential,
|
||||||
|
) -> std::result::Result<Vec<u8>, desc_enc::DecryptionError> {
|
||||||
|
let decrypt = desc_enc::HsDescEncryption {
|
||||||
|
blinded_id: &self.blinded_id(),
|
||||||
|
encryption_cookie: None,
|
||||||
|
subcredential,
|
||||||
|
revision: self.revision_counter,
|
||||||
|
string_const: b"hsdir-superencrypted-data",
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut body = decrypt.decrypt(&self.encrypted_body[..])?;
|
||||||
|
let n_padding = body.iter().rev().take_while(|n| **n == 0).count();
|
||||||
|
body.truncate(body.len() - n_padding);
|
||||||
|
Ok(body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// An `HsDescOuter` whose signatures have not yet been verified, and whose
|
/// An `HsDescOuter` whose signatures have not yet been verified, and whose
|
||||||
/// timeliness has not been checked.
|
/// timeliness has not been checked.
|
||||||
|
@ -249,6 +279,15 @@ mod test {
|
||||||
// TODO HS: Add checks for the specific fields here once I'm more
|
// TODO HS: Add checks for the specific fields here once I'm more
|
||||||
// confident that this is the example descriptor I'm using.
|
// confident that this is the example descriptor I'm using.
|
||||||
|
|
||||||
|
let subcred: tor_hscrypto::Subcredential =
|
||||||
|
hex_literal::hex!("78210A0D2C72BB7A0CAF606BCD938B9A3696894FDDDBC3B87D424753A7E3DF37")
|
||||||
|
.into();
|
||||||
|
let inner = desc.decrypt_body(&subcred).unwrap();
|
||||||
|
|
||||||
|
assert!(std::str::from_utf8(&inner)
|
||||||
|
.unwrap()
|
||||||
|
.starts_with("desc-auth-type"));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue