Подпись ECDSA с C # и Bouncy Castle не соответствует подписи MS ECDsa

Я пытаюсь связаться с Apple Music API из проекта Xamarin.Forms. Поскольку реализация Microsoft ECDsa недоступна в Xamarin.Android и Xamarin.iOS, я пытаюсь обойти это ограничение с помощью пакета Nuget Portable.BouncyCastle. В целом процесс, похоже, работает так, как задумано, но при попытке вызвать Apple Music API с подписанным токеном разработчика я всегда получаю HTTP / 2 401.

У меня есть работающая реализация MS ECDsa, которую я использую в проекте ASP.NET Core для связи со службой push-уведомлений Apple, поэтому я написал небольшой демонстрационный инструмент, который генерирует ключи в обоих подходах, результат: подписи не match и вариант MS ECDsa действительно работает, я получаю правильный ответ API от Apple.

Я просмотрел множество образцов в сети и не могу понять, что я на самом деле делаю неправильно, так что, возможно, кто-то здесь может указать мне правильное направление.

Подход к "Надувному замку":

    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:

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

Жду ваших отзывов.


person Bastian Noffer    schedule 16.03.2021    source источник
comment
Одна из проблем в решении BouncyCastle, вероятно, заключается в том, что SignerUtilities.GetSigner("SHA-256withECDSA") возвращает подпись в формате ASN.1, но в контексте JWT используется формат (r, s). Более новые версии BC поддерживают этот формат, например с SHA-256withPLAIN-ECDSA. Если проблема не исчезнет, ​​было бы полезно, если бы вы могли заполнить оба кода для воспроизведения, то есть образцы пар ключей, вызывающий код и образцы для сгенерированных JWT.   -  person user 9014097    schedule 16.03.2021


Ответы (1)


Благодаря комментарию Topaco Я тестировал SHA-256withPLAIN-ECDSA, и проблема была решена. Подписи не идентичны, однако API Apple Music принимает подписанный токен JWT и отвечает должным образом. Так что спасибо Topaco за указание на это.

person Bastian Noffer    schedule 16.03.2021
comment
Подписи не идентичны, потому что оба решения используют недетерминированный вариант ECDSA, то есть каждый раз генерируется другая подпись, даже с одинаковым заголовком / полезной нагрузкой и идентичным ключом. Хорошо. - person user 9014097; 16.03.2021