转换椭圆曲线参数(BC 到 MS)

Translating Elliptic Curve parameters (BC to MS)

我正在尝试生成 ECDSA 自签名证书,如 中所述。将 bartonjs 的答案中的所有部分放在一起并使用 Net.Framework 4.7(或 Net.Core 2.0)以下代码似乎有效,尽管还剩下一些歧义(至少一个):

我不确定如何正确地将私钥('D' 参数)从 BC-BigInteger 转换为 MS-byte[]。使用 BigInteger.ToByteArray() 抛出异常:

CryptographicException: The specified key parameters are not valid. Q.X and Q.Y are required fields. Q.X, Q.Y must be the same length. If D is specified it must be the same length as Q.X and Q.Y for named curves or the same length as Order for explicit curves.

同时验证 ECParameters(方法 ECParameters.Validate())。使用 BigInteger.ToByteArrayUnsigned() 提供了更好的结果(在数百个生成的密钥对上有一个失败),但仍然...

当使用 ToByteArray() 时,转换后的 'D' 通常长一个字节('D' 有 33 个字节,而 D.X 和 D.Y 有 32 个字节)。使用 ToByteArrayUnsigned() 'D' 有时会短一个字节。

所以我的问题是是否可以使用 ToByteArrayUnsigned()

private const string NCryptExportPolicyProperty = "Export Policy";
private const string SignatureAlgorithm = "Sha256WithECDSA";
private static readonly ECCurve MsCurve = ECCurve.NamedCurves.nistP256;
private static readonly DerObjectIdentifier BcCurve = SecObjectIdentifiers.SecP256r1; // must correspond with MsCurve

public static X509Certificate2 Create()
{    
    // 1. generate keys:
    IAsymmetricCipherKeyPairGenerator bcKeyGen = GeneratorUtilities.GetKeyPairGenerator("ECDSA");
    bcKeyGen.Init(new ECKeyGenerationParameters(BcCurve, new SecureRandom()));

    ECPrivateKeyParameters bcPrivKey;
    ECPublicKeyParameters bcPublKey;

    bool validated;
    ECParameters msEcp;
    do
    {
        AsymmetricCipherKeyPair bcKeyPair = bcKeyGen.GenerateKeyPair();
        bcPrivKey = (ECPrivateKeyParameters)bcKeyPair.Private;
        bcPublKey = (ECPublicKeyParameters)bcKeyPair.Public;

        // 2. ensure generated bc-keys can be translated to cng (see exception below)
        msEcp = new ECParameters();
        msEcp.Curve = MsCurve;
        msEcp.D = bcPrivKey.D.ToByteArrayUnsigned(); // or bcPrivKey.D.ToByteArray() ??
        msEcp.Q.X = bcPublKey.Q.XCoord.GetEncoded();
        msEcp.Q.Y = bcPublKey.Q.YCoord.GetEncoded();

        try
        {
            msEcp.Validate();
            validated = true;
        }
        catch (Exception e)
        {
            // Validate() occasionally throws CryptographicException: 
            // The specified key parameters are not valid. Q.X and Q.Y are required fields. Q.X, Q.Y must be the same length. If D is specified it must be the same length as Q.X and Q.Y for named curves or the same length as Order for explicit curves.
            // e.g.: D = 31, Q.X = 32, Q.Y = 32.
            validated = false;
            Console.WriteLine("D = {0}, Q.X = {1}, Q.Y = {2}. {3}: {4}", msEcp.D.Length, msEcp.Q.X.Length, msEcp.Q.Y.Length, e.GetType().Name, e.Message);
        }
    } while (!validated);

    // 3. create x509 certificate:
    X509V3CertificateGenerator bcCertGen = new X509V3CertificateGenerator();
    bcCertGen.SetPublicKey(bcPublKey);
    // .. set subject, validity period etc
    ISignatureFactory sigFac = new Asn1SignatureFactory(SignatureAlgorithm, bcPrivKey);
    Org.BouncyCastle.X509.X509Certificate bcX509Cert = bcCertGen.Generate(sigFac);
    byte[] x509CertEncoded = bcX509Cert.GetEncoded();

    X509Certificate2 msNewCert;

    // 4. use translated (and validated) parameters:
    using (ECDsaCng msEcdsa = new ECDsaCng())
    {
        msEcdsa.ImportParameters(msEcp);

        CngKey msPrivateKey = msEcdsa.Key;

        // 5. make private key exportable:
        byte[] bytes = BitConverter.GetBytes((int)(CngExportPolicies.AllowExport | CngExportPolicies.AllowPlaintextExport));
        CngProperty pty = new CngProperty(NCryptExportPolicyProperty, bytes, CngPropertyOptions.Persist);
        msPrivateKey.SetProperty(pty);

        // 6. tie keys together:
        using (X509Certificate2 msPubCertOnly = new X509Certificate2(x509CertEncoded))
        {
            msNewCert = MateECDsaPrivateKey(msPubCertOnly, msPrivateKey); // method from bartonjs's answer
        }
    }

    return msNewCert;
}

提前致谢

当您获得太多字节(在本例中为 33 个)时,第一个字节应该是 0x00,您需要将其删除。当你得到的太少时(从技术上讲 D=1 是有效的)你需要插入零来填充数组。

原因是 .NET 的结构期望 D 看起来像它对基础 Windows CNG 导入 API 所做的那样,这意味着 D 是一个固定的无符号大端大整数。 BouncyCastle 为您提供 BER INTEGER 编码,当最高有效字节(bytes[0],big endian)的高位设置为一个应该被视为正数的数字时,它需要插入一个 0x00 字节。

BER 也有使用最小字节数的规则,这就是为什么有时 BouncyCastle 给出的数字太小。

Q.X 和 Q.Y 可以,因为 ECPoint 编码规则指定了一个固定大小的大端整数,其大小由曲线确定;这就是为什么 BouncyCastle 有 GetEncoded 方法而不只是 ToByteArrayUnsigned.

的原因
private static byte[] FixSize(byte[] input, int expectedSize)
{
    if (input.Length == expectedSize)
    {
        return input;
    }

    byte[] tmp;

    if (input.Length < expectedSize)
    {
        tmp = new byte[expectedSize];
        Buffer.BlockCopy(input, 0, tmp, expectedSize - input.Length, input.Length);
        return tmp;
    }

    if (input.Length > expectedSize + 1 || input[0] != 0)
    {
        throw new InvalidOperationException();
    }

    tmp = new byte[expectedSize];
    Buffer.BlockCopy(input, 1, tmp, 0, expectedSize);
    return tmp;
}

...

msEcp = new ECParameters();
msEcp.Curve = MsCurve;
msEcp.Q.X = bcPublKey.Q.XCoord.GetEncoded();
msEcp.Q.Y = bcPublKey.Q.YCoord.GetEncoded();
msEcp.D = FixSize(bcPrivKey.D.ToByteArrayUnsigned(), msEcp.Q.X.Length);

下面的代码可以帮到你,你可以使用bouncy castle库生成算法:

private static ECDsa GetEllipticCurveAlgorithm(string privateKey)
    {
        var keyParams = (ECPrivateKeyParameters)PrivateKeyFactory
            .CreateKey(Convert.FromBase64String(privateKey));

        var normalizedECPoint = keyParams.Parameters.G.Multiply(keyParams.D).Normalize();

        return ECDsa.Create(new ECParameters
        {
            Curve = ECCurve.CreateFromValue(keyParams.PublicKeyParamSet.Id),
            D = keyParams.D.ToByteArrayUnsigned(),
            Q =
        {
            X = normalizedECPoint.XCoord.GetEncoded(),
            Y = normalizedECPoint.YCoord.GetEncoded()
        }
        });
    }

并通过以下方式生成令牌:

var signatureAlgorithm = GetEllipticCurveAlgorithm(privateKey);

            ECDsaSecurityKey eCDsaSecurityKey = new ECDsaSecurityKey(signatureAlgorithm)
            {
                KeyId = settings.Apple.KeyId
            };

            var handler = new JwtSecurityTokenHandler();   
            var token = handler.CreateJwtSecurityToken(
                issuer: iss,
                audience: AUD,
                subject: new ClaimsIdentity(new List<Claim> { new Claim("sub", sub) }),
                expires: DateTime.UtcNow.AddMinutes(5), 
                issuedAt: DateTime.UtcNow,
                notBefore: DateTime.UtcNow,
                signingCredentials: new SigningCredentials(eCDsaSecurityKey, SecurityAlgorithms.EcdsaSha256));