Search code examples
javascriptc#diffie-hellman

.NET Core 5.0 to Javascript DFH Key exchange not working


We are trying to get a browser based app using JS to exchange keys with a .Net Core 5.0 server using Eliptical Curve Diffie Hellman. Our app requires a shared secret on both ends for some specific processing we do (not encryption, but our own process), and we want that secret to be derived rather than transmitted for security purposes. We have searched around for a suitable solution and patching various ones together, we have come up with this, but it fails with an exception on the C# side. Basically, our pseudo code is as follows (we have only gotten to Step 3 so far):

  1. Client (Bob) generates a ECDH / P-256 key pair in the client using the window.crypto.subtle library.
  2. Bob exports the public key from this pair and issues a GET to the server (Alice) to get her public key (passing the bobPublicKeyB64 as a query arg).
  3. Alice takes the incoming request and calls a C# method to use Bob's public key to create a shared secret.
  4. Alice stores this shared secret in memcache and returns her public key to Bob.
  5. Bob then uses Alice's public key to get his own shared secret and stores it in "session storage" on the browser.

The code that sends Bob's public key to Alice is not shown here, it is a standard XMLHttpRequest. It does work, however, and the server is invoked as witnessed by the failures in the C# code. We have verified that the bobPublicKeyB64 is exactly the same between Bob when he sends it and Alice when she gets it.

