Работаем с GPG подписями и шифрованием в C#

В данной статье мы рассмотрим методы работы с GPG подписями и шифрованием в C#, а также создадим простой класс для упрощения этих действий.

Введение

Иногда возникает необходимость работы с GPG в приложениях или библиотеках, написанных на C#.

Встроенных средств для этого в языке нет, однако существует мощная криптографическая библиотека BouncyCastle, изначально написанная на Java, но затем полностью портированная на платформу .NET.

Данная библиотека имеет достаточно сложный API, поэтому в данной статье мы воспользуемся шаблоном проектирования «Фасад» и реализуем собственный класс с простым и удобным API для работы с ней.

Базовая настройка

Подключим BouncyCastle к проекту при помощи стандартного пакетного менеджера NuGet любым удобным способом (при помощи интегрированной среды разработки Visual Studio (GUI), либо через консоль (CLI)).

Существуют несколько различных реализаций, поэтому для .NET Framework выберем стандартную версию без каких-либо префиксов или суффиксов, а для NET Standard и Core — адаптированную под них. Информация об этом указана в описании устанавливаемых пакетов.

Все необходимые зависимости и ссылки будут добавлены в проект автоматически.

Создание общего класса

Создадим новый класс в выбранном пространстве имён и назовём его например GPGFacade:

using Org.BouncyCastle.Bcpg;
using Org.BouncyCastle.Bcpg.OpenPgp;
using Org.BouncyCastle.Security;
using System;
using System.IO;
using System.Text;

namespace project.gpg
{
    public static class GPGFacade
    {
    }
}

Т.к. класс будет содержать только статические функции, объявим его ключевым словом static.

Работаем с публичными ключами

В GPG, как и во всех криптографических системах с открытыми ключами, они применяется как для шифрования, так и для проверки действительности цифровых подписей всех типов.

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

Добавим в наш класс GPGFacade реализацию в виде функции GetPublicKey():

private static PgpPublicKey GetPublicKey()
{
    using (MemoryStream KeyStream = new MemoryStream(Encoding.ASCII.GetBytes(Properties.Resources.GPGPublicKey)))
    using (Stream DecoderStream = PgpUtilities.GetDecoderStream(KeyStream))
    {
        PgpPublicKeyRingBundle RingBundle = new PgpPublicKeyRingBundle(DecoderStream);
        foreach (PgpPublicKeyRing Ring in RingBundle.GetKeyRings())
        {
            foreach (PgpPublicKey Key in Ring.GetPublicKeys())
            {
                if (Key.IsEncryptionKey && !Key.IsRevoked())
                {
                    return Key;
                }
            }
        }
    }
    return null;
}

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

Т.к. данная функция не предназначена для внешнего использования, следуя принципам инкапсуляции, объявим её закрытой при помощи ключевого слова private.

Проверка цифровых подписей

Самым популярным действием является конечно же проверка отсоединённых цифровых подписей (detached digital signature).

Они представляют собой файлы с расширением .sig (двоичный формат), либо .asc (ASCII формат) и по умолчанию содержат имя оригинального файла. Например если документ называется file.pdf, то его подпись будет находиться в file.pdf.sig.

Добавим в наш класс GPGFacade функцию VerifySignedFile(string, string) с реализацией механизма проверки ЭЦП:

public static bool VerifySignedFile(string SignatureFileName, string SourceFileName)
{
    using (FileStream SignatureFileStream = new FileStream(SignatureFileName, FileMode.Open))
    using (Stream SignatureFileDecoderStream = PgpUtilities.GetDecoderStream(SignatureFileStream))
    {
        PgpObjectFactory ObjectFactory = new PgpObjectFactory(SignatureFileDecoderStream);
        if (!(ObjectFactory.NextPgpObject() is PgpSignatureList SignatureList)) throw new InvalidOperationException("Failed to create an instance of the PgpObjectFactory.");
        SignatureList[0].InitVerify(GetPublicKey());
        using (FileStream SourceFileStream = new FileStream(SourceFileName, FileMode.Open))
        using (BufferedStream BufferedSourceFileStream = new BufferedStream(SourceFileStream))
        {
            const int BlockSize = 4096;
            byte[] Buffer = new byte[BlockSize];
            int BytesRead = 0;
            while ((BytesRead = BufferedSourceFileStream.Read(Buffer, 0, BlockSize)) != 0)
            {
                SignatureList[0].Update(Buffer, 0, BytesRead);
            }
        }
        return SignatureList[0].Verify();
    }
}

