Разные подписи при подписании одних и тех же данных с использованием одного и того же закрытого ключа в Pkcs11Interop и RSACryptoServiceProvider

Я хочу использовать аппаратный модуль безопасности (HSM) для подписи строки. В частности, для этого я использую ePass3003Auto. Я сохранил сертификат в HSM, у которого есть закрытый ключ. Используя пакет Pkcs11Interop и механизм CKM.CKM_SHA256_RSA_PKCS, я получаю результат X. Чтобы проверить точность процесса подписи, я также использую RSACryptoServiceProvider для подписи тех же данных с использованием аргументов HashAlgorithmName.SHA256 и RSASignaturePadding.Pkcs1, что дает результат Y.

Что я сделал до сих пор?

  • Я проверил закрытый ключ и подтвердил, что ключ, используемый в RSACryptoServiceProvider, идентичен ключу, хранящемуся в HSM. Чтобы еще больше убедиться в этом, я протестировал оба метода, используя SHA1, и получил одинаковые подписи, что указывает на то, что ключи действительно одинаковы.
  • Я убедился, что данные, передаваемые в оба алгоритма, идентичны и проблема не в кодировке. Массивы byte, передаваемые методам подписи, также одинаковы.
  • Я пробовал обе конфигурации x86 и x64, но поскольку HSM имеет только одну библиотеку и нет отдельной библиотеки для разных архитектур, разницы в результатах не было.
  • Я использовал другую программу, чтобы проверить результат подписи тех же данных с помощью HSM, которая также дала результат Y. Таким образом, похоже, что RSACryptoServiceProvider генерирует правильную подпись, а мой подход с использованием Pkcs11Interop неверен.

Вот упрощенная версия моего кода:

using Net.Pkcs11Interop.Common;
using Net.Pkcs11Interop.HighLevelAPI;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

namespace TestSigningProblem
{
    internal class Program
    {
        internal static string SignUsingHSM(string data, string tokenId, string pin)
        {
            const string modulePath = @"C:\Windows\System32\ShuttleCsp11_3003.dll";
            var factories = new Pkcs11InteropFactories();

            using (var pkcs11Library = factories.Pkcs11LibraryFactory.LoadPkcs11Library(factories, modulePath, AppType.MultiThreaded))
            {
                // Find first slot with token present
                var slot = pkcs11Library.GetSlotList(SlotsType.WithOrWithoutTokenPresent).First();

                // Open RW session
                using (var session = slot.OpenSession(SessionType.ReadWrite))
                {
                    // Login as normal user
                    session.Login(CKU.CKU_USER, pin);

                    // Specify signing mechanism
                    var mechanism = session.Factories.MechanismFactory.Create(CKM.CKM_SHA256_RSA_PKCS);

                    var sourceData = Encoding.UTF8.GetBytes(data);

                    var pkcs11UriBuilder = new Pkcs11UriBuilder
                    {
                        ModulePath = modulePath,
                        PinValue = pin,
                        Type = CKO.CKO_PRIVATE_KEY
                    };

                    var searchTemplate = Pkcs11UriUtils.GetObjectAttributes(new Pkcs11Uri(pkcs11UriBuilder.ToString()), session.Factories.ObjectAttributeFactory);
                    var allObjects = session.FindAllObjects(searchTemplate);
                    var foundObject = allObjects.FirstOrDefault(x => session.GetAttributeValue(x, new List<CKA>() { CKA.CKA_ID }).First().GetValueAsString().StartsWith(tokenId, StringComparison.OrdinalIgnoreCase));
                    if (foundObject == null)
                    {
                        throw new Exception("Certificate not found");
                    }

                    // Sign data
                    var signature = session.Sign(mechanism, foundObject, sourceData);

                    session.Logout();

                    return ConvertUtils.BytesToBase64String(signature);
                }
            }
        }

