From ae908390b5e6879f5b3048e9eec6d727fc4ce191 Mon Sep 17 00:00:00 2001 From: mghazwi Date: Mon, 1 Jun 2026 15:42:45 +0200 Subject: [PATCH] reference impl: time, hs_id, key_cert, descriptor. --- Cargo.toml | 2 +- README.md | 9 +- .../{ed25519-keyblind => }/.gitignore | 0 .../{ed25519-keyblind => }/Cargo.toml | 16 +- rust_reference/README.md | 67 ++ rust_reference/ed25519-keyblind/README.md | 8 - rust_reference/src/cert/key_cert.rs | 369 ++++++++++ rust_reference/src/cert/mod.rs | 1 + .../ed25519_keyblind}/conversion.rs | 22 +- .../src => src/ed25519_keyblind}/ed25519.rs | 11 +- .../ed25519_keyblind}/key_blinding.rs | 25 +- .../lib.rs => src/ed25519_keyblind/mod.rs} | 3 +- rust_reference/src/error.rs | 157 ++++ rust_reference/src/hs_desc/desc_enc.rs | 136 ++++ rust_reference/src/hs_desc/inner.rs | 295 ++++++++ rust_reference/src/hs_desc/mod.rs | 19 + rust_reference/src/hs_desc/outer.rs | 674 ++++++++++++++++++ rust_reference/src/hs_id/mod.rs | 397 +++++++++++ rust_reference/src/lib.rs | 6 + rust_reference/src/time/mod.rs | 267 +++++++ .../{ed25519-keyblind => }/tests/compat.rs | 28 +- 21 files changed, 2438 insertions(+), 74 deletions(-) rename rust_reference/{ed25519-keyblind => }/.gitignore (100%) rename rust_reference/{ed25519-keyblind => }/Cargo.toml (65%) create mode 100644 rust_reference/README.md delete mode 100644 rust_reference/ed25519-keyblind/README.md create mode 100644 rust_reference/src/cert/key_cert.rs create mode 100644 rust_reference/src/cert/mod.rs rename rust_reference/{ed25519-keyblind/src => src/ed25519_keyblind}/conversion.rs (92%) rename rust_reference/{ed25519-keyblind/src => src/ed25519_keyblind}/ed25519.rs (98%) rename rust_reference/{ed25519-keyblind/src => src/ed25519_keyblind}/key_blinding.rs (96%) rename rust_reference/{ed25519-keyblind/src/lib.rs => src/ed25519_keyblind/mod.rs} (98%) create mode 100644 rust_reference/src/error.rs create mode 100644 rust_reference/src/hs_desc/desc_enc.rs create mode 100644 rust_reference/src/hs_desc/inner.rs create mode 100644 rust_reference/src/hs_desc/mod.rs create mode 100644 rust_reference/src/hs_desc/outer.rs create mode 100644 rust_reference/src/hs_id/mod.rs create mode 100644 rust_reference/src/lib.rs create mode 100644 rust_reference/src/time/mod.rs rename rust_reference/{ed25519-keyblind => }/tests/compat.rs (82%) diff --git a/Cargo.toml b/Cargo.toml index c6d5707..eeaf944 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,4 +2,4 @@ resolver = "3" -members = ["rust_reference/ed25519-keyblind", "rust_reference/mix_address"] +members = ["rust_reference"] diff --git a/README.md b/README.md index 7fd2d50..b3031db 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,12 @@ early experimentation with the protocol. The protocol requires the following: - [x] Ed25519 with key blinding -- [ ] Ed25519 Key certificates -- [ ] Hidden services identifiers -- [ ] Hidden services descriptors +- [x] Ed25519 Key certificates +- [x] Hidden services identifiers +- [x] Time and Epochs in hidden services +- [x] Hidden services descriptors - [ ] Introduction point protocol -- [ ] Bidirectional connection with surbs +- [ ] client-service connection The reference implementations are in Rust and provided in [rust_reference](./rust_reference). diff --git a/rust_reference/ed25519-keyblind/.gitignore b/rust_reference/.gitignore similarity index 100% rename from rust_reference/ed25519-keyblind/.gitignore rename to rust_reference/.gitignore diff --git a/rust_reference/ed25519-keyblind/Cargo.toml b/rust_reference/Cargo.toml similarity index 65% rename from rust_reference/ed25519-keyblind/Cargo.toml rename to rust_reference/Cargo.toml index 15f3991..4be015f 100644 --- a/rust_reference/ed25519-keyblind/Cargo.toml +++ b/rust_reference/Cargo.toml @@ -1,27 +1,33 @@ [package] -name = "ed25519-keyblind" +name = "mix-hs" version = "0.42.0" edition = "2024" rust-version = "1.89" license = "MIT OR Apache-2.0" -description = "Ed25519 signatures wth key blinding" -keywords = ["tor", "cryptography", "ed25519"] +description = "Hidden services protocol over Mix" +keywords = ["tor", "cryptography", "mix"] categories = ["cryptography"] [dependencies] +aes = "0.8.4" +getrandom = "0.4.2" +hmac = "0.13.0-pre.4" curve25519-dalek = "5.0.0-pre.6" +ctr = "0.9.2" digest = "0.11" ed25519-dalek = { version = "3.0.0-pre.7", features = ["batch", "hazmat"] } rand_core = "0.10" -serde = "1.0.103" +serde = { version = "1.0.103", features = ["derive"] } sha2 = "0.11" signature = "3" thiserror = "2" x25519-dalek = { version = "3.0.0-pre.6", features = ["static_secrets"] } zeroize = "1.5" +itertools = "0.14.0" +data-encoding = "2.7.0" +humantime = "2.3.0" [dev-dependencies] hex = "0.4" hex-literal = "1.0" tor-llcrypto = { version = "0.42.0", features = ["cvt-x25519", "hsv3-client", "hsv3-service"]} - diff --git a/rust_reference/README.md b/rust_reference/README.md new file mode 100644 index 0000000..1575fef --- /dev/null +++ b/rust_reference/README.md @@ -0,0 +1,67 @@ +## Hidden Services Over Mix - reference +The reference implementation of the hidden services protocol over mix in Rust. +The crate contains implementation of the following: + +### Ed25519-KeyBlind + +Ed25519 signature with key blinding. + +This crate wrap ed25519-dalek and adds Tor-specific +Ed25519 key blinding. + +This crate is based on Arti Tor client and compatible with its implementation, see [tests](./tests/compat.rs). + +### Time + +Hidden-service time is represented by `HSEpoch`. + +An epoch is a fixed-length interval counted from the Unix epoch. It is used +when deriving epoch-specific blinded identity keys, discovery keys, and +certificates. + +### Hidden services identifiers (HSId) + +`HsId` is the public hidden-service identity. It is the validated Ed25519 public key behind a `.mix` address. + +The module provides: + +- `HsId`: compact public service identity bytes. +- `HsIdKey`: validated Ed25519 public identity key. +- `HsIdKeypair`: expanded identity keypair for signing. +- `HsBlindIdKey` and `HsBlindIdKeypair`: epoch-specific blinded identity keys. +- `DiscoveryKey`: descriptor lookup key derived from the blinded key, public entropy, and replica number. + +### Key Certificates (KeyCert) + +`KeyCert` binds a certified public key to a signing key for a specific `HSEpoch`. + +Certificates currently support these roles: + +- `DescriptorSigningKey`: a blinded hidden-service identity key certifies the + short-term descriptor signing key. +- `IntroAuthenticationKey`: an introduction authentication key certifies the + descriptor signing key for one intro point. +- `ServiceEncryptionKey`: the descriptor signing key certifies an X25519 + service encryption key for one intro point (TODO: need to cross certify this key). + +### Descriptor + +Hidden-service descriptors are split into: + +- an outer signed layer, visible to discovery nodes. +- an encrypted inner layer, visible only to clients that know the service identity. + +The outer layer contains the descriptor epoch, descriptor signing certificate, +revision counter, encrypted inner layer, and descriptor signature. + +Validation checks the descriptor-signing certificate, verifies the outer signature, and +confirms the descriptor's blinded key maps to the expected discovery key. + +The inner layer contains introduction points. Each introduction point includes +public mix routing information, an introduction authentication certificate, and +a service encryption certificate. After decryption, inner validation checks +that those certificates match the descriptor signing key and epoch. + +Inner-layer encryption uses descriptor-specific key material derived from the +hidden-service identity, blinded key, random salt, and revision counter. The +ciphertext is authenticated with HMAC before decryption. diff --git a/rust_reference/ed25519-keyblind/README.md b/rust_reference/ed25519-keyblind/README.md deleted file mode 100644 index f7477a8..0000000 --- a/rust_reference/ed25519-keyblind/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Ed25519-KeyBlind - -Ed25519 signature with key blinding. - -This crate wrap ed25519-dalek and adds Tor-specific -Ed25519 key blinding. - -This crate is based on Arti Tor client and compatible with its implementation, see [tests](testsompat.rs). diff --git a/rust_reference/src/cert/key_cert.rs b/rust_reference/src/cert/key_cert.rs new file mode 100644 index 0000000..6f65609 --- /dev/null +++ b/rust_reference/src/cert/key_cert.rs @@ -0,0 +1,369 @@ +use crate::ed25519_keyblind::ed25519; +use crate::error::KeyCertError; +use crate::time::HSEpoch; +use x25519_dalek as x25519; + +const CERTIFIED_KEY_ED25519: u8 = 1; +const CERTIFIED_KEY_X25519: u8 = 2; + +/// Public key certified by a [`KeyCert`]. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CertifiedKey { + Ed25519(ed25519::PublicKey), + X25519(x25519::PublicKey), +} + +impl CertifiedKey { + pub fn key_type(self) -> u8 { + match self { + Self::Ed25519(_) => CERTIFIED_KEY_ED25519, + Self::X25519(_) => CERTIFIED_KEY_X25519, + } + } + + pub fn as_bytes(&self) -> &[u8; 32] { + match self { + Self::Ed25519(key) => key.as_bytes(), + Self::X25519(key) => key.as_bytes(), + } + } + + pub fn to_bytes(self) -> [u8; 32] { + match self { + Self::Ed25519(key) => key.to_bytes(), + Self::X25519(key) => key.to_bytes(), + } + } + + pub fn as_ed25519(self) -> Option { + match self { + Self::Ed25519(key) => Some(key), + Self::X25519(_) => None, + } + } + + pub fn as_x25519(self) -> Option { + match self { + Self::Ed25519(_) => None, + Self::X25519(key) => Some(key), + } + } + + fn from_type_and_bytes(key_type: u8, bytes: [u8; 32]) -> Result { + match key_type { + CERTIFIED_KEY_ED25519 => ed25519::PublicKey::from_bytes(&bytes) + .map(Self::Ed25519) + .map_err(|_| KeyCertError::InvalidPublicKey), + CERTIFIED_KEY_X25519 => Ok(Self::X25519(x25519::PublicKey::from(bytes))), + _ => Err(KeyCertError::UnsupportedCertifiedKeyType), + } + } +} + +impl From for CertifiedKey { + fn from(value: ed25519::PublicKey) -> Self { + Self::Ed25519(value) + } +} + +impl From for CertifiedKey { + fn from(value: x25519::PublicKey) -> Self { + Self::X25519(value) + } +} + +/// Hidden-service descriptor key certificate role. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum HsCertType { + DescriptorSigningKey, + IntroAuthenticationKey, + ServiceEncryptionKey, +} + +impl HsCertType { + pub(crate) fn to_byte(self) -> u8 { + match self { + Self::DescriptorSigningKey => 1, + Self::IntroAuthenticationKey => 2, + Self::ServiceEncryptionKey => 3, + } + } + + pub(crate) fn from_byte(value: u8) -> Option { + match value { + 1 => Some(Self::DescriptorSigningKey), + 2 => Some(Self::IntroAuthenticationKey), + 3 => Some(Self::ServiceEncryptionKey), + _ => None, + } + } +} + +/// A signed key certificate. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct KeyCert { + pub cert_type: HsCertType, + pub epoch: HSEpoch, + pub certified_key: CertifiedKey, + pub signing_key: ed25519::PublicKey, + pub signature: ed25519::Signature, +} + +impl KeyCert { + /// Create and sign a key certificate with a normal Ed25519 keypair. + pub fn new( + cert_type: HsCertType, + epoch: HSEpoch, + certified_key: impl Into, + signing_key: &ed25519::Keypair, + ) -> Self { + Self::new_with_signer( + cert_type, + epoch, + certified_key.into(), + signing_key.verifying_key(), + |msg| signing_key.sign(msg), + ) + } + + /// Create and sign a key certificate with an expanded Ed25519 keypair. + pub fn new_expanded( + cert_type: HsCertType, + epoch: HSEpoch, + certified_key: impl Into, + signing_key: &ed25519::ExpandedKeypair, + ) -> Self { + Self::new_with_signer( + cert_type, + epoch, + certified_key.into(), + *signing_key.public(), + |msg| signing_key.sign(msg), + ) + } + + pub(crate) fn new_with_signer( + cert_type: HsCertType, + epoch: HSEpoch, + certified_key: CertifiedKey, + signing_key: ed25519::PublicKey, + sign: F, + ) -> Self + where + F: FnOnce(&[u8]) -> ed25519::Signature, + { + let unsigned = unsigned_bytes(cert_type, epoch, certified_key, signing_key); + let signature = sign(&unsigned); + + Self { + cert_type, + epoch, + certified_key, + signing_key, + signature, + } + } + + /// Return the public key that signed this certificate. + pub fn signing_public_key(&self) -> ed25519::PublicKey { + self.signing_key + } + + /// Verify this certificate signature. + pub fn verify(&self) -> Result<(), KeyCertError> { + self.signing_public_key() + .verify(&self.unsigned_bytes(), &self.signature) + .map_err(|_| KeyCertError::InvalidSignature) + } + + /// Serialize this certificate in a deterministic byte format. + pub fn to_bytes(&self) -> Vec { + let mut out = self.unsigned_bytes(); + out.extend_from_slice(&self.signature.to_bytes()); + out + } + + /// Deserialize and verify a certificate produced by [`KeyCert::to_bytes`]. + pub fn from_bytes(bytes: &[u8]) -> Result { + let (cert_type_byte, rest) = bytes.split_first().ok_or(KeyCertError::MalformedPayload)?; + let cert_type = + HsCertType::from_byte(*cert_type_byte).ok_or(KeyCertError::UnsupportedCertType)?; + let (epoch_bytes, rest) = rest + .split_first_chunk::<16>() + .ok_or(KeyCertError::MalformedPayload)?; + let epoch = HSEpoch::from_bytes(epoch_bytes).map_err(|_| KeyCertError::MalformedPayload)?; + let (certified_key_type, rest) = + rest.split_first().ok_or(KeyCertError::MalformedPayload)?; + let (certified_key_bytes, rest) = rest + .split_first_chunk::<32>() + .ok_or(KeyCertError::MalformedPayload)?; + let certified_key = + CertifiedKey::from_type_and_bytes(*certified_key_type, *certified_key_bytes)?; + let (signing_key_bytes, rest) = rest + .split_first_chunk::<32>() + .ok_or(KeyCertError::MalformedPayload)?; + let signing_key = ed25519::PublicKey::from_bytes(&signing_key_bytes) + .map_err(|_| KeyCertError::InvalidPublicKey)?; + let (signature_bytes, rest) = rest + .split_first_chunk::<64>() + .ok_or(KeyCertError::MalformedPayload)?; + if !rest.is_empty() { + return Err(KeyCertError::MalformedPayload); + } + let signature = ed25519::Signature::from_bytes(signature_bytes); + + let cert = Self { + cert_type, + epoch, + certified_key, + signing_key, + signature, + }; + cert.verify()?; + Ok(cert) + } + + fn unsigned_bytes(&self) -> Vec { + unsigned_bytes( + self.cert_type, + self.epoch, + self.certified_key, + self.signing_key, + ) + } +} + +fn unsigned_bytes( + cert_type: HsCertType, + epoch: HSEpoch, + certified_key: CertifiedKey, + signing_key: ed25519::PublicKey, +) -> Vec { + let mut out = Vec::new(); + write_u8(&mut out, cert_type.to_byte()); + out.extend_from_slice(&epoch.to_bytes()); + write_u8(&mut out, certified_key.key_type()); + out.extend_from_slice(certified_key.as_bytes()); + out.extend_from_slice(signing_key.as_bytes()); + out +} + +fn write_u8(out: &mut Vec, value: u8) { + out.push(value); +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_keypair() -> ed25519::Keypair { + ed25519::Keypair::from_bytes(&[7_u8; 32]) + } + + fn test_epoch() -> HSEpoch { + HSEpoch::from_parts(86_400, 42) + } + + fn test_cert_type() -> HsCertType { + HsCertType::IntroAuthenticationKey + } + + #[test] + fn key_cert_bytes_roundtrip() { + let signing_key = test_keypair(); + let certified_key = ed25519::Keypair::from_bytes(&[1_u8; 32]).verifying_key(); + let cert = KeyCert::new(test_cert_type(), test_epoch(), certified_key, &signing_key); + let bytes = cert.to_bytes(); + + assert_eq!(bytes.len(), 1 + 16 + 1 + 32 + 32 + 64); + assert_eq!(KeyCert::from_bytes(&bytes).unwrap(), cert); + } + + #[test] + fn rejects_wrong_key_cert_length() { + let signing_key = test_keypair(); + let certified_key = ed25519::Keypair::from_bytes(&[1_u8; 32]).verifying_key(); + let cert = KeyCert::new(test_cert_type(), test_epoch(), certified_key, &signing_key); + let mut bytes = cert.to_bytes(); + bytes.pop(); + + assert!(matches!( + KeyCert::from_bytes(&bytes), + Err(KeyCertError::MalformedPayload) + )); + } + + #[test] + fn expanded_keypair_signs_key_cert() { + let signing_key = ed25519::ExpandedKeypair::from(&test_keypair()); + let certified_key = ed25519::Keypair::from_bytes(&[1_u8; 32]).verifying_key(); + let cert = KeyCert::new_expanded( + HsCertType::DescriptorSigningKey, + test_epoch(), + certified_key, + &signing_key, + ); + + cert.verify().unwrap(); + assert_eq!(cert.cert_type, HsCertType::DescriptorSigningKey); + assert_eq!(cert.signing_key, *signing_key.public()); + } + + #[test] + fn x25519_key_can_be_certified() { + let signing_key = test_keypair(); + let certified_key = x25519::PublicKey::from([9_u8; 32]); + let cert = KeyCert::new( + HsCertType::ServiceEncryptionKey, + test_epoch(), + certified_key, + &signing_key, + ); + + cert.verify().unwrap(); + assert_eq!(cert.cert_type, HsCertType::ServiceEncryptionKey); + assert_eq!(cert.certified_key.as_x25519().unwrap(), certified_key); + } + + #[test] + fn tampered_key_cert_signature_fails() { + let signing_key = test_keypair(); + let certified_key = ed25519::Keypair::from_bytes(&[1_u8; 32]).verifying_key(); + let cert = KeyCert::new(test_cert_type(), test_epoch(), certified_key, &signing_key); + let mut bytes = cert.to_bytes(); + bytes[1 + 16 + 1 + 32 + 32] ^= 1; + + assert!(matches!( + KeyCert::from_bytes(&bytes), + Err(KeyCertError::InvalidSignature) + )); + } + + #[test] + fn tampered_key_cert_type_fails() { + let signing_key = test_keypair(); + let certified_key = ed25519::Keypair::from_bytes(&[1_u8; 32]).verifying_key(); + let cert = KeyCert::new(test_cert_type(), test_epoch(), certified_key, &signing_key); + let mut bytes = cert.to_bytes(); + bytes[0] = HsCertType::ServiceEncryptionKey.to_byte(); + + assert!(matches!( + KeyCert::from_bytes(&bytes), + Err(KeyCertError::InvalidSignature) + )); + } + + #[test] + fn rejects_unknown_key_cert_type() { + let signing_key = test_keypair(); + let certified_key = ed25519::Keypair::from_bytes(&[1_u8; 32]).verifying_key(); + let cert = KeyCert::new(test_cert_type(), test_epoch(), certified_key, &signing_key); + let mut bytes = cert.to_bytes(); + bytes[0] = 255; + + assert!(matches!( + KeyCert::from_bytes(&bytes), + Err(KeyCertError::UnsupportedCertType) + )); + } +} diff --git a/rust_reference/src/cert/mod.rs b/rust_reference/src/cert/mod.rs new file mode 100644 index 0000000..21bf6eb --- /dev/null +++ b/rust_reference/src/cert/mod.rs @@ -0,0 +1 @@ +pub mod key_cert; diff --git a/rust_reference/ed25519-keyblind/src/conversion.rs b/rust_reference/src/ed25519_keyblind/conversion.rs similarity index 92% rename from rust_reference/ed25519-keyblind/src/conversion.rs rename to rust_reference/src/ed25519_keyblind/conversion.rs index 4732e14..1fa03b7 100644 --- a/rust_reference/ed25519-keyblind/src/conversion.rs +++ b/rust_reference/src/ed25519_keyblind/conversion.rs @@ -7,20 +7,16 @@ //! Note that these formulas are not standardized; don't use //! it for anything besides cross-certification in hidden service. -use digest::Digest; -use zeroize::{Zeroize as _, Zeroizing}; -use x25519_dalek::{StaticSecret as XStaticSecret, PublicKey as XPublicKey}; -use sha2::Sha512; -use crate::ed25519::{ExpandedKeypair, PublicKey, Keypair}; +use super::ed25519::{ExpandedKeypair, Keypair, PublicKey}; use curve25519_dalek::montgomery::MontgomeryPoint; - +use digest::Digest; +use sha2::Sha512; +use x25519_dalek::{PublicKey as XPublicKey, StaticSecret as XStaticSecret}; +use zeroize::{Zeroize as _, Zeroizing}; /// Convert a curve25519 public key (with sign bit) to an ed25519 /// public key, for use in key cross-certification. -pub fn convert_curve25519_to_ed25519_public( - pubkey: &XPublicKey, - signbit: u8, -) -> Option { +pub fn convert_curve25519_to_ed25519_public(pubkey: &XPublicKey, signbit: u8) -> Option { let point = MontgomeryPoint(*pubkey.as_bytes()); let edpoint = point.to_edwards(signbit)?; @@ -97,9 +93,7 @@ pub fn convert_curve25519_to_ed25519_private( /// /// This panic should be impossible unless we have upgraded x25519-dalek without auditing this /// function. -pub fn convert_ed25519_to_curve25519_private( - keypair: &Keypair, -) -> XStaticSecret { +pub fn convert_ed25519_to_curve25519_private(keypair: &Keypair) -> XStaticSecret { // Generate the key according to section-5.1.5 of rfc8032 let h = Sha512::digest(keypair.to_bytes()); @@ -116,4 +110,4 @@ pub fn convert_ed25519_to_curve25519_private( bytes.zeroize(); secret -} \ No newline at end of file +} diff --git a/rust_reference/ed25519-keyblind/src/ed25519.rs b/rust_reference/src/ed25519_keyblind/ed25519.rs similarity index 98% rename from rust_reference/ed25519-keyblind/src/ed25519.rs rename to rust_reference/src/ed25519_keyblind/ed25519.rs index c848c11..e5be28a 100644 --- a/rust_reference/ed25519-keyblind/src/ed25519.rs +++ b/rust_reference/src/ed25519_keyblind/ed25519.rs @@ -1,9 +1,9 @@ //! Ed25519 wrapper for the `ed25519_dalek` use curve25519_dalek::Scalar; -use std::fmt::Debug; use ed25519_dalek::hazmat::ExpandedSecretKey; use ed25519_dalek::{Signer as _, Verifier as _}; +use std::fmt::Debug; /// An Ed25519 signature. #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -110,7 +110,6 @@ impl<'a> From<&'a [u8; 64]> for Signature { } } - /// A variant of [`Keypair`] containing an [`ExpandedSecretKey`]. #[allow(clippy::exhaustive_structs)] pub struct ExpandedKeypair { @@ -151,9 +150,8 @@ impl ExpandedKeypair { /// Reconstruct a key from its byte representation as returned by /// `to_secret_key_bytes()`. pub fn from_secret_key_bytes(bytes: [u8; 64]) -> Self { - let scalar = Scalar::from_bytes_mod_order( - bytes[0..32].try_into().expect("wrong length on slice"), - ); + let scalar = + Scalar::from_bytes_mod_order(bytes[0..32].try_into().expect("wrong length on slice")); let hash_prefix = bytes[32..64].try_into().expect("wrong length on slice"); let secret = ExpandedSecretKey { scalar, @@ -183,7 +181,6 @@ impl From for PublicKey { } } - /// An ed25519 signature, plus the document that it signs and its /// public key. #[derive(Clone, Debug)] @@ -246,4 +243,4 @@ pub fn validate_batch(sigs: &[&ValidatableEd25519Signature]) -> bool { } ed25519_dalek::verify_batch(&ed_msgs[..], &ed_sigs[..], &ed_pks[..]).is_ok() } -} \ No newline at end of file +} diff --git a/rust_reference/ed25519-keyblind/src/key_blinding.rs b/rust_reference/src/ed25519_keyblind/key_blinding.rs similarity index 96% rename from rust_reference/ed25519-keyblind/src/key_blinding.rs rename to rust_reference/src/ed25519_keyblind/key_blinding.rs index b4174be..2d3f8f2 100644 --- a/rust_reference/ed25519-keyblind/src/key_blinding.rs +++ b/rust_reference/src/ed25519_keyblind/key_blinding.rs @@ -7,25 +7,12 @@ //! descriptors, without knowing which services they represent. We //! implement this blinding operation via [`blind_pubkey`]. -pub use sha2::Sha512; -use digest::Digest; -use thiserror::Error; - +use super::ed25519::{ExpandedKeypair, PublicKey}; +use crate::error::BlindingError; use curve25519_dalek::scalar::Scalar; +use digest::Digest; use ed25519_dalek::hazmat::ExpandedSecretKey; -use crate::ed25519::{ExpandedKeypair, PublicKey}; - -/// An error occurred during a key-blinding operation. -#[derive(Error, Debug, PartialEq, Eq)] -#[non_exhaustive] -pub enum BlindingError { - /// A bad public key was provided for blinding - #[error("Public key was invalid")] - BadPubkey, - /// Dalek failed the scalar multiplication - #[error("Key blinding failed")] - BlindingFailed, -} +pub use sha2::Sha512; /// Convert this dalek error to a BlindingError impl From for BlindingError { @@ -65,7 +52,7 @@ fn clamp_blinding_factor(h: [u8; 32]) -> Scalar { /// /// This function can fail if the input is not actually a valid /// Ed25519 public key. -fn blind_pubkey(pk: &PublicKey, h: [u8; 32]) -> Result { +pub fn blind_pubkey(pk: &PublicKey, h: [u8; 32]) -> Result { use curve25519_dalek::edwards::CompressedEdwardsY; // clamp @@ -146,7 +133,7 @@ pub fn blind_keypair( #[cfg(test)] mod tests { use super::*; - use crate::ed25519::Keypair; + use crate::ed25519_keyblind::ed25519::Keypair; #[test] fn blinding() { diff --git a/rust_reference/ed25519-keyblind/src/lib.rs b/rust_reference/src/ed25519_keyblind/mod.rs similarity index 98% rename from rust_reference/ed25519-keyblind/src/lib.rs rename to rust_reference/src/ed25519_keyblind/mod.rs index e1ca6fb..e187b3a 100644 --- a/rust_reference/ed25519-keyblind/src/lib.rs +++ b/rust_reference/src/ed25519_keyblind/mod.rs @@ -1,4 +1,3 @@ +pub mod conversion; pub mod ed25519; pub mod key_blinding; -pub mod conversion; - diff --git a/rust_reference/src/error.rs b/rust_reference/src/error.rs new file mode 100644 index 0000000..d557ec7 --- /dev/null +++ b/rust_reference/src/error.rs @@ -0,0 +1,157 @@ +use thiserror::Error; + +/// An error occurred during a key-blinding operation. +#[derive(Error, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum BlindingError { + /// A bad public key was provided for blinding + #[error("Public key was invalid")] + BadPubkey, + /// Dalek failed the scalar multiplication + #[error("Key blinding failed")] + BlindingFailed, +} + +/// Error that can occur parsing an `HsId` from a `.mix` domain name +#[derive(Error, Clone, Debug)] +#[non_exhaustive] +pub enum HsIdParseError { + /// Supplied domain name string does not end in `.mix` + #[error("Domain name does not end in .mix")] + NotMixDomain, + + /// The decoded address had an unexpected length. + #[error("Invalid length for .mix address")] + InvalidLength, + + /// Base32 decoding failed + /// + /// `position` is indeed the (byte) position in the input string + #[error("Invalid base32 in .mix address")] + InvalidBase32(#[from] data_encoding::DecodeError), + + /// The address does not encode a valid Ed25519 public key. + #[error("Invalid Ed25519 public key in .mix address")] + InvalidPublicKey, + + /// Checksum failed + #[error("Checksum failed, .mix address corrupted")] + WrongChecksum, +} + +/// An error that occurs when creating or manipulating a [`HSEpoch`] +#[derive(Clone, Debug, Error)] +#[non_exhaustive] +pub enum TimePeriodError { + /// We couldn't represent the time period in the way we were trying to + /// represent it, since it outside of the range supported by the data type. + #[error("Time period out was out of range")] + OutOfRange, + + /// The time period couldn't be constructed because its interval was + /// invalid. + #[error("Invalid time period interval")] + IntervalInvalid, + + /// The encoded time period was malformed. + #[error("Invalid encoded time period")] + EncodingInvalid, +} + +#[derive(Debug, Error)] +pub enum KeyCertError { + #[error("key certificate payload is malformed")] + MalformedPayload, + + #[error("key certificate signing key is invalid")] + InvalidPublicKey, + + #[error("key certificate certified key type is unsupported")] + UnsupportedCertifiedKeyType, + + #[error("key certificate type is unsupported")] + UnsupportedCertType, + + #[error("key certificate signature is invalid")] + InvalidSignature, +} + +/// Errors returned while encoding, decoding, or validating hidden-service +/// descriptors. +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum HsDescError { + /// An embedded key certificate was malformed or did not verify. + #[error("key certificate error")] + KeyCert(#[from] KeyCertError), + + /// Ed25519 key blinding failed. + #[error("key blinding failed")] + Blinding(#[from] BlindingError), + + /// A byte string did not contain a valid Ed25519 public key. + #[error("invalid Ed25519 public key")] + InvalidPublicKey, + + /// A descriptor, certificate, or inner-layer signature was invalid. + #[error("invalid signature")] + InvalidSignature, + + /// The encoded descriptor version is not supported by this implementation. + #[error("unsupported hidden-service descriptor version {0}")] + UnsupportedVersion(u8), + + /// A length field could not be represented in the descriptor wire format. + #[error("descriptor field is too large")] + FieldTooLarge, + + /// A descriptor byte string was truncated or otherwise malformed. + #[error("malformed descriptor: {0}")] + Malformed(&'static str), + + /// The descriptor contained trailing bytes after a complete object. + #[error("descriptor contains trailing bytes")] + TrailingBytes, + + /// A descriptor lifetime of zero seconds, or a lifetime with sub-second + /// precision, was requested. + #[error("invalid descriptor validity period")] + InvalidValidity, + + /// The descriptor's epoch does not include the validation time. + #[error("descriptor epoch is not current")] + EpochNotCurrent, + + /// The blinded key in the descriptor certificate did not match the hidden + /// service identity and epoch. + #[error("descriptor blinded key mismatch")] + BlindedKeyMismatch, + + /// The discovery key derived from the descriptor did not match the expected + /// lookup key. + #[error("descriptor discovery key mismatch")] + DiscoveryKeyMismatch, + + /// The descriptor-signing key certificate does not certify the key that + /// signed the descriptor. + #[error("descriptor signing key certificate mismatch")] + DescriptorSigningKeyMismatch, + + /// An introduction-point certificate was signed by the wrong key or + /// certified the wrong key. + #[error("introduction point certificate mismatch: {0}")] + IntroPointCertificateMismatch(&'static str), + + /// A descriptor inner layer did not contain any introduction points. + #[error("descriptor inner layer has no introduction points")] + MissingIntroPoints, + + /// The encrypted inner-layer MAC did not verify. + #[error("descriptor inner-layer MAC check failed")] + InvalidMac, + + /// The operating system random source failed while generating descriptor + /// encryption salt. + #[error("random source failed")] + Random(#[from] getrandom::Error), +} diff --git a/rust_reference/src/hs_desc/desc_enc.rs b/rust_reference/src/hs_desc/desc_enc.rs new file mode 100644 index 0000000..436a8dc --- /dev/null +++ b/rust_reference/src/hs_desc/desc_enc.rs @@ -0,0 +1,136 @@ +//! Types and functions for onion service descriptor encryption. + +use super::{EncryptedBlob, RevisionCounter}; +use crate::ed25519_keyblind::ed25519; +use crate::error::HsDescError; +use crate::hs_id::HsId; +use aes::Aes128; +use ctr::Ctr128BE; +use ctr::cipher::{KeyIvInit, StreamCipher}; +use digest::{Digest, KeyInit}; +use hmac::{Hmac, Mac}; +use sha2::Sha256; + +type Aes128Ctr = Ctr128BE; +type HmacSha256 = Hmac; +pub(crate) const KAPPA: usize = 16; + +/// Parameters for encrypting or decrypting part of an onion service descriptor. +pub(super) struct HsDescEncryption<'a> { + /// blinded public key + pub(super) blinded_key: &'a ed25519::PublicKey, + /// The hidden service identifier + pub(super) hs_id: &'a HsId, + /// The current revision of the hidden service descriptor + pub(super) revision: RevisionCounter, +} + +impl<'a> HsDescEncryption<'a> { + /// Encrypt a given bytestring using these encryption parameters. + pub(super) fn encrypt(&self, salt: [u8; KAPPA], data: &[u8]) -> EncryptedBlob { + let enc_key = self.kdf(salt, b"blob encryption key"); + let enc_iv = self.kdf(salt, b"blob iv"); + let mut blob = data.to_vec(); + let mut cipher = + Aes128Ctr::new_from_slices(&enc_key, &enc_iv).expect("AES-128 key and IV are fixed"); + cipher.apply_keystream(&mut blob); + + let mac = self.mac(salt, &blob); + EncryptedBlob { salt, blob, mac } + } + + /// Decrypt a given [`EncryptedBlob`] that was first encrypted using these + /// encryption parameters. + pub(super) fn decrypt(&self, encrypted_blob: &EncryptedBlob) -> Result, HsDescError> { + let mut mac = self.mac_context(encrypted_blob.salt); + mac.update(&encrypted_blob.salt); + mac.update(&encrypted_blob.blob); + mac.verify_truncated_left(&encrypted_blob.mac) + .map_err(|_| HsDescError::InvalidMac)?; + + let enc_key = self.kdf(encrypted_blob.salt, b"blob encryption key"); + let enc_iv = self.kdf(encrypted_blob.salt, b"blob iv"); + let mut plaintext = encrypted_blob.blob.clone(); + let mut cipher = + Aes128Ctr::new_from_slices(&enc_key, &enc_iv).expect("AES-128 key and IV are fixed"); + cipher.apply_keystream(&mut plaintext); + + Ok(plaintext) + } + + fn mac(&self, salt: [u8; KAPPA], blob: &[u8]) -> [u8; KAPPA] { + let mut mac = self.mac_context(salt); + mac.update(&salt); + mac.update(blob); + let tag = mac.finalize().into_bytes(); + + let mut out = [0_u8; KAPPA]; + out.copy_from_slice(&tag[..KAPPA]); + out + } + + fn mac_context(&self, salt: [u8; KAPPA]) -> HmacSha256 { + let mac_key = self.kdf(salt, b"mac key"); + HmacSha256::new_from_slice(&mac_key).expect("HMAC accepts fixed-size keys") + } + + /// Returns key material using domain-separated SHA256 + /// the keys to be used for encrypting the inner layer and computing MAC + fn kdf(&self, salt: [u8; KAPPA], key_domain: &[u8]) -> [u8; KAPPA] { + // keys = KDF(key_domain || "inner layer" || salt || hs_id || blinded_key || revision counter) + let mut hash = Sha256::new(); + Digest::update(&mut hash, key_domain); + Digest::update(&mut hash, b"inner layer"); + Digest::update(&mut hash, salt); + Digest::update(&mut hash, self.hs_id.as_bytes()); + Digest::update(&mut hash, self.blinded_key.as_bytes()); + Digest::update(&mut hash, self.revision.to_be_bytes()); + let output = hash.finalize(); + + let mut output_key = [0_u8; KAPPA]; + output_key.copy_from_slice(&output[..KAPPA]); + output_key + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::ed25519_keyblind::ed25519; + + #[test] + fn roundtrip_basics() { + let identity = ed25519::Keypair::from_bytes(&[7_u8; 32]); + let hs_id = HsId::from_public_key(identity.verifying_key()); + let blinded = ed25519::Keypair::from_bytes(&[9_u8; 32]).verifying_key(); + let params = HsDescEncryption { + blinded_key: &blinded, + hs_id: &hs_id, + revision: 3, + }; + + let blob = params.encrypt([1_u8; KAPPA], b"hello descriptor"); + + assert_ne!(blob.blob, b"hello descriptor"); + assert_eq!(params.decrypt(&blob).unwrap(), b"hello descriptor"); + } + + #[test] + fn rejects_bad_mac() { + let identity = ed25519::Keypair::from_bytes(&[7_u8; 32]); + let hs_id = HsId::from_public_key(identity.verifying_key()); + let blinded = ed25519::Keypair::from_bytes(&[9_u8; 32]).verifying_key(); + let params = HsDescEncryption { + blinded_key: &blinded, + hs_id: &hs_id, + revision: 3, + }; + let mut blob = params.encrypt([1_u8; KAPPA], b"hello descriptor"); + blob.mac[0] ^= 1; + + assert!(matches!( + params.decrypt(&blob), + Err(HsDescError::InvalidMac) + )); + } +} diff --git a/rust_reference/src/hs_desc/inner.rs b/rust_reference/src/hs_desc/inner.rs new file mode 100644 index 0000000..4e3c466 --- /dev/null +++ b/rust_reference/src/hs_desc/inner.rs @@ -0,0 +1,295 @@ +use crate::cert::key_cert::HsCertType; +use crate::cert::key_cert::{CertifiedKey, KeyCert}; +use crate::ed25519_keyblind::ed25519; +use crate::error::HsDescError; +use crate::time::HSEpoch; +use serde::{Deserialize, Serialize}; +use x25519_dalek as x25519; + +/// Public routing information for a mix introduction point. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct MixPubInfo { + /// Peer identifier bytes. The concrete peer-id format is supplied by the + /// mix/discovery layer. + pub peer_id: Vec, + /// Multiaddress bytes. This remains byte-oriented so this descriptor crate + /// does not need to depend on libp2p types. + pub multi_addr: Vec, + /// Public mix-routing key for building a Sphinx packet to this intro point. + pub mix_pk: [u8; 32], +} + +/// One introduction point advertised in an inner descriptor layer. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct IntroPoint { + /// Public routing information for the intro point. + pub info: MixPubInfo, + /// Certificate signed by `auth_key` and certifying the descriptor signing + /// key. Clients use `auth_key` to address the service at this intro point. + pub auth_key_cert: KeyCert, + /// Certificate signed by the descriptor signing key and certifying the + /// service encryption key for this intro point. + pub service_key_cert: KeyCert, +} + +/// The plaintext descriptor inner layer. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct InnerLayer { + /// Introduction points that can forward bootstrap requests to the service. + pub intro_points: Vec, + /// Optional DOS/spam-resistance parameters. The concrete format is left to + /// the transport/discovery integration. + pub dos_params: Vec, +} + +impl MixPubInfo { + /// Create public mix routing information. + pub fn new( + peer_id: impl Into>, + multi_addr: impl Into>, + mix_pk: [u8; 32], + ) -> Self { + Self { + peer_id: peer_id.into(), + multi_addr: multi_addr.into(), + mix_pk, + } + } +} + +impl IntroPoint { + /// Return the introduction authentication key certified for this intro + /// point. + pub fn auth_key(&self) -> ed25519::PublicKey { + self.auth_key_cert.signing_key + } + + /// Return the service encryption key certified for this intro point. + pub fn service_key(&self) -> Option { + self.service_key_cert.certified_key.as_x25519() + } + + /// Validate this intro point against the descriptor signing key and epoch. + pub fn validate( + &self, + desc_key: ed25519::PublicKey, + epoch: HSEpoch, + ) -> Result<(), HsDescError> { + self.auth_key_cert.verify()?; + self.service_key_cert.verify()?; + + let auth_cert = &self.auth_key_cert; + if auth_cert.epoch != epoch { + return Err(HsDescError::IntroPointCertificateMismatch( + "auth key certificate epoch", + )); + } + if auth_cert.cert_type != HsCertType::IntroAuthenticationKey { + return Err(HsDescError::IntroPointCertificateMismatch( + "auth key certificate type", + )); + } + if auth_cert.certified_key != CertifiedKey::Ed25519(desc_key) { + return Err(HsDescError::IntroPointCertificateMismatch( + "auth key certificate subject", + )); + } + let service_cert = &self.service_key_cert; + if service_cert.epoch != epoch { + return Err(HsDescError::IntroPointCertificateMismatch( + "service key certificate epoch", + )); + } + if service_cert.cert_type != HsCertType::ServiceEncryptionKey { + return Err(HsDescError::IntroPointCertificateMismatch( + "service key certificate type", + )); + } + if service_cert.signing_key != desc_key { + return Err(HsDescError::IntroPointCertificateMismatch( + "service key certificate signer", + )); + } + if service_cert.certified_key.as_x25519().is_none() { + return Err(HsDescError::IntroPointCertificateMismatch( + "service key certificate subject type", + )); + } + + Ok(()) + } +} + +impl InnerLayer { + /// Construct an inner layer from intro points and optional DOS parameters. + pub fn new(intro_points: Vec, dos_params: Vec) -> Self { + Self { + intro_points, + dos_params, + } + } + + /// Encode the inner layer in a canonical binary form. + pub fn to_bytes(&self) -> Result, HsDescError> { + let mut out = Vec::new(); + push_len(&mut out, self.intro_points.len())?; + for intro in &self.intro_points { + push_field(&mut out, &intro.info.peer_id)?; + push_field(&mut out, &intro.info.multi_addr)?; + out.extend_from_slice(&intro.info.mix_pk); + push_field(&mut out, &intro.auth_key_cert.to_bytes())?; + push_field(&mut out, &intro.service_key_cert.to_bytes())?; + } + push_field(&mut out, &self.dos_params)?; + Ok(out) + } + + /// Decode an inner layer. + pub fn from_bytes(bytes: &[u8]) -> Result { + let mut input = bytes; + let intro_count = take_u32(&mut input, "intro point count")? as usize; + let mut intro_points = Vec::with_capacity(intro_count); + + for _ in 0..intro_count { + let peer_id = take_field(&mut input, "intro peer id")?.to_vec(); + let multi_addr = take_field(&mut input, "intro multiaddr")?.to_vec(); + let mix_pk = take_array::<32>(&mut input, "intro mix public key")?; + let auth_key_cert = KeyCert::from_bytes(take_field(&mut input, "auth key cert")?)?; + let service_key_cert = + KeyCert::from_bytes(take_field(&mut input, "service key cert")?)?; + intro_points.push(IntroPoint { + info: MixPubInfo { + peer_id, + multi_addr, + mix_pk, + }, + auth_key_cert, + service_key_cert, + }); + } + + let dos_params = take_field(&mut input, "dos params")?.to_vec(); + ensure_consumed(input)?; + + Ok(Self { + intro_points, + dos_params, + }) + } + + /// Validate all certificates in the inner layer. + pub fn validate( + &self, + desc_key: ed25519::PublicKey, + epoch: HSEpoch, + ) -> Result<(), HsDescError> { + if self.intro_points.is_empty() { + return Err(HsDescError::MissingIntroPoints); + } + + for intro in &self.intro_points { + intro.validate(desc_key, epoch)?; + } + + Ok(()) + } +} + +fn push_len(out: &mut Vec, len: usize) -> Result<(), HsDescError> { + let len = u32::try_from(len).map_err(|_| HsDescError::FieldTooLarge)?; + out.extend_from_slice(&len.to_be_bytes()); + Ok(()) +} + +fn push_field(out: &mut Vec, field: &[u8]) -> Result<(), HsDescError> { + push_len(out, field.len())?; + out.extend_from_slice(field); + Ok(()) +} + +fn take_u32(input: &mut &[u8], name: &'static str) -> Result { + Ok(u32::from_be_bytes(take_array::<4>(input, name)?)) +} + +fn take_array( + input: &mut &[u8], + name: &'static str, +) -> Result<[u8; N], HsDescError> { + if input.len() < N { + return Err(HsDescError::Malformed(name)); + } + let (field, rest) = input.split_at(N); + *input = rest; + field.try_into().map_err(|_| HsDescError::Malformed(name)) +} + +fn take_field<'a>(input: &mut &'a [u8], name: &'static str) -> Result<&'a [u8], HsDescError> { + let len = take_u32(input, name)? as usize; + if input.len() < len { + return Err(HsDescError::Malformed(name)); + } + let (field, rest) = input.split_at(len); + *input = rest; + Ok(field) +} + +fn ensure_consumed(input: &[u8]) -> Result<(), HsDescError> { + if input.is_empty() { + Ok(()) + } else { + Err(HsDescError::TrailingBytes) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn key(seed: u8) -> ed25519::Keypair { + ed25519::Keypair::from_bytes(&[seed; 32]) + } + + fn epoch() -> HSEpoch { + HSEpoch::from_parts(86_400, 12) + } + + fn intro_point() -> IntroPoint { + let desc_key = key(1); + let auth_key = key(2); + let service_key = x25519::PublicKey::from([3_u8; 32]); + + IntroPoint { + info: MixPubInfo::new( + b"peer".to_vec(), + b"/ip4/127.0.0.1/tcp/9001".to_vec(), + [4; 32], + ), + auth_key_cert: KeyCert::new( + HsCertType::IntroAuthenticationKey, + epoch(), + desc_key.verifying_key(), + &auth_key, + ), + service_key_cert: KeyCert::new( + HsCertType::ServiceEncryptionKey, + epoch(), + service_key, + &desc_key, + ), + } + } + + #[test] + fn inner_layer_roundtrip() { + let inner = InnerLayer::new(vec![intro_point()], b"dos".to_vec()); + let bytes = inner.to_bytes().unwrap(); + + assert_eq!(InnerLayer::from_bytes(&bytes).unwrap(), inner); + } + + #[test] + fn validates_intro_point_certificates() { + let intro = intro_point(); + intro.validate(key(1).verifying_key(), epoch()).unwrap(); + } +} diff --git a/rust_reference/src/hs_desc/mod.rs b/rust_reference/src/hs_desc/mod.rs new file mode 100644 index 0000000..23867f2 --- /dev/null +++ b/rust_reference/src/hs_desc/mod.rs @@ -0,0 +1,19 @@ +//! Hidden-service descriptor support for the Mix hidden-service protocol. +//! +//! This follows the descriptor structure from the specification: +//! - an outer signed layer that directories can validate, +//! - and an encrypted inner layer that contains introduction-point information for clients. + +mod desc_enc; +mod inner; +mod outer; + +pub use crate::cert::key_cert::KeyCert; +pub use crate::error::HsDescError; +pub use inner::{InnerLayer, IntroPoint, MixPubInfo}; +pub use outer::{ + DescriptorBuildParams, EncryptedBlob, HiddenServiceDescriptor, OuterLayer, ValidatedOuter, +}; + +/// The descriptor revision counter. +pub type RevisionCounter = u64; diff --git a/rust_reference/src/hs_desc/outer.rs b/rust_reference/src/hs_desc/outer.rs new file mode 100644 index 0000000..6d0f289 --- /dev/null +++ b/rust_reference/src/hs_desc/outer.rs @@ -0,0 +1,674 @@ +use super::RevisionCounter; +use super::desc_enc::{HsDescEncryption, KAPPA}; +use super::inner::InnerLayer; +use crate::cert::key_cert::{HsCertType, KeyCert}; +use crate::ed25519_keyblind::ed25519; +use crate::error::HsDescError; +use crate::hs_id::{ + DiscoveryKey, HsBlindIdKey, HsBlindIdKeypair, HsId, HsIdKey, compute_discovery_key, +}; +use crate::time::HSEpoch; +use serde::{Deserialize, Serialize}; +use std::time::SystemTime; + +const DESCRIPTOR_VERSION: u8 = 1; +const OUTER_SIGNATURE_DOMAIN: &str = "mix-hs-desc-outer"; +const OUTER_SIGNATURE_TYPE: &[u8] = b"/mix-hs/descriptor/outer/1"; + +/// Encrypted descriptor payload. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct EncryptedBlob { + /// Random salt authenticated with the ciphertext. + pub salt: [u8; KAPPA], + /// AES-128-CTR ciphertext. + pub blob: Vec, + /// HMAC-SHA256 tag truncated to 128 bits. + pub mac: [u8; KAPPA], +} + +/// Metadata proven by a validated descriptor outer layer. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ValidatedOuter { + /// The epoch in which this descriptor is valid. + pub epoch: HSEpoch, + /// Epoch-specific blinded hidden-service identity key. + pub blinded_key: HsBlindIdKey, + /// Descriptor signing key certified by the blinded key. + pub desc_key: ed25519::PublicKey, + /// Monotonic version counter for descriptors under the same discovery key. + pub revision_counter: RevisionCounter, +} + +/// The signed descriptor outer layer. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct OuterLayer { + /// The epoch in which this descriptor is valid. + pub validity: HSEpoch, + /// Certificate binding the descriptor signing key to the epoch-specific + /// blinded hidden-service identity key. + pub desc_key_cert: KeyCert, + /// Monotonic version counter for descriptors under the same discovery key. + pub revision_counter: RevisionCounter, + /// Encrypted inner layer. + pub inner_layer: EncryptedBlob, + /// Signature by the descriptor signing key over all previous outer fields. + pub signature: ed25519::Signature, +} + +/// A hidden-service descriptor as stored in discovery. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct HiddenServiceDescriptor { + outer: OuterLayer, +} + +/// Inputs for constructing a hidden-service descriptor. +pub struct DescriptorBuildParams<'a> { + /// Long-term hidden-service identity (`hs_id`). + pub hs_id: HsId, + /// Descriptor epoch. + pub epoch: HSEpoch, + /// Public epoch-specific entropy used to derive the discovery key. + pub public_entropy: [u8; 32], + /// Replica number used to derive a descriptor discovery key. + pub replica_num: u8, + /// Descriptor revision counter. + pub revision_counter: RevisionCounter, + /// Epoch-specific blinded signing keypair. + pub blinded_key: &'a HsBlindIdKeypair, + /// Short-term descriptor signing keypair. + pub desc_key: &'a ed25519::Keypair, + /// Plaintext inner layer. + pub inner_layer: InnerLayer, +} + +impl EncryptedBlob { + /// Encrypt an inner layer according to the descriptor inner-layer scheme. + pub fn encrypt( + hs_id: &HsId, + blinded_key: &ed25519::PublicKey, + revision_counter: RevisionCounter, + plaintext: &[u8], + ) -> Result { + let mut salt = [0_u8; KAPPA]; + getrandom::fill(&mut salt)?; + Ok(Self::encrypt_with_salt( + hs_id, + blinded_key, + revision_counter, + salt, + plaintext, + )) + } + + /// Encrypt an inner layer with a caller-provided salt. + /// + /// This is mainly useful for deterministic tests and fixtures. + pub fn encrypt_with_salt( + hs_id: &HsId, + blinded_key: &ed25519::PublicKey, + revision_counter: RevisionCounter, + salt: [u8; KAPPA], + plaintext: &[u8], + ) -> Self { + let params = HsDescEncryption { + blinded_key, + hs_id, + revision: revision_counter, + }; + params.encrypt(salt, plaintext) + } + + /// Decrypt and authenticate an encrypted inner layer. + pub fn decrypt( + &self, + hs_id: &HsId, + blinded_key: &ed25519::PublicKey, + revision_counter: RevisionCounter, + ) -> Result, HsDescError> { + let params = HsDescEncryption { + blinded_key, + hs_id, + revision: revision_counter, + }; + params.decrypt(self) + } + + fn encode(&self, out: &mut Vec) -> Result<(), HsDescError> { + out.extend_from_slice(&self.salt); + push_field(out, &self.blob)?; + out.extend_from_slice(&self.mac); + Ok(()) + } + + fn decode(input: &mut &[u8]) -> Result { + let salt = take_array::(input, "encrypted blob salt")?; + let blob = take_field(input, "encrypted blob ciphertext")?.to_vec(); + let mac = take_array::(input, "encrypted blob mac")?; + Ok(Self { salt, blob, mac }) + } +} + +impl OuterLayer { + /// Return the descriptor signing key certified by the outer layer. + pub fn desc_key(&self) -> Option { + self.desc_key_cert.certified_key.as_ed25519() + } + + /// Return the blinded key that signed the descriptor-signing key + /// certificate. + pub fn blinded_key(&self) -> HsBlindIdKey { + HsBlindIdKey::from(self.desc_key_cert.signing_key) + } + + fn unsigned_bytes(&self) -> Result, HsDescError> { + let mut out = Vec::new(); + encode_outer_unsigned_fields( + &mut out, + self.validity, + &self.desc_key_cert, + self.revision_counter, + &self.inner_layer, + )?; + Ok(out) + } + + pub(super) fn sign( + validity: HSEpoch, + desc_key_cert: KeyCert, + revision_counter: RevisionCounter, + inner_layer: EncryptedBlob, + desc_key: &ed25519::Keypair, + ) -> Result { + let unsigned = OuterLayer { + validity, + desc_key_cert, + revision_counter, + inner_layer, + signature: ed25519::Signature::from_bytes(&[0_u8; 64]), + }; + let signature = desc_key.sign(&outer_signature_payload(&unsigned.unsigned_bytes()?)); + + Ok(Self { + signature, + ..unsigned + }) + } + + /// Verify the descriptor-signing certificate and the outer signature. + pub fn validate_signature(&self) -> Result { + if self.validity.length() == 0 { + return Err(HsDescError::InvalidValidity); + } + + self.desc_key_cert.verify()?; + let cert = &self.desc_key_cert; + if cert.epoch != self.validity { + return Err(HsDescError::DescriptorSigningKeyMismatch); + } + if cert.cert_type != HsCertType::DescriptorSigningKey { + return Err(HsDescError::DescriptorSigningKeyMismatch); + } + + let desc_key = cert + .certified_key + .as_ed25519() + .ok_or(HsDescError::DescriptorSigningKeyMismatch)?; + desc_key + .verify( + &outer_signature_payload(&self.unsigned_bytes()?), + &self.signature, + ) + .map_err(|_| HsDescError::InvalidSignature)?; + + Ok(ValidatedOuter { + epoch: self.validity, + blinded_key: self.blinded_key(), + desc_key, + revision_counter: self.revision_counter, + }) + } +} + +impl HiddenServiceDescriptor { + /// Create a descriptor from its outer layer. + pub fn new(outer: OuterLayer) -> Self { + Self { outer } + } + + /// Return the descriptor outer layer. + pub fn outer(&self) -> &OuterLayer { + &self.outer + } + + /// Build, encrypt, and sign a descriptor. + pub fn build(params: DescriptorBuildParams<'_>) -> Result<(DiscoveryKey, Self), HsDescError> { + let mut salt = [0_u8; KAPPA]; + getrandom::fill(&mut salt)?; + Self::build_with_salt(params, salt) + } + + fn build_with_salt( + params: DescriptorBuildParams<'_>, + salt: [u8; KAPPA], + ) -> Result<(DiscoveryKey, Self), HsDescError> { + if params.epoch.length() == 0 { + return Err(HsDescError::InvalidValidity); + } + + let hs_id_key = + HsIdKey::try_from(params.hs_id).map_err(|_| HsDescError::InvalidPublicKey)?; + let (expected_blinded, discovery_key) = hs_id_key.compute_blinded_and_discovery_key( + params.epoch, + ¶ms.public_entropy, + params.replica_num, + )?; + if expected_blinded.to_bytes() != params.blinded_key.public().to_bytes() { + return Err(HsDescError::BlindedKeyMismatch); + } + + let desc_key_cert = KeyCert::new_with_signer( + HsCertType::DescriptorSigningKey, + params.epoch, + params.desc_key.verifying_key().into(), + params.blinded_key.public_key(), + |msg| params.blinded_key.sign(msg), + ); + let inner_plaintext = params.inner_layer.to_bytes()?; + let encrypted_inner = EncryptedBlob::encrypt_with_salt( + ¶ms.hs_id, + ¶ms.blinded_key.public_key(), + params.revision_counter, + salt, + &inner_plaintext, + ); + let outer = OuterLayer::sign( + params.epoch, + desc_key_cert, + params.revision_counter, + encrypted_inner, + params.desc_key, + )?; + + Ok((discovery_key, Self { outer })) + } + + /// Encode the descriptor in canonical binary form. + pub fn to_bytes(&self) -> Result, HsDescError> { + let mut out = Vec::new(); + out.push(DESCRIPTOR_VERSION); + out.extend_from_slice(&self.outer.unsigned_bytes()?); + out.extend_from_slice(&self.outer.signature.to_bytes()); + Ok(out) + } + + /// Decode a descriptor from canonical binary form. + pub fn from_bytes(bytes: &[u8]) -> Result { + let mut input = bytes; + + let version = take_u8(&mut input, "descriptor version")?; + if version != DESCRIPTOR_VERSION { + return Err(HsDescError::UnsupportedVersion(version)); + } + + let validity = HSEpoch::from_bytes(&take_array::<16>(&mut input, "descriptor epoch")?) + .map_err(|_| HsDescError::Malformed("descriptor epoch"))?; + let desc_key_cert = KeyCert::from_bytes(take_field(&mut input, "desc key cert")?)?; + let revision_counter = take_u64(&mut input, "revision counter")?; + let inner_layer = EncryptedBlob::decode(&mut input)?; + let sig_bytes = take_array::<64>(&mut input, "outer signature")?; + ensure_consumed(input)?; + + Ok(Self { + outer: OuterLayer { + validity, + desc_key_cert, + revision_counter, + inner_layer, + signature: ed25519::Signature::from_bytes(&sig_bytes), + }, + }) + } + + /// Validate the outer layer as a discovery node would. + /// + /// This checks the descriptor-signing certificate, validates the outer + /// signature, optionally checks the epoch against `now`, and confirms that + /// the descriptor's blinded key maps to the expected discovery key. + pub fn validate_outer( + &self, + expected_discovery_key: &[u8; 32], + public_entropy: &[u8; 32], + replica_num: u8, + now: Option, + ) -> Result { + let meta = self.outer.validate_signature()?; + if let Some(now) = now { + if !meta.epoch.contains(now) { + return Err(HsDescError::EpochNotCurrent); + } + } + + let derived = compute_discovery_key(&meta.blinded_key, public_entropy, replica_num); + if derived.as_bytes() != expected_discovery_key { + return Err(HsDescError::DiscoveryKeyMismatch); + } + + Ok(meta) + } + + /// Validate the outer layer for a known hidden-service identity. + pub fn validate_outer_for_identity( + &self, + hs_id: &[u8; 32], + epoch: HSEpoch, + public_entropy: &[u8; 32], + replica_num: u8, + now: Option, + ) -> Result { + let hs_id = HsId::from_bytes(*hs_id).map_err(|_| HsDescError::InvalidPublicKey)?; + let hs_id_key = HsIdKey::try_from(hs_id).map_err(|_| HsDescError::InvalidPublicKey)?; + let (blinded_key, discovery_key) = + hs_id_key.compute_blinded_and_discovery_key(epoch, public_entropy, replica_num)?; + let meta = + self.validate_outer(discovery_key.as_bytes(), public_entropy, replica_num, now)?; + + if meta.epoch != epoch || meta.blinded_key != blinded_key { + return Err(HsDescError::BlindedKeyMismatch); + } + Ok(meta) + } + + /// Validate the outer layer for a known hidden-service identity. + pub fn validate_outer_for_hs_id( + &self, + hs_id: HsId, + epoch: HSEpoch, + public_entropy: &[u8; 32], + replica_num: u8, + now: Option, + ) -> Result { + self.validate_outer_for_identity(hs_id.as_bytes(), epoch, public_entropy, replica_num, now) + } + + /// Decrypt the inner layer with a known hidden-service identity. + pub fn decrypt_inner(&self, hs_id: &[u8; 32]) -> Result { + let hs_id = HsId::from_bytes(*hs_id).map_err(|_| HsDescError::InvalidPublicKey)?; + let plaintext = self.outer.inner_layer.decrypt( + &hs_id, + &self.outer.desc_key_cert.signing_key, + self.outer.revision_counter, + )?; + InnerLayer::from_bytes(&plaintext) + } + + /// Validate the outer layer, decrypt the inner layer, and validate all + /// intro-point certificates. + pub fn validate_decrypt_inner( + &self, + hs_id: &[u8; 32], + epoch: HSEpoch, + public_entropy: &[u8; 32], + replica_num: u8, + now: Option, + ) -> Result { + let meta = + self.validate_outer_for_identity(hs_id, epoch, public_entropy, replica_num, now)?; + let inner = self.decrypt_inner(hs_id)?; + inner.validate(meta.desc_key, meta.epoch)?; + Ok(inner) + } + + /// Decrypt the inner layer from a public [`HsId`]. + pub fn decrypt_inner_for_hs_id(&self, hs_id: HsId) -> Result { + self.decrypt_inner(hs_id.as_bytes()) + } + + /// Validate the outer layer, decrypt the inner layer, and validate all + /// intro-point certificates for a public [`HsId`]. + pub fn validate_decrypt_inner_for_hs_id( + &self, + hs_id: HsId, + epoch: HSEpoch, + public_entropy: &[u8; 32], + replica_num: u8, + now: Option, + ) -> Result { + self.validate_decrypt_inner(hs_id.as_bytes(), epoch, public_entropy, replica_num, now) + } +} + +fn encode_outer_unsigned_fields( + out: &mut Vec, + validity: HSEpoch, + desc_key_cert: &KeyCert, + revision_counter: RevisionCounter, + inner_layer: &EncryptedBlob, +) -> Result<(), HsDescError> { + out.extend_from_slice(&validity.to_bytes()); + push_field(out, &desc_key_cert.to_bytes())?; + out.extend_from_slice(&revision_counter.to_be_bytes()); + inner_layer.encode(out)?; + Ok(()) +} + +fn outer_signature_payload(unsigned_outer: &[u8]) -> Vec { + signed_payload(OUTER_SIGNATURE_DOMAIN, OUTER_SIGNATURE_TYPE, unsigned_outer) +} + +fn signed_payload(domain_separation: &str, payload_type: &[u8], payload: &[u8]) -> Vec { + let mut out = Vec::new(); + push_signature_field(&mut out, domain_separation.as_bytes()); + push_signature_field(&mut out, payload_type); + push_signature_field(&mut out, payload); + out +} + +fn push_signature_field(out: &mut Vec, field: &[u8]) { + out.extend_from_slice(&(field.len() as u64).to_be_bytes()); + out.extend_from_slice(field); +} + +fn push_len(out: &mut Vec, len: usize) -> Result<(), HsDescError> { + let len = u32::try_from(len).map_err(|_| HsDescError::FieldTooLarge)?; + out.extend_from_slice(&len.to_be_bytes()); + Ok(()) +} + +fn push_field(out: &mut Vec, field: &[u8]) -> Result<(), HsDescError> { + push_len(out, field.len())?; + out.extend_from_slice(field); + Ok(()) +} + +fn take_u8(input: &mut &[u8], name: &'static str) -> Result { + let (value, rest) = input.split_first().ok_or(HsDescError::Malformed(name))?; + *input = rest; + Ok(*value) +} + +fn take_u64(input: &mut &[u8], name: &'static str) -> Result { + Ok(u64::from_be_bytes(take_array::<8>(input, name)?)) +} + +fn take_u32(input: &mut &[u8], name: &'static str) -> Result { + Ok(u32::from_be_bytes(take_array::<4>(input, name)?)) +} + +fn take_array( + input: &mut &[u8], + name: &'static str, +) -> Result<[u8; N], HsDescError> { + if input.len() < N { + return Err(HsDescError::Malformed(name)); + } + let (field, rest) = input.split_at(N); + *input = rest; + field.try_into().map_err(|_| HsDescError::Malformed(name)) +} + +fn take_field<'a>(input: &mut &'a [u8], name: &'static str) -> Result<&'a [u8], HsDescError> { + let len = take_u32(input, name)? as usize; + if input.len() < len { + return Err(HsDescError::Malformed(name)); + } + let (field, rest) = input.split_at(len); + *input = rest; + Ok(field) +} + +fn ensure_consumed(input: &[u8]) -> Result<(), HsDescError> { + if input.is_empty() { + Ok(()) + } else { + Err(HsDescError::TrailingBytes) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::hs_id::HsIdKeypair; + use std::time::UNIX_EPOCH; + use x25519_dalek as x25519; + + fn key(seed: u8) -> ed25519::Keypair { + ed25519::Keypair::from_bytes(&[seed; 32]) + } + + fn epoch() -> HSEpoch { + HSEpoch::from_parts(86_400, 17) + } + + fn descriptor() -> (HsId, DiscoveryKey, HiddenServiceDescriptor) { + let identity = key(1); + let hs_id = HsId::from_public_key(identity.verifying_key()); + let hs_id_keypair = HsIdKeypair::from(&identity); + let (blinded_key, discovery_key) = hs_id_keypair + .compute_blinded_keypair_and_discovery_key(epoch(), &[9_u8; 32], 1) + .unwrap(); + let desc_key = key(2); + let auth_key = key(3); + let service_key = x25519::PublicKey::from([4_u8; 32]); + + let intro = crate::hs_desc::IntroPoint { + info: crate::hs_desc::MixPubInfo::new( + b"peer-1".to_vec(), + b"/ip4/127.0.0.1/tcp/9999".to_vec(), + [5; 32], + ), + auth_key_cert: KeyCert::new( + HsCertType::IntroAuthenticationKey, + epoch(), + desc_key.verifying_key(), + &auth_key, + ), + service_key_cert: KeyCert::new( + HsCertType::ServiceEncryptionKey, + epoch(), + service_key, + &desc_key, + ), + }; + let inner = InnerLayer::new(vec![intro], b"dos".to_vec()); + + let (_, descriptor) = HiddenServiceDescriptor::build_with_salt( + DescriptorBuildParams { + hs_id, + epoch: epoch(), + public_entropy: [9_u8; 32], + replica_num: 1, + revision_counter: 7, + blinded_key: &blinded_key, + desc_key: &desc_key, + inner_layer: inner, + }, + [8_u8; KAPPA], + ) + .unwrap(); + + (hs_id, discovery_key, descriptor) + } + + #[test] + fn descriptor_roundtrip_validate_and_decrypt() { + let (hs_id, discovery_key, descriptor) = descriptor(); + + let meta = descriptor + .validate_outer(discovery_key.as_bytes(), &[9_u8; 32], 1, None) + .unwrap(); + assert_eq!(meta.epoch, epoch()); + assert_eq!(meta.revision_counter, 7); + + let inner = descriptor + .validate_decrypt_inner_for_hs_id(hs_id, epoch(), &[9_u8; 32], 1, None) + .unwrap(); + assert_eq!(inner.intro_points.len(), 1); + assert_eq!( + inner.intro_points[0].service_key().unwrap().to_bytes(), + [4_u8; 32] + ); + assert_eq!(inner.dos_params, b"dos"); + + let encoded = descriptor.to_bytes().unwrap(); + let decoded = HiddenServiceDescriptor::from_bytes(&encoded).unwrap(); + assert_eq!(decoded, descriptor); + decoded + .validate_decrypt_inner_for_hs_id(hs_id, epoch(), &[9_u8; 32], 1, None) + .unwrap(); + } + + #[test] + fn descriptor_checks_epoch_when_requested() { + let (_, discovery_key, descriptor) = descriptor(); + + let in_epoch = epoch().range().unwrap().start; + descriptor + .validate_outer(discovery_key.as_bytes(), &[9_u8; 32], 1, Some(in_epoch)) + .unwrap(); + + assert!(matches!( + descriptor.validate_outer(discovery_key.as_bytes(), &[9_u8; 32], 1, Some(UNIX_EPOCH)), + Err(HsDescError::EpochNotCurrent) + )); + } + + #[test] + fn descriptor_rejects_wrong_discovery_key() { + let (_, _, descriptor) = descriptor(); + + assert!(matches!( + descriptor.validate_outer(&[0_u8; 32], &[9_u8; 32], 1, None), + Err(HsDescError::DiscoveryKeyMismatch) + )); + } + + #[test] + fn descriptor_rejects_tampered_signature() { + let (_, _, descriptor) = descriptor(); + let mut encoded = descriptor.to_bytes().unwrap(); + let last = encoded.last_mut().unwrap(); + *last ^= 0x80; + let decoded = HiddenServiceDescriptor::from_bytes(&encoded).unwrap(); + + assert!(matches!( + decoded.outer().validate_signature(), + Err(HsDescError::InvalidSignature) + )); + } + + #[test] + fn encrypted_blob_rejects_bad_mac() { + let (hs_id, _, descriptor) = descriptor(); + let mut blob = descriptor.outer().inner_layer.clone(); + blob.mac[0] ^= 1; + + assert!(matches!( + blob.decrypt( + &hs_id, + &descriptor.outer().desc_key_cert.signing_key, + descriptor.outer().revision_counter, + ), + Err(HsDescError::InvalidMac) + )); + } +} diff --git a/rust_reference/src/hs_id/mod.rs b/rust_reference/src/hs_id/mod.rs new file mode 100644 index 0000000..d74930e --- /dev/null +++ b/rust_reference/src/hs_id/mod.rs @@ -0,0 +1,397 @@ +use crate::ed25519_keyblind::{ed25519, key_blinding}; +use crate::error::{BlindingError, HsIdParseError}; +use crate::time::HSEpoch; +use digest::Digest; +use itertools::{Itertools, chain}; +use sha2::Sha256; +use std::fmt; +use std::hash::{Hash, Hasher}; +use std::str::FromStr; + +/// The fixed string `.mix` +pub const HSID_MIX_SUFFIX: &str = ".mix"; + +/// The identity of a hidden service. (`hs_id`) +/// +/// This is the decoded and validated ed25519 public key that is encoded as a +/// `${base32}.mix` address. When expanded, it is a public key whose +/// corresponding secret key is controlled by the hidden service. +/// +/// Note: This is a separate type from [`HsIdKey`] because it is about 6x +/// smaller. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct HsId([u8; 32]); + +#[derive(Copy, Clone, Eq, PartialEq)] +pub struct HsIdKey(ed25519::PublicKey); + +pub struct HsIdKeypair(ed25519::ExpandedKeypair); + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct HsBlindIdKey(ed25519::PublicKey); + +pub struct HsBlindIdKeypair(ed25519::ExpandedKeypair); + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct DiscoveryKey([u8; 32]); + +impl HsId { + /// Construct an [`HsId`] from a valid Ed25519 public key. + pub fn from_public_key(public_key: ed25519::PublicKey) -> Self { + Self(public_key.to_bytes()) + } + + /// Construct an [`HsId`] from its raw public key bytes. + pub fn from_bytes(bytes: [u8; 32]) -> Result { + ed25519::PublicKey::from_bytes(&bytes).map(Self::from_public_key) + } + + /// Return a reference to the raw public key bytes. + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } + + /// Return the raw public key bytes. + pub fn to_bytes(self) -> [u8; 32] { + self.0 + } + + /// Calculates CHECKSUM + fn onion_checksum(&self) -> [u8; 2] { + let mut h = Sha256::new(); + h.update(b"hidden-service-checksum"); + h.update(self.0.as_ref()); + h.finalize()[..2] + .try_into() + .expect("slice of fixed size wasn't that size") + } +} + +/// Compute the 32-byte "blinding factor" used to compute blinded public +/// (and secret) keys. +/// +/// Returns the value `h = H(...)`, before clamping. +fn blinding_factor(pk: &HsIdKey, cur_period: HSEpoch) -> [u8; 32] { + // We generate our key blinding factor as + // h = H(BLIND_STRING | hs_id | epoch) + // Where: + // H is SHA-256. + // hs_id is this public key `HsIdKey`. + // BLIND_STRING = "key blinding" + // epoch = u64_be(interval_num) || u64_be(length). + + /// String used as part of input to blinding hash. + const BLIND_STRING: &[u8] = b"key blinding"; + + let mut h = Sha256::new(); + h.update(BLIND_STRING); + h.update(pk.0.as_bytes()); + h.update(cur_period.to_bytes()); + + h.finalize().into() +} + +/// Given a blinded public key, public entropy, and replica number, compute the discovery key. +pub fn compute_discovery_key( + blinded_key: &HsBlindIdKey, + entropy: &[u8; 32], + replica_num: u8, +) -> DiscoveryKey { + // discovery_key = H("discovery key" | blinded-public-key | entropy | u64_be(replica_num)). + let mut h = Sha256::new(); + h.update(b"discovery key"); + h.update(blinded_key.as_bytes()); + h.update(entropy); + h.update(u64::from(replica_num).to_be_bytes()); + DiscoveryKey(h.finalize().into()) +} + +impl fmt::Display for HsId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let checksum = self.onion_checksum(); + let binary = chain!(self.0.as_ref(), &checksum,).cloned().collect_vec(); + let mut b32 = data_encoding::BASE32_NOPAD.encode(&binary); + b32.make_ascii_lowercase(); + write!(f, "{}{}", b32, HSID_MIX_SUFFIX) + } +} + +impl HsIdKey { + /// Construct an [`HsIdKey`] from a valid Ed25519 public key. + pub fn from_public_key(public_key: ed25519::PublicKey) -> Self { + Self(public_key) + } + + /// Return a reference to the byte representation of this key. + pub fn as_bytes(&self) -> &[u8; 32] { + self.0.as_bytes() + } + + /// Return the byte representation of this key. + pub fn to_bytes(&self) -> [u8; 32] { + self.0.to_bytes() + } + + /// Return a representation of this key as an [`HsId`]. + /// + /// ([`HsId`] is much smaller, and easier to store.) + pub fn id(&self) -> HsId { + HsId(self.0.to_bytes().into()) + } + + /// Derive the blinded key for this HS identity during `cur_period`. + pub fn compute_blinded_key(&self, cur_period: HSEpoch) -> Result { + let r = blinding_factor(self, cur_period); + + let blinded_key = HsBlindIdKey::from(key_blinding::blind_pubkey(&self.0, r)?); + + Ok(blinded_key) + } + + /// Derive the blinded key and discovery key for this HS identity during `cur_period`. + pub fn compute_blinded_and_discovery_key( + &self, + cur_period: HSEpoch, + entropy: &[u8; 32], + replica_num: u8, + ) -> Result<(HsBlindIdKey, DiscoveryKey), BlindingError> { + let r = blinding_factor(self, cur_period); + + let blinded_key = HsBlindIdKey::from(key_blinding::blind_pubkey(&self.0, r)?); + + let discovery_key = compute_discovery_key(&blinded_key, entropy, replica_num); + + Ok((blinded_key, discovery_key)) + } +} + +impl TryFrom for HsIdKey { + type Error = signature::Error; + + fn try_from(value: HsId) -> Result { + ed25519::PublicKey::from_bytes(&value.0).map(HsIdKey) + } +} + +impl HsBlindIdKey { + /// Return a reference to the byte representation of this blinded key. + pub fn as_bytes(&self) -> &[u8; 32] { + self.0.as_bytes() + } + + /// Return the byte representation of this blinded key. + pub fn to_bytes(&self) -> [u8; 32] { + self.0.to_bytes() + } + + /// Return the underlying Ed25519 public key. + pub fn public_key(&self) -> ed25519::PublicKey { + self.0 + } +} + +impl HsBlindIdKeypair { + /// Return the public part of this blinded keypair. + pub fn public(&self) -> HsBlindIdKey { + HsBlindIdKey(*self.0.public()) + } + + /// Sign a message using this blinded keypair. + pub fn sign(&self, message: &[u8]) -> ed25519::Signature { + self.0.sign(message) + } + + /// Return the underlying Ed25519 public key. + pub fn public_key(&self) -> ed25519::PublicKey { + *self.0.public() + } +} + +impl DiscoveryKey { + /// Return a reference to the byte representation of this discovery key. + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } + + /// Return the byte representation of this discovery key. + pub fn to_bytes(&self) -> [u8; 32] { + self.0 + } +} + +impl From for HsBlindIdKey { + fn from(value: ed25519::PublicKey) -> Self { + Self(value) + } +} + +impl From for HsId { + fn from(value: ed25519::PublicKey) -> Self { + Self::from_public_key(value) + } +} + +impl From for HsIdKey { + fn from(value: ed25519::PublicKey) -> Self { + Self::from_public_key(value) + } +} + +impl Hash for HsIdKey { + fn hash(&self, state: &mut H) { + self.0.as_bytes().hash(state); + } +} + +impl Hash for HsBlindIdKey { + fn hash(&self, state: &mut H) { + self.0.as_bytes().hash(state); + } +} +impl From for HsId { + fn from(value: HsIdKey) -> Self { + value.id() + } +} + +impl From<&HsIdKeypair> for HsIdKey { + fn from(value: &HsIdKeypair) -> Self { + Self(*value.0.public()) + } +} + +impl From for HsIdKey { + fn from(value: HsIdKeypair) -> Self { + Self(*value.0.public()) + } +} + +impl From<&HsBlindIdKeypair> for HsBlindIdKey { + fn from(value: &HsBlindIdKeypair) -> Self { + Self(*value.0.public()) + } +} + +impl From for HsBlindIdKey { + fn from(value: HsBlindIdKeypair) -> Self { + Self(*value.0.public()) + } +} + +impl From<&ed25519::Keypair> for HsIdKeypair { + fn from(value: &ed25519::Keypair) -> Self { + Self(ed25519::ExpandedKeypair::from(value)) + } +} + +impl From for HsIdKeypair { + fn from(value: ed25519::ExpandedKeypair) -> Self { + Self(value) + } +} + +impl FromStr for HsId { + type Err = HsIdParseError; + + fn from_str(value: &str) -> Result { + let encoded = value + .strip_suffix(HSID_MIX_SUFFIX) + .ok_or(HsIdParseError::NotMixDomain)?; + let decoded = + data_encoding::BASE32_NOPAD.decode(encoded.to_ascii_uppercase().as_bytes())?; + let decoded: [u8; 34] = decoded + .try_into() + .map_err(|_| HsIdParseError::InvalidLength)?; + + let hs_id_bytes = decoded[0..32].try_into().expect("fixed slice"); + let checksum: [u8; 2] = decoded[32..34].try_into().expect("fixed slice"); + let hs_id = HsId::from_bytes(hs_id_bytes).map_err(|_| HsIdParseError::InvalidPublicKey)?; + + if hs_id.onion_checksum() != checksum { + return Err(HsIdParseError::WrongChecksum); + } + + Ok(hs_id) + } +} + +impl HsIdKeypair { + /// Derive the blinded key for this identity during `cur_period`. + pub fn compute_blinded_keypair( + &self, + cur_period: HSEpoch, + ) -> Result { + let public_key = HsIdKey::from(self); + let r = blinding_factor(&public_key, cur_period); + + let blinded_keypair = HsBlindIdKeypair(key_blinding::blind_keypair(&self.0, r)?); + + Ok(blinded_keypair) + } + + /// Derive the blinded key for this identity during `cur_period`. + pub fn compute_blinded_keypair_and_discovery_key( + &self, + cur_period: HSEpoch, + entropy: &[u8; 32], + replica_num: u8, + ) -> Result<(HsBlindIdKeypair, DiscoveryKey), BlindingError> { + let public_key = HsIdKey::from(self); + let r = blinding_factor(&public_key, cur_period); + + let blinded_keypair = HsBlindIdKeypair(key_blinding::blind_keypair(&self.0, r)?); + + let blinded_public_key = blinded_keypair.public(); + let discovery_key = compute_discovery_key(&blinded_public_key, entropy, replica_num); + + Ok((blinded_keypair, discovery_key)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_hs_id() -> HsId { + let keypair = ed25519::Keypair::from_bytes(&[42_u8; 32]); + HsId::from_public_key(keypair.verifying_key()) + } + + #[test] + fn mix_address_roundtrip() { + let hs_id = test_hs_id(); + let address = hs_id.to_string(); + + assert!(address.ends_with(HSID_MIX_SUFFIX)); + assert_eq!(address.parse::().unwrap(), hs_id); + assert_eq!(HsId::from_bytes(hs_id.to_bytes()).unwrap(), hs_id); + } + + #[test] + fn rejects_missing_mix_suffix() { + let address = test_hs_id().to_string().replace(HSID_MIX_SUFFIX, ""); + + assert!(matches!( + address.parse::(), + Err(HsIdParseError::NotMixDomain) + )); + } + + #[test] + fn rejects_bad_checksum() { + let address = test_hs_id().to_string(); + let encoded = address.strip_suffix(HSID_MIX_SUFFIX).unwrap(); + let mut decoded = data_encoding::BASE32_NOPAD + .decode(encoded.to_ascii_uppercase().as_bytes()) + .unwrap(); + decoded[33] ^= 1; + let mut tampered = data_encoding::BASE32_NOPAD.encode(&decoded); + tampered.make_ascii_lowercase(); + tampered.push_str(HSID_MIX_SUFFIX); + + assert!(matches!( + tampered.parse::(), + Err(HsIdParseError::WrongChecksum) + )); + } +} diff --git a/rust_reference/src/lib.rs b/rust_reference/src/lib.rs new file mode 100644 index 0000000..5886219 --- /dev/null +++ b/rust_reference/src/lib.rs @@ -0,0 +1,6 @@ +pub mod cert; +pub mod ed25519_keyblind; +pub mod error; +pub mod hs_desc; +pub mod hs_id; +pub mod time; diff --git a/rust_reference/src/time/mod.rs b/rust_reference/src/time/mod.rs new file mode 100644 index 0000000..5c9af89 --- /dev/null +++ b/rust_reference/src/time/mod.rs @@ -0,0 +1,267 @@ +//! Manipulate time in hidden services +//! +use crate::error::TimePeriodError; +use humantime::format_rfc3339_seconds; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; +use std::time::{Duration, SystemTime}; + +/// A period of time (epoch), as used in the hidden service. +/// +/// A `HSEpoch` is defined as a duration (in seconds), and the number of such +/// durations that have elapsed since the Unix epoch. So +/// for example, the interval "(86400 seconds length, 15 intervals)", covers `1970-01-16T00:00:00` up to but not including +/// `1970-01-17T00:00:00`. +/// +/// These time periods are used to derive a different blinded key during +/// each period from each hidden service identifier. +#[derive(Deserialize, Serialize, Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct HSEpoch { + /// Index of the time periods that have passed since the unix epoch. + pub(crate) interval_num: u64, + /// The length of a time period, in **seconds**. + pub(crate) length: u64, +} + +/// Two [`HSEpoch`]s are ordered with respect to one another if they have the +/// same interval length and offset. +impl PartialOrd for HSEpoch { + fn partial_cmp(&self, other: &Self) -> Option { + if self.length == other.length { + Some(self.interval_num.cmp(&other.interval_num)) + } else { + None + } + } +} + +impl Display for HSEpoch { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "#{} ", self.interval_num())?; + match self.range() { + Ok(r) => { + let sec = self.length; + write!(f, "{}..+{}", format_rfc3339_seconds(r.start), sec,) + } + Err(_) => write!(f, "overflow! {self:?}"), + } + } +} + +impl HSEpoch { + /// Construct an `HSEpoch` of a given `length` that contains `when`. + /// + /// The `length` value is rounded down to the nearest second + /// + /// Return None if the Duration is too large or too small, or if `when` + /// cannot be represented as a time period. + pub fn new(length: Duration, when: SystemTime) -> Result { + let length_in_sec = + u32::try_from(length.as_secs()).map_err(|_| TimePeriodError::IntervalInvalid)?; + if length_in_sec == 0 || length.subsec_nanos() != 0 { + return Err(TimePeriodError::IntervalInvalid); + } + let interval_num = when + .duration_since(SystemTime::UNIX_EPOCH) + .map_err(|_| TimePeriodError::OutOfRange)? + .as_secs() + / u64::from(length_in_sec); + Ok(HSEpoch { + interval_num, + length: u64::from(length_in_sec), + }) + } + + pub fn from_parts(length: u64, interval_num: u64) -> Self { + Self { + interval_num, + length, + } + } + + /// Serialize this epoch as `interval_num || length`, using big-endian u64s. + pub fn to_bytes(&self) -> [u8; 16] { + let mut out = [0_u8; 16]; + out[0..8].copy_from_slice(&self.interval_num.to_be_bytes()); + out[8..16].copy_from_slice(&self.length.to_be_bytes()); + out + } + + /// Deserialize an epoch produced by [`HSEpoch::to_bytes`]. + pub fn from_bytes(bytes: &[u8]) -> Result { + let bytes: &[u8; 16] = bytes + .try_into() + .map_err(|_| TimePeriodError::EncodingInvalid)?; + let interval_num = u64::from_be_bytes(bytes[0..8].try_into().expect("fixed slice")); + let length = u64::from_be_bytes(bytes[8..16].try_into().expect("fixed slice")); + Ok(Self::from_parts(length, interval_num)) + } + + /// Return the time period after this one. + /// + /// Return None if this is the last representable time period. + pub fn next(&self) -> Option { + Some(HSEpoch { + interval_num: self.interval_num.checked_add(1)?, + ..*self + }) + } + /// Return the time period before this one. + /// + /// Return None if this is the first representable time period. + pub fn prev(&self) -> Option { + Some(HSEpoch { + interval_num: self.interval_num.checked_sub(1)?, + ..*self + }) + } + /// Return true if this epoch contains `when`. + /// + /// # Limitations + /// + /// This function always returns false if the time period contains any times + /// that cannot be represented as a `SystemTime`. + pub fn contains(&self, when: SystemTime) -> bool { + match self.range() { + Ok(r) => r.contains(&when), + Err(_) => false, + } + } + /// Return a range representing the [`SystemTime`] values contained within + /// this time period. + /// + /// Return None if this time period contains any times that cannot be + /// represented as a `SystemTime`. + pub fn range(&self) -> Result, TimePeriodError> { + (|| { + let start_sec = self.length.checked_mul(self.interval_num)?; + let end_sec = start_sec.checked_add(self.length)?; + let start = SystemTime::UNIX_EPOCH.checked_add(Duration::from_secs(start_sec))?; + let end = SystemTime::UNIX_EPOCH.checked_add(Duration::from_secs(end_sec))?; + Some(start..end) + })() + .ok_or(TimePeriodError::OutOfRange) + } + + /// Return the numeric index of this time period. + /// + /// This function should only be used when encoding the time period for + /// cryptographic purposes. + pub fn interval_num(&self) -> u64 { + self.interval_num + } + + /// Return the length of this time period as a number of seconds. + /// + /// This function should only be used when encoding the time period for + /// cryptographic purposes. + pub fn length(&self) -> u64 { + self.length + } +} + +#[cfg(test)] +mod test { + use super::*; + use humantime::{parse_duration, parse_rfc3339}; + + fn assert_eq_from_parts(period: HSEpoch) { + assert_eq!( + period, + HSEpoch::from_parts(period.length(), period.interval_num()) + ); + } + + #[test] + fn check_testvec() { + let time = parse_rfc3339("2016-04-13T11:00:00Z").unwrap(); + let one_day = parse_duration("1day").unwrap(); + let period = HSEpoch::new(one_day, time).unwrap(); + assert_eq!(period.interval_num, 16904); + assert!(period.contains(time)); + assert_eq_from_parts(period); + + let same_period_time = parse_rfc3339("2016-04-13T23:59:59Z").unwrap(); + let period = HSEpoch::new(one_day, same_period_time).unwrap(); + assert_eq!(period.interval_num, 16904); // still the same. + assert!(period.contains(same_period_time)); + assert_eq_from_parts(period); + + assert_eq!(period.prev().unwrap().interval_num, 16903); + assert_eq!(period.next().unwrap().interval_num, 16905); + + let time2 = parse_rfc3339("2016-04-14T00:00:00Z").unwrap(); + let period2 = HSEpoch::new(one_day, time2).unwrap(); + assert_eq!(period2.interval_num, 16905); + assert!(period < period2); + assert!(period2 > period); + assert_eq!(period.next().unwrap(), period2); + assert_eq!(period2.prev().unwrap(), period); + assert!(period2.contains(time2)); + assert!(!period2.contains(time)); + assert!(!period.contains(time2)); + + assert_eq!( + period.range().unwrap(), + parse_rfc3339("2016-04-13T00:00:00Z").unwrap() + ..parse_rfc3339("2016-04-14T00:00:00Z").unwrap() + ); + assert_eq!( + period2.range().unwrap(), + parse_rfc3339("2016-04-14T00:00:00Z").unwrap() + ..parse_rfc3339("2016-04-15T00:00:00Z").unwrap() + ); + assert_eq_from_parts(period2); + } + + #[test] + fn range_uses_seconds() { + let period = HSEpoch::from_parts(90, 2); + + assert_eq!( + period.range().unwrap(), + parse_rfc3339("1970-01-01T00:03:00Z").unwrap() + ..parse_rfc3339("1970-01-01T00:04:30Z").unwrap() + ); + } + + #[test] + fn bytes_roundtrip() { + let period = HSEpoch::from_parts(86_400, 42); + let bytes = period.to_bytes(); + + assert_eq!(bytes.len(), 16); + assert_eq!(HSEpoch::from_bytes(&bytes).unwrap(), period); + assert!(matches!( + HSEpoch::from_bytes(&bytes[..15]), + Err(TimePeriodError::EncodingInvalid) + )); + } + + #[test] + fn range_reports_overflow() { + let period = HSEpoch::from_parts(2, u64::MAX); + + assert!(matches!(period.range(), Err(TimePeriodError::OutOfRange))); + assert!(!period.contains(SystemTime::UNIX_EPOCH)); + } + + #[test] + fn rejects_invalid_lengths() { + assert!(matches!( + HSEpoch::new(Duration::ZERO, SystemTime::UNIX_EPOCH), + Err(TimePeriodError::IntervalInvalid) + )); + assert!(matches!( + HSEpoch::new(Duration::new(1, 1), SystemTime::UNIX_EPOCH), + Err(TimePeriodError::IntervalInvalid) + )); + assert!(matches!( + HSEpoch::new( + Duration::from_secs(u64::from(u32::MAX) + 1), + SystemTime::UNIX_EPOCH + ), + Err(TimePeriodError::IntervalInvalid) + )); + } +} diff --git a/rust_reference/ed25519-keyblind/tests/compat.rs b/rust_reference/tests/compat.rs similarity index 82% rename from rust_reference/ed25519-keyblind/tests/compat.rs rename to rust_reference/tests/compat.rs index 054761b..766546b 100644 --- a/rust_reference/ed25519-keyblind/tests/compat.rs +++ b/rust_reference/tests/compat.rs @@ -2,10 +2,9 @@ use hex_literal::hex; -use ed25519_keyblind::{conversion, ed25519, key_blinding}; +use mix_hs::ed25519_keyblind::{conversion, ed25519, key_blinding}; use tor_llcrypto::pk as arti_pk; -use x25519_dalek::{StaticSecret as XStaticSecret, PublicKey as XPublicKey}; - +use x25519_dalek::{PublicKey as XPublicKey, StaticSecret as XStaticSecret}; #[test] fn keypair_bytes_and_signature_match_arti() { @@ -19,7 +18,10 @@ fn keypair_bytes_and_signature_match_arti() { let original = arti_pk::ed25519::ExpandedKeypair::from_secret_key_bytes(expanded_secret).unwrap(); - assert_eq!(extracted.to_secret_key_bytes(), original.to_secret_key_bytes()); + assert_eq!( + extracted.to_secret_key_bytes(), + original.to_secret_key_bytes() + ); assert_eq!(extracted.public().to_bytes(), original.public().to_bytes()); assert_eq!( extracted.sign(message).to_bytes(), @@ -40,8 +42,7 @@ fn curve25519_conversion_match_arti() { let extracted_pub = conversion::convert_curve25519_to_ed25519_public(&extracted_pk, 1).unwrap(); let original_pub = - arti_pk::keymanip::convert_curve25519_to_ed25519_public(&original_curve_pk, 1) - .unwrap(); + arti_pk::keymanip::convert_curve25519_to_ed25519_public(&original_curve_pk, 1).unwrap(); assert_eq!(extracted_pub.to_bytes(), original_pub.to_bytes()); let (extracted_ed, extracted_signbit) = @@ -54,25 +55,24 @@ fn curve25519_conversion_match_arti() { extracted_ed.to_secret_key_bytes(), original_ed.to_secret_key_bytes() ); - assert_eq!(extracted_ed.public().to_bytes(), original_ed.public().to_bytes()); + assert_eq!( + extracted_ed.public().to_bytes(), + original_ed.public().to_bytes() + ); } #[test] fn blinded_keypair_and_signature_match_arti() { let seed = hex!("67e3aa7a14fac8445d15e45e38a523481a69ae35513c9e4143eb1c2196729a0e"); - let blinding_param = - hex!("ac78a1d46faf3bfbbdc5af5f053dc6dc9023ed78236bec1760dadfd0b2603760"); + let blinding_param = hex!("ac78a1d46faf3bfbbdc5af5f053dc6dc9023ed78236bec1760dadfd0b2603760"); let message = b"bit-for-bit blinded Ed25519 compatibility"; let extracted_kp = ed25519::ExpandedKeypair::from(&ed25519::Keypair::from_bytes(&seed)); let original_kp = - arti_pk::ed25519::ExpandedKeypair::from(&arti_pk::ed25519::Keypair::from_bytes( - &seed, - )); + arti_pk::ed25519::ExpandedKeypair::from(&arti_pk::ed25519::Keypair::from_bytes(&seed)); let extracted_blinded = key_blinding::blind_keypair(&extracted_kp, blinding_param).unwrap(); - let original_blinded = - arti_pk::keymanip::blind_keypair(&original_kp, blinding_param).unwrap(); + let original_blinded = arti_pk::keymanip::blind_keypair(&original_kp, blinding_param).unwrap(); assert_eq!( extracted_blinded.to_secret_key_bytes(),