Search code examples
c#xamarinbouncycastleecdsaapple-musickit

ECDSA signature with C# and Bouncy Castle does not match MS ECDsa signature


I am trying to communicate with the Apple Music API from a Xamarin.Forms project. Due to the Microsoft ECDsa implementation not being available on Xamarin.Android and Xamarin.iOS I am trying to work around that limitation using the Portable.BouncyCastle Nuget package. Overall the process seems to be working as intended, but when trying to call the Apple Music API with the signed developer token I always receive a HTTP/2 401.

I have a working MS ECDsa implementation that I use in a ASP.NET Core project for communication with the Apple Push Notification Service, so I wrote a quick little demo tool that would generate the keys in both approaches, the result: The signatures do not match and the MS ECDsa variant actually works, I am getting the proper API response from Apple.

I looked at a lot of samples on the net and I can not see what I am actually doing wrong, so perhaps someone here can point me into the right direction.

Bouncy Castle approach:

    public static AsymmetricCipherKeyPair GetKeys(string data)
    {
        var tag = $"{_className}.GetECDsa";
        try
        {
            byte[] byteArray = Encoding.ASCII.GetBytes(data);
            MemoryStream stream = new MemoryStream(byteArray);

            using (TextReader reader = new StreamReader(stream))
            {
                var ecPrivateKeyParameters =
                    (ECPrivateKeyParameters)new PemReader(reader).ReadObject();
                var q = ecPrivateKeyParameters.Parameters.G.Multiply(ecPrivateKeyParameters.D).Normalize();

                var ecPublicKeyParameters = new ECPublicKeyParameters(q, ecPrivateKeyParameters.Parameters);
                return new AsymmetricCipherKeyPair(ecPublicKeyParameters, ecPrivateKeyParameters);
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
        return null;
    }
    
    public static string CreateToken(AsymmetricCipherKeyPair keyPair, string p8privateKeyId, string teamId, DateTime date)
    {
        var tag = $"{_className}.CreateJwtToken";
        try
        {
            var header = JsonHelper.Serialize(new { alg = "ES256", kid = p8privateKeyId });
            var payload = JsonHelper.Serialize(new { iss = teamId, iat = ToEpoch(date), exp = ToEpoch(date.AddSeconds(15777000)) });

            var headerBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(header));
            var payloadBasae64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(payload));
            var unsignedJwtData = $"{headerBase64}.{payloadBasae64}";
            var signature = GetSignature(unsignedJwtData, keyPair);

            return $"{unsignedJwtData}.{Convert.ToBase64String(signature)}";
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
        return null;
    }

    private static int ToEpoch(DateTime time)
    {
        var span = DateTime.UtcNow - new DateTime(1970, 1, 1);
        return Convert.ToInt32(span.TotalSeconds);
    }
    
    private static byte[] GetSignature(string plainText, AsymmetricCipherKeyPair key)
    {
        var encoder = new UTF8Encoding();
        var inputData = encoder.GetBytes(plainText);

        var signer = SignerUtilities.GetSigner("SHA-256withECDSA");
        signer.Init(true, key.Private);
        signer.BlockUpdate(inputData, 0, inputData.Length);

        return signer.GenerateSignature();
    }

MS ECDsa approach:

    public static string GetPrivateKey(string p8privateKey)
    {
        var tag = $"{_className}.GetPrivateKey";
        try
        {
            var dsa = GetECDsa(p8privateKey);
            var keyBytes = dsa.ExportPkcs8PrivateKey();
            return Convert.ToBase64String(keyBytes);
        }
        catch (Exception ex)
        {
           Console.WriteLine(ex.Message);
        }
        return null;
    }
    
    private static ECDsa GetECDsa(string p8privateKey)
    {
        var tag = $"{_className}.GetECDsa";
        try
        {
            byte[] byteArray = Encoding.ASCII.GetBytes(p8privateKey);
            MemoryStream stream = new MemoryStream(byteArray);

            using (TextReader reader = new StreamReader(stream))
            {
                var ecPrivateKeyParameters =
                    (ECPrivateKeyParameters)new PemReader(reader).ReadObject();
                var q = ecPrivateKeyParameters.Parameters.G.Multiply(ecPrivateKeyParameters.D).Normalize();

                return ECDsa.Create(new ECParameters
                {
                    Curve = ECCurve.CreateFromValue(ecPrivateKeyParameters.PublicKeyParamSet.Id),
                    D = ecPrivateKeyParameters.D.ToByteArrayUnsigned(),
                    Q =
                        {
                            X = q.XCoord.GetEncoded(),
                            Y = q.YCoord.GetEncoded()
                        }
                });
            }
        }
        catch (Exception ex)
        {
           Console.WriteLine(ex.Message);
        }
        return null;
    }
    
    public static string CreateJwtToken(string p8privateKey, string p8privateKeyId, string teamId, DateTime date)
    {
        var tag = $"{_className}.CreateJwtToken";
        try
        {
            var header = JsonHelper.Serialize(new { alg = "ES256", kid = p8privateKeyId });
            var payload = JsonHelper.Serialize(new { iss = teamId, iat = ToEpoch(date), exp = ToEpoch(date.AddSeconds(15777000)) });

            using var dsa = ECDsa.Create("ECDsa");

            var keyBytes = Convert.FromBase64String(p8privateKey);
            dsa.ImportPkcs8PrivateKey(keyBytes, out _);

            var headerBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(header));
            var payloadBasae64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(payload));
            var unsignedJwtData = $"{headerBase64}.{payloadBasae64}";
            var unsignedJwtBytes = Encoding.UTF8.GetBytes(unsignedJwtData);
            var signature = dsa.SignData(unsignedJwtBytes, 0, unsignedJwtBytes.Length, HashAlgorithmName.SHA256);

            return $"{unsignedJwtData}.{Convert.ToBase64String(signature)}";
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
        return null;
    }

I am looking forward to your feedback.


Solution

  • Thanks to the comment of Topaco I tested with SHA-256withPLAIN-ECDSA and it solved the issue. The signatures are not identical, however the Apple Music API accepts the signed JWT Token and responds properly. So thanks Topaco for pointing this out.