C# and PHP ECDH not matching

2019-09-14 18:02发布

问题:

I'm trying to generate a shared secret between a web server running PHP and a C# desktop application. I'm aware of the BouncyCastle library, but I'd prefer not having to use it since it's pretty huge.

I'm using phpecc and ECDiffieHellmanCng and trying to generate a shared secret between the two parties but I'm having issues with exporting/importing in C#.

It seems phpecc requires der/pem format in order to import a key, and ECDiffieHellmanCng doesn't seem to have any easy way to export in a compatible format.

Would I need to write my own pem/der encoder and decoder in order to do this or is there some alternative easier way?

Currently I'm doing the following in C#:

using (var ecdh = new ECDiffieHellmanCng())
        {
            ecdh.HashAlgorithm = CngAlgorithm.ECDiffieHellmanP384;
            ecdh.KeyDerivationFunction = ECDiffieHellmanKeyDerivationFunction.Hash;

            var encoded = EncodePem(ecdh.PublicKey.ToByteArray()); 
            //... do something with encoded
        }

private static string EncodePem(byte[] data)
    {
        var pemDat = new StringBuilder();
        var chunk = new char[64];

        pemDat.AppendLine("-----BEGIN PUBLIC KEY-----");

        var encodedData = Convert.ToBase64String(data);
        for (var i = 0; i < encodedData.Length; i += chunk.Length)
        {
            var index = 0;
            while (index != chunk.Length && i + index < encodedData.Length)
            {
                chunk[index] = encodedData[i + index];
                index++;
            }
            pemDat.AppendLine(new string(chunk));
        }

        pemDat.AppendLine("-----END PUBLIC KEY-----");
        return pemDat.ToString();
    }

Obviously the above is only doing the pem encoding, so on the php side it returns an error when it's trying to parse it:

Type: Runtime

Exception Message: Invalid data.

File: /.../vendor/mdanter/ecc/src/Serializer/PublicKey/Der/Parser.php

Line: 49

回答1:

.NET Core 1.0 and .NET Framework 4.7 have the ECParameters struct to import/export keys. The ToByteArray() method you called is producing a CNG EccPublicBlob which has very little to do with the SEC-1 ECParameters format.

I'm going to assume that you wanted to use secp384r1/NIST P-384, even though you specified that as a hash algorithm. If you want some other curve, you'll need to do some translations.

