Skip to content

Commit

Permalink
[refactor] hyperledger-iroha#4174: Stop encoding X25519 keys as Ed25519
Browse files Browse the repository at this point in the history
Signed-off-by: Daniil Polyakov <[email protected]>
  • Loading branch information
Arjentix committed Feb 13, 2024
1 parent 39fb088 commit ee6d385
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 124 deletions.
Binary file modified configs/peer/executor.wasm
Binary file not shown.
39 changes: 30 additions & 9 deletions crypto/src/kex/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,22 @@
mod x25519;

#[cfg(not(feature = "std"))]
use alloc::vec::Vec;

pub use x25519::X25519Sha256;

use crate::{Error, KeyGenOption, PrivateKey, PublicKey, SessionKey};
use crate::{error::ParseError, KeyGenOption, SessionKey};

/// A Generic trait for key exchange schemes. Each scheme provides a way to generate keys and
/// do a diffie-hellman computation
pub trait KeyExchangeScheme {
/// Generate a new instance of the scheme
/// Public key used by the scheme.
type PublicKey: Send;
/// Private key used by the scheme.
type PrivateKey: Send;

/// Generate a new instance of the scheme.
fn new() -> Self;

/// Create new keypairs. If
Expand All @@ -21,20 +29,33 @@ pub trait KeyExchangeScheme {
/// then used to seed the [`ChaChaRng`](rand_chacha::ChaChaRng)
/// - `options` is [`FromPrivateKey`](KeyGenOption::FromPrivateKey), the corresponding public key is returned. This should be used for
/// static Diffie-Hellman and loading a long-term key.
fn keypair(&self, options: KeyGenOption) -> (PublicKey, PrivateKey);
fn keypair(
&self,
options: KeyGenOption<Self::PrivateKey>,
) -> (Self::PublicKey, Self::PrivateKey);

/// Compute the diffie-hellman shared secret.
/// `local_private_key` is the key generated from calling `keypair` while
/// `remote_public_key` is the key received from a different call to `keypair` from another party.
fn compute_shared_secret(
&self,
local_private_key: &Self::PrivateKey,
remote_public_key: &Self::PublicKey,
) -> SessionKey;

/// Get byte representation of a public key.
//
// TODO: Return `[u8; Self::PUBLIC_KEY_SIZE]` after https://github.com/rust-lang/rust/issues/76560
fn encode_public_key(pk: &Self::PublicKey) -> &[u8];

/// Decode public key from byte representation.
///
/// # Errors
///
/// Returns an error if the computation fails, i.e. remote key is invalid.
fn compute_shared_secret(
&self,
local_private_key: &PrivateKey,
remote_public_key: &PublicKey,
) -> Result<SessionKey, Error>;
/// Any error during key decoding, e.g. wrong `bytes` length.
//
// TODO: Accept `[u8; Self::PUBLIC_KEY_SIZE]` after https://github.com/rust-lang/rust/issues/76560
fn decode_public_key(bytes: Vec<u8>) -> Result<Self::PublicKey, ParseError>;

/// Size of the shared secret in bytes.
const SHARED_SECRET_SIZE: usize;
Expand Down
111 changes: 39 additions & 72 deletions crypto/src/kex/x25519.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#[cfg(not(feature = "std"))]
use alloc::{borrow::ToOwned as _, boxed::Box};
use core::borrow::Borrow as _;
use alloc::{format, vec::Vec};

use arrayref::array_ref;
use iroha_primitives::const_vec::ConstVec;
Expand All @@ -9,102 +8,75 @@ use rand::rngs::OsRng;
use rand::SeedableRng;
use rand_chacha::ChaChaRng;
use sha2::Digest;
use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret};
use x25519_dalek::{PublicKey, StaticSecret};
use zeroize::Zeroize;

use super::KeyExchangeScheme;
use crate::{Error, KeyGenOption, ParseError, PrivateKey, PublicKey, SessionKey};
use crate::{error::ParseError, KeyGenOption, SessionKey};

/// Implements the [`KeyExchangeScheme`] using X25519 key exchange and SHA256 hash function.
#[derive(Copy, Clone)]
pub struct X25519Sha256;

impl KeyExchangeScheme for X25519Sha256 {
type PublicKey = PublicKey;
type PrivateKey = StaticSecret;

fn new() -> Self {
Self
}

/// # Note about implementation
///
/// We encode the `X25519` public key as an [`Ed25519`](PublicKey::Ed25519) public key which is
/// a not so good idea, because we have to do extra computations and extra error handling.
///
/// See #4174 for more details.
fn keypair(&self, mut option: KeyGenOption) -> (PublicKey, PrivateKey) {
let (pk, sk) = match option {
fn keypair(
&self,
mut option: KeyGenOption<Self::PrivateKey>,
) -> (Self::PublicKey, Self::PrivateKey) {
match option {
#[cfg(feature = "rand")]
KeyGenOption::Random => {
let rng = OsRng;
let sk = StaticSecret::random_from_rng(rng);
let pk = X25519PublicKey::from(&sk);
let pk = PublicKey::from(&sk);
(pk, sk)
}
KeyGenOption::UseSeed(ref mut s) => {
let hash = sha2::Sha256::digest(s.as_slice());
s.zeroize();
let rng = ChaChaRng::from_seed(*array_ref!(hash.as_slice(), 0, 32));
let sk = StaticSecret::random_from_rng(rng);
let pk = X25519PublicKey::from(&sk);
let pk = PublicKey::from(&sk);
(pk, sk)
}
KeyGenOption::FromPrivateKey(ref s) => {
let crate::PrivateKeyInner::Ed25519(s) = s.0.borrow() else {
panic!("Wrong private key type, expected `Ed25519`, got {s:?}")
};
let sk = StaticSecret::from(*array_ref!(s.as_bytes(), 0, 32));
let pk = X25519PublicKey::from(&sk);
(pk, sk)
KeyGenOption::FromPrivateKey(ref sk) => {
let pk = PublicKey::from(sk);
(pk, sk.clone())
}
};

let montgomery = curve25519_dalek::MontgomeryPoint(pk.to_bytes());
// 0 here means the positive sign, but it doesn't matter, because in
// `compute_shared_secret()` we convert it back to Montgomery form losing the sign.
let edwards = montgomery
.to_edwards(0)
.expect("Montgomery to Edwards conversion failed");
let edwards_compressed = edwards.compress();

(
PublicKey(Box::new(crate::PublicKeyInner::Ed25519(
crate::ed25519::PublicKey::from_bytes(edwards_compressed.as_bytes()).expect(
"Ed25519 public key should be possible to create from X25519 public key",
),
))),
PrivateKey(Box::new(crate::PrivateKeyInner::Ed25519(
crate::ed25519::PrivateKey::from_bytes(sk.as_bytes()),
))),
)
}
}

fn compute_shared_secret(
&self,
local_private_key: &PrivateKey,
remote_public_key: &PublicKey,
) -> Result<SessionKey, Error> {
let crate::PrivateKeyInner::Ed25519(local_private_key) = local_private_key.0.borrow()
else {
panic!("Wrong private key type, expected `Ed25519`, got {local_private_key:?}")
};
let crate::PublicKeyInner::Ed25519(remote_public_key) = remote_public_key.0.borrow() else {
panic!("Wrong public key type, expected `Ed25519`, got {remote_public_key:?}")
};

local_private_key: &Self::PrivateKey,
remote_public_key: &Self::PublicKey,
) -> SessionKey {
let sk = StaticSecret::from(*local_private_key.as_bytes());

let pk_slice: &[u8; 32] = remote_public_key.as_bytes();
let edwards_compressed =
curve25519_dalek::edwards::CompressedEdwardsY::from_slice(pk_slice)
.expect("Ed25519 public key has 32 bytes");
let edwards = edwards_compressed.decompress().ok_or_else(|| {
ParseError("Invalid public key: failed to decompress edwards point".to_owned())
})?;
let montgomery = edwards.to_montgomery();
let pk = X25519PublicKey::from(montgomery.to_bytes());

let shared_secret = sk.diffie_hellman(&pk);
let shared_secret = sk.diffie_hellman(remote_public_key);
let hash = sha2::Sha256::digest(shared_secret.as_bytes());
Ok(SessionKey(ConstVec::new(hash.as_slice().to_vec())))
SessionKey(ConstVec::new(hash.as_slice().to_vec()))
}

fn encode_public_key(pk: &Self::PublicKey) -> &[u8] {
pk.as_bytes()
}

fn decode_public_key(bytes: Vec<u8>) -> Result<Self::PublicKey, ParseError> {
let bytes = <[u8; Self::PUBLIC_KEY_SIZE]>::try_from(bytes).map_err(|_| {
ParseError(format!(
"X25519 public key should be {} size long",
Self::PUBLIC_KEY_SIZE
))
})?;
Ok(PublicKey::from(bytes))
}

const SHARED_SECRET_SIZE: usize = 32;
Expand All @@ -122,16 +94,11 @@ mod tests {
let (public_key1, secret_key1) = scheme.keypair(KeyGenOption::Random);

let (public_key2, secret_key2) = scheme.keypair(KeyGenOption::Random);
let shared_secret1 = scheme
.compute_shared_secret(&secret_key2, &public_key1)
.unwrap();
let shared_secret2 = scheme
.compute_shared_secret(&secret_key1, &public_key2)
.unwrap();
let shared_secret1 = scheme.compute_shared_secret(&secret_key2, &public_key1);
let shared_secret2 = scheme.compute_shared_secret(&secret_key1, &public_key2);
assert_eq!(shared_secret1.payload(), shared_secret2.payload());

let (public_key2, _secret_key1) =
scheme.keypair(KeyGenOption::FromPrivateKey(Box::new(secret_key1)));
let (public_key2, _secret_key1) = scheme.keypair(KeyGenOption::FromPrivateKey(secret_key1));
assert_eq!(public_key2, public_key1);
}
}
8 changes: 4 additions & 4 deletions crypto/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,14 @@ impl FromStr for Algorithm {
/// Options for key generation
#[cfg(not(feature = "ffi_import"))]
#[derive(Debug, Clone)]
pub enum KeyGenOption {
pub enum KeyGenOption<K = PrivateKey> {
/// Use random number generator
#[cfg(feature = "rand")]
Random,
/// Use seed
UseSeed(Vec<u8>),
/// Derive from private key
FromPrivateKey(Box<PrivateKey>),
FromPrivateKey(K),
}

ffi::ffi_item! {
Expand Down Expand Up @@ -158,7 +158,7 @@ impl KeyGenConfiguration {

/// Construct using private key with [`Ed25519`](Algorithm::Ed25519) algorithm
#[must_use]
pub fn from_private_key(private_key: impl Into<Box<PrivateKey>>) -> Self {
pub fn from_private_key(private_key: impl Into<PrivateKey>) -> Self {
Self {
key_gen_option: KeyGenOption::FromPrivateKey(private_key.into()),
algorithm: Algorithm::default(),
Expand Down Expand Up @@ -546,7 +546,7 @@ impl FromStr for PublicKey {
impl From<PrivateKey> for PublicKey {
fn from(private_key: PrivateKey) -> Self {
let algorithm = private_key.algorithm();
let key_gen_option = KeyGenOption::FromPrivateKey(Box::new(private_key));
let key_gen_option = KeyGenOption::FromPrivateKey(private_key);

let inner = match algorithm {
Algorithm::Ed25519 => {
Expand Down
6 changes: 3 additions & 3 deletions crypto/src/signature/ed25519.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ mod test {
#[test]
fn ed25519_load_keys() {
let secret = PrivateKey::from_hex(Algorithm::Ed25519, PRIVATE_KEY).unwrap();
let (p1, s1) = Ed25519Sha512::keypair(KeyGenOption::FromPrivateKey(Box::new(secret)));
let (p1, s1) = Ed25519Sha512::keypair(KeyGenOption::FromPrivateKey(secret));

assert_eq!(
PrivateKey(Box::new(crate::PrivateKeyInner::Ed25519(s1))),
Expand All @@ -108,7 +108,7 @@ mod test {
#[test]
fn ed25519_verify() {
let secret = PrivateKey::from_hex(Algorithm::Ed25519, PRIVATE_KEY).unwrap();
let (p, _) = Ed25519Sha512::keypair(KeyGenOption::FromPrivateKey(Box::new(secret)));
let (p, _) = Ed25519Sha512::keypair(KeyGenOption::FromPrivateKey(secret));

Ed25519Sha512::verify(MESSAGE_1, hex::decode(SIGNATURE_1).unwrap().as_slice(), &p).unwrap();

Expand All @@ -129,7 +129,7 @@ mod test {
#[test]
fn ed25519_sign() {
let secret = PrivateKey::from_hex(Algorithm::Ed25519, PRIVATE_KEY).unwrap();
let (p, s) = Ed25519Sha512::keypair(KeyGenOption::FromPrivateKey(Box::new(secret)));
let (p, s) = Ed25519Sha512::keypair(KeyGenOption::FromPrivateKey(secret));

let sig = Ed25519Sha512::sign(MESSAGE_1, &s);
Ed25519Sha512::verify(MESSAGE_1, &sig, &p).unwrap();
Expand Down
8 changes: 4 additions & 4 deletions crypto/src/signature/secp256k1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,9 @@ mod test {
#[test]
fn secp256k1_compatibility() {
let secret = private_key();
let (p, s) = EcdsaSecp256k1Sha256::keypair(KeyGenOption::FromPrivateKey(Box::new(
let (p, s) = EcdsaSecp256k1Sha256::keypair(KeyGenOption::FromPrivateKey(
crate::PrivateKey(Box::new(crate::PrivateKeyInner::Secp256k1(secret))),
)));
));

let _sk = secp256k1::SecretKey::from_slice(&s.to_bytes()).unwrap();
let _pk = secp256k1::PublicKey::from_slice(&p.to_sec1_bytes()).unwrap();
Expand Down Expand Up @@ -206,9 +206,9 @@ mod test {
#[test]
fn secp256k1_sign() {
let secret = private_key();
let (pk, sk) = EcdsaSecp256k1Sha256::keypair(KeyGenOption::FromPrivateKey(Box::new(
let (pk, sk) = EcdsaSecp256k1Sha256::keypair(KeyGenOption::FromPrivateKey(
crate::PrivateKey(Box::new(crate::PrivateKeyInner::Secp256k1(secret))),
)));
));

let sig = EcdsaSecp256k1Sha256::sign(MESSAGE_1, &sk);
EcdsaSecp256k1Sha256::verify(MESSAGE_1, &sig, &pk).unwrap();
Expand Down
Loading

0 comments on commit ee6d385

Please sign in to comment.