Skip to content

Commit

Permalink
ed: Add back SigningKey::to_scalar_bytes (#599)
Browse files Browse the repository at this point in the history
* Brought back SigningKey::to_scalar_bytes; added regression test

* Updated SigningKey::to_scalar docs and tests
  • Loading branch information
rozbb committed Nov 14, 2023
1 parent ac51ef6 commit 04f811a
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 33 deletions.
4 changes: 4 additions & 0 deletions ed25519-dalek/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

Entries are listed in reverse chronological order per undeprecated major series.

# Unreleased

* Add `SigningKey::to_scalar_bytes` for getting the unclamped scalar from signing key

# 2.x series

## 2.0.0
Expand Down
1 change: 1 addition & 0 deletions ed25519-dalek/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ zeroize = { version = "1.5", default-features = false, optional = true }

[dev-dependencies]
curve25519-dalek = { version = "4", path = "../curve25519-dalek", default-features = false, features = ["digest", "rand_core"] }
x25519-dalek = { version = "2", path = "../x25519-dalek", default-features = false, features = ["static_secrets"] }
blake2 = "0.10"
sha3 = "0.10"
hex = "0.4"
Expand Down
41 changes: 37 additions & 4 deletions ed25519-dalek/src/signing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -483,22 +483,55 @@ impl SigningKey {
self.verifying_key.verify_strict(message, signature)
}

/// Convert this signing key into a byte representation of a(n) (unreduced) Curve25519 scalar.
/// Convert this signing key into a byte representation of an unreduced, unclamped Curve25519
/// scalar. This is NOT the same thing as `self.to_scalar().to_bytes()`, since `to_scalar()`
/// performs a clamping step, which changes the value of the resulting scalar.
///
/// This can be used for performing X25519 Diffie-Hellman using Ed25519 keys. The bytes output
/// by this function are a valid secret key for the X25519 public key given by
/// `self.verifying_key().to_montgomery()`.
/// by this function are a valid corresponding [`StaticSecret`](https://docs.rs/x25519-dalek/2.0.0/x25519_dalek/struct.StaticSecret.html#impl-From%3C%5Bu8;+32%5D%3E-for-StaticSecret)
/// for the X25519 public key given by `self.verifying_key().to_montgomery()`.
///
/// # Note
///
/// We do NOT recommend this usage of a signing/verifying key. Signing keys are usually
/// We do NOT recommend using a signing/verifying key for encryption. Signing keys are usually
/// long-term keys, while keys used for key exchange should rather be ephemeral. If you can
/// help it, use a separate key for encryption.
///
/// For more information on the security of systems which use the same keys for both signing
/// and Diffie-Hellman, see the paper
/// [On using the same key pair for Ed25519 and an X25519 based KEM](https://eprint.iacr.org/2021/509).
pub fn to_scalar_bytes(&self) -> [u8; 32] {
// Per the spec, the ed25519 secret key sk is expanded to
// (scalar_bytes, hash_prefix) = SHA-512(sk)
// where the two outputs are both 32 bytes. scalar_bytes is what we return. Its clamped and
// reduced form is what we use for signing (see impl ExpandedSecretKey)
let mut buf = [0u8; 32];
let scalar_and_hash_prefix = Sha512::default().chain_update(self.secret_key).finalize();
buf.copy_from_slice(&scalar_and_hash_prefix[..32]);
buf
}

/// Convert this signing key into a Curve25519 scalar. This is computed by clamping and
/// reducing the output of [`Self::to_scalar_bytes`].
///
/// This can be used anywhere where a Curve25519 scalar is used as a private key, e.g., in
/// [`crypto_box`](https://docs.rs/crypto_box/0.9.1/crypto_box/struct.SecretKey.html#impl-From%3CScalar%3E-for-SecretKey).
///
/// # Note
///
/// We do NOT recommend using a signing/verifying key for encryption. Signing keys are usually
/// long-term keys, while keys used for key exchange should rather be ephemeral. If you can
/// help it, use a separate key for encryption.
///
/// For more information on the security of systems which use the same keys for both signing
/// and Diffie-Hellman, see the paper
/// [On using the same key pair for Ed25519 and an X25519 based KEM](https://eprint.iacr.org/2021/509).
pub fn to_scalar(&self) -> Scalar {
// Per the spec, the ed25519 secret key sk is expanded to
// (scalar_bytes, hash_prefix) = SHA-512(sk)
// where the two outputs are both 32 bytes. To use for signing, scalar_bytes must be
// clamped and reduced (see ExpandedSecretKey::from_bytes). We return the clamped and
// reduced form.
ExpandedSecretKey::from(&self.secret_key).scalar
}
}
Expand Down
72 changes: 43 additions & 29 deletions ed25519-dalek/tests/x25519.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,63 +4,77 @@ use curve25519_dalek::scalar::{clamp_integer, Scalar};
use ed25519_dalek::SigningKey;
use hex_literal::hex;
use sha2::{Digest, Sha512};

