commit da1c6c5de70e20afd069095253a8ece9b2fdff70 Author: mghazwi Date: Mon May 25 14:14:35 2026 +0200 reference impl for ed25519 with key blinding. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e282ab7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +#IDE Related +.idea + +# Cargo build +/target +Cargo.lock +/output + +# Profile-guided optimization +/tmp +pgo-data.profdata + +# MacOS nuisances +.DS_Store \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c6d5707 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,5 @@ +[workspace] + +resolver = "3" + +members = ["rust_reference/ed25519-keyblind", "rust_reference/mix_address"] diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..1185acd --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,6 @@ +All crates of this repo are licensed under either of + +* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) +* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7fd2d50 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +## Mix-Hidden-Services +This repository contains implementations +of the primitives needed for +the hidden services protocol over Mix as well as +early experimentation with the protocol. +The protocol requires the following: + +- [x] Ed25519 with key blinding +- [ ] Ed25519 Key certificates +- [ ] Hidden services identifiers +- [ ] Hidden services descriptors +- [ ] Introduction point protocol +- [ ] Bidirectional connection with surbs + +The reference implementations are in Rust and provided +in [rust_reference](./rust_reference). + +The Nim implementation to be used and integrated later are provided in [nim_impl](./nim_impl). + +#### Specs: +The full specification of mix-hidden-services protocol is [here](https://github.com/logos-co/logos-lips/pull/330) + +#### POC +The experiments above are intended to result in an early proof of concept (POC) that combines the individual sub-protocols into a working hidden services over Mix. \ No newline at end of file diff --git a/rust_reference/ed25519-keyblind/.gitignore b/rust_reference/ed25519-keyblind/.gitignore new file mode 100644 index 0000000..e282ab7 --- /dev/null +++ b/rust_reference/ed25519-keyblind/.gitignore @@ -0,0 +1,14 @@ +#IDE Related +.idea + +# Cargo build +/target +Cargo.lock +/output + +# Profile-guided optimization +/tmp +pgo-data.profdata + +# MacOS nuisances +.DS_Store \ No newline at end of file diff --git a/rust_reference/ed25519-keyblind/Cargo.toml b/rust_reference/ed25519-keyblind/Cargo.toml new file mode 100644 index 0000000..15f3991 --- /dev/null +++ b/rust_reference/ed25519-keyblind/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "ed25519-keyblind" +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"] +categories = ["cryptography"] + +[dependencies] +curve25519-dalek = "5.0.0-pre.6" +digest = "0.11" +ed25519-dalek = { version = "3.0.0-pre.7", features = ["batch", "hazmat"] } +rand_core = "0.10" +serde = "1.0.103" +sha2 = "0.11" +signature = "3" +thiserror = "2" +x25519-dalek = { version = "3.0.0-pre.6", features = ["static_secrets"] } +zeroize = "1.5" + +[dev-dependencies] +hex = "0.4" +hex-literal = "1.0" +tor-llcrypto = { version = "0.42.0", features = ["cvt-x25519", "hsv3-client", "hsv3-service"]} + diff --git a/rust_reference/ed25519-keyblind/README.md b/rust_reference/ed25519-keyblind/README.md new file mode 100644 index 0000000..f7477a8 --- /dev/null +++ b/rust_reference/ed25519-keyblind/README.md @@ -0,0 +1,8 @@ +# Ed25519-KeyBlind + +Ed25519 signature with key blinding. + +This crate wrap ed25519-dalek and adds Tor-specific +Ed25519 key blinding. + +This crate is based on Arti Tor client and compatible with its implementation, see [tests](testsompat.rs). diff --git a/rust_reference/ed25519-keyblind/src/conversion.rs b/rust_reference/ed25519-keyblind/src/conversion.rs new file mode 100644 index 0000000..4732e14 --- /dev/null +++ b/rust_reference/ed25519-keyblind/src/conversion.rs @@ -0,0 +1,119 @@ +//! Tor does some interesting and not-standard things with its +//! curve25519 and ed25519 keys +//! +//! In order to prove ownership of a curve25519 private key (i.e X25519), Tor +//! converts it into an ed25519 key, and then uses that ed25519 key to +//! sign its identity key. +//! 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 curve25519_dalek::montgomery::MontgomeryPoint; + + +/// 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 { + let point = MontgomeryPoint(*pubkey.as_bytes()); + let edpoint = point.to_edwards(signbit)?; + + // TODO: This is inefficient; we shouldn't have to re-compress + // this point to get the public key we wanted. But there's no way + // with the current API that I can to construct an ed25519 public + // key from a compressed point. + let compressed_y = edpoint.compress(); + PublicKey::from_bytes(compressed_y.as_bytes()).ok() +} + +/// Convert a curve25519 private key to an ed25519 private key (and +/// give a sign bit) to use with it, for use in key cross-certification. +/// +/// *NEVER* use these keys to sign inputs that may be generated by an +/// attacker. +/// +/// # Panics +/// +/// If the `debug_assertions` feature is enabled, this function will +/// double-check that the key it is about to return is the right +/// private key for the public key returned by +/// `convert_curve25519_to_ed25519_public`. +/// +/// This panic should be impossible unless there are implementation +/// bugs. +pub fn convert_curve25519_to_ed25519_private( + privkey: &XStaticSecret, +) -> Option<(ExpandedKeypair, u8)> { + let h = Sha512::new() + .chain_update(privkey.to_bytes()) + .chain_update(&b"Derive high part of ed25519 key from curve25519 key\0"[..]) + .finalize(); + + let mut bytes = Zeroizing::new([0_u8; 64]); + bytes[0..32].clone_from_slice(&privkey.to_bytes()); + bytes[32..64].clone_from_slice(&h[0..32]); + + let secret = ed25519_dalek::hazmat::ExpandedSecretKey::from_bytes(&bytes); + let public: PublicKey = PublicKey((&secret).into()); + let signbit = public.as_bytes()[31] >> 7; + + #[cfg(debug_assertions)] + { + let curve_pubkey1 = XPublicKey::from(privkey); + let ed_pubkey1 = convert_curve25519_to_ed25519_public(&curve_pubkey1, signbit)?; + assert_eq!(ed_pubkey1, public); + } + + Some((ExpandedKeypair { public, secret }, signbit)) +} + +/// Convert an ed25519 private key to a curve25519 private key. +/// +/// This creates a curve25519 key as described in section-5.1.5 of RFC8032: the bytes of the secret +/// part of `keypair` are hashed using SHA-512, and the result is clamped (the first 3 bits of the +/// first byte are cleared, the highest bit of the last byte is cleared, the second highest bit of +/// the last byte is set). +/// +/// Note: Using the same keypair for multiple purposes (such as key-exchange and signing) is +/// considered bad practice. +/// See [On using the same key pair for Ed25519 and an X25519 based +/// KEM](https://eprint.iacr.org/2021/509.pdf). +/// +/// It's important to note that converting a private key from ed25519 -> curve25519 -> ed25519 will +/// yield an [`ExpandedKeypair`] that is _not_ identical to the +/// expanded version of the original [`Keypair`](ed25519_dalek::SigningKey): the lower halves (the keys) of +/// the expanded key pairs will be the same, but their upper halves (the nonces) will be different. +/// +/// # Panics +/// +/// If the `debug_assertions` feature is enabled, this function will double-check that the key it +/// is about to return is clamped. +/// +/// 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 { + // Generate the key according to section-5.1.5 of rfc8032 + let h = Sha512::digest(keypair.to_bytes()); + + let mut bytes = Zeroizing::new([0_u8; 32]); + bytes.clone_from_slice(&h[0..32]); + + // Clamp the bytes. We do not necessarily have to do this, since + // x25519-dalek will handle clamping before it does any computation, but we + // want to make sure that the StaticSecret we generate is in the usual + // format. + let mut bytes = curve25519_dalek::scalar::clamp_integer(*bytes); + + let secret = XStaticSecret::from(bytes); + bytes.zeroize(); + + secret +} \ No newline at end of file diff --git a/rust_reference/ed25519-keyblind/src/ed25519.rs b/rust_reference/ed25519-keyblind/src/ed25519.rs new file mode 100644 index 0000000..c848c11 --- /dev/null +++ b/rust_reference/ed25519-keyblind/src/ed25519.rs @@ -0,0 +1,249 @@ +//! 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 _}; + +/// An Ed25519 signature. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Signature(pub(crate) ed25519_dalek::Signature); + +/// An Ed25519 keypair. +#[derive(Debug)] +pub struct Keypair(pub(crate) ed25519_dalek::SigningKey); + +/// An Ed25519 public key. +#[derive(Clone, Copy, Debug, Eq)] +pub struct PublicKey(pub(crate) ed25519_dalek::VerifyingKey); + +impl<'a> From<&'a Keypair> for PublicKey { + fn from(value: &'a Keypair) -> Self { + PublicKey((&value.0).into()) + } +} + +impl PartialEq for PublicKey { + fn eq(&self, other: &Self) -> bool { + self.as_bytes() == (other.as_bytes()) + } +} + +impl PublicKey { + /// Construct a public key from its byte representation. + pub fn from_bytes(bytes: &[u8; 32]) -> Result { + Ok(PublicKey(ed25519_dalek::VerifyingKey::from_bytes(bytes)?)) + } + /// Return a reference to the byte representation of this public key. + pub fn as_bytes(&self) -> &[u8; 32] { + self.0.as_bytes() + } + /// Return the byte representation of this public key. + pub fn to_bytes(&self) -> [u8; 32] { + self.0.to_bytes() + } + /// Verify a signature using this public key. + pub fn verify(&self, message: &[u8], signature: &Signature) -> Result<(), signature::Error> { + self.0.verify(message, &signature.0) + } +} +impl Keypair { + /// Generate a new random ed25519 keypair. + pub fn generate(csprng: &mut R) -> Self { + Self(ed25519_dalek::SigningKey::generate(csprng)) + } + /// Construct an ed25519 keypair from the byte representation of its secret key. + pub fn from_bytes(bytes: &[u8; 32]) -> Self { + Self(ed25519_dalek::SigningKey::from_bytes(bytes)) + } + /// Return a reference to the byte representation of the secret key in this keypair. + pub fn as_bytes(&self) -> &[u8; 32] { + self.0.as_bytes() + } + /// Return to the byte representation of the secret key in this keypair. + pub fn to_bytes(&self) -> [u8; 32] { + self.0.to_bytes() + } + /// Return the public key in this keypair. + pub fn verifying_key(&self) -> PublicKey { + PublicKey(*self.0.as_ref()) + } + /// Verify a signature generated with this keypair. + pub fn verify(&self, message: &[u8], signature: &Signature) -> Result<(), signature::Error> { + self.0.verify(message, &signature.0) + } + /// Sign a message using this keypair. + pub fn sign(&self, message: &[u8]) -> Signature { + Signature(self.0.sign(message)) + } +} +impl Signature { + /// Construct this signature from its byte representation. + pub fn from_bytes(bytes: &[u8; 64]) -> Self { + Self(ed25519_dalek::Signature::from_bytes(bytes)) + } + /// Return the byte representation of this signature. + pub fn to_bytes(&self) -> [u8; 64] { + self.0.to_bytes() + } +} +impl<'a> TryFrom<&'a [u8]> for PublicKey { + type Error = signature::Error; + + fn try_from(value: &'a [u8]) -> Result { + Ok(Self(ed25519_dalek::VerifyingKey::try_from(value)?)) + } +} +impl<'a> From<&'a [u8; 32]> for Keypair { + fn from(value: &'a [u8; 32]) -> Self { + Self(ed25519_dalek::SigningKey::from(value)) + } +} +impl From<[u8; 64]> for Signature { + fn from(value: [u8; 64]) -> Self { + Signature(value.into()) + } +} +impl<'a> From<&'a [u8; 64]> for Signature { + fn from(value: &'a [u8; 64]) -> Self { + Signature(value.into()) + } +} + + +/// A variant of [`Keypair`] containing an [`ExpandedSecretKey`]. +#[allow(clippy::exhaustive_structs)] +pub struct ExpandedKeypair { + /// The secret part of the key: {scalar, hash_prefix} + pub(crate) secret: ExpandedSecretKey, + /// The public part of this key. + pub(crate) public: PublicKey, +} + +impl ExpandedKeypair { + /// Return the public part of this expanded keypair. + pub fn public(&self) -> &PublicKey { + &self.public + } + + /// Compute a signature over a message using this keypair. + pub fn sign(&self, message: &[u8]) -> Signature { + use sha2::Sha512; + + Signature(ed25519_dalek::hazmat::raw_sign::( + &self.secret, + message, + &self.public.0, + )) + } + + /// Return a representation of the secret key in this keypair. + /// + /// (Since it is an expanded secret key, we represent it as its scalar part + /// followed by its hash_prefix.) + pub fn to_secret_key_bytes(&self) -> [u8; 64] { + let mut output = [0_u8; 64]; + output[0..32].copy_from_slice(&self.secret.scalar.to_bytes()); + output[32..64].copy_from_slice(&self.secret.hash_prefix); + output + } + + /// 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 hash_prefix = bytes[32..64].try_into().expect("wrong length on slice"); + let secret = ExpandedSecretKey { + scalar, + hash_prefix, + }; + let public = PublicKey((&secret).into()); + Self { secret, public } + } + + // NOTE: There is deliberately no constructor here that takes a (secret, + // public) pair. If there were, you could construct a pair with a + // mismatched public key. See issues with this [here](https://github.com/MystenLabs/ed25519-unsafe-libs). +} + +impl<'a> From<&'a Keypair> for ExpandedKeypair { + fn from(kp: &'a Keypair) -> ExpandedKeypair { + ExpandedKeypair { + secret: kp.as_bytes().into(), + public: kp.into(), + } + } +} + +impl From for PublicKey { + fn from(ekp: ExpandedKeypair) -> PublicKey { + ekp.public + } +} + + +/// An ed25519 signature, plus the document that it signs and its +/// public key. +#[derive(Clone, Debug)] +pub struct ValidatableEd25519Signature { + /// The key that allegedly produced the signature + key: PublicKey, + /// The alleged signature + sig: Signature, + /// The entire body of text that is allegedly signed here. + entire_text_of_signed_thing: Vec, +} + +impl ValidatableEd25519Signature { + /// Create a new ValidatableEd25519Signature + pub fn new(key: PublicKey, sig: Signature, text: &[u8]) -> Self { + ValidatableEd25519Signature { + key, + sig, + entire_text_of_signed_thing: text.into(), + } + } + + /// View the interior of this signature object. + pub(crate) fn as_parts(&self) -> (&PublicKey, &Signature, &[u8]) { + (&self.key, &self.sig, &self.entire_text_of_signed_thing[..]) + } + + /// Return a reference to the underlying Signature. + pub fn signature(&self) -> &Signature { + &self.sig + } + + fn is_valid(&self) -> bool { + self.key + .verify(&self.entire_text_of_signed_thing[..], &self.sig) + .is_ok() + } +} + +/// Perform a batch verification operation on the provided signatures +/// +/// Return `true` if _every_ signature is valid; otherwise return `false`. +pub fn validate_batch(sigs: &[&ValidatableEd25519Signature]) -> bool { + if sigs.is_empty() { + // ed25519_dalek has nonzero cost for a batch-verification of + // zero sigs. + true + } else if sigs.len() == 1 { + // Validating one signature in the traditional way is faster. + sigs[0].is_valid() + } else { + let mut ed_msgs = Vec::new(); + let mut ed_sigs = Vec::new(); + let mut ed_pks = Vec::new(); + for ed_sig in sigs { + let (pk, sig, msg) = ed_sig.as_parts(); + ed_sigs.push(sig.0); + ed_pks.push(pk.0); + ed_msgs.push(msg); + } + ed25519_dalek::verify_batch(&ed_msgs[..], &ed_sigs[..], &ed_pks[..]).is_ok() + } +} \ No newline at end of file diff --git a/rust_reference/ed25519-keyblind/src/key_blinding.rs b/rust_reference/ed25519-keyblind/src/key_blinding.rs new file mode 100644 index 0000000..b4174be --- /dev/null +++ b/rust_reference/ed25519-keyblind/src/key_blinding.rs @@ -0,0 +1,300 @@ +//! Key blinding functions for use with public keys. +//! +//! In Tor's v3 onion service design, Tor uses a _key blinding_ +//! algorithm to derive a publicly known Ed25519 key from a different +//! Ed25519 key used as the .onion address. This algorithm allows +//! directories to validate the signatures on onion service +//! 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 curve25519_dalek::scalar::Scalar; +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, +} + +/// Convert this dalek error to a BlindingError +impl From for BlindingError { + fn from(_: ed25519_dalek::SignatureError) -> BlindingError { + BlindingError::BlindingFailed + } +} + +/// Helper to clamp a random value (a hash digest `h`), so it can be used as a blinding factor. +/// clamp_integer: +/// h[0] &= 248; +/// h[31] &= 63; +/// h[31] |= 64; +/// Then interpret those bytes as a scalar modulo the curve group order. +fn clamp_blinding_factor(h: [u8; 32]) -> Scalar { + Scalar::from_bytes_mod_order(curve25519_dalek::scalar::clamp_integer(h)) +} + +/// Blind the ed25519 public key `pk` using the blinding factor +/// `h`, and return the blinded public key. +/// +/// `pk` is the public key, and +/// `h` is the hash digest `h = H(...)`, before clamping. +/// +/// Blinding is basically: +/// z = clamp(h) +/// pk' = z * pk +/// +/// # outputs: the blinded public key` pk'` +/// +/// Note that the approach used to clamp `h` to a scalar means +/// that different possible values for `h` may yield the same +/// output for a given `pk`. This and other limitations make this +/// function unsuitable for use outside the context of hidden services +/// +/// # Errors: +/// +/// This function can fail if the input is not actually a valid +/// Ed25519 public key. +fn blind_pubkey(pk: &PublicKey, h: [u8; 32]) -> Result { + use curve25519_dalek::edwards::CompressedEdwardsY; + + // clamp + let blinding_factor = clamp_blinding_factor(h); + + // Convert the public key to a point on the curve + let pubkey_point = CompressedEdwardsY(pk.to_bytes()) + .decompress() + .ok_or(BlindingError::BadPubkey)?; + + // Do the scalar multiplication and get a point back + let blinded_pubkey_point = (blinding_factor * pubkey_point).compress(); + // Turn the point back into bytes and return it + Ok(PublicKey::from_bytes(&blinded_pubkey_point.0)?) +} + +/// Blind the ed25519 key pair using the hash digest `h`, and +/// return the blinded ed25519 key pair. +/// +/// `h` is the hash digest `h = H(...)`, before clamping. +/// +/// key pair blinding: +/// z = clamp(h) +/// sk'.x = z * sk.x (scalar part of `ExpandedSecretKey`) +/// sk'.d = SHA512("key blinding nonce" || sk.d)[0..32] (hash prefix part of `ExpandedSecretKey`) +/// pk' = z * pk +/// +/// Note that the approach used to clamp `h` to a scalar means that +/// different possible values for `h` may yield the same output for a given +/// `pk`. This and other limitations make this function unsuitable for use +/// outside the context of hidden services +/// +/// # Errors +/// +/// This function can fail if the input is not actually a valid Ed25519 secret +/// key. +pub fn blind_keypair( + keypair: &ExpandedKeypair, + h: [u8; 32], +) -> Result { + use zeroize::Zeroizing; + + /// Fixed string used for blinding the original nonce. + /// Technically, any string would do, but this one is the Tor standard one. + const RH_BLIND_STRING: &[u8] = b"Derive temporary signing key hash input"; + + let blinding_factor = clamp_blinding_factor(h); + + let blinded_secret_scalar = keypair.secret.scalar * blinding_factor; + + // this is only 32 bytes + let blinded_secret_hash_prefix = { + let mut h = Sha512::new(); + h.update(RH_BLIND_STRING); + h.update(keypair.secret.hash_prefix); + let mut d = Zeroizing::new([0_u8; 64]); + h.finalize_into((&mut *d).into()); + d[0..32].try_into().expect("slice cast failed") + }; + + let secret = ExpandedSecretKey { + scalar: blinded_secret_scalar, + hash_prefix: blinded_secret_hash_prefix, + }; + let public = PublicKey(ed25519_dalek::VerifyingKey::from(&secret)); + + { + // Make sure that the public key that derives from our + // blinded key is the same as the key that we get when we re-blind the + // public key. + let public2 = blind_pubkey(keypair.public(), h)?; + assert_eq!(public, public2); + } + + Ok(ExpandedKeypair { secret, public }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ed25519::Keypair; + + #[test] + fn blinding() { + // Test the ed25519 blinding function. + // + // These test vectors are from Tor ed25519 implementation and related + // functions. These were automatically generated by the + // ed25519_exts_ref.py script in little-t-tor and they are also used by + // little-t-tor and onionbalance: + + let seckeys = vec![ + b"26c76712d89d906e6672dafa614c42e5cb1caac8c6568e4d2493087db51f0d36", + b"fba7a5366b5cb98c2667a18783f5cf8f4f8d1a2ce939ad22a6e685edde85128d", + b"67e3aa7a14fac8445d15e45e38a523481a69ae35513c9e4143eb1c2196729a0e", + b"d51385942033a76dc17f089a59e6a5a7fe80d9c526ae8ddd8c3a506b99d3d0a6", + b"5c8eac469bb3f1b85bc7cd893f52dc42a9ab66f1b02b5ce6a68e9b175d3bb433", + b"eda433d483059b6d1ff8b7cfbd0fe406bfb23722c8f3c8252629284573b61b86", + b"4377c40431c30883c5fbd9bc92ae48d1ed8a47b81d13806beac5351739b5533d", + b"c6bbcce615839756aed2cc78b1de13884dd3618f48367a17597a16c1cd7a290b", + b"c6bbcce615839756aed2cc78b1de13884dd3618f48367a17597a16c1cd7a290b", + b"c6bbcce615839756aed2cc78b1de13884dd3618f48367a17597a16c1cd7a290b", + ]; + let expanded_seckeys = vec![ + b"c0a4de23cc64392d85aa1da82b3defddbea946d13bb053bf8489fa9296281f495022f1f7ec0dcf52f07d4c7965c4eaed121d5d88d0a8ff546b06116a20e97755", + b"18a8a69a06790dac778e882f7e868baacfa12521a5c058f5194f3a729184514a2a656fe7799c3e41f43d756da8d9cd47a061316cfe6147e23ea2f90d1ca45f30", + b"58d84f8862d2ecfa30eb491a81c36d05b574310ea69dae18ecb57e992a896656b982187ee96c15bf4caeeab2d0b0ae4cd0b8d17470fc7efa98bb26428f4ef36d", + b"50702d20b3550c6e16033db5ad4fba16436f1ecc7485be6af62b0732ceb5d173c47ccd9d044b6ea99dd99256adcc9c62191be194e7cb1a5b58ddcec85d876a2b", + b"7077464c864c2ed5ed21c9916dc3b3ba6256f8b742fec67658d8d233dadc8d5a7a82c371083cc86892c2c8782dda2a09b6baf016aec51b689183ae59ce932ff2", + b"8883c1387a6c86fc0bd7b9f157b4e4cd83f6885bf55e2706d2235d4527a2f05311a3595953282e436df0349e1bb313a19b3ddbf7a7b91ecce8a2c34abadb38b3", + b"186791ac8d03a3ac8efed6ac360467edd5a3bed2d02b3be713ddd5be53b3287ee37436e5fd7ac43794394507ad440ecfdf59c4c255f19b768a273109e06d7d8e", + b"b003077c1e52a62308eef7950b2d532e1d4a7eea50ad22d8ac11b892851f1c40ffb9c9ff8dcd0c6c233f665a2e176324d92416bfcfcd1f787424c0c667452d86", + b"b003077c1e52a62308eef7950b2d532e1d4a7eea50ad22d8ac11b892851f1c40ffb9c9ff8dcd0c6c233f665a2e176324d92416bfcfcd1f787424c0c667452d86", + b"b003077c1e52a62308eef7950b2d532e1d4a7eea50ad22d8ac11b892851f1c40ffb9c9ff8dcd0c6c233f665a2e176324d92416bfcfcd1f787424c0c667452d86", + ]; + + let pubkeys = vec![ + b"c2247870536a192d142d056abefca68d6193158e7c1a59c1654c954eccaff894", + b"1519a3b15816a1aafab0b213892026ebf5c0dc232c58b21088d88cb90e9b940d", + b"081faa81992e360ea22c06af1aba096e7a73f1c665bc8b3e4e531c46455fd1dd", + b"73cfa1189a723aad7966137cbffa35140bb40d7e16eae4c40b79b5f0360dd65a", + b"66c1a77104d86461b6f98f73acf3cd229c80624495d2d74d6fda1e940080a96b", + b"d21c294db0e64cb2d8976625786ede1d9754186ae8197a64d72f68c792eecc19", + b"c4d58b4cf85a348ff3d410dd936fa460c4f18da962c01b1963792b9dcc8a6ea6", + b"95126f14d86494020665face03f2d42ee2b312a85bc729903eb17522954a1c4a", + b"95126f14d86494020665face03f2d42ee2b312a85bc729903eb17522954a1c4a", + b"95126f14d86494020665face03f2d42ee2b312a85bc729903eb17522954a1c4a", + ]; + let params = vec![ + "54a513898b471d1d448a2f3c55c1de2c0ef718c447b04497eeb999ed32027823", + "831e9b5325b5d31b7ae6197e9c7a7baf2ec361e08248bce055908971047a2347", + "ac78a1d46faf3bfbbdc5af5f053dc6dc9023ed78236bec1760dadfd0b2603760", + "f9c84dc0ac31571507993df94da1b3d28684a12ad14e67d0a068aba5c53019fc", + "b1fe79d1dec9bc108df69f6612c72812755751f21ecc5af99663b30be8b9081f", + "81f1512b63ab5fb5c1711a4ec83d379c420574aedffa8c3368e1c3989a3a0084", + "97f45142597c473a4b0e9a12d64561133ad9e1155fe5a9807fe6af8a93557818", + "3f44f6a5a92cde816635dfc12ade70539871078d2ff097278be2a555c9859cd0", + "0000000000000000000000000000000000000000000000000000000000000000", + "1111111111111111111111111111111111111111111111111111111111111111", + ]; + let blinded_pubkeys = vec![ + "1fc1fa4465bd9d4956fdbdc9d3acb3c7019bb8d5606b951c2e1dfe0b42eaeb41", + "1cbbd4a88ce8f165447f159d9f628ada18674158c4f7c5ead44ce8eb0fa6eb7e", + "c5419ad133ffde7e0ac882055d942f582054132b092de377d587435722deb028", + "3e08d0dc291066272e313014bfac4d39ad84aa93c038478a58011f431648105f", + "59381f06acb6bf1389ba305f70874eed3e0f2ab57cdb7bc69ed59a9b8899ff4d", + "2b946a484344eb1c17c89dd8b04196a84f3b7222c876a07a4cece85f676f87d9", + "c6b585129b135f8769df2eba987e76e089e80ba3a2a6729134d3b28008ac098e", + "0eefdc795b59cabbc194c6174e34ba9451e8355108520554ec285acabebb34ac", + "312404d06a0a9de489904b18d5233e83a50b225977fa8734f2c897a73c067952", + "952a908a4a9e0e5176a2549f8f328955aca6817a9fdc59e3acec5dec50838108", + ]; + let blinded_seckeys = vec![ + "293c3acff4e902f6f63ddc5d5caa2a57e771db4f24de65d4c28df3232f47fa01171d43f24e3f53e70ec7ac280044ac77d4942dee5d6807118a59bdf3ee647e89", + "38b88f9f9440358da544504ee152fb475528f7c51c285bd1c68b14ade8e29a07b8ceff20dfcf53eb52b891fc078c934efbf0353af7242e7dc51bb32a093afa29", + "4d03ce16a3f3249846aac9de0a0075061495c3b027248eeee47da4ddbaf9e0049217f52e92797462bd890fc274672e05c98f2c82970d640084781334aae0f940", + "51d7db01aaa0d937a9fd7c8c7381445a14d8fa61f43347af5460d7cd8fda9904509ecee77082ce088f7c19d5a00e955eeef8df6fa41686abc1030c2d76807733", + "1f76cab834e222bd2546efa7e073425680ab88df186ff41327d3e40770129b00b57b95a440570659a440a3e4771465022a8e67af86bdf2d0990c54e7bb87ff9a", + "c23588c23ee76093419d07b27c6df5922a03ac58f96c53671456a7d1bdbf560ec492fc87d5ec2a1b185ca5a40541fdef0b1e128fd5c2380c888bfa924711bcab", + "3ed249c6932d076e1a2f6916975914b14e8c739da00992358b8f37d3e790650691b4768f8e556d78f4bdcb9a13b6f6066fe81d3134ae965dc48cd0785b3af2b8", + "288cbfd923cb286d48c084555b5bdd06c05e92fb81acdb45271367f57515380e053d9c00c81e1331c06ab50087be8cfc7dc11691b132614474f1aa9c2503cccd", + "e5cd03eb4cc456e11bc36724b558873df0045729b22d8b748360067a7770ac02053d9c00c81e1331c06ab50087be8cfc7dc11691b132614474f1aa9c2503cccd", + "2cf7ed8b163f5af960d2fc62e1883aa422a6090736b4f18a5456ddcaf78ede0c053d9c00c81e1331c06ab50087be8cfc7dc11691b132614474f1aa9c2503cccd", + ]; + + for i in 0..pubkeys.len() { + let sk: [u8; 32] = hex::decode(seckeys[i]).unwrap().try_into().unwrap(); + let esk: ExpandedSecretKey = ExpandedSecretKey::from(&sk); + let kp = Keypair((&sk).into()); + + let esk_bytes_from_c_tor = hex::decode(expanded_seckeys[i]).unwrap(); + // Because of the differences in how we calculate the scalar, we + // don't get the same _representation_ of the scalar as we did with + // the C tor implementation. + // + // Therefore we have to do through this silliness to check our result. + let c_scalar = + Scalar::from_bytes_mod_order(esk_bytes_from_c_tor[0..32].try_into().unwrap()); + assert_eq!(c_scalar, esk.scalar); + assert_eq!( + hex::encode(esk.hash_prefix), + hex::encode(&esk_bytes_from_c_tor[32..]) + ); + + let public = PublicKey((&esk).into()); + let kp_in = ExpandedKeypair { + secret: esk, + public, + }; + + let pk = + PublicKey::from_bytes(&hex::decode(pubkeys[i]).unwrap()[..].try_into().unwrap()) + .unwrap(); + assert_eq!(pk, PublicKey((&kp.0).into())); + + let param = hex::decode(params[i]).unwrap().try_into().unwrap(); + // Blind the secret key, and make sure that the result is expected. + let blinded_kp = blind_keypair(&kp_in, param).unwrap(); + assert_eq!( + hex::encode(blinded_kp.to_secret_key_bytes()), + blinded_seckeys[i] + ); + + // Make sure that the secret key can be encoded and decoded. + { + let blinded_kp2 = + ExpandedKeypair::from_secret_key_bytes(blinded_kp.to_secret_key_bytes()); + assert_eq!(blinded_kp2.public, blinded_kp.public); + assert_eq!(blinded_kp2.secret.scalar, blinded_kp.secret.scalar); + assert_eq!( + blinded_kp2.secret.hash_prefix, + blinded_kp.secret.hash_prefix + ); + } + + let blinded_pk = blind_pubkey(&pk, param).unwrap(); + + // Make sure blinded pk is as expected. + assert_eq!(hex::encode(blinded_pk.to_bytes()), blinded_pubkeys[i]); + + // Make sure that signature made with blinded sk is validated by + // blinded pk. + let sig = blinded_kp.sign(b"hello world"); + blinded_pk.verify(b"hello world", &sig).unwrap(); + + let blinded_sk_scalar = blinded_kp.secret.scalar; + let pk2 = blinded_sk_scalar * curve25519_dalek::constants::ED25519_BASEPOINT_POINT; + let pk2 = pk2.compress(); + assert_eq!(pk2.as_bytes(), blinded_pk.as_bytes()); + } + } +} diff --git a/rust_reference/ed25519-keyblind/src/lib.rs b/rust_reference/ed25519-keyblind/src/lib.rs new file mode 100644 index 0000000..e1ca6fb --- /dev/null +++ b/rust_reference/ed25519-keyblind/src/lib.rs @@ -0,0 +1,4 @@ +pub mod ed25519; +pub mod key_blinding; +pub mod conversion; + diff --git a/rust_reference/ed25519-keyblind/tests/compat.rs b/rust_reference/ed25519-keyblind/tests/compat.rs new file mode 100644 index 0000000..054761b --- /dev/null +++ b/rust_reference/ed25519-keyblind/tests/compat.rs @@ -0,0 +1,89 @@ +//! compatibility checks against the Tor client Arti original `tor-llcrypto` code. + +use hex_literal::hex; + +use ed25519_keyblind::{conversion, ed25519, key_blinding}; +use tor_llcrypto::pk as arti_pk; +use x25519_dalek::{StaticSecret as XStaticSecret, PublicKey as XPublicKey}; + + +#[test] +fn keypair_bytes_and_signature_match_arti() { + let expanded_secret = hex!( + "c0a4de23cc64392d85aa1da82b3defddbea946d13bb053bf8489fa9296281f49" + "5022f1f7ec0dcf52f07d4c7965c4eaed121d5d88d0a8ff546b06116a20e97755" + ); + let message = b"bit-for-bit expanded Ed25519 compatibility"; + + let extracted = ed25519::ExpandedKeypair::from_secret_key_bytes(expanded_secret); + 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.public().to_bytes(), original.public().to_bytes()); + assert_eq!( + extracted.sign(message).to_bytes(), + original.sign(message).to_bytes() + ); +} + +#[test] +fn curve25519_conversion_match_arti() { + let curve_secret = hex!("5c8eac469bb3f1b85bc7cd893f52dc42a9ab66f1b02b5ce6a68e9b175d3bb433"); + + let extracted_sk = XStaticSecret::from(curve_secret); + let original_sk = arti_pk::curve25519::StaticSecret::from(curve_secret); + let extracted_pk = XPublicKey::from(&extracted_sk); + let original_curve_pk = arti_pk::curve25519::PublicKey::from(&original_sk); + + assert_eq!(extracted_pk.to_bytes(), original_curve_pk.to_bytes()); + + 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(); + assert_eq!(extracted_pub.to_bytes(), original_pub.to_bytes()); + + let (extracted_ed, extracted_signbit) = + conversion::convert_curve25519_to_ed25519_private(&extracted_sk).unwrap(); + let (original_ed, original_signbit) = + arti_pk::keymanip::convert_curve25519_to_ed25519_private(&original_sk).unwrap(); + + assert_eq!(extracted_signbit, original_signbit); + assert_eq!( + extracted_ed.to_secret_key_bytes(), + original_ed.to_secret_key_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 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, + )); + + 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(); + + assert_eq!( + extracted_blinded.to_secret_key_bytes(), + original_blinded.to_secret_key_bytes() + ); + assert_eq!( + extracted_blinded.public().to_bytes(), + original_blinded.public().to_bytes() + ); + assert_eq!( + extracted_blinded.sign(message).to_bytes(), + original_blinded.sign(message).to_bytes() + ); +}