Once this is working, we will harden the storage methodology on both ends, but first we need to get the exchange working. The comments in the Alice (C#) block of code show where it fails. the getDerivedKey method (commented out for now) came from this post - ECDH nodejs and C# key exchange and it fails with a different exception (I am sure that both failures are due to some mismatch between the JS library and the .Net implementation, but we cannot get a handle on it). Any and all help is greatly appreciated - the basic question is "How can we get Bob and Alice on speaking terms, when Bob is JS and Alice is C#?"

Following is the JS code on Bob:

async function setUpDFHKeys() {
let bobPublicKeyB64;
let bobPrivateKeyB64;
try {

    const bobKey = await window.crypto.subtle.generateKey(
        { name: 'ECDH', namedCurve: 'P-256' },
        true,
        ["deriveKey"]
    );
            
    const publicKeyData = await window.crypto.subtle.exportKey("raw", bobKey.publicKey);
    const publicKeyBytes = new Uint8Array(publicKeyData);
    const publicKeyB64 = btoa(publicKeyBytes);        
    bobPublicKeyB64 = publicKeyB64;

    const privateKeyData = await window.crypto.subtle.exportKey("pkcs8", bobKey.privateKey);
    const privateKeyBytes = new Uint8Array(privateKeyData);
    const privateKeyB64 = btoa(String.fromCharCode.apply(null, privateKeyBytes));
    bobPrivateKeyB64 = privateKeyB64;
}
catch (error) {
    console.log("Could not setup DFH Keys " + error);
}};

Following is the C# code on Alice:

  private string WorkWithJSPublicKey(string bobPublicKeyB64, out string alicePublicKey)
    {
        try
        {
            //
            // Alice is this server.
            // Bob is a browser that uses the window.crypto.subtle.generateKey with 'ECDH' and 'P-256' as parameters
            // and window.crypto.subtle.exportKey of the key.publicKey in "raw" format (this is then converted to a B64 string using btoa)
            //
            using (ECDiffieHellman alice = ECDiffieHellman.Create(ECCurve.NamedCurves.nistP256))
            {
                //
                // Get the public key info from Bob and convert to B64 string to return to Alice
                //
                Span<byte> exported = new byte[alice.KeySize];
                int len = 0;
                alice.TryExportSubjectPublicKeyInfo(exported, out len);
                alicePublicKey = Convert.ToBase64String(exported.Slice(0, len));
                //
                // Get Alice's private key to use to generate a shared secret
                //
                byte[] alicePrivateKey = alice.ExportECPrivateKey();
                //
                // Import Bob's public key after converting it to bytes
                //
                var bobPubKeyBytes = Convert.FromBase64String(bobPublicKeyB64);
                //
                // TRY THIS... (Bombs with "The specified curve 'nistP256' or its parameters are not valid for this platform").
                //
                // getDerivedKey(bobPubKeyBytes, alice);
                //
                // This throws exception ("The provided data is tagged with 'Universal' class value '20', but it should have been 'Universal' class value '16'.")
                //
                alice.ImportSubjectPublicKeyInfo(bobPubKeyBytes, out len);
                //
                // Once Alice knows about Bob, create a shared secret and return it.
                //
                byte[] sharedSecret = alice.DeriveKeyMaterial(alice.PublicKey);
                return Convert.ToBase64String(sharedSecret);
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, ex.Message);
            alicePublicKey = string.Empty;
            return string.Empty;
        }
    }

And the code for the getDerivedKey (borrowed from ECDH nodejs and C# key exchange ) is shown below:

static byte[] getDerivedKey(byte[] key1, ECDiffieHellman alice)
    {
        byte[] keyX = new byte[key1.Length / 2];
        byte[] keyY = new byte[keyX.Length];
        Buffer.BlockCopy(key1, 1, keyX, 0, keyX.Length);
        Buffer.BlockCopy(key1, 1 + keyX.Length, keyY, 0, keyY.Length);
        ECParameters parameters = new ECParameters
        {
            Curve = ECCurve.NamedCurves.nistP256,
            Q = {
                    X = keyX,
                    Y = keyY,
                },
        };
        byte[] derivedKey;
        using (ECDiffieHellman bob = ECDiffieHellman.Create(parameters))
        using (ECDiffieHellmanPublicKey bobPublic = bob.PublicKey)
        {
            return derivedKey = alice.DeriveKeyFromHash(bobPublic, HashAlgorithmName.SHA256);
        }
    }

Solution

  • The solution is simpler if the public key on the Web Crypto side is exported in X.509/SPKI format rather than as a raw key, since .NET 5 has a dedicated import method, ImportSubjectPublicKeyInfo(), for this format. Furthermore, this is consistent with the C# code where the public key is also exported in X.509/SPKI format. In the following example, the Web Crypto code exports the public key in X.509/SPKI format.

    Step 1 - Web Crypto side (Bob): Generate EC key pair

    The following Web Crypto code creates an ECDH key pair and exports the public key in X.509/SPKI format (and the private key in PKCS8 format). Note that exporting in PKCS8 format does not work under Firefox due to a bug, see also here:

    setUpDFHKeys().then(() => {});
    
    async function setUpDFHKeys() {
        var bobKey = await window.crypto.subtle.generateKey(
            { name: 'ECDH', namedCurve: 'P-256' },
            true,
            ["deriveKey"]
        );
        var publicKeyData = await window.crypto.subtle.exportKey("spki", bobKey.publicKey);
        var publicKeyBytes = new Uint8Array(publicKeyData);
        var publicKeyB64 = btoa(String.fromCharCode.apply(null, publicKeyBytes));        
        console.log("Bob's public: \n" + publicKeyB64.replace(/(.{56})/g,'$1\n'));
        var privateKeyData = await window.crypto.subtle.exportKey("pkcs8", bobKey.privateKey);
        var privateKeyBytes = new Uint8Array(privateKeyData);
        var privateKeyB64 = btoa(String.fromCharCode.apply(null, privateKeyBytes));
        console.log("Bob's private:\n" + privateKeyB64.replace(/(.{56})/g,'$1\n'));
    };

    A possible output is the following key pair, which is used as the key pair of the Web Crypto side in the further course:

    Bob's public:  MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2X9cW7P2g4Db3BEgfjs8lwpgukPY4Qg3mcLwVJW+WwA7lbiz+N1MIL3y+JumBF1qIdyx24r5+Sr4c4iYsTWh2w== 
    Bob's private: MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg72fE/+7WX5aKAMiy8kTkCTVeGR8oOlKuoQ8iXTQWmxGhRANCAATZf1xbs/aDgNvcESB+OzyXCmC6Q9jhCDeZwvBUlb5bADuVuLP43UwgvfL4m6YEXWoh3LHbivn5KvhziJixNaHb
    

    Step 2 - C# side (Alice): Import public key from Web Crypto side and generate shared secret

    The following C# code generates an ECDH key pair, exports the public key in X.509/SPKI format (and the private key in PKCS8 format) and determines the shared secret. To obtain the shared secret, the Web Crypto side's public key in X.509/SPKI format is imported with ImportSubjectPublicKeyInfo().

    string alicePublicKey;
    string bobPublicKeyB64 = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2X9cW7P2g4Db3BEgfjs8lwpgukPY4Qg3mcLwVJW+WwA7lbiz+N1MIL3y+JumBF1qIdyx24r5+Sr4c4iYsTWh2w==";
    string sharedSecret = WorkWithJSPublicKey(bobPublicKeyB64, out alicePublicKey);
    Console.WriteLine("Alice's shared secret: " + sharedSecret);
    

    with

    private static string WorkWithJSPublicKey(string bobPublicKeyB64, out string alicePublicKey)
    {
        alicePublicKey = null;
        using (ECDiffieHellman alice = ECDiffieHellman.Create(ECCurve.NamedCurves.nistP256))
        {
            alicePublicKey = Convert.ToBase64String(alice.ExportSubjectPublicKeyInfo());
            Console.WriteLine("Alice's public:        " + alicePublicKey);
            Console.WriteLine("Alice's private:       " + Convert.ToBase64String(alice.ExportPkcs8PrivateKey()));
            ECDiffieHellman bob = ECDiffieHellman.Create();
            bob.ImportSubjectPublicKeyInfo(Convert.FromBase64String(bobPublicKeyB64), out _);
            byte[] sharedSecret = alice.DeriveKeyMaterial(bob.PublicKey);
            return Convert.ToBase64String(sharedSecret);
        }           
    }
    

    A possible output is the following key pair and shared secret. The key pair is used as the key pair of the C# side in the further course:

    Alice's public:        MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJVUW57L2QeswZhnIp5gjMSiHhqyOVTsPUq2QwHv+R4jQetMQ8JDT+3VQyP/dPpskUhzDd3lKxdRBaiZrWby+VQ==
    Alice's private:       MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgbho81UNFdNwULs7IoWk1wSy2PP9soSlt4/bveAtoPBOhRANCAAQlVRbnsvZB6zBmGcinmCMxKIeGrI5VOw9SrZDAe/5HiNB60xDwkNP7dVDI/90+myRSHMN3eUrF1EFqJmtZvL5V
    Alice's shared secret: hayYCAA23oC98d1SxhFpfiYgY5DVElmEno4851HtgKM=
        
    

    Step 3 - Web Crypto side (Bob): Import public key from C# side and generate shared secret

    The following Web Crypto code creates the shared secret. For this the public key of the C# side must be imported. Note that (analogous to the export) the import in PKCS8 format does not work under Firefox due to a bug:

    getSharedSecret().then(() => {});
    
    async function getSharedSecret() {      
        var bobPrivateKeyB64 = 'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg72fE/+7WX5aKAMiy8kTkCTVeGR8oOlKuoQ8iXTQWmxGhRANCAATZf1xbs/aDgNvcESB+OzyXCmC6Q9jhCDeZwvBUlb5bADuVuLP43UwgvfL4m6YEXWoh3LHbivn5KvhziJixNaHb';
        var alicePublicKeyB64 = 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJVUW57L2QeswZhnIp5gjMSiHhqyOVTsPUq2QwHv+R4jQetMQ8JDT+3VQyP/dPpskUhzDd3lKxdRBaiZrWby+VQ==';     
        var privateKey = await window.crypto.subtle.importKey(
            "pkcs8", 
            new Uint8Array(_base64ToArrayBuffer(bobPrivateKeyB64)),
            { name: "ECDH", namedCurve: "P-256" },
            true, 
            ["deriveKey", "deriveBits"] 
        );
        var publicKey = await window.crypto.subtle.importKey(
            "spki", 
            new Uint8Array(_base64ToArrayBuffer(alicePublicKeyB64)),
           { name: "ECDH", namedCurve: "P-256"},
           true, 
           [] 
        );
        var sharedSecret = await window.crypto.subtle.deriveBits(
            { name: "ECDH", namedCurve: "P-256", public: publicKey },
            privateKey, 
           256 
        );
        var sharedSecretHash = await crypto.subtle.digest('SHA-256', sharedSecret);
        var sharedSecretHashB64 = btoa(String.fromCharCode.apply(null, new Uint8Array(sharedSecretHash)));
        console.log("Bob's shared secret: " + sharedSecretHashB64.replace(/(.{64})/g,'$1\n'));
    };
    
    // from https://stackoverflow.com/a/21797381/9014097
    function _base64ToArrayBuffer(base64) {
        var binary_string = window.atob(base64);
        var len = binary_string.length;
        var bytes = new Uint8Array(len);
        for (var i = 0; i < len; i++) {
            bytes[i] = binary_string.charCodeAt(i);
        }
        return bytes.buffer;
    }

    On the Web Crypto side, this results in the shared secret:

    Bob's shared secret: hayYCAA23oC98d1SxhFpfiYgY5DVElmEno4851HtgKM=
    

    in accordance with the C# side.


    Note that DeriveKeyMaterial() on the C# side does not return the actual shared secret S, but the SHA-256 hash H(S) of the shared secret. Since hashes are not reversible, the actual shared secret cannot be determined. Therefore, the only option is to create H(S) on the Web Crypto side by explicit hashing with SHA-256, see also here.