diff --git a/ed25519-dalek/CHANGELOG.md b/ed25519-dalek/CHANGELOG.md index c3fc94a..1561d05 100644 --- a/ed25519-dalek/CHANGELOG.md +++ b/ed25519-dalek/CHANGELOG.md @@ -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 diff --git a/ed25519-dalek/Cargo.toml b/ed25519-dalek/Cargo.toml index 8fd4ecc..447976a 100644 --- a/ed25519-dalek/Cargo.toml +++ b/ed25519-dalek/Cargo.toml @@ -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" diff --git a/ed25519-dalek/src/signing.rs b/ed25519-dalek/src/signing.rs index ec866d9..23fab8f 100644 --- a/ed25519-dalek/src/signing.rs +++ b/ed25519-dalek/src/signing.rs @@ -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 } } diff --git a/ed25519-dalek/tests/x25519.rs b/ed25519-dalek/tests/x25519.rs index 48dab27..11e72a8 100644 --- a/ed25519-dalek/tests/x25519.rs +++ b/ed25519-dalek/tests/x25519.rs @@ -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, ); }