Приветствую всех. В этой статье я бы хотел рассказать о работе с электронной подписью средствами библиотеки BouncyCastle на C#.

Весь код указанный здесь находится в репозитории github и доступен в виде nuget пакета.

dotnet add package ElectronicSignature --version 1.0.1

Итак, Bouncy Castle - это криптографическая библиотека на языке Java/C#, которая предоставляет реализации различных криптографических алгоритмов и протоколов, таких как AES, RSA, DSA, Diffie-Hellman, Elliptic Curve Cryptography (ECC), OpenPGP, TLS и многих других. Библиотека также предоставляет инструменты для работы с сертификатами и подписями, а также для генерации случайных чисел и хэш-функций. Bouncy Castle была разработана с открытым исходным кодом и имеет довольно гибкую лицензию, что позволяет использовать ее в коммерческих проектах. Библиотека имеет высокую производительность и может быть использована на различных платформах, включая Android.

Использовать мы будем именно функционал по работе с ключевыми парами RSA и ECDSA, а так же с сертификатами.

Электронная подпись - это цифровой аналог обычной подписи, используемый для подтверждения подлинности и целостности электронных документов, сообщений и транзакций. Электронная подпись создается с помощью криптографических алгоритмов, которые обеспечивают надежную защиту от подделки и изменения документов.

Для создания электронной подписи используется ключевая пара, состоящая из закрытого и открытого ключа. Закрытый ключ используется для создания подписи, а открытый ключ - для ее проверки. Подпись содержит информацию о документе и ключе, используемом для ее создания, и является уникальной для каждого документа.

Проверка электронной подписи позволяет убедиться в том, что документ не был изменен после создания подписи, и что подпись была создана с использованием правильного ключа. Электронная подпись имеет широкое применение в различных областях, включая электронную коммерцию, финансы, правительство и другие сферы деятельности.

Кратко об электронной подписи

Принцип электронной подписи состоит в том что верифицируемая(приватная) сторона создает ключевую пару состоящую из открытого(публичного) и закрытого(приватного) ключа. Приватный ключ хранится только у нее и к нему не должно быть доступа со стороны. Публичный же ключ является общедоступным и предоставляется верифицирующей(публичная) стороне.

Далее, приватная сторона может подписывать сообщения своим приватным ключом и публичная сторона может верифицировать эту подпись публичным ключом. Таким образом публичная сторона может быть уверена в том что сообщения были получены именно от владельца приватного ключа из этой ключевой пары а не от посторонних лиц.

В свою очередь публичная сторона может шифровать своим публичным ключом сообщение и передать приватной стороне. Данные зашифрованные публичными ключами могут быть расшифрованы только приватным ключом, т.к. алгоритм создания ключевой пары является асинхронным и шифровка и расшифровка является односторонней. Приватный ключ не может шифровать данные т.к. публичный ключ является общедоступным, приватный ключ способен только "подписывать", и публичный ключ может эту подпись только верифицировать. Таким образом публичная сторона может передавать зашифрованные сообщения приватной стороне т.к. только она может их расшифровать.

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

Работа с средствами Bouncy Castle

Теперь давайте приступим к коду.

Первым делом необходимо создать ключевую пару (RSA или ECDSA):

//RSA key pair
var keyGenerationParameters = new KeyGenerationParameters(new SecureRandom(), keySize);
var keyPairGenerator = new RsaKeyPairGenerator();
keyPairGenerator.Init(keyGenerationParameters);

var rsaKeyPair = keyPairGenerator.GenerateKeyPair();

//ECDSA key pair
var curve = ECNamedCurveTable.GetByName("secp256k1");
var domainParameters = new ECDomainParameters(curve.Curve, curve.G, curve.N, curve.H, curve.GetSeed());
var keyGenerationParameters = new ECKeyGenerationParameters(domainParameters, new SecureRandom());
var generator = new ECKeyPairGenerator();
generator.Init(keyGenerationParameters);

var ecdsaKeyPair = generator.GenerateKeyPair();

Для публичного и приватного ключа в файлы формата .pem используем следующий код