В качестве параметров здесь ожидаются две строки — SignatureFileName (полный путь к файлу отсоединённой цифровой подписи) и SourceFileName (полный путь к исходному файлу, целостность которого требуется проверить).

Исходный файл считывается максимально эффективно — при помощи буфера, блоками по 4 КБ, чтобы в случае проверки огромных объёмов не израсходовать всю память системы.

Функция входит в публичный API и её предполагается использовать в других классах, поэтому объявим её открытой ключевым словом public.

Т.к. мы знаем полное имя файла подписи, то можем автоматически вычислить и имя исходного файла. Для этого добавим перегруженную версию VerifySignedFile(string) с единственным параметром — SignatureFileName:

public static bool VerifySignedFile(string SignatureFileName)
{
    return VerifySignedFile(SignatureFileName, Path.Combine(Path.GetDirectoryName(SignatureFileName), Path.GetFileNameWithoutExtension(SignatureFileName)));
}

Шифрование файлов

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

Добавим в наш класс GPGFacade функцию EncryptFile(string, string) с реализацией механизма шифрования файлов:

public static void EncryptFile(string SourceFileName, string DestinationFileName)
{
    PgpEncryptedDataGenerator EncryptedDataGenerator = new PgpEncryptedDataGenerator(SymmetricKeyAlgorithmTag.Aes256, true, new SecureRandom());
    PgpCompressedDataGenerator CompressedDataGenerator = new PgpCompressedDataGenerator(CompressionAlgorithmTag.Zip);
    PgpLiteralDataGenerator LiteralDataGenerator = new PgpLiteralDataGenerator();
    EncryptedDataGenerator.AddMethod(GetPublicKey());

    using (FileStream InputFileStream = new FileStream(SourceFileName, FileMode.Open))
    using (FileStream OutputFileStream = new FileStream(DestinationFileName, FileMode.Create))
    using (Stream EncryptedDataGeneratorStream = EncryptedDataGenerator.Open(OutputFileStream, new byte[4096]))
    using (Stream CompressedDataGeneratorStream = CompressedDataGenerator.Open(EncryptedDataGeneratorStream))
    using (Stream LiteralDataGeneratorStream = LiteralDataGenerator.Open(CompressedDataGeneratorStream, PgpLiteralData.Binary, String.Empty, InputFileStream.Length, DateTime.Now))
    {
        InputFileStream.CopyTo(LiteralDataGeneratorStream);
    }
}

Она принимает две строки в качестве параметров — SourceFileName (полный путь к исходному файлу, который требуется зашифровать) и DestinationFileName (полный путь к исходному файлу, в котором будет сохранён результат).

Данная функция также входит в публичный API, поэтому объявляем её открытой.

Для удобства создадим перегруженную версию EncryptFile(string) с единственным параметром — SourceFileName, сохраняющую результат в том же каталоге, в файле с добавлением расширения .gpg:

public static void EncryptFile(string SourceFileName)
{
    EncryptFile(SourceFileName, SourceFileName + ".gpg");
}

Литература

При написании данной статьи использовалась литература из следующих источников:

4 commentary to post

  1. Будет статья про перенос SSH ключей в TPM?

    1. Благодарим за интересную идею. Возможно, в ближайшее время такая статья появится на сайте.

Обсуждение закрыто.