jedisct1 / libsodium

A modern, portable, easy to use crypto library.
https://libsodium.org
Other
12.15k stars 1.73k forks source link

How can one convert an ed25519 scalar into x25519 for encryption? #1026

Closed AndrejMitrovic closed 3 years ago

AndrejMitrovic commented 3 years ago

This might be an unusual question.

I have a set of key-pairs (Alice & Bob), which used crypto_core_ed25519_scalar_random and crypto_scalarmult_ed25519_base_noclamp to generate their key-pairs, respectively.

These low-level primitives were used to implement Schnorr signatures in the system I'm interfacing with.

Now, I'd like Alice to send an encrypted payload to Bob. So I would need to derive a symmetric key for this. I wanted to use crypto_box_easy. However I need to convert the ed25519 scalar into an x25519 key. But I don't see any (correct) way of doing this using libsodium.

I've looked at using crypto_sign_ed25519_sk_to_curve25519 and crypto_sign_ed25519_pk_to_curve25519 for this, but it doesn't seem to work with keys generated with crypto_core_ed25519_scalar_random. It only works with keys generated with crypto_sign_ed25519_keypair instead.


I'm showing two brief examples of my attempts. This first example uses crypto_sign_ed25519_keypair and crypto_sign_ed25519_sk_to_curve25519 and everything works OK (example is in D, you may ignore the irrelevant parts of the code):

unittest
{
    // alice: generate ed25519 key pair
    ubyte[crypto_sign_ed25519_PUBLICKEYBYTES] ed25519_alice_pk;
    ubyte[crypto_sign_ed25519_SECRETKEYBYTES] ed25519_alice_sk;
    crypto_sign_ed25519_keypair(ed25519_alice_pk.ptr, ed25519_alice_sk.ptr);

    // alice: convert ed25519 to x25519
    ubyte[crypto_scalarmult_curve25519_BYTES] x25519_alice_pk;
    ubyte[crypto_scalarmult_curve25519_BYTES] x25519_alice_sk;
    crypto_sign_ed25519_sk_to_curve25519(x25519_alice_sk.ptr, ed25519_alice_sk.ptr);
    crypto_sign_ed25519_pk_to_curve25519(x25519_alice_pk.ptr, ed25519_alice_pk.ptr);

    // bob: generate ed25519 key pair
    ubyte[crypto_sign_ed25519_PUBLICKEYBYTES] ed25519_bob_pk;
    ubyte[crypto_sign_ed25519_SECRETKEYBYTES] ed25519_bob_sk;
    crypto_sign_ed25519_keypair(ed25519_bob_pk.ptr, ed25519_bob_sk.ptr);

    // bob: convert ed25519 to x25519
    ubyte[crypto_scalarmult_curve25519_BYTES] x25519_bob_pk;
    ubyte[crypto_scalarmult_curve25519_BYTES] x25519_bob_sk;
    crypto_sign_ed25519_sk_to_curve25519(x25519_bob_sk.ptr, ed25519_bob_sk.ptr);
    crypto_sign_ed25519_pk_to_curve25519(x25519_bob_pk.ptr, ed25519_bob_pk.ptr);

    static struct S
    {
        int x = 123;
    }

    S s;
    const payload = s.serializeFull();

    ubyte[crypto_box_NONCEBYTES] nonce;
    randombytes_buf(nonce.ptr, nonce.length);

    auto ciphertext_len = crypto_box_MACBYTES + payload.length;
    ubyte[] ciphertext = new ubyte[](ciphertext_len);
    if (crypto_box_easy(ciphertext.ptr, payload.ptr, payload.length, nonce.ptr,
        x25519_bob_pk.ptr, x25519_alice_sk.ptr) != 0)
        assert(0);

    ubyte[] decrypted = new ubyte[](payload.length);
    if (crypto_box_open_easy(decrypted.ptr, ciphertext.ptr, ciphertext_len, nonce.ptr,
        x25519_alice_pk.ptr, x25519_bob_sk.ptr) != 0)
        assert(0);

    S deserialized = deserializeFull!S(decrypted);
    assert(deserialized.x == s.x);
}

This works OK.


In this second example, I try to use the keys generated with crypto_core_ed25519_scalar_random / crypto_scalarmult_ed25519_base_noclamp and converting them using crypto_sign_ed25519_sk_to_curve25519 and crypto_sign_ed25519_pk_to_curve25519, respectively.

However the decryption fails:

