reference impl: time, hs_id, key_cert, descriptor.

This commit is contained in:
mghazwi 2026-06-01 15:42:45 +02:00
parent da1c6c5de7
commit ae908390b5
No known key found for this signature in database
GPG Key ID: 646E567CAD7DB607
21 changed files with 2438 additions and 74 deletions

View File

@ -2,4 +2,4 @@
resolver = "3"
members = ["rust_reference/ed25519-keyblind", "rust_reference/mix_address"]
members = ["rust_reference"]

View File

@ -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).

View File

@ -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"]}

67
rust_reference/README.md Normal file
View File

@ -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.

View File

@ -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).

View File

@ -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<ed25519::PublicKey> {
match self {
Self::Ed25519(key) => Some(key),
Self::X25519(_) => None,
}
}
pub fn as_x25519(self) -> Option<x25519::PublicKey> {
match self {
Self::Ed25519(_) => None,
Self::X25519(key) => Some(key),
}
}
fn from_type_and_bytes(key_type: u8, bytes: [u8; 32]) -> Result<Self, KeyCertError> {
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<ed25519::PublicKey> for CertifiedKey {
fn from(value: ed25519::PublicKey) -> Self {
Self::Ed25519(value)
}
}
impl From<x25519::PublicKey> 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<Self> {
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<CertifiedKey>,
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<CertifiedKey>,
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<F>(
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<u8> {
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<Self, KeyCertError> {
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<u8> {
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<u8> {
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<u8>, 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)
));
}
}

View File

@ -0,0 +1 @@
pub mod key_cert;

View File

@ -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<PublicKey> {
pub fn convert_curve25519_to_ed25519_public(pubkey: &XPublicKey, signbit: u8) -> Option<PublicKey> {
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
}
}

View File

@ -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<ExpandedKeypair> 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()
}
}
}

View File

@ -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<ed25519_dalek::SignatureError> 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<PublicKey, BlindingError> {
pub fn blind_pubkey(pk: &PublicKey, h: [u8; 32]) -> Result<PublicKey, BlindingError> {
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() {

View File

@ -1,4 +1,3 @@
pub mod conversion;
pub mod ed25519;
pub mod key_blinding;
pub mod conversion;

157
rust_reference/src/error.rs Normal file
View File

@ -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),
}

View File

@ -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<Aes128>;
type HmacSha256 = Hmac<Sha256>;
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<Vec<u8>, 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)
));
}
}

View File

@ -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<u8>,
/// Multiaddress bytes. This remains byte-oriented so this descriptor crate
/// does not need to depend on libp2p types.
pub multi_addr: Vec<u8>,
/// 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<IntroPoint>,
/// Optional DOS/spam-resistance parameters. The concrete format is left to
/// the transport/discovery integration.
pub dos_params: Vec<u8>,
}
impl MixPubInfo {
/// Create public mix routing information.
pub fn new(
peer_id: impl Into<Vec<u8>>,
multi_addr: impl Into<Vec<u8>>,
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<x25519::PublicKey> {
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<IntroPoint>, dos_params: Vec<u8>) -> Self {
Self {
intro_points,
dos_params,
}
}
/// Encode the inner layer in a canonical binary form.
pub fn to_bytes(&self) -> Result<Vec<u8>, 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<Self, HsDescError> {
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<u8>, 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<u8>, 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<u32, HsDescError> {
Ok(u32::from_be_bytes(take_array::<4>(input, name)?))
}
fn take_array<const N: usize>(
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();
}
}

View File

@ -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;

View File

@ -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<u8>,
/// 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<Self, HsDescError> {
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<Vec<u8>, HsDescError> {
let params = HsDescEncryption {
blinded_key,
hs_id,
revision: revision_counter,
};
params.decrypt(self)
}
fn encode(&self, out: &mut Vec<u8>) -> 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<Self, HsDescError> {
let salt = take_array::<KAPPA>(input, "encrypted blob salt")?;
let blob = take_field(input, "encrypted blob ciphertext")?.to_vec();
let mac = take_array::<KAPPA>(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<ed25519::PublicKey> {
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<Vec<u8>, 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<Self, HsDescError> {
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<ValidatedOuter, HsDescError> {
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,
&params.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(
&params.hs_id,
&params.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<Vec<u8>, 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<Self, HsDescError> {
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<SystemTime>,
) -> Result<ValidatedOuter, HsDescError> {
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<SystemTime>,
) -> Result<ValidatedOuter, HsDescError> {
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<SystemTime>,
) -> Result<ValidatedOuter, HsDescError> {
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<InnerLayer, HsDescError> {
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<SystemTime>,
) -> Result<InnerLayer, HsDescError> {
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<InnerLayer, HsDescError> {
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<SystemTime>,
) -> Result<InnerLayer, HsDescError> {
self.validate_decrypt_inner(hs_id.as_bytes(), epoch, public_entropy, replica_num, now)
}
}
fn encode_outer_unsigned_fields(
out: &mut Vec<u8>,
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<u8> {
signed_payload(OUTER_SIGNATURE_DOMAIN, OUTER_SIGNATURE_TYPE, unsigned_outer)
}
fn signed_payload(domain_separation: &str, payload_type: &[u8], payload: &[u8]) -> Vec<u8> {
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<u8>, field: &[u8]) {
out.extend_from_slice(&(field.len() as u64).to_be_bytes());
out.extend_from_slice(field);
}
fn push_len(out: &mut Vec<u8>, 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<u8>, 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<u8, HsDescError> {
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<u64, HsDescError> {
Ok(u64::from_be_bytes(take_array::<8>(input, name)?))
}
fn take_u32(input: &mut &[u8], name: &'static str) -> Result<u32, HsDescError> {
Ok(u32::from_be_bytes(take_array::<4>(input, name)?))
}
fn take_array<const N: usize>(
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)
));
}
}

View File

@ -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<Self, signature::Error> {
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<HsBlindIdKey, BlindingError> {
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<HsId> for HsIdKey {
type Error = signature::Error;
fn try_from(value: HsId) -> Result<Self, Self::Error> {
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<ed25519::PublicKey> for HsBlindIdKey {
fn from(value: ed25519::PublicKey) -> Self {
Self(value)
}
}
impl From<ed25519::PublicKey> for HsId {
fn from(value: ed25519::PublicKey) -> Self {
Self::from_public_key(value)
}
}
impl From<ed25519::PublicKey> for HsIdKey {
fn from(value: ed25519::PublicKey) -> Self {
Self::from_public_key(value)
}
}
impl Hash for HsIdKey {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.as_bytes().hash(state);
}
}
impl Hash for HsBlindIdKey {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.as_bytes().hash(state);
}
}
impl From<HsIdKey> 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<HsIdKeypair> 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<HsBlindIdKeypair> 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<ed25519::ExpandedKeypair> for HsIdKeypair {
fn from(value: ed25519::ExpandedKeypair) -> Self {
Self(value)
}
}
impl FromStr for HsId {
type Err = HsIdParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
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<HsBlindIdKeypair, 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)?);
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::<HsId>().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::<HsId>(),
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::<HsId>(),
Err(HsIdParseError::WrongChecksum)
));
}
}

View File

@ -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;

View File

@ -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<std::cmp::Ordering> {
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<Self, TimePeriodError> {
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<Self, TimePeriodError> {
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<Self> {
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<Self> {
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<std::ops::Range<SystemTime>, 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)
));
}
}

View File

@ -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(),