/// Helper function to return the bytes corresponding to the input bytes after being clamped and
/// reduced mod 2^255 - 19
fn clamp_and_reduce(bytes: &[u8]) -> [u8; 32] {
assert_eq!(bytes.len(), 32);
Scalar::from_bytes_mod_order(clamp_integer(bytes.try_into().unwrap())).to_bytes()
}
use x25519_dalek::{PublicKey as XPublicKey, StaticSecret as XStaticSecret};

/// Tests that X25519 Diffie-Hellman works when using keys converted from Ed25519.
// TODO: generate test vectors using another implementation of Ed25519->X25519
#[test]
fn ed25519_to_x25519_dh() {
// Keys from RFC8032 test vectors (from section 7.1)
let ed25519_secret_key_a =
hex!("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60");
let ed25519_secret_key_b =
hex!("4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb");
let ed_secret_key_a = hex!("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60");
let ed_secret_key_b = hex!("4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb");

let ed_signing_key_a = SigningKey::from_bytes(&ed_secret_key_a);
let ed_signing_key_b = SigningKey::from_bytes(&ed_secret_key_b);

let ed25519_signing_key_a = SigningKey::from_bytes(&ed25519_secret_key_a);
let ed25519_signing_key_b = SigningKey::from_bytes(&ed25519_secret_key_b);
// Create an x25519 static secret from the ed25519 signing key
let scalar_bytes_a = ed_signing_key_a.to_scalar_bytes();
let scalar_bytes_b = ed_signing_key_b.to_scalar_bytes();
let x_static_secret_a = XStaticSecret::from(scalar_bytes_a);
let x_static_secret_b = XStaticSecret::from(scalar_bytes_b);

let scalar_a = ed25519_signing_key_a.to_scalar();
let scalar_b = ed25519_signing_key_b.to_scalar();
// Compute the secret scalars too
let scalar_a = ed_signing_key_a.to_scalar();
let scalar_b = ed_signing_key_b.to_scalar();

// Compare the scalar bytes to the first 32 bytes of SHA-512(secret_key). We have to clamp and
// reduce the SHA-512 output because that's what the spec does before using the scalars for
// anything.
assert_eq!(scalar_bytes_a, &Sha512::digest(ed_secret_key_a)[..32]);
assert_eq!(scalar_bytes_b, &Sha512::digest(ed_secret_key_b)[..32]);

// Compare the scalar with the clamped and reduced scalar bytes
assert_eq!(
scalar_a.to_bytes(),
clamp_and_reduce(&Sha512::digest(ed25519_secret_key_a)[..32]),
scalar_a,
Scalar::from_bytes_mod_order(clamp_integer(scalar_bytes_a))
);
assert_eq!(
scalar_b.to_bytes(),
clamp_and_reduce(&Sha512::digest(ed25519_secret_key_b)[..32]),
scalar_b,
Scalar::from_bytes_mod_order(clamp_integer(scalar_bytes_b))
);

let x25519_public_key_a = ed25519_signing_key_a.verifying_key().to_montgomery();
let x25519_public_key_b = ed25519_signing_key_b.verifying_key().to_montgomery();

let x_public_key_a = XPublicKey::from(&x_static_secret_a);
let x_public_key_b = XPublicKey::from(&x_static_secret_b);
assert_eq!(
x25519_public_key_a.to_bytes(),
x_public_key_a.to_bytes(),
hex!("d85e07ec22b0ad881537c2f44d662d1a143cf830c57aca4305d85c7a90f6b62e")
);
assert_eq!(
x25519_public_key_b.to_bytes(),
x_public_key_b.to_bytes(),
hex!("25c704c594b88afc00a76b69d1ed2b984d7e22550f3ed0802d04fbcd07d38d47")
);

// Test the claim made in the comments of SigningKey::to_scalar_bytes, i.e., that the resulting
// scalar is a valid private key for the x25519 pubkey represented by
// `sk.verifying_key().to_montgomery()`
assert_eq!(
ed_signing_key_a.verifying_key().to_montgomery().as_bytes(),
x_public_key_a.as_bytes()
);
assert_eq!(
ed_signing_key_b.verifying_key().to_montgomery().as_bytes(),
x_public_key_b.as_bytes()
);

// Check that Diffie-Hellman works
let expected_shared_secret =
hex!("5166f24a6918368e2af831a4affadd97af0ac326bdf143596c045967cc00230e");

assert_eq!(
(x25519_public_key_a * scalar_b).to_bytes(),
expected_shared_secret
x_static_secret_a.diffie_hellman(&x_public_key_b).to_bytes(),
expected_shared_secret,
);
assert_eq!(
(x25519_public_key_b * scalar_a).to_bytes(),
expected_shared_secret
x_static_secret_b.diffie_hellman(&x_public_key_a).to_bytes(),
expected_shared_secret,
);
}

0 comments on commit 04f811a

Please sign in to comment.