ed: Add back SigningKey::to_scalar_bytes (#599)

* Brought back SigningKey::to_scalar_bytes; added regression test

* Updated SigningKey::to_scalar docs and tests
This commit is contained in:
Michael Rosenberg 2023-11-14 13:23:48 -05:00 committed by GitHub
parent ac51ef6ecf
commit 04f811ad21
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 85 additions and 33 deletions

View file

@ -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

View file

@ -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"

View file

@ -483,15 +483,43 @@ 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.
///
@ -499,6 +527,11 @@ impl SigningKey {
/// 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
}
}

View file

@ -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 ed25519_signing_key_a = SigningKey::from_bytes(&ed25519_secret_key_a);
let ed25519_signing_key_b = SigningKey::from_bytes(&ed25519_secret_key_b);
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 scalar_a = ed25519_signing_key_a.to_scalar();
let scalar_b = ed25519_signing_key_b.to_scalar();
// 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);
// 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,
);
}