6

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.

JoelHess
  • 1,166
  • 2
  • 15
  • 28

3 Answers3

6

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()
            }
        });
    }
}
Kyle McClellan
  • 664
  • 7
  • 23
  • ImportPkcs8PrivateKey works on my Windows computer but fails on Windows Server for me. I had to use the BouncyCastle version. – Gandalf458 Mar 27 '23 at 22:02
5

ECDsaCng is an ECDSA implementation using Windows CNG. It's specific to Windows, so not supported on Linux.

The cross-platform way to do this would be

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

    // the stuff in your current using 
}

Of course, PKCS#8 to ECParameters isn't the easiest thing in the world. But we can give it a go. In another answer there's a breakdown of building a PKCS#8 for RSA.

Let's take this blob:

308187020100301306072A8648CE3D020106082A8648CE3D030107046D306B02
0101042070A12C2DB16845ED56FF68CFC21A472B3F04D7D6851BF6349F2D7D5B
3452B38AA144034200048101ECE47464A6EAD70CF69A6E2BD3D88691A3262D22
CBA4F7635EAFF26680A8D8A12BA61D599235F67D9CB4D58F1783D3CA43E78F0A
5ABAA624079936C0C3A9

It breaks down like

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

The BIT STRING at the end is "the public key". Since it starts with 04 (which it usually will, unless the sender is mad at you) it represents an "uncompressed point", meaning the first half of what's left is the X coordinate, and the remainder is the Y coordinate. So from this structure you might get something like

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

This happens to be the key used in section D.1 (NIST P-256 / secp256r1) of Suite B Implementer’s Guide to FIPS 186-3 (ECDSA).

Since the EC key format is mercifully short on INTEGER values (which can require padding bytes) you can build a manual extractor for each keysize you want to support. Or you can go the live DER reading route. Or you can try to get your private key serialized in a more friendly form for your application.

Community
  • 1
  • 1
bartonjs
  • 30,352
  • 2
  • 71
  • 111
0

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));
Shah Zain
  • 388
  • 4
  • 10