The (.NET) ECParameters struct will only help you get started. Turning that into a file requires translating it into a PEM-encoded DER-encoded ASN.1-based structure. (But if you're sticking with NIST P-256/384/521, you can do it with the byte[] you currently have)

In SEC 1 v2.0 we get the following structures:

SubjectPublicKeyInfo ::= SEQUENCE {
  algorithm AlgorithmIdentifier {{ECPKAlgorithms}} (WITH COMPONENTS {algorithm, parameters}),
  subjectPublicKey BIT STRING
}

ECPKAlgorithms ALGORITHM ::= {
  ecPublicKeyType |
  ecPublicKeyTypeRestricted |
  ecPublicKeyTypeSupplemented |
  {OID ecdh PARMS ECDomainParameters {{SECGCurveNames}}} |
  {OID ecmqv PARMS ECDomainParameters {{SECGCurveNames}}},
  ...
}

ecPublicKeyType ALGORITHM ::= {
  OID id-ecPublicKey PARMS ECDomainParameters {{SECGCurveNames}}
}

ECDomainParameters{ECDOMAIN:IOSet} ::= CHOICE {
  specified SpecifiedECDomain,
  named ECDOMAIN.&id({IOSet}),
  implicitCA NULL
}

An elliptic curve point itself is represented by the following type
  ECPoint ::= OCTET STRING
whose value is the octet string obtained from the conversion routines given in Section 2.3.3.

Distilling this down to the relevant parts, you need to write

SEQUENCE (SubjectPublicKeyInfo)
  SEQUENCE (AlgorithmIdentifier)
    OBJECT IDENTIFIER id-ecPublicKey
    OBJECT IDENTIFIER secp384r1 (or whatever named curve you're using)
  BIT STRING
    public key encoded as ECPoint

The AlgorithmIdentifier contains data that's fixed given you don't change the curve:

SEQUENCE (AlgorithmIdentifier)
30 xx [yy [zz]]
   OBJECT IDENTIFIER id-ecPublicKey (1.2.840.10045.2.1)
   06 07 2A 86 48 CE 3D 02 01
   OBJECT IDENTIFIER secp384r1 (1.3.132.0.34)
   06 05 2B 81 04 00 22

and we can now count how many bytes were in the payload: 16 (0x10), so we fill in the length:

30 10 06 07  2A 86 48 CE   3D 02 01 06  05 2B 81 04
00 22

The public key encoding that everyone understands is "uncompressed point", which is

04 th eb yt es of x. th eb yt es of y.

Turns out, that has a fixed size for a given curve, too, so unlike most things that are DER encoded, you can do this in one pass :). For secp384r1 the x and y coordinate are each 384 bit values, or (384 + 7)/8 == 48 bytes, so the ECPoint is 48 + 48 + 1 == 97 (0x61) bytes. Then it needs to be wrapped in a BIT STRING, which adds one payload byte and the length and tag. So, we get:

private static byte[] s_secp384r1PublicPrefix = {
    // SEQUENCE (SubjectPublicKeyInfo, 0x76 bytes)
    0x30, 0x76,
    // SEQUENCE (AlgorithmIdentifier, 0x10 bytes)
    0x30, 0x10,
    // OBJECT IDENTIFIER (id-ecPublicKey)
    0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01,
    // OBJECT IDENTIFIER (secp384r1)
    0x06, 0x05, 0x2B, 0x81, 0x04, 0x00, 0x22,
    // BIT STRING, 0x61 content bytes, 0 unused bits.
    0x03, 0x62, 0x00,
    // Uncompressed EC point
    0x04,
}

...

using (ECDiffieHellman ecdh = ECDiffieHellman.Create())
{
    ecdh.KeySize = 384;

    byte[] prefix = s_secp384r1PublicPrefix;
    byte[] derPublicKey = new byte[120];
    Buffer.BlockCopy(prefix, 0, derPublicKey, 0, prefix.Length);

    byte[] cngBlob = ecdh.PublicKey.ToByteArray();
    Debug.Assert(cngBlob.Length == 104);

    Buffer.BlockCopy(cngBlob, 8, derPublicKey, prefix.Length, cngBlob.Length - 8);

    // Now move it to PEM
    StringBuilder builder = new StringBuilder();
    builder.AppendLine("-----BEGIN PUBLIC KEY-----");
    builder.AppendLine(
        Convert.ToBase64String(derPublicKey, Base64FormattingOptions.InsertLineBreaks));
    builder.AppendLine("-----END PUBLIC KEY-----");

    Console.WriteLine(builder.ToString());
}

Running the output I got from that into OpenSSL:

$ openssl ec -pubin -text -noout
read EC key
(paste)
-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEwpbxYmcsNvr14D8k+0VQCkSY4WCV/3V10AiIq7sFdmUX
9+0DMuuLDmcKjL1ZFEFk0yHCPpY+pdkYtzPwE+dsApCPT3Ljk0AxHQBTSo4yjwsElMoA4Mtp8Qdo
LZD1Nx6v
-----END PUBLIC KEY-----
Private-Key: (384 bit)
pub:
    04:c2:96:f1:62:67:2c:36:fa:f5:e0:3f:24:fb:45:
    50:0a:44:98:e1:60:95:ff:75:75:d0:08:88:ab:bb:
    05:76:65:17:f7:ed:03:32:eb:8b:0e:67:0a:8c:bd:
    59:14:41:64:d3:21:c2:3e:96:3e:a5:d9:18:b7:33:
    f0:13:e7:6c:02:90:8f:4f:72:e3:93:40:31:1d:00:
    53:4a:8e:32:8f:0b:04:94:ca:00:e0:cb:69:f1:07:
    68:2d:90:f5:37:1e:af
ASN1 OID: secp384r1
NIST CURVE: P-384