        internal static string SignUsingPrivateKey(string data, string privateKey)
        {
            var pem = $"-----BEGIN PRIVATE KEY-----\n{privateKey}\n-----END PRIVATE KEY-----"; // Add header and footer
            var pemReader = new PemReader(new StringReader(pem));
            var rsaParams = DotNetUtilities.ToRSAParameters((RsaPrivateCrtKeyParameters)pemReader.ReadObject());
            var cryptoServiceProvider = new RSACryptoServiceProvider();
            cryptoServiceProvider.ImportParameters(rsaParams);
            var dataBytes = Encoding.UTF8.GetBytes(data);
            return Convert.ToBase64String(cryptoServiceProvider.SignData(dataBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
        }

        static void Main(string[] args)
        {
            var data = "T";
            var privateKey = File.ReadAllText(@"private.key");
            Console.WriteLine(SignUsingHSM(data, "********************************", "********"));
            Console.WriteLine(SignUsingPrivateKey(data, privateKey));
            Console.ReadKey();
        }
    }
}

ИМХО, вам следует попробовать использовать библиотеку Pkcs11Interop.X509Store, которая построена на основе Pkcs11Interop и занимается всеми деталями низкого уровня, с которыми вы пытаетесь здесь бороться.

jariq 27.05.2024 12:55

Вы проверяете достоверность подписи с помощью функции Verify, а не другого метода подписи. Большинство алгоритмов подписи включают в подпись ту или иную форму переменной/случайной исходной информации, что означает, что даже подписание одного и того же объекта, одним и тем же ключом, одним и тем же способом каждый раз приведет к получению другой подписи. Я не говорю, что это относится к механизму, который вы выбрали, но предполагаю, что это плохая привычка.

rip... 28.05.2024 03:22

@jariq спасибо за поддержку. Я воспользовался вашим предложением и опубликовал свой рабочий код как принятый ответ.

Ahmad Badkoubehei 31.05.2024 23:19

@rip... ты прав. Но я оказался в конкретной ситуации, когда мои сгенерированные подписи были недействительны, и я пытался найти причину. Поэтому я использовал другой алгоритм хеширования, чтобы подтвердить правильность моего метода подписи. Кроме того, Pkcs#1 не включает случайное заполнение и является детерминированным. Поэтому мы должны получить одну и ту же подпись, когда подписываем одни и те же данные одним и тем же закрытым ключом.

Ahmad Badkoubehei 31.05.2024 23:29

@AhmadBadkoubehei Я имею дело со многими программистами приложений безопасности (заметьте, не «профессионалами». Назовем их «грамматистами-любителями»), которые думают, что каждая схема подписи детерминирована. Вы четко понимаете разницу между детерминистическим и вероятностным, поэтому вы не из их числа :)

rip... 01.06.2024 19:55
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
5
77
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Для тех, у кого могут возникнуть подобные проблемы, я публикую свой рабочий код на основе библиотеки Pkcs11Interop.X509Store, предложенной jariq в комментариях. Этот пакет зависит от библиотеки Pkcs11Interop, которая позволяет выполнять операции высокого уровня.

public string Sign(string data, string serialNumber)
{
    var libraryPath = @"C:\Windows\System32\ShuttleCsp11_3003.dll";

    using (var store = new Pkcs11X509Store(libraryPath, new UiPinProvider()))
    {
        var certificate = FindCertificateBySerialNumber(store, serialNumber) ?? throw new Exception("Certificate not found");
        var rsa = certificate.GetRSAPrivateKey();
        using (var sha256 = SHA256.Create())
        {
            var binaryData = Encoding.UTF8.GetBytes(data);
            var hash = sha256.ComputeHash(binaryData);
            var formatter = new RSAPKCS1SignatureFormatter(rsa);
            formatter.SetHashAlgorithm("SHA256");
            byte[] signature = formatter.CreateSignature(hash);
            return Convert.ToBase64String(signature);
        }
    }
}

public Pkcs11X509Certificate FindCertificateBySerialNumber(Pkcs11X509Store store, string serialNumber)
{
    foreach (var slot in store.Slots)
    {
        if (slot.Token == null)
            continue;

        if (!slot.Token.Info.Initialized)
            continue;

        foreach (var certificate in slot.Token.Certificates)
        {
            if (certificate.Info?.ParsedCertificate?.SerialNumber?.Equals(serialNumber, StringComparison.OrdinalIgnoreCase) == true)
            {
                return certificate;
            }
        }
    }

    return null;
}

Другие вопросы по теме