using (var privateKeyWriter = new StreamWriter(@"rsaPrivateKey.pem"))
{
    var pemWriter = new PemWriter(privateKeyWriter);
    pemWriter.WriteObject(rsaKeyPair.Private);
}

using (var publicKeyWriter = new StreamWriter(@"rsaPublicKey.pem"))
{
    var pemWriter = new PemWriter(publicKeyWriter);
    pemWriter.WriteObject(rsaKeyPair.Public);
}

Теперь когда у нас есть ключевая пара давайте создадим сертификаты для удобства.

Сначала можем создать запрос на создание сертификата (CSR). Код ниже взят из тестового консольного проекта в репозитории библиотеки.

var keyPair = keyPairPath.GetKeyPairFromPem();
var csr = Cryptography.GenerateCSR("Test_KG", "Test_Chuy", "Test_Bishkek", "Test_osh", "Test_ITZone", "Test_Name", CryptographyAlgorithm.SHA256withRSA, keyPair);
csr.ToPemFile(csrPath);

Вот реализация метода GenerateCSR:

public static Pkcs10CertificationRequest GenerateCSR(string country,
                                                         string state,
                                                         string locality,
                                                         string organization,
                                                         string organizationalUnit,
                                                         string commonName,
                                                         CryptographyAlgorithm algorithm,
                                                         AsymmetricCipherKeyPair keyPair)
    {
        var subject = new X509Name($"C={country}, ST={state}, L={locality}, O={organization}, OU={organizationalUnit}, CN={commonName}");

        var algorithmName = algorithm.ToString();

        var isRSA = algorithmName.IsRsaAlgorithm();

        var csr = keyPair.Private switch
        {
            ECPrivateKeyParameters when !isRSA => new Pkcs10CertificationRequest(algorithmName, subject, keyPair.Public, null, keyPair.Private),
            RsaPrivateCrtKeyParameters when isRSA => new Pkcs10CertificationRequest(algorithmName, subject, keyPair.Public, null, keyPair.Private),
            _ => throw new Exception("Unknown key pair type")
        };

        return csr;
    }

Далее создадим самоподписанный сертификат

var selfSignedCert = Cryptography.GenerateSelfSignedCert(csrPath.GetCSRPemFile(),
                                                                 keyPairPath.GetPrivateKeyFromPem(),
                                                                 DateTime.UtcNow,
                                                                 DateTime.UtcNow.AddYears(1));
selfSignedCert.ToPemFile(selfSignedCertPath);

Реализация GenerateSelfSignedCert

public static X509Certificate GenerateSelfSignedCert(Pkcs10CertificationRequest csr,
                                                         AsymmetricKeyParameter privateKey,
                                                         DateTime startDate,
                                                         DateTime endDate,
                                                         CryptographyAlgorithm algorithm = CryptographyAlgorithm.SHA256withRSA)
    {
        // Create a CSR-based self-signed certificate
        var csrInfo = csr.GetCertificationRequestInfo();
        var certGenerator = new X509V3CertificateGenerator();
        var randomGenerator = new CryptoApiRandomGenerator();
        var random = new SecureRandom(randomGenerator);
        var serialNumber = BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(long.MaxValue), random);

        certGenerator.SetSerialNumber(serialNumber);
        certGenerator.SetIssuerDN(csrInfo.Subject);
        certGenerator.SetNotBefore(startDate);
        certGenerator.SetNotAfter(endDate);
        certGenerator.SetSubjectDN(csrInfo.Subject);
        certGenerator.SetPublicKey(csr.GetPublicKey());

        // Add the BasicConstraints and SubjectKeyIdentifier extensions
        certGenerator.AddExtension(X509Extensions.BasicConstraints, true, new BasicConstraints(false));
        certGenerator.AddExtension(X509Extensions.SubjectKeyIdentifier, false, new SubjectKeyIdentifierStructure(csr.GetPublicKey()));

        // Create a signature factory for the specified algorithm and private key
        ISignatureFactory signatureFactory = new Asn1SignatureFactory(algorithm.ToString(), privateKey);

        // Sign the certificate with the specified signature algorithm
        return certGenerator.Generate(signatureFactory);
    }

Теперь создадим приватный PFX сертификат который будет содержать ключевую пару

var certificate = new X509Certificate2(selfSignedCertPath);
var keyPair = keyPairPath.GetKeyPairFromPem();

var bcRsaPrivateKey = (RsaPrivateCrtKeyParameters)keyPair.Private;
var rsaParameters = DotNetUtilities.ToRSAParameters(bcRsaPrivateKey);
var rsaKey = RSA.Create(rsaParameters);

// Assuming you have an X509Certificate2 named certificate
var exportableCertificate = certificate.CopyWithPrivateKey(rsaKey);

// Create password for certificate protection
var passwordForCertificateProtection = new SecureString();

foreach (var @char in "1234")
   passwordForCertificateProtection.AppendChar(@char);

// Export certificate to a file.
File.WriteAllBytes(privateCertPath, exportableCertificate.Export(X509ContentType.Pfx, passwordForCertificateProtection));

Мы теперь можем создавать публичные сертификаты подписанные нашим приватным сертификатом

var signedCert = Cryptography.GenerateSignedCertificate(csrPath.GetCSRPemFile(),
                                                                privateCertPath.GetPrivateCert(privateCertPass),
                                                                privateCertPath,
                                                                DateTime.UtcNow,
                                                                DateTime.UtcNow.AddYears(1));
        signedCert.ToPemFile(signedCertPath);

Реализация метода GenerateSignedCertificate

public static X509Certificate GenerateSignedCertificate(Pkcs10CertificationRequest csr,
                                                            X509Certificate2 pfx,
                                                            string? pfxPassword,
                                                            DateTime startDate,
                                                            DateTime endDate,
                                                            CryptographyAlgorithm algorithm = CryptographyAlgorithm.SHA256withRSA)
    {
        AsymmetricKeyParameter pfxPrivateKey;

        try
        {
            pfxPrivateKey = DotNetUtilities.GetKeyPair(pfx.GetRSAPrivateKey()).Private ?? DotNetUtilities.GetKeyPair(pfx.GetECDsaPrivateKey()).Private;
        }
        catch (Exception)
        {
            pfxPrivateKey = pfx.GetPrivateKeyFromCert(pfxPassword);
        }

        var pfxBouncyCastleCertificate = DotNetUtilities.FromX509Certificate(pfx);

        // Создание сертификата на основе CSR
        var csrInfo = csr.GetCertificationRequestInfo();
        var certGenerator = new X509V3CertificateGenerator();

        var randomGenerator = new CryptoApiRandomGenerator();
        var random = new SecureRandom(randomGenerator);
        var serialNumber = BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(long.MaxValue), random);

        certGenerator.SetSerialNumber(serialNumber);
        certGenerator.SetIssuerDN(pfxBouncyCastleCertificate.SubjectDN);
        certGenerator.SetNotBefore(startDate);
        certGenerator.SetNotAfter(endDate);
        certGenerator.SetSubjectDN(csrInfo.Subject);
        certGenerator.SetPublicKey(csr.GetPublicKey());

        // Adding extensions
        certGenerator.AddExtension(X509Extensions.BasicConstraints.Id, false, new BasicConstraints(false));
        certGenerator.AddExtension(X509Extensions.SubjectKeyIdentifier.Id, false, new SubjectKeyIdentifierStructure(csr.GetPublicKey()));

        // Creating a Signature and Generating a Certificate
        ISignatureFactory signatureFactory = new Asn1SignatureFactory(algorithm.ToString(), pfxPrivateKey);

        return certGenerator.Generate(signatureFactory);
    }

Теперь уже мы можем при помощи публичного сертификата шифровать данные и дешифровать приватным PFX сертификатом

var message = "Hello world";
var encoded = Cryptography.EncryptDataByPublicCert(message, signedCertPath.GetPublicCert());
var data = Cryptography.DecryptDataWithPrivateCert(encoded, privateCertPath.GetPrivateCert(privateCertPass), privateCertPass);

Console.WriteLine(Encoding.UTF8.GetString(data));

Метод для шифрования публичным ключом

public static byte[] EncryptDataByPublicCert(byte[] data, X509Certificate2 publicCert)
    {
        var envelopGenerator = new CmsEnvelopedDataGenerator();
        var cert = new X509CertificateParser().ReadCertificate(publicCert.RawData);
        envelopGenerator.AddKeyTransRecipient(cert);

        return envelopGenerator.Generate(new CmsProcessableByteArray(data), CmsEnvelopedGenerator.DesEde3Cbc).GetEncoded();
    }

Метод для дешифрования приватным ключом

public static byte[] DecryptDataWithPrivateCert(byte[] encryptedData, X509Certificate2 privateCert, string ? password)
    {
        AsymmetricKeyParameter key;

        try
        {
            key = DotNetUtilities.GetKeyPair(privateCert.GetRSAPrivateKey()).Private ?? DotNetUtilities.GetKeyPair(privateCert.GetECDsaPrivateKey()).Private;
        }
        catch (Exception)
        {
            key = privateCert.GetPrivateKeyFromCert(password);
        }

        var x509Certificate = DotNetUtilities.FromX509Certificate(privateCert);

        var recipientInfos = new CmsEnvelopedData(encryptedData).GetRecipientInfos();

        RecipientInformation? firstRecipient = null;

        foreach (var recipientInfo in recipientInfos.GetRecipients())
        {
            if(recipientInfo.RecipientID.Issuer.Equivalent(x509Certificate.IssuerDN) || recipientInfo.RecipientID.SerialNumber.Equals(x509Certificate.SerialNumber))
                firstRecipient = recipientInfo;
        }

        return firstRecipient!.GetContent(key);
    }

В тестовом консольном проекте также предоставлены примеры шифровки и дешифровки при помощи самих ключей.

public static void TestDecryptWithKey()
    {
        var message = "Hello world";
        var keyPair = keyPairPath.GetKeyPairFromPem();
        var encoded = Cryptography.EncryptDataWithPulicKey(message, keyPair.Public);
        var data = Cryptography.DecryptDataWithPrivateKey(encoded, keyPair.Private);

        Console.WriteLine(data);
    }

Извлечение данных из подписи(сигнатуры) приватного ключа

var message = "Hello world";
var signature = Cryptography.SignDataByPrivateCert(message, privateCertPath.GetPrivateCert(privateCertPass));

var data = Cryptography.ExtractSignedData(signature);
Console.WriteLine(Encoding.UTF8.GetString(data));

Можно подписать сообщение самим приватным ключом а не сертификатом и проверить подпись публичным ключом

var message = "Hello world";
var signature = Cryptography.SignDataByPrivateKey(message, privateKeyPath.GetPrivateKeyFromPem());

if(Cryptography.VerifySignedByPublicKey(message, signature, publicKeyPath.GetPublicKeyFromPem()))
    Console.WriteLine("True");
else
    Console.WriteLine("False");

Верифицировать подпись что она была сделана именно тем приватным сертификатом которым был подписан наш публичный подписанный сертификат.

var message = "Hello world";
var signature = Cryptography.SignDataByPrivateCert(message, privateCertPath.GetPrivateCert(privateCertPass));

if(Cryptography.VerifySignedDataByCertIssuer(signature, signedCertPath.GetPublicCert(), out var data))
{
    if (data != null)
        Console.WriteLine(Encoding.UTF8.GetString(data));
}
else
{
    Console.WriteLine("False");
}

Верифицировать подпись что она была сделана приватным сертификатом которыму мы доверяем.

public static void TestVerifySignedDataRootCertAndTrustCommunication()
    {
        var message = "Hello world";
        var signature = Cryptography.SignDataByPrivateCert(message, privateCertPath.GetPrivateCert(privateCertPass));

        if(Cryptography.VerifySignedDataRootCertAndTrustCommunication(signature, privateCertPath.GetPrivateCert(privateCertPass), out var data))
        {
            if (data != null)
                Console.WriteLine(Encoding.UTF8.GetString(data));
        }
        else
        {
            Console.WriteLine("False");
        }
    }

Верифицировать подпись что она была сделана именно тем же самым сертификатом которым мы проверяем подпись.

