15

I am working on a project to integrate with the new Push API that exists in Firefox and is being developed as a W3C standard.

Part of this is encrypting the data. The server will receive a Diffie Hellman P256 Curve (Generated in JS using var key = subscription.getKey('p256dh');)

An example of this when converted to a .NET base64 is

BOAiqZO6ucAzDlZKKhF1aLjNpU8+R2Pfsz4bQzNpV145D+agNxvLqyu5Q2tLalK2w31RpoDHE8Sipo0m2jiX4WA=

However I ran into issues generating the Derived Material.

var key1 = Convert.FromBase64String("<stringFromAbove>").ToList() // You can criticize my .toList inefficiencies later

// .NET doesn't like the key without these prefixes. See here
// http://stackoverflow.com/questions/24251336/import-a-public-key-from-somewhere-else-to-cngkey
// I know the bytes don't match that post, but that is because the key type is different between their example and mine.
var keyType = new byte[] { 0x45, 0x43, 0x4B, 0x31 };
var keyLength = new byte[] { 0x20, 0x00, 0x00, 0x00 };
key1.RemoveAt(0);
key1 = keyType.Concat(keyLength).Concat(key1).ToList();

ECDiffieHellmanCng a = new ECDiffieHellmanCng();
a.KeyDerivationFunction = ECDiffieHellmanKeyDerivationFunction.Hash;
// If I set this as CngAlgorithm.Sha256 it works, but that's not what Firefox gives me.
a.HashAlgorithm = CngAlgorithm.ECDiffieHellmanP256; 
a.KeySize = 256; // It complains if I don't add this since keys are different lengths.

// Now time to actually import the key
CngKey k = CngKey.Import(key1.ToArray(), CngKeyBlobFormat.EccPublicBlob); // Works successfully
byte[] derivedMaterial = a.DeriveKeyMaterial(k); // Exception Here

System.Security.Cryptography.CryptographicException: The requested operation is not supported.

What do I not understand correctly (or on the more sad side, what is not implemented correctly (or at all) in windows/.NET)?

As an alternative, if somebody could explain how to port this Node JS library to .NET that'd work too (I think that's a bit of a reach)

Update
I needed to keep working through the rest of the problem and not be held up by the encryption, so I used a Node.JS Wrapper to allow for further development on the .NET side. The node code simply generates the local public key and the Shared secret and returns those values to me. I still need to get this working without the Node wrapper.

