Derive same X25519 public from either Ed25519-public or Ed25519-secret

191 views Asked by At

My fictional user only has knowledge of an unclamped Ed25519 public key. He wants to calculate the same X25519 public key that I myself got from my Ed25519 secret.

So, given an Ed25519 key pair, I am looking for a way to get to the same X25519 public key in both of these two scenarios:

  1. when converting an unclamped Ed25519 public key to X25519
  2. when deriving the X25519 public key from the Ed25519 secret

It works with clamped

I have a Ed25519 key pair ed_pk and ed_sk where the public key is clamped:

// ed_sk=5BAA0D..
// ed_pk=466EC8..

... and I derive an X25519 public key x_pk from the Ed25519 secret:

crypto_scalarmult_base(x_pk, ed_sk);
// x_pk=1321BF..

Note the value of x_pk. It matches what I get when converting over from the Ed25519 public key:

crypto_sign_ed25519_pk_to_curve25519(x_pk, ed_pk);
// x_pk=1321BF..

1321BF.. either way - Great!


But when I have an unclamped Ed25519 public key to begin with:

crypto_scalarmult_ed25519_base_noclamp(ed_pk, ed_sk);

... the conversion creates a different number for x_pk:

// ed_pk=A63B27..
crypto_sign_ed25519_pk_to_curve25519(x_pk, ed_pk);
// x_pk=B9FD38..

Can I (A) convert a public key that wasn't clamped to its clamped version? or (B) amend the Ed25519-X25519 conversion in a way that gets me matching results when working with unclamped variants?

1

There are 1 answers

0
Frank Denis On

If you absolutely want to use the same key for both operations, you can use Edwards25519 for key exchange instead of complicating the protocol with Curve25519 conversions.

What is often confused as the Ed25519 secret is the seed. The actual secret scalar is usually not exposed in APIs, and handled internally by computing SHA-512(seed), and truncating the output to the first 256 bits.

Here's simple Zig code that does what you want:

const std = @import("std");
const crypto = std.crypto;
const Curve25519 = crypto.ecc.Curve25519;
const Edwards25519 = crypto.ecc.Edwards25519;

pub fn main() !void {
    // Generate a random Ed25519 secret seed
    var ed25519_seed: [32]u8 = undefined;
    crypto.random.bytes(&ed25519_seed);

    // In Ed25519, the Edwards25519 secret scalar is SHA-512(seed)[0..32]
    var h: [64]u8 = undefined;
    crypto.hash.sha2.Sha512.hash(&ed25519_seed, &h, .{});
    const edwards25519_secret = Edwards25519.scalar.reduce(h[0..32].*);

    // Unclamped Edwards25519 public key, and Curve25519 projection
    const edwards25519_public = try Edwards25519.basePoint.mul(edwards25519_secret);
    const curve25519_public = try Curve25519.fromEdwards25519(edwards25519_public);

    // Curve25519 secret is the same as the Edwards25519 secret
    const curve25519_secret = edwards25519_secret;
    const curve25519_public2 = try Curve25519.basePoint.mul(curve25519_secret);

    // Both Curve25519 public keys are the same
    std.debug.print("{s}\n{s}\n", .{
        std.fmt.bytesToHex(curve25519_public.toBytes(), .lower),
        std.fmt.bytesToHex(curve25519_public2.toBytes(), .lower),
    });
}