public static void TestVerifySignedDataBySameCert()
    {
        var message = "Hello world";
        var signature = Cryptography.SignDataByPrivateCert(message, privateCertPath.GetPrivateCert(privateCertPass));

        if(Cryptography.VerifySignedDataBySameCert(signature, privateCertPath.GetPrivateCert(privateCertPass), out var data))
        {
            if (data != null)
                Console.WriteLine(Encoding.UTF8.GetString(data));
        }
        else
        {
            Console.WriteLine("False");
        }
    }

Вот полный стек вызовов рассмотренных методов.

//The given code appears to be a sequence of function calls to various tests related to digital certificates, cryptography, and security.
//Let's break down each of these functions and their potential purpose.


//This test function could be generating a public-private key pair using the RSA encryption algorithm.
//This is a commonly used algorithm for generating secure keys and is often used for secure communication.
Tests.TestGenerateRSAKeyPair();

//This test function may be writing a key-pair to a file or creating a private and public keys.
//Private keys are used in asymmetric encryption, where one key is used for encryption and another key is used for decryption.
Tests.TestWriteKeyPairInPemFile();

//This test function may be generating a Certificate Signing Request (CSR), which is a message sent to a certificate authority to request a digital certificate.
Tests.TestGenerateCSR();

//This test function may be generating a self-signed certificate.
//Self-signed certificates are digital certificates that are signed by the same entity that issues the certificate.
Tests.TestGenerateSelfSignedCert();

//This test function could be creating a PFX file, which is a file format used to store private keys and certificates in a secure manner.
Tests.CreatePfx();

//This test function may be generating a signed certificate.
//A signed certificate is a digital certificate that has been signed by a trusted third-party, known as a certificate authority.
Tests.TestGenerateSignedCert();

//This test function may be decrypting data using a certificate.
//In asymmetric encryption, the public key is used for encryption and the private key is used for decryption.
Tests.TestDecryptWithCert();

//This test function may be decrypting data using a private key.
Tests.TestDecryptWithKey();

//This test function may be extracting a signed certificate from a private certificate.
Tests.TestExtractSignedByPrivateCert();

//This test function may be verifying that a certificate has been signed by a private key.
Tests.TestVerifySignedByPrivateKey();

//This test function may be verifying that the public key and the private key are a match.
Tests.TestVerifyMatchBetweenPublicAndPrivateKeys();

//This test function may be verifying that a certificate has been signed by a private certificate.
//ATTENTION!!! When this method is called, the result will be "false" if any of the certificates under test are not in the trust store;
Tests.TestVerifySignedByPrivateCert();

//This test function may be verifying that signed data can be trusted using a root certificate.
//ATTENTION!!! When this method is called, the result will be "false" if any of the certificates under test are not in the trust store;
Tests.TestVerifySignedDataRootCertAndTrustCommunication();

//This test function may be verifying that signed data can be trusted using the same certificate.
//ATTENTION!!! When this method is called, the result will be "false" if any of the certificates under test are not in the trust store;
Tests.TestVerifySignedDataBySameCert();

В данной версии библиотеки реализована работа с алгоритмами RSA и ECDSA. В будущем я добавлю функционал и по другим алгоритмам.

В репозитории предоставлен полный исходный код библиотеки с подробной документацией и тестовый консольный проект который можно запускать сразу из "коробки".

Благодарю за внимание. Буду рад и признателен если дадите фидбек по найденным недоработкам, ошибкам или советам по улучшению кода и функционала.

Комментарии (3)


  1. vin2809
    20.04.2023 04:49

    Спасибо. Мне понравилось.


  1. shai_hulud
    20.04.2023 04:49

    А существует документация к Bouncy Castle? Вот я хочу их X509CertificateBulder использовать, там не всё интуитиивно, описании на методах нет :( Даже для Java версии подошло бы.


    1. StivenDiplet Автор
      20.04.2023 04:49
      +1

      Документация у BouncyCastle очень массивная и мультиверсионная, оффсайт https://www.bouncycastle.org.
      Для изучения библиотеки следует шерстить официальный сайт, различные статьи и посты на StackOverFlow.

      Вот к примеру ссылка на документацию X509CertificateBulder
      https://www.bouncycastle.org/docs/pkixdocs1.5on/index.html?org/bouncycastle/cert/X509v3CertificateBuilder.html