Because of this test I can confirm that the rest of the code (not included here) works, so the issue definitely lies in the code above (and my inability to generate the derived key material if the HashAlgorithm is specified as CngAlgorithm.ECDiffieHellmanP256

Dan Drews
  • 1,918
  • 15
  • 36
  • On what OS and Framework version are you working on? – Tamas Ionut Feb 27 '16 at 17:48
  • Running on Windows. .NET framework 4.6 – Dan Drews Feb 27 '16 at 20:33
  • Have you seen this: http://stackoverflow.com/questions/31330363/does-ecdiffiehellmancng-in-net-have-a-key-derivation-function-that-implements-n – Simon Mourier Feb 28 '16 at 18:06
  • @SimonMourier Yes I did see that one. Doesn't seem to do what I need. When I try to import the code I get errors with the key being the wrong length. It's been a couple of days so I don't remember the exact error, but there were problems. – Dan Drews Feb 28 '16 at 18:08
  • 1
    @SimonMourier The error I get: "Additional information: DER length more than 4 bytes: 38" – Dan Drews Feb 29 '16 at 14:42
  • 1
    But, ECDiffieHellmanP256 is not a hash algorithm is it? What do you mean by "that's not what Firefox gives me"? – Simon Mourier Mar 01 '16 at 17:25
  • @SimonMourier ECDiffieHellmanP256 is an option in the CngAlgorithm Enum, which is what that is expecting. Whether it's a hash algorithm or not really never occurred to me, but it is an option in the enum so there should probably be documentation on why it is not supported. The question explains that in Firefox I am trying to get the subscription auth key and it returns to me a Diffie Hellman P256 Curve. – Dan Drews Mar 01 '16 at 17:36
  • In fact it's fairly logical, you specify a hash KDF, so you need a hash algorithm (and KDF only support HASH, HMAC or TLS anyway). The enum just lists all standard CNG algorithms, not only hash ones. ECDiffieHellmanP256 is not hash but key exchange hence the error. Also, down the road, .NET uses NCryptDeriveKey (https://msdn.microsoft.com/library/windows/desktop/aa376252.aspx) with HASH as the KDF parameter. Still, why can't you/your firefox app use the key returned with SHA256 (the default hash algorithm uses by .NET for DeriveKey)? – Simon Mourier Mar 01 '16 at 18:06
  • @SimonMourier I know very little about encryption. All I know is that I tried that and it did not produce the correct results (Firefox's push server returned an error related to encryption). Could it be that since we are using different algorithms, we are deriving different shared secrets? So I encrypt it with what I believe the secret to be, and they do so with what they expect the secret to be and they do not match up? That's what I've been assuming. – Dan Drews Mar 01 '16 at 18:08
  • The problem may be *after* your sample code, yes. You need to show us what you do once you have that key from DeriveKeyMaterial, and what does the push server do with the key on its side also, so you can compare both. It may be a small problem, similar to te keyType+keyLength fixes you use. – Simon Mourier Mar 01 '16 at 18:39
  • I don't have access to the Mozilla push server. It is a hosted thing based on the spec outlined in the links above. – Dan Drews Mar 01 '16 at 19:38
  • Ok, so that's a lot more complicated. Specs are in fact here: https://tools.ietf.org/html/draft-ietf-webpush-encryption-01#section-3 and here: https://tools.ietf.org/html/draft-thomson-http-encryption-01#section-3.2 your link is an example of such an implementation but it relies on multiple node.js packages (there is a salt to use, etc.). Your code is just a very small part of the whole thing. It's certainly possible to do this in .NET but there's work to do.. – Simon Mourier Mar 01 '16 at 23:38
  • I was able to implement the rest by porting the JS over. Took a while but it worked. All I need now is the shared secret to be generated :) – Dan Drews Mar 01 '16 at 23:45

2 Answers2

5

This solution is only confirmed working on Windows 10 64-bit. It is confirmed not working on Windows 8.1 64 bit, and is untested on other platforms.

The problem is that ECDiffieHellmanP256 is not a hash algorithm, but you are specifying to use a hash key derivation function. Your KeyDerivationFunction should be set to ECDiffieHellmanKeyDerivationFunction.Tls, and you need to specify the seed and label for the KDF.

Your fixed code looks like this:

var key1 = Convert.FromBase64String("BOAiqZO6ucAzDlZKKhF1aLjNpU8+R2Pfsz4bQzNpV145D+agNxvLqyu5Q2tLalK2w31RpoDHE8Sipo0m2jiX4WA=").ToList();
var keyType = new byte[] { 0x45, 0x43, 0x4B, 0x31 };
var keyLength = new byte[] { 0x20, 0x00, 0x00, 0x00 };
key1.RemoveAt(0);
key1 = keyType.Concat(keyLength).Concat(key1).ToList();

ECDiffieHellmanCng a = new ECDiffieHellmanCng();
a.KeyDerivationFunction = ECDiffieHellmanKeyDerivationFunction.Tls;

byte[] label = new byte[32];
string labelStr = "The purpose";
Encoding.ASCII.GetBytes(labelStr, 0, labelStr.Length, label, 0);
a.Label = label;

byte[] seed = new byte[32];
RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
rng.GetBytes(seed);
a.Seed = seed;

a.HashAlgorithm = CngAlgorithm.ECDiffieHellmanP256;
a.KeySize = 256;

CngKey k = CngKey.Import(key1.ToArray(), CngKeyBlobFormat.EccPublicBlob);
byte[] derivedMaterial = a.DeriveKeyMaterial(k);

Note that I set a nonsense value to the a.Label property.

The NIST SP 800-108 publication defines the label as:

Label – A string that identifies the purpose for the derived keying material, which is encoded as a binary string.

I'm not sure what the purpose should be set to in your specific context. If anyone has a better understanding what this string should be, please leave a comment.

Also note that if you're going to call this function repeatedly, you should probably keep a persistent copy of the RNGCryptoServiceProvider and use that.

Thanks to a comment by Simon Mourier which got me on the right track.

Community
  • 1
  • 1
Gediminas Masaitis
  • 2,956
  • 10
  • 33
  • Your code gives me the very helpful exception: `The parameter is incorrect.` Last time I had this issue it had to do with the "KeyType" and "KeyLength" properties (adding those fixed it), when I called `import` is when I would get it. Now it is happening on the DeriveKeyMaterial Call – Dan Drews Mar 03 '16 at 23:02
  • @DanDrews That's strange, it works fine for me on .NET 4.6. Apart from what you mentioned, you can get that exception when your initial `key1` is not valid. Make sure that's not the case by temporarily hardcoding in the string in my example (which I took from your example). – Gediminas Masaitis Mar 03 '16 at 23:15
  • Just tried in a new project with a direct copy/paste of your code, set framework to .NET 4.6 On this machine I'm running Windows 8.1, what are you running? – Dan Drews Mar 03 '16 at 23:18
  • @DanDrews I'm running Windows 10 64 bit. If you want I can try to compile a library assembly, see if it works on your machine. – Gediminas Masaitis Mar 03 '16 at 23:26
  • I'm on my way home now. I have a Windows 10 machine there. Hard to say if that's the issue or not but I'll try it and get back to you in a few hours. I'm really hoping to give you the bounty and appreciate the help! – Dan Drews Mar 03 '16 at 23:29
  • @DanDrews I just confirmed that it failed with `The parameter is incorrect` on my Windows 8.1 64-bit laptop. Tried both using the exact same executable which I sent over, and recompiled on the machine. Sadly my fix seems to be very platform-specific.. – Gediminas Masaitis Mar 03 '16 at 23:49
  • If it works for me on Windows 10 I'm gonna go ahead and award you the bounty. We are going to be upgrading all of the Dev machines to Windows 10 soon, and our Server is the newest Server OS I believe. We have the NODE wrapper for now and hopefully we help someone along the way :) – Dan Drews Mar 03 '16 at 23:53
  • 1
    @DanDrews Don't rush it, wait a day or two. As much as I'd like to "cash in", it's perfectly possible that someone else, or perhaps me myself, will come up with a better platform-independent solution. The correct thing to do now would be to upvote, since it did work. Just don't forget it if nothing else happens for a long time :) – Gediminas Masaitis Mar 03 '16 at 23:55
  • Worked on my Windows 10 machine. I'll hold off on the acceptance. Thanks a ton for your help though! – Dan Drews Mar 03 '16 at 23:57
  • @DanDrews I updated my answer to contain the platform dependability warning. And I just noticed that your bounty expires tomorrow. Waiting a day or two may not be an option after all :) Since the time is almost up, the question you need to ask yourself now is whether or not this only answer satisfies your question, or did no one answer it fully. – Gediminas Masaitis Mar 04 '16 at 00:10
  • Doesn't matter :) can't get a bounty refund. Yours will auto accept if I don't get other answers. I was honestly afraid of not getting an answer with how long it took. I am very happy with this one – Dan Drews Mar 04 '16 at 00:29
  • 1
    @DanDrews I think you misunderstood how bounty auto-awarding works. There are some citeria (in this case my answer doesn't meet them), and in no case is a full bounty auto-awarded. See [here](http://meta.stackexchange.com/q/16065/133693) for more details. – Gediminas Masaitis Mar 04 '16 at 00:45
-1

KeyDerivationFunction is post processing: https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.ecdiffiehellmancng?view=netframework-4.7#remarks

so maybe it's Computing the Master Secret https://tools.ietf.org/html/rfc5246#section-8.1

ECDiffieHellmanCng.Label = Encoding.ASCII.GetBytes("master secret");
ECDiffieHellmanCng.Seed = clientRandom.Concat(serverRandom).ToArray();
//there is also a function like this:
//ECDiffieHellmanCng.DeriveKeyTls(ECDiffieHellmanPublicKey otherPartyPublicKey, byte[] prfLabel, byte[] prfSeed)

so Label and Seed must be used in PRF, which is kinda werid I think, why ECDiffieHellman do PRF anyway

sorry I should have added a comment to accepted answer but you must have 50 reputation to comment

Leowan
  • 11
  • 4