unittest
{
    // alice: generate ed25519 key pair
    ubyte[crypto_core_ed25519_BYTES] ed25519_alice_pk;
    ubyte[crypto_core_ed25519_SCALARBYTES] ed25519_alice_sk;
    crypto_core_ed25519_scalar_random(ed25519_alice_sk.ptr);
    crypto_scalarmult_ed25519_base_noclamp(ed25519_alice_pk.ptr, ed25519_alice_sk.ptr);

    // alice: convert ed25519 to x25519
    ubyte[crypto_scalarmult_curve25519_BYTES] x25519_alice_pk;
    ubyte[crypto_scalarmult_curve25519_BYTES] x25519_alice_sk;
    crypto_sign_ed25519_sk_to_curve25519(x25519_alice_sk.ptr, ed25519_alice_sk.ptr);
    crypto_sign_ed25519_pk_to_curve25519(x25519_alice_pk.ptr, ed25519_alice_pk.ptr);

    // bob: generate ed25519 key pair
    ubyte[crypto_core_ed25519_BYTES] ed25519_bob_pk;
    ubyte[crypto_core_ed25519_SCALARBYTES] ed25519_bob_sk;
    crypto_core_ed25519_scalar_random(ed25519_bob_sk.ptr);
    crypto_scalarmult_ed25519_base_noclamp(ed25519_bob_pk.ptr, ed25519_bob_sk.ptr);

    // bob: convert ed25519 to x25519
    ubyte[crypto_scalarmult_curve25519_BYTES] x25519_bob_pk;
    ubyte[crypto_scalarmult_curve25519_BYTES] x25519_bob_sk;
    crypto_sign_ed25519_sk_to_curve25519(x25519_bob_sk.ptr, ed25519_bob_sk.ptr);
    crypto_sign_ed25519_pk_to_curve25519(x25519_bob_pk.ptr, ed25519_bob_pk.ptr);

    static struct S
    {
        int x = 123;
    }

    S s;
    const payload = s.serializeFull();

    ubyte[crypto_box_NONCEBYTES] nonce;
    randombytes_buf(nonce.ptr, nonce.length);

    auto ciphertext_len = crypto_box_MACBYTES + payload.length;
    ubyte[] ciphertext = new ubyte[](ciphertext_len);
    if (crypto_box_easy(ciphertext.ptr, payload.ptr, payload.length, nonce.ptr,
        x25519_bob_pk.ptr, x25519_alice_sk.ptr) != 0)
        assert(0);

    ubyte[] decrypted = new ubyte[](payload.length);

    /******
          FAILS HERE
    */
    if (crypto_box_open_easy(decrypted.ptr, ciphertext.ptr, ciphertext_len, nonce.ptr,
        x25519_alice_pk.ptr, x25519_bob_sk.ptr) != 0)
        assert(0);

    S deserialized = deserializeFull!S(decrypted);
    assert(deserialized.x == s.x);
}

I must be doing something wrong. I know I can just use crypto_box_keypair to generate an entirely new key-pair, but what I really want is to derive the encryption key-pairs from the existing key-pairs generated with crypto_core_ed25519_scalar_random. Is it possible somehow?

Thank you!

AndrejMitrovic commented 3 years ago

P.S. I've consulted with https://libsodium.gitbook.io/doc/advanced/ed25519-curve25519, but again that only works with crypto_sign_ed25519_keypair as mentioned before.

jedisct1 commented 3 years ago

I'd recommend doing the DH exchange with your existing key pairs instead of juggling between curves.

Use crypto_scalarmult_ed25519() to do so, then hash the output as in crypto_kx to get your shared secret, and use crypto_secretstream or one of the AEADs.

jedisct1 commented 3 years ago

I know I can just use crypto_box_keypair to generate an entirely new key-pair, but what I really want is to derive the encryption key-pairs from the existing key-pairs generated with crypto_core_ed25519_scalar_random

Wait, if you just need to create an X25519 key pair from a seed (which can be the scalar you already got, although using a KDF would be better hygiene), you can use crypto_box_seed_keypair().

AndrejMitrovic commented 3 years ago

I'd recommend doing the DH exchange with your existing key pairs instead of juggling between curves.

Ah that makes a lot of sense! Thanks! I'll try it out.

Feel free to close if you think this resolves it.

AndrejMitrovic commented 3 years ago

Wait, if you just need to create an X25519 key pair from a seed (which can be the scalar you already got, although using a KDF would be better hygiene), you can use crypto_box_seed_keypair().

I'm a little confused about this suggestion. If the crypto_box_seed_keypair is used, wouldn't Bob need to send the public key part back to Alice?

AndrejMitrovic commented 3 years ago

Thanks a lot, that made a lot of sense in the end. I failed to realize I can just do basic ECC math on the curve to extract the shared key to use with the various encryption routines such as crypto_secretbox_easy. Everything works fine now. Thank you @jedisct1!