mirror of
https://github.com/saymrwulf/betrusted-curve25519-dalek-source.git
synced 2026-09-04 20:24:07 +00:00
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:
parent
ac51ef6ecf
commit
04f811ad21
4 changed files with 85 additions and 33 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue