bcgit / bc-csharp

BouncyCastle.NET Cryptography Library (Mirror)
https://www.bouncycastle.org/csharp
MIT License
1.63k stars 546 forks source link

CalculateAgreement error using ECDHBasicAgreement #120

Closed iodev closed 6 years ago

iodev commented 6 years ago

I have a blocking issue (using Portable.BouncyCastle v1.8.1.3 nuget package). Using the following code, the error is isolated. Within 55 exchanges, between receiver and sender, the agreement calculation will not match on the receiving side, at some random point. The occasion that the error occurs is flagged out in the console app attached; that is, when sender and receiver do not calculate the same agreement. To avoid the initial exchange error that would occur, you can uncomment the initial keys, or leave it as it is here, with two initial exchanges, not reporting the error. So, in Main, if you use the 4 keys, then you can comment out the first two initial exchanges before the loop.

Here is the code:

using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Agreement;
using Org.BouncyCastle.Utilities;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Math.EC;
using Org.BouncyCastle.Crypto.EC;
using Org.BouncyCastle.Asn1.X9;
namespace My
{
    public struct AsymmetricKeyPair
    {
        public byte[] priv;
        public byte[] pub;
    }
    public class EC25519CKA
    {
        public AsymmetricKeyPair State;
        public byte[] RUpd;
        public byte[] SUpd;
        private const int KEYSIZE = 33;
        // Returns a new Diffie-Hellman key pair
        public void Generate(ref byte[] priv, ref byte[] pub)
        {
            ECKeyPairGenerator dhGen = new ECKeyPairGenerator();
            SecureRandom rng = new SecureRandom();
            dhGen.Init(new ECKeyGenerationParameters(ecparameters, rng));
            AsymmetricCipherKeyPair dhPair = dhGen.GenerateKeyPair();
            priv = ((ECPrivateKeyParameters)dhPair.Private).D.ToByteArrayUnsigned();
            pub = ((ECPublicKeyParameters)dhPair.Public).Q.GetEncoded(true);
        }
        public byte[] GetAgreement(byte[] Privkey, byte[] Pubkey)
        {
            ECPrivateKeyParameters privateKey = new ECPrivateKeyParameters(new BigInteger(Privkey), ecparameters);
            ECPoint pt = ecurve.DecodePoint(Pubkey);
            ECPublicKeyParameters publicKey = new ECPublicKeyParameters(pt, ecparameters);
            ECDHBasicAgreement basicAgreement = new ECDHBasicAgreement();
            basicAgreement.Init(privateKey);
            BigInteger agreement = basicAgreement.CalculateAgreement(publicKey);
            return BigIntegers.AsUnsignedByteArray(agreement);
        }
        ECDomainParameters ecparameters;
        ECCurve ecurve;
        ECPoint epoint;
        BigInteger ecn;
        public EC25519CKA()
        {
            State = new AsymmetricKeyPair() { pub = new byte[KEYSIZE], priv = new byte[KEYSIZE] };
            X9ECParameters ecp = CustomNamedCurves.GetByName("curve25519");
            ecurve = ecp.Curve;
            epoint = ecp.G;
            ecn = ecp.N;
            ecparameters = new ECDomainParameters(ecurve, epoint, ecn);
            InitialGeneration(); /// avoid warning
        }
        private void InitialGeneration()
        {
            // both Alice and Bob have private keys initialized
            // PopulateObject replaces these from JSON
            Generate(ref State.priv, ref State.pub);
        }
        public void Receive(byte[] keymaterial)
        {
            Console.WriteLine($"RUpd => {Convert.ToBase64String(State.priv)} {Convert.ToBase64String(keymaterial)}");
            RUpd = GetAgreement(State.priv, keymaterial);
            Console.WriteLine($"RUpd: {Convert.ToBase64String(RUpd)}");
            keymaterial.CopyTo(State.pub, 0);
        }
        public byte[] Send()
        {
            AsymmetricKeyPair tempPair = new AsymmetricKeyPair() { pub = new byte[State.pub.Length], priv = new byte[State.priv.Length] };
            // this generates and saves new priv, while generating new pub
            Generate(ref State.priv, ref tempPair.pub);
            // this uses previous pub, with new priv
            Console.WriteLine($"SUpd => {Convert.ToBase64String(State.priv)} {Convert.ToBase64String(State.pub)}");
            SUpd = GetAgreement(State.priv, State.pub);
            Console.WriteLine($"SUpd: {Convert.ToBase64String(SUpd)}");
            // sets stores the new pub in SLast
            return (tempPair.pub);
        }
    }
    class MainClass
    {
        public static void Main(string[] args)
        {
            var ecdhA = new EC25519CKA();
            var ecdhB = new EC25519CKA();
            /* you can start with these 4 keys to initialize, to avoid initial exchange error */
            //ecdhA.State.priv = Convert.FromBase64String("BozkoUceQN/5qF8SqhRU4K7UFL3evaQpvWEP4webyto=");
            //ecdhA.State.pub = Convert.FromBase64String("Amui2YWWsuCirkHkrDxiWyv+wasSt8/UJSxWZcF9Tdb/");
            //ecdhB.State.priv = Convert.FromBase64String("CgZJsQ5o0jejASt9ujvGlI2YK4bRSg63pcFlpJKy2es=");
            //ecdhB.State.pub = Convert.FromBase64String("AmrptG+cwVHqQQykUyam37R85hka6pQmyS20ZMUTMgSo");
            /* or just do an initial exchange of keys */
            Exchange(ref ecdhA, ref ecdhB);
            Exchange(ref ecdhB, ref ecdhA);
            for (int i = 0; i < 55; i++)
            {
                Exchange(ref ecdhA, ref ecdhB);
                // At some random point, an exchange error will occur, and then after an initial exchange error it will resume
                if(!Arrays.AreEqual(ecdhA.SUpd, ecdhB.RUpd))
                    Console.WriteLine($"[ERROR] ******************");
                Exchange(ref ecdhB, ref ecdhA);
                if (!Arrays.AreEqual(ecdhB.SUpd, ecdhA.RUpd))
                    Console.WriteLine($"[ERROR] ******************");
            }
        }
        public static void Exchange(ref EC25519CKA ecdh_s, ref EC25519CKA ecdh_r)
        {
            var e1 = ecdh_s.Send();
            ecdh_r.Receive(e1);
        }
    }
}

Attaching runnable console solution. My.zip

peterdettman commented 6 years ago

The errors go away if I change the first line of GetAgreement to use:

new BigInteger(1, Privkey)

This ensures the byte array is interpreted as a positive integer. It's not obvious to me where Privkey picks up a signed value (i.e. high-bit 1), but all of the code seems a bit confused about converting to/from byte arrays.

e.g. the last line of GetAgreement should be:

return BigIntegers.AsUnsignedByteArray(basicAgreement.GetFieldSize(), agreement);

so that the result is of a fixed length.