mirror of
https://github.com/logos-storage/mix-hidden-services.git
synced 2026-06-13 17:19:37 +00:00
reference impl: time, hs_id, key_cert, descriptor.
This commit is contained in:
parent
da1c6c5de7
commit
ae908390b5
@ -2,4 +2,4 @@
|
||||
|
||||
resolver = "3"
|
||||
|
||||
members = ["rust_reference/ed25519-keyblind", "rust_reference/mix_address"]
|
||||
members = ["rust_reference"]
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -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
67
rust_reference/README.md
Normal 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.
|
||||
@ -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).
|
||||
369
rust_reference/src/cert/key_cert.rs
Normal file
369
rust_reference/src/cert/key_cert.rs
Normal 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)
|
||||
));
|
||||
}
|
||||
}
|
||||
1
rust_reference/src/cert/mod.rs
Normal file
1
rust_reference/src/cert/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod key_cert;
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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() {
|
||||
@ -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
157
rust_reference/src/error.rs
Normal 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),
|
||||
}
|
||||
136
rust_reference/src/hs_desc/desc_enc.rs
Normal file
136
rust_reference/src/hs_desc/desc_enc.rs
Normal 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)
|
||||
));
|
||||
}
|
||||
}
|
||||
295
rust_reference/src/hs_desc/inner.rs
Normal file
295
rust_reference/src/hs_desc/inner.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
19
rust_reference/src/hs_desc/mod.rs
Normal file
19
rust_reference/src/hs_desc/mod.rs
Normal 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;
|
||||
674
rust_reference/src/hs_desc/outer.rs
Normal file
674
rust_reference/src/hs_desc/outer.rs
Normal 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,
|
||||
¶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<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)
|
||||
));
|
||||
}
|
||||
}
|
||||
397
rust_reference/src/hs_id/mod.rs
Normal file
397
rust_reference/src/hs_id/mod.rs
Normal 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)
|
||||
));
|
||||
}
|
||||
}
|
||||
6
rust_reference/src/lib.rs
Normal file
6
rust_reference/src/lib.rs
Normal 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;
|
||||
267
rust_reference/src/time/mod.rs
Normal file
267
rust_reference/src/time/mod.rs
Normal 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)
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -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(),
|
||||
Loading…
x
Reference in New Issue
Block a user