mirror of
https://github.com/logos-storage/mix-hidden-services.git
synced 2026-06-13 09:09:25 +00:00
reference impl for ed25519 with key blinding.
This commit is contained in:
commit
da1c6c5de7
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
#IDE Related
|
||||
.idea
|
||||
|
||||
# Cargo build
|
||||
/target
|
||||
Cargo.lock
|
||||
/output
|
||||
|
||||
# Profile-guided optimization
|
||||
/tmp
|
||||
pgo-data.profdata
|
||||
|
||||
# MacOS nuisances
|
||||
.DS_Store
|
||||
5
Cargo.toml
Normal file
5
Cargo.toml
Normal file
@ -0,0 +1,5 @@
|
||||
[workspace]
|
||||
|
||||
resolver = "3"
|
||||
|
||||
members = ["rust_reference/ed25519-keyblind", "rust_reference/mix_address"]
|
||||
6
LICENSE.md
Normal file
6
LICENSE.md
Normal file
@ -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.
|
||||
24
README.md
Normal file
24
README.md
Normal file
@ -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.
|
||||
14
rust_reference/ed25519-keyblind/.gitignore
vendored
Normal file
14
rust_reference/ed25519-keyblind/.gitignore
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
#IDE Related
|
||||
.idea
|
||||
|
||||
# Cargo build
|
||||
/target
|
||||
Cargo.lock
|
||||
/output
|
||||
|
||||
# Profile-guided optimization
|
||||
/tmp
|
||||
pgo-data.profdata
|
||||
|
||||
# MacOS nuisances
|
||||
.DS_Store
|
||||
27
rust_reference/ed25519-keyblind/Cargo.toml
Normal file
27
rust_reference/ed25519-keyblind/Cargo.toml
Normal file
@ -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"]}
|
||||
|
||||
8
rust_reference/ed25519-keyblind/README.md
Normal file
8
rust_reference/ed25519-keyblind/README.md
Normal file
@ -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).
|
||||
119
rust_reference/ed25519-keyblind/src/conversion.rs
Normal file
119
rust_reference/ed25519-keyblind/src/conversion.rs
Normal file
@ -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<PublicKey> {
|
||||
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
|
||||
}
|
||||
249
rust_reference/ed25519-keyblind/src/ed25519.rs
Normal file
249
rust_reference/ed25519-keyblind/src/ed25519.rs
Normal file
@ -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<PublicKey> 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<Self, signature::Error> {
|
||||
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<R: rand_core::CryptoRng + ?Sized>(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<Self, Self::Error> {
|
||||
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::<Sha512>(
|
||||
&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<ExpandedKeypair> 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<u8>,
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
300
rust_reference/ed25519-keyblind/src/key_blinding.rs
Normal file
300
rust_reference/ed25519-keyblind/src/key_blinding.rs
Normal file
@ -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<ed25519_dalek::SignatureError> 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<PublicKey, BlindingError> {
|
||||
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<ExpandedKeypair, BlindingError> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
4
rust_reference/ed25519-keyblind/src/lib.rs
Normal file
4
rust_reference/ed25519-keyblind/src/lib.rs
Normal file
@ -0,0 +1,4 @@
|
||||
pub mod ed25519;
|
||||
pub mod key_blinding;
|
||||
pub mod conversion;
|
||||
|
||||
89
rust_reference/ed25519-keyblind/tests/compat.rs
Normal file
89
rust_reference/ed25519-keyblind/tests/compat.rs
Normal file
@ -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()
|
||||
);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user