For a scenario in which only an X25519 keypair is available but I need to create and verify a signature with what I have, I am trying to convert an X25519 public key into an Ed25519 verification key that can successfully verify a signature made using the X25519 private key.
While generally frowned upon (I know the reasons, but as stated, the X25519 keypair is all I have), there is an implementation in the wild that does this, available as C code. I am trying to follow this example.
As the surrounding software uses the dalek set of crates, I use this library to do the conversion as well. My conversion function for the public key reads:
pub(crate) fn x25519_pub_to_ed25519_ver(
public_key: [u8; 32],
) -> Result<VerifyingKey, SignatureError> {
match MontgomeryPoint(public_key).to_edwards(SIGN_BIT) {
None => Err(SignatureError::new()),
Some(key) => VerifyingKey::from_bytes(key.compress().as_bytes()),
}
}
This calculates the Edwards point from the Montgomery point, and I have verified the to_edwards code to do exactly what the reference code linekd above does.
For the signing party, I figured I need to make sure to use a private key that will yield the correct sign, as the transformation between X25519 and Ed25519 public keys is not 1:1. My reference library also has an example how to produce a matching private key, which I tried to immitate:
pub(crate) fn x25519_priv_to_ed25519_sig(private_key: &StaticSecret) -> SigningKey {
let clamped = clamp_integer(private_key.to_bytes());
let signing_key = SigningKey::from_bytes(&clamped);
let test_public_key = signing_key.verifying_key().to_bytes();
if (test_public_key[31] & 0x80) >> 7 == SIGN_BIT {
signing_key
} else {
let scalar = Scalar::from_bytes_mod_order(clamped);
let neg = scalar.invert();
SigningKey::from_bytes(&neg.to_bytes())
}
}
My test code, sadly, does not work, the public key generated does not match the Ed25519 verification key derived directly from the calculated Ed25519 secret key:
const TEST_MESSAGE: &[u8] = b"Das Pferd frisst keinen Gurkensalat.";
fn main() {
let private_key = StaticSecret::random();
println!("X25519 private key: {:02x?}", private_key.to_bytes());
let public_key = PublicKey::from(&private_key);
let signing_key = x25519_priv_to_ed25519_sig(&private_key);
println!("Ed25519 signing key: {:02x?}", signing_key.to_bytes());
let signature = signing_key.sign(TEST_MESSAGE);
let verifying_key = x25519_pub_to_ed25519_ver(public_key.to_bytes()).unwrap();
println!(
"Transformed verifying key: {:02x?}",
verifying_key.to_bytes()
);
println!(
"Real verifying key: {:02x?}",
signing_key.verifying_key().to_bytes()
);
let result = verifying_key.verify(TEST_MESSAGE, &signature);
assert!(result.is_ok());
}
At which point am I doing the conversion wrong?
A minimal working example, ready to be run with cargo
, can be found on Codeberg.
The
verifying_key()
as generated byed25519_dalek
is not directly the point derived from the scalar by multiplication on the curve. That's because the secret key used for signing in real Ed25519 is not the scalar itself, but the result of a SHA-512 key derivation over the scret scalar, yielding 32 bytes as the new scalar and 32 bytes as pseudo-random hash input for the signature.In order for independent key derivation for the signing and verifying part to work, a different key derivation algorithm is necessary, as laid out by Signal's XEdDSA specification.