(C#) Calculate key share using private key and public key on (EC)DHE x25519

1.7k Views Asked by At

I am working with (EC)DHE encryption type x25519 and I have a big problem on calculating shared key.

I have three key:

  • Alice’s private key:

    a : "984a382e1e48d2a522a0e81b92fd13517e904316c6687a59d66cd2e5d9519a53"
    
  • Alice’s public key:

    Q(a) = a*G(a) : "3db045ba8a16efd9e15de287158097ee754ce5d76e83c5e434109dd132a4736d"
    
  • Bob’s public key:

    Q(b) =  b*G(b) : "74676252b0757ba3cb945ea053d9d65897a22e01592f7fa9c9503b818cd9df5a"
    

So now I need to combine Alice’s private key and Bob’s public key like this (to find a shared key between them):

Z = a * Q(b) = a * b * G(b)

Do anyone help me with this problem using C#? (I need a programming code).

2

There are 2 best solutions below

3
On BEST ANSWER

I am working with (EC)DHE encryption type x25519 and I have a big problem on calculating shared key.

Microsoft has no default implementation of the elliptic curve x25519. However their implementations of cryptographic Diffie Hellman objects allows us to define our own curve.

Once we define our own curve to use (x25519) we can use Microsoft's ECDiffieHellmanCng implementation to import the curve, generate keys, and create shared secrets.

Thanks to Yasar_yy for his question about an unrelated topic on x25519 he implemented the curve for us.

We implement a curve using the ECCurve class

public static ECCurve Curve25519 {get; init;} = new ECCurve()
{
    CurveType = ECCurve.ECCurveType.PrimeMontgomery,
    B = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
    A = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x07, 0x6d, 0x06 }, // 486662
    G = new ECPoint()
    {
        X = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9 },
        Y = new byte[] { 0x20, 0xae, 0x19, 0xa1, 0xb8, 0xa0, 0x86, 0xb4, 0xe0, 0x1e, 0xdd, 0x2c, 0x77, 0x48, 0xd1, 0x4c,
        0x92, 0x3d, 0x4d, 0x7e, 0x6d, 0x7c, 0x61, 0xb2, 0x29, 0xe9, 0xc5, 0xa2, 0x7e, 0xce, 0xd3, 0xd9 }
    },
    Prime = new byte[] { 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xed },
    //Prime = new byte[] { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
    Order = new byte[] { 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x14, 0xde, 0xf9, 0xde, 0xa2, 0xf7, 0x9c, 0xd6, 0x58, 0x12, 0x63, 0x1a, 0x5c, 0xf5, 0xd3, 0xed },
    Cofactor = new byte[] { 8 }
};

After we define the curve we want to use we just need to generate the keys for the curve and the rest is standard for using the ECDiffieHellmanCng class.

public class Person
{
    public string Name {get; set;}

    public byte[] PublicKey {get; private set;}

    public byte[] PrivateKey {get; private set;}

    private ECParameters EncryptionParameters;

    public void GenerateInitialKeys()
    {
        using (ECDiffieHellmanCng bob = new ECDiffieHellmanCng())
        {
            // we have to generate the key explicitly using the curve we defined, the auto-generated keys can not be used
            bob.GenerateKey(Curve25519);

            // assign what algorithms for derivation and hashing should be used
            bob.KeyDerivationFunction = ECDiffieHellmanKeyDerivationFunction.Hash;
            bob.HashAlgorithm = CngAlgorithm.Sha256;

            // save the keys
            PublicKey = bob.PublicKey.ToByteArray();
            PrivateKey = bob.ExportECPrivateKey();

            // export the curve information so we can create a shared secret later
            EncryptionParameters = bob.ExportParameters(true);
        }
    }

    public void CreateSharedSecret(byte[] OtherPublicKey)
    {
        if(EncryptionParameters is null)
        {
            throw new NullReferenceException($"{nameof(EncryptionParameters)} must not be null, invoke {nameof(GenerateInitialKeys)} to generate new keys and {nameof(EncryptionParameters)}");
        }
        using (ECDiffieHellmanCng bob = new ECDiffieHellmanCng())
        {
            // import the curve information from when generated our initial keys
            bob.ImportParameters(EncryptionParameters);

            // assign what algorithms for derivation and hashing should be used
            bob.KeyDerivationFunction = ECDiffieHellmanKeyDerivationFunction.Hash;
            bob.HashAlgorithm = CngAlgorithm.Sha256;

            // import the byte[] as a key, note EccFullPublicBlob is required, otherwise a generic runtime error will throw and will contain absolutely no useful information
            CngKey otherKey = CngKey.Import(OtherPublicKey, CngKeyBlobFormat.EccFullPublicBlob)

            // Save the shared secret
            PrivateKey = bob.DeriveKeyMaterial(otherKey);
        }
    }

    // This is just here to visually verify the private keys for equality because we don't speak or read byte[]
    public string ExportPrivateKey()
    {
        return Convert.ToBase64String(PrivateKey ?? Array.Empty<byte>());
    }
}

To use this very basic class we just call GenerateKeys and subsequently CreateSharedSecret.

Person alice = new();
Person bob = new();

alice.GenerateInitialKeys();

bob.GenerateInitialKeys();

alice.CreateSharedSecret(bob.PublicKey);

bob.CreateSharedSecret(alice.PublicKey);

Console.WriteLine(alice.ExportPrivateKey() == bob.ExportPrivateKey());
// ideally should output: true
0
On

A comfortable alternative to the built-in functionality (see the other answer) is BouncyCastle, which allows a more compact implementation and also supports easy import of raw X25519 keys:

using Org.BouncyCastle.Crypto.Agreement;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Utilities.Encoders;
...
X25519PrivateKeyParameters privateKeyAlice = new X25519PrivateKeyParameters(Hex.Decode("a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4"), 0);
X25519PublicKeyParameters publicKeyBob = new X25519PublicKeyParameters(Hex.Decode("e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c"), 0);

X25519Agreement agreementAlice = new X25519Agreement();
agreementAlice.Init(privateKeyAlice);
byte[] secretAlice = new byte[agreementAlice.AgreementSize];
agreementAlice.CalculateAgreement(publicKeyBob, secretAlice, 0);

Console.WriteLine(Hex.ToHexString(secretAlice)); // c3da55379de9c6908e94ea4df28d084f32eccf03491c71f754b4075577a28552

In the posted example, a test vector from Rfc7748 (which specifies curve 25519, among others) was used.

In contrast to the built-in functionality (see here), BC pleasantly provides the unmodified shared secret, which can be used to derive keys as needed (e.g. by applying a digest).