ECDsa Signing in .Net Core on Linux

Viewed 3092

I'm trying to create a C# implementation to send Pushes to Apple via their HTTP/2 APNS Endpoint with .Net core in Docker. Part of this requires sending an encrypted JWT Authorization Token along with the payload. With .Net core, I can sign the token when running on Windows, but when running in the Linux Docker image, it tips over loading the Key.

When running in the .net Core Docker Image, I get a platformnotsupported Exception on loading the key.

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

How can I do this from .Net Core on Linux?

Thanks.

3 Answers

Update: As of .NET Core 3.0, cross-platform PKCS is now built into the framework.

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

Original Answer:

As others have pointed out, BouncyCastle is a cross-platform alternative to Windows CNG. None of the other examples out there were quite working for me, though. Maybe this will help someone:

<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>

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

If your private key length is greater than 31 bytes, which it usually is, you need to get encoded X and Y in order for it to work.

Here is the code for this:

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

Afterward, you can generate a token like this:

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));
Related