在 Linux 上登录 .Net Core 的 ECDsa

Joe*_*ess 6 apple-push-notifications ecdsa .net-core

我正在尝试创建一个 C# 实现,通过他们的 HTTP/2 APNS 端点和 Docker 中的 .Net 核心向 Apple 发送推送。其中一部分需要将加密的 JWT 授权令牌与有效负载一起发送。使用 .Net 核心,我可以在 Windows 上运行时对令牌进行签名,但是在 Linux Docker 映像中运行时,它会提示加载密钥。

在 .net Core Docker Image 中运行时,我在加载密钥时遇到 platformnotsupported Exception。

    public static string SignES256(string privateKey, string header, string payload)
    {

        // This is the failing Call
        CngKey key = CngKey.Import(Convert.FromBase64String(privateKey), CngKeyBlobFormat.Pkcs8PrivateBlob);

        using (ECDsaCng dsa = new ECDsaCng(key))
        {
            var unsignedJwtData =
                System.Convert.ToBase64String(Encoding.UTF8.GetBytes(header)) + "." + System.Convert.ToBase64String(Encoding.UTF8.GetBytes(payload));
            var unsignedJwtDataBytes = Encoding.UTF8.GetBytes(unsignedJwtData);

            var signature =
                dsa.SignData(unsignedJwtDataBytes, 0, unsignedJwtDataBytes.Length, HashAlgorithmName.SHA256 );
            return unsignedJwtData + "." + System.Convert.ToBase64String(signature);
        }
    }
Run Code Online (Sandbox Code Playgroud)

如何从 Linux 上的 .Net Core 执行此操作?

谢谢。

Kyl*_*lan 6

更新:从 .NET Core 3.0 开始,跨平台 PKCS 现在已内置到框架中

private static ECDsa GetEllipticCurveAlgorithm(string privateKey)
{
    var result = ECDsa.Create();
    result.ImportPkcs8PrivateKey(Convert.FromBase64String(privateKey), out _);
    return result;
}
Run Code Online (Sandbox Code Playgroud)

原答案:

正如其他人指出的那样,BouncyCastle 是 Windows CNG 的跨平台替代品。不过,那里的其他示例都没有对我有用。也许这会帮助某人:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Portable.BouncyCastle" Version="1.8.5" />
    <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="5.4.0" />
  </ItemGroup>

</Project>

Run Code Online (Sandbox Code Playgroud)
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Cryptography;
using Microsoft.IdentityModel.Tokens;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;

public class ApnsTokenProvider
{
    private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler();

    // Turn off caching so TokenHandler doesn't try to reuse disposed algorithms.
    private readonly CryptoProviderFactory _signingFactory = new CryptoProviderFactory
    {
        CacheSignatureProviders = false
    };

    public string GetToken(string teamId, string keyId, string privateKey)
    {
        using (var algorithm = GetEllipticCurveAlgorithm(privateKey))
        {
            var credentials = new SigningCredentials(new ECDsaSecurityKey(algorithm)
            {
                KeyId = keyId,
                CryptoProviderFactory = _signingFactory
            }, SecurityAlgorithms.EcdsaSha256);

            return _tokenHandler.CreateEncodedJwt(new SecurityTokenDescriptor
            {
                Issuer = teamId,
                SigningCredentials = credentials
            });
        }
    }

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

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

        return ECDsa.Create(new ECParameters
        {
            Curve = ECCurve.CreateFromValue(keyParams.PublicKeyParamSet.Id),
            D = keyParams.D.ToByteArrayUnsigned(),
            Q =
            {
                X = q.XCoord.GetEncoded(),
                Y = q.YCoord.GetEncoded()
            }
        });
    }
}
Run Code Online (Sandbox Code Playgroud)


bar*_*njs 5

ECDsaCng 是使用 Windows CNG 的 ECDSA 实现。它特定于 Windows,因此在 Linux 上不受支持。

这样做的跨平台方法是

using (ECDsa ecdsa = ECDsa.Create())
{
    ecdsa.ImportParameters(Pkcs8ToParameters(privateKey));

    // the stuff in your current using 
}
Run Code Online (Sandbox Code Playgroud)

当然,从 PKCS#8 到 ECParameters 并不是世界上最简单的事情。但我们可以试一试。在另一个答案中有为 RSA 构建 PKCS#8 的细分。

让我们以这个 blob 为例:

308187020100301306072A8648CE3D020106082A8648CE3D030107046D306B02
0101042070A12C2DB16845ED56FF68CFC21A472B3F04D7D6851BF6349F2D7D5B
3452B38AA144034200048101ECE47464A6EAD70CF69A6E2BD3D88691A3262D22
CBA4F7635EAFF26680A8D8A12BA61D599235F67D9CB4D58F1783D3CA43E78F0A
5ABAA624079936C0C3A9
Run Code Online (Sandbox Code Playgroud)

它崩溃了

30 /* SEQUENCE */
   81 87 (payload is 0x87 bytes)

   02 /* INTEGER */ 01 (1 byte) 00 // Integer: 0. // validate this

   30 /* SEQUENCE */ 13 (0x13 bytes)

      06 /* OBJECT IDENTIFIER */ 07 (7 bytes)
         2A8648CE3D0201  (1.2.840.10045.2.1 / ecPublicKey) // validate this

      06 /* OBJECT IDENTIFIER */ 08 (8 bytes)
         2A8648CE3D030107 (1.2.840.10045.3.1.7 / secp256r1) // save this, curveName

   04 /* OCTET STREAM (byte[]) */ 6D (0x6D bytes)
      // Since the constructed (0x20) bit isn't set in the tag normally we stop here,
      // but we know from the ecPublicKey context that this is also DER data.

      30 /* SEQUENCE */ 6B (0x6B bytes)

         02 /* Integer */ 01 (1 byte) 01 // Integer: 1. // validate this.

         04 /* OCTET STREAM (byte[]) */ 20 (0x20 bytes / 256 bits)
            70A12C2DB16845ED56FF68CFC21A472B3F04D7D6851BF6349F2D7D5B3452B38A // save this: D

         A1 /* CONSTRUCTED CONTEXT SPECIFIC 1 */ 44 (0x44 bytes)

            03 /* BIT STRING (byte[] if the first byte is 0x00) */ 66 (0x66 bytes)

               00 // Oh, good, it's a normal byte[]. Validate this.

               // Formatting will become apparent. Save this.
               04
               8101ECE47464A6EAD70CF69A6E2BD3D88691A3262D22CBA4F7635EAFF26680A8
               D8A12BA61D599235F67D9CB4D58F1783D3CA43E78F0A5ABAA624079936C0C3A9
Run Code Online (Sandbox Code Playgroud)

末尾的 BIT STRING 是“公钥”。因为它以04(通常会这样,除非发件人对你生气)它代表一个“未压缩的点”,这意味着剩下的前半部分是 X 坐标,其余部分是 Y 坐标。所以从这个结构你可能会得到类似的东西

string curveOid;

// You can decode the OID, or special case it.
switch (curveName)
{
    case "2A8648CE3D030107":
        // secp256r1
        curveOid = "1.2.840.10045.3.1.7";
        break;
    case "2B81040022"
        // secp384r1
        curveOid = "1.3.132.0.34";
        break;
    case "2B81040023":
        // secp521r1
        curveOid = "1.3.132.0.35";
        break;
    default:
        throw new InvalidOperationException();
 }

 return new ECParameters
 {
     Curve = ECCurve.CreateFromValid(curveOid),

     // We saved this.
     D = d,

     Q = new ECPoint
     {
        X = x,
        Y = y
     },
 }
Run Code Online (Sandbox Code Playgroud)

这恰好是Suite B 实施者指南 FIPS 186-3 (ECDSA) 的D.1 节 (NIST P-256 / secp256r1) 中使用的密钥。

由于 EC 密钥格式在 INTEGER 值(可能需要填充字节)方面非常短,因此您可以为要支持的每个密钥大小构建一个手动提取器。或者你可以去现场 DER 阅读路线。或者您可以尝试以更友好的形式为您的应用程序序列化您的私钥。