From f192429442b80e33b1827e3901c09841992f337d Mon Sep 17 00:00:00 2001 From: Nick Mathewson Date: Thu, 19 Jan 2023 12:28:29 -0500 Subject: [PATCH] hscrypto: Implement key blinding. This implementation was made based on the specification, and then validated against itself, and against C Tor. --- Cargo.lock | 2 + crates/tor-hscrypto/Cargo.toml | 7 +- crates/tor-hscrypto/src/pk.rs | 231 ++++++++++++++++++++++++++++++-- crates/tor-hscrypto/src/time.rs | 4 +- 4 files changed, 233 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7398f66fe..ffccff718 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3909,7 +3909,9 @@ dependencies = [ "paste", "rand_core 0.6.4", "serde", + "signature 1.6.4", "thiserror", + "tor-basic-utils", "tor-llcrypto", ] diff --git a/crates/tor-hscrypto/Cargo.toml b/crates/tor-hscrypto/Cargo.toml index 07a146cbd..87ace1d9c 100644 --- a/crates/tor-hscrypto/Cargo.toml +++ b/crates/tor-hscrypto/Cargo.toml @@ -19,9 +19,14 @@ digest = "0.10.0" paste = "1" rand_core = "0.6.2" serde = { version = "1.0.103", features = ["derive"] } +signature = "1" thiserror = "1" -tor-llcrypto = { version = "0.4.0", path = "../tor-llcrypto" } +tor-llcrypto = { version = "0.4.0", path = "../tor-llcrypto", features = [ + "hsv3-client", + "hsv3-service", +] } [dev-dependencies] hex-literal = "0.3" humantime = "2" +tor-basic-utils = { version = "0.5.0", path = "../tor-basic-utils" } diff --git a/crates/tor-hscrypto/src/pk.rs b/crates/tor-hscrypto/src/pk.rs index 8aa52e800..1a3e8f82c 100644 --- a/crates/tor-hscrypto/src/pk.rs +++ b/crates/tor-hscrypto/src/pk.rs @@ -9,7 +9,9 @@ // only in a single module somewhere else, it would make sense to just use the // underlying type. -use tor_llcrypto::pk::{curve25519, ed25519}; +use digest::Digest; +use tor_llcrypto::d::Sha3_256; +use tor_llcrypto::pk::{curve25519, ed25519, keymanip}; use crate::macros::{define_bytes, define_pk_keypair}; use crate::time::TimePeriod; @@ -38,18 +40,122 @@ define_pk_keypair! { // // NOTE: This is a separate type from OnionId because it is about 6x larger. It // is an expanded form, used for doing actual cryptography. -pub struct OnionIdKey(ed25519::PublicKey) / OnionIdSecretKey(ed25519::SecretKey); +pub struct OnionIdKey(ed25519::PublicKey) / OnionIdSecretKey(ed25519::ExpandedSecretKey); } -// TODO hs: implement TryFrom for OnionIdKey, and From for OnionId. +impl OnionIdKey { + /// Return a representation of this key as an [`OnionId`]. + /// + /// ([`OnionId`] is much smaller, and easier to store.) + pub fn id(&self) -> OnionId { + OnionId(self.0.to_bytes().into()) + } +} +impl TryFrom for OnionIdKey { + type Error = signature::Error; + + fn try_from(value: OnionId) -> Result { + ed25519::PublicKey::from_bytes(value.0.as_ref()).map(OnionIdKey) + } +} +impl From for OnionId { + fn from(value: OnionIdKey) -> Self { + value.id() + } +} impl OnionIdKey { /// Derive the blinded key and subcredential for this identity during `cur_period`. pub fn compute_blinded_key( &self, - cur_period: &TimePeriod, - ) -> (BlindedOnionIdKey, crate::Subcredential) { - todo!() // TODO hs. The underlying crypto is already done in tor_llcrypto::pk::keymanip + cur_period: TimePeriod, + ) -> Result<(BlindedOnionIdKey, crate::Subcredential), keymanip::BlindingError> { + // TODO hs: decide whether we want to support this kind of shared secret; C Tor does not. + let secret = b""; + let param = self.blinding_parameter(secret, cur_period); + + let blinded_key = keymanip::blind_pubkey(&self.0, param)?; + let subcredential_bytes: [u8; 32] = { + // N_hs_subcred = H("subcredential" | N_hs_cred | blinded-public-key). + // where + // N_hs_cred = H("credential" | public-identity-key) + let n_hs_cred: [u8; 32] = { + let mut h = Sha3_256::new(); + h.update(b"credential"); + h.update(self.0.as_bytes()); + h.finalize().into() + }; + let mut h = Sha3_256::new(); + h.update(b"subcredential"); + h.update(n_hs_cred); + h.update(blinded_key.as_bytes()); + h.finalize().into() + }; + + Ok((blinded_key.into(), subcredential_bytes.into())) + } + + /// Compute the 32-byte "blinding parameters" used to compute blinded public + /// (and secret) keys. + fn blinding_parameter(&self, secret: &[u8], cur_period: TimePeriod) -> [u8; 32] { + // We generate our key blinding parameter as + // h = H(BLIND_STRING | A | s | B | N) + // Where: + // H is SHA3-256. + // A is this public key. + // BLIND_STRING = "Derive temporary signing key" | INT_1(0) + // s is an optional secret (not implemented here.) + // B is the ed25519 basepoint. + // N = "key-blind" || INT_8(period_num) || INT_8(period_length). + + /// String used as part of input to blinding hash. + const BLIND_STRING: &[u8] = b"Derive temporary signing key\0"; + /// String representation of our Ed25519 basepoint. + const ED25519_BASEPOINT: &[u8] = + b"(15112221349535400772501151409588531511454012693041857206046113283949847762202, \ + 46316835694926478169428394003475163141307993866256225615783033603165251855960)"; + + let mut h = Sha3_256::new(); + h.update(BLIND_STRING); + h.update(self.0.as_bytes()); + h.update(secret); + h.update(ED25519_BASEPOINT); + h.update(b"key-blind"); + h.update(cur_period.interval_num.to_be_bytes()); + h.update(u64::from(cur_period.length_in_sec).to_be_bytes()); + + h.finalize().into() + } +} + +impl OnionIdSecretKey { + /// Derive the blinded key and subcredential for this identity during `cur_period`. + pub fn compute_blinded_key( + &self, + cur_period: TimePeriod, + ) -> Result< + ( + BlindedOnionIdKey, + BlindedOnionIdSecretKey, + crate::Subcredential, + ), + keymanip::BlindingError, + > { + // TODO hs: as above, decide if we want this. + let secret = b""; + + // Note: This implementation is somewhat inefficient, as it recomputes + // the PublicKey, and computes our blinding parameters twice. But we + // only do this on an onion service once per time period: the + // performance does not matter. + + let public_key: OnionIdKey = ed25519::PublicKey::from(&self.0).into(); + let (blinded_public_key, subcredential) = public_key.compute_blinded_key(cur_period)?; + + let param = public_key.blinding_parameter(secret, cur_period); + let blinded_secret_key = keymanip::blind_seckey(&self.0, param)?; + + Ok((blinded_public_key, blinded_secret_key.into(), subcredential)) } } @@ -70,8 +176,33 @@ define_bytes! { pub struct BlindedOnionId([u8; 32]); } -// TODO hs: implement TryFrom for BlindedOnionIdKey, and -// From for BlindedOnionId. +impl BlindedOnionIdKey { + /// Return a representation of this key as a [`BlindedOnionId`]. + /// + /// ([`BlindedOnionId`] is much smaller, and easier to store.) + pub fn id(&self) -> BlindedOnionId { + BlindedOnionId(self.0.to_bytes().into()) + } +} +impl TryFrom for BlindedOnionIdKey { + type Error = signature::Error; + + fn try_from(value: BlindedOnionId) -> Result { + ed25519::PublicKey::from_bytes(value.0.as_ref()).map(BlindedOnionIdKey) + } +} +impl From for BlindedOnionId { + fn from(value: BlindedOnionIdKey) -> Self { + value.id() + } +} + +impl BlindedOnionIdSecretKey { + /// Compute a signature of `message` with this key, using the corresponding `public_key`. + pub fn sign(&self, message: &[u8], public_key: &BlindedOnionIdKey) -> ed25519::Signature { + self.0.sign(message, &public_key.0) + } +} define_pk_keypair! { /// A key used to sign onion service descriptors. (`KP_desc_sign`) @@ -136,3 +267,87 @@ pub struct ClientSecretKeys { desc_auth: Option, intro_auth: Option, } + +#[cfg(test)] +mod test { + use hex_literal::hex; + use signature::Verifier; + use std::time::{Duration, SystemTime}; + use tor_basic_utils::test_rng::testing_rng; + use tor_llcrypto::util::rand_compat::RngCompatExt as _; + + use super::*; + + #[test] + fn key_blinding_blackbox() { + let mut rng = testing_rng().rng_compat(); + let when = TimePeriod::new(Duration::from_secs(3600), SystemTime::now()).unwrap(); + let keypair = ed25519::Keypair::generate(&mut rng); + let id_pub = OnionIdKey::from(keypair.public); + let id_sec = OnionIdSecretKey::from(ed25519::ExpandedSecretKey::from(&keypair.secret)); + + let (blinded_pub, subcred1) = id_pub.compute_blinded_key(when).unwrap(); + let (blinded_pub2, blinded_sec, subcred2) = id_sec.compute_blinded_key(when).unwrap(); + + assert_eq!(subcred1.as_ref(), subcred2.as_ref()); + assert_eq!(blinded_pub.0.to_bytes(), blinded_pub2.0.to_bytes()); + assert_eq!(blinded_pub.id(), blinded_pub2.id()); + + let message = b"Here is a terribly important string to authenticate."; + let other_message = b"Hey, that is not what I signed!"; + let sign = blinded_sec.sign(message, &blinded_pub2); + + assert!(blinded_pub.as_ref().verify(message, &sign).is_ok()); + assert!(blinded_pub.as_ref().verify(other_message, &sign).is_err()); + } + + #[test] + fn key_blinding_testvec() { + // Test vectors generated with C tor. + let id = OnionId::from(hex!( + "833990B085C1A688C1D4C8B1F6B56AFAF5A2ECA674449E1D704F83765CCB7BC6" + )); + let id_pubkey = OnionIdKey::try_from(id).unwrap(); + let id_seckey = OnionIdSecretKey::from( + ed25519::ExpandedSecretKey::from_bytes(&hex!( + "D8C7FF0E31295B66540D789AF3E3DF992038A9592EEA01D8B7CBA06D6E66D159 + 4D6167696320576F7264733A20737065697373636F62616C742062697669756D" + )) + .unwrap(), + ); + let time_period = TimePeriod::new( + Duration::from_secs(1440), + humantime::parse_rfc3339("1970-01-22T01:50:33Z").unwrap(), + ) + .unwrap(); + assert_eq!(time_period.interval_num, 1234); + + let param = id_pubkey.blinding_parameter(b"", time_period); + assert_eq!( + param, + hex!("379E50DB31FEE6775ABD0AF6FB7C371E060308F4F847DB09FE4CFE13AF602287") + ); + + let (blinded_pub1, subcred1) = id_pubkey.compute_blinded_key(time_period).unwrap(); + assert_eq!( + blinded_pub1.0.to_bytes(), + hex!("3A50BF210E8F9EE955AE0014F7A6917FB65EBF098A86305ABB508D1A7291B6D5") + ); + assert_eq!( + subcred1.as_ref(), + &hex!("635D55907816E8D76398A675A50B1C2F3E36B42A5CA77BA3A0441285161AE07D") + ); + + let (blinded_pub2, blinded_sec, subcred2) = + id_seckey.compute_blinded_key(time_period).unwrap(); + assert_eq!(blinded_pub1.0.to_bytes(), blinded_pub2.0.to_bytes()); + assert_eq!(subcred1.as_ref(), subcred2.as_ref()); + assert_eq!( + blinded_sec.0.to_bytes(), + hex!( + "A958DC83AC885F6814C67035DE817A2C604D5D2F715282079448F789B656350B + 4540FE1F80AA3F7E91306B7BF7A8E367293352B14A29FDCC8C19F3558075524B" + ) + ); + } +} diff --git a/crates/tor-hscrypto/src/time.rs b/crates/tor-hscrypto/src/time.rs index e3cf9413c..c69885315 100644 --- a/crates/tor-hscrypto/src/time.rs +++ b/crates/tor-hscrypto/src/time.rs @@ -21,9 +21,9 @@ use std::time::{Duration, SystemTime}; #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct TimePeriod { /// Index of the time periods that have passed since the unix epoch. - interval_num: u64, + pub(crate) interval_num: u64, /// The length of a time period, in seconds. - length_in_sec: u32, + pub(crate) length_in_sec: u32, } /// The difference between the Unix epoch and our starting time.