C# 和 PHP ECDH 不匹配

C# and PHP ECDH not matching

我正在尝试在 Web 服务器 运行 PHP 和 C# 桌面应用程序之间生成共享密钥。我知道 BouncyCastle 库,但我不想使用它,因为它非常庞大。

我正在使用 phpecc and ECDiffieHellmanCng 并尝试生成双方之间的共享密钥,但我在 C# 中遇到 exporting/importing 问题。

似乎 phpecc 需要 der/pem 格式才能导入密钥,ECDiffieHellmanCng 似乎没有任何简单的方法可以以兼容的格式导出。

我是否需要编写自己的 pem/der 编码器和解码器才能做到这一点,或者是否有其他更简单的方法?

目前我正在用 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();
    }

显然上面只进行了 pem 编码,所以在 php 方面它 returns 试图解析它时出错:

Type: Runtime

Exception Message: Invalid data.

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

Line: 49

.NET Core 1.0 和 .NET Framework 4.7 have the ECParameters struct to import/export keys. The ToByteArray() method you called is producing a CNG EccPublicBlob 与 SEC-1 ECParameters 格式关系不大。

我假设您想使用 secp384r1/NIST P-384,即使您将其指定为哈希算法。如果你想要一些其他的曲线,你需要做一些翻译。

(.NET) ECParameters 结构只会帮助您入门。将其转换为文件需要将其转换为 PEM 编码的 DER 编码的基于 ASN.1 的结构。 (但是如果你坚持使用 NIST P-256/384/521,你可以使用你当前拥有的 byte[] 来完成)

SEC 1 v2.0中我们得到以下结构:

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.

将其分解为相关部分,您需要编写

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

如果您不更改曲线,AlgorithmIdentifier 包含固定的数据:

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

我们现在可以计算有效载荷中有多少字节:16(0x10),所以我们填写长度:

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

大家理解的public键编码就是"uncompressed point",也就是

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

事实证明,对于给定的曲线,它也具有固定大小,因此与大多数 DER 编码的东西不同,您可以一次性完成此操作 :)。对于 secp384r1,x 和 y 坐标均为 384 位值,或 (384 + 7)/8 == 48 字节,因此 ECPoint 为 48 + 48 + 1 == 97 (0x61) 字节。然后它需要被包裹在一个 BIT STRING 中,它添加了一个有效载荷字节以及长度和标签。所以,我们得到:

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());
}

运行 我从中得到的输出到 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