[Перевод] Восторг безопасника — технология для шифрования образов контейнеров

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

7ly81ocrw-hkiyjnzbslocfbkwa.jpeg

Обеспечение конфиденциальности данных и кода в образах контейнеров


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

Успех технологии контейнеризации в основном зависит от безопасности контейнеров на различных этапах их жизненного цикла. Одна из проблем безопасности — наличие уязвимостей внутри отдельных контейнеров. Для их выявления пайплайны DevOps, используемые для создания контейнеров, дополняют сканерами, которые ищут в контейнерах пакеты с возможными уязвимостями и предупреждают их владельцев или технических специалистов в случае их обнаружения. Vulnerability Advisor в IBM Cloud является примером такой утилиты.

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

Еще одна потенциальная проблема безопасности — это изоляция контейнера. Технологии безопасности среды исполнения Linux, такие как пространство имен, контрольные группы (cgroups), возможности Linux, а также профили SELinux, AppArmor и Seccomp, помогают ограничить процессы контейнеров и изолировать контейнеры друг от друга во время исполнения.

В этой статье рассматривается все еще актуальная проблема безопасности предприятий в отношении конфиденциальности данных и кода в образах контейнеров. Основная цель безопасности при работе с образами контейнеров — позволить создавать и распространять зашифрованные образы контейнеров, чтобы сделать их доступными только определенному кругу получателей. В этом случае другие могут иметь доступ к этим образам, но они не смогут запускать их или видеть конфиденциальные данные внутри них. Шифрование контейнеров основано на существующей криптографии, такой как технологии шифрования Ривеста-Шамира-Адлемана (RSA), эллиптической кривой и Advanced Encryption Standard (AES), также известный как Rijndael — симметричный алгоритм блочного шифрования.

Вводные


Чтобы получить максимальную пользу от прочтения этой статьи, вы должны быть знакомы с контейнерами Linux и образами контейнеров, а также иметь представление об основах безопасности.

Смежные работы по шифрованию и контейнерам


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

Зашифрованные файловые системы существуют во многих операционных системах на предприятиях и могут поддерживать монтирование зашифрованных разделов и каталогов. Зашифрованные файловые системы могут даже поддерживать загрузку с зашифрованного загрузочного диска. Linux поддерживает шифрование на уровне блочного устройства с помощью драйвера dm-encrypt; ecryptfs является одним из примеров зашифрованной файловой системы. Для Linux доступны другие решения для шифрования файлов с открытым исходным кодом. В ОС Windows шифрование поддерживает файловая система NTFS v3.0. Кроме того, многие производители создают диски с самошифрованием. Для образов виртуальных машин существует решение, подобное зашифрованным дискам. Эмулятор машины (ПК) QEMU с открытым исходным кодом и продукты виртуализации VMware поддерживают зашифрованные образы виртуальных машин.

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

Структура


Экосистема Docker сформировалась для того, чтобы стандартизировать форматы образов контейнеров с помощью группы стандартов Open Container Initiative (OCI), которая теперь контролирует формат времени исполнения контейнера (runtime-spec) и формат образа контейнера (image-spec). Поскольку работа команды требовала расширения существующего формата образа контейнера, мы выделили расширение стандарта для поддержки зашифрованных образов. В следующих разделах описывается существующий формат образа контейнера и расширения.

На верхнем уровне контейнер может состоять из документа в Java Script Object Notation (JSON), который представляет собой список манифестов образов. Например, вы можете использовать этот список манифестов, когда для образа контейнера используются несколько архитектур или платформ. Список манифестов содержит ссылки на манифесты контейнеров, по одной для каждой комбинации архитектуры и операционной системы. Например, поддерживаемые архитектуры включают amd64, arm и ppc64le, а к поддерживаемым операционным системам относится Linux или Windows. Пример списка манифестов показан на скриншоте ниже:

qridetnrxlv_btpeowvntc6qijo.jpeg

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

Уровень ниже списка манифестов — это манифест. Манифест также является документом JSON и содержит упорядоченный список ссылок на слои образов. Эти ссылки содержат mediaType, описывающий формат слоя. Формат может описывать, сжимается ли слой, и если да, то каким образом. Например, каждый уровень может быть сохранен в виде файла .tar, содержащего файлы, которые были добавлены на определенном этапе сборки при выполнении docker build в файле Docker. Для повышения эффективности хранения слои часто также упаковываются с использованием сжатых файлов .gzip. Пример документа манифеста показан на следующем скриншоте:

b1myrlqn_wb4obdnv9lhmzjx44c.jpeg

Как показано, манифесты и уровни ссылаются через «дайджест», который обычно представляет собой хеш-функцию sha256 в документах JSON. Манифесты и слои обычно хранятся в виде файлов в файловой системе. Зачастую имена файлов представляют собой хеш-функции над содержимым, что упрощает их поиск и загрузку. Следствием этого метода хеш-функции является то, что небольшое изменение в документе, на который есть ссылка, вызывает изменения во всех документах, которые на него ссылаются, вплоть до списка манифестов.

В рамках проекта нашей команды мы создали шифрование образов на основе гибридной схемы шифрования с использованием открытого и симметричного ключей. Симметричные ключи используются для массового шифрования данных (применяются для многоуровневого шифрования), а открытые ключи используются для упаковки симметричных ключей. Мы использовали три различные технологии шифрования на основе открытых ключей: OpenPGP, JSON Web Encryption (JWE) и PKCS#7.

OpenPGP


OpenPGP — это технология шифрования и подписи, которая обычно используется для шифрования и подписи сообщений электронной почты. Сообщества с открытым исходным кодом также часто используют его для подписания коммитов (тегов) исходного кода в репозиториях git. Это интернет-стандарт, определенный IETF в RFC480, и его можно рассматривать как открытую версию предыдущей проприетарной технологии PGP.

OpenPGP имеет собственный формат для ключей RSA. Ключи обычно хранятся в файле keyring и могут быть импортированы из простых файлов ключей OpenPGP. Наиболее удобный аспект keyring OpenPGP состоит в том, что открытые ключи могут быть связаны с адресами электронной почты их владельцев. Вы можете работать с несколькими получателями сообщения, просто выбрав список получателей по их адресам электронной почты, которые затем отображаются в открытых ключах для этих получателей. Кроме того, вокруг этой технологии была создана сеть доверия: вы можете найти открытые ключи многих пользователей, упорядоченные по их адресам электронной почты. Например, эти ключи часто используются для подписания тегов git.

Можно использовать формат зашифрованного сообщения OpenPGP для шифрования массовой рассылки сообщения для нескольких получателей. Заголовок сообщения OpenPGP содержит по одному блоку для каждого получателя. Каждый блок содержит 64-битный идентификатор ключа, который указывает алгоритму дешифрования, где необходимо пытаться дешифровать соответствующий закрытый ключ. После того как зашифрованный большой двоичный объект внутри блока расшифрован, он показывает симметричный ключ, который можно использовать для дешифрования массовой рассылки сообщения. Зашифрованный большой двоичный объект с открытым ключом каждого получателя показывает один и тот же симметричный ключ.

Мы использовали OpenPGP аналогичным образом, но в данном случае зашифрованное сообщение, которое он передает, не является слоем. Вместо этого оно содержит документ JSON, который, в свою очередь, содержит симметричный ключ, используемый для шифрования и дешифрования как слоя, так и вектора инициализации. Мы называем этот ключ ключом шифрования слоя (LEK), он представляет собой форму ключа шифрования данных. Преимущество этого метода в том, что нам нужен только один LEK. С помощью LEK мы шифруем слой для одного или нескольких получателей. У каждого получателя (образа контейнера) может быть свой тип ключа, и это не обязательно должен быть ключ OpenPGP. Например, это может быть простой ключ RSA. Пока у нас есть возможность использовать этот ключ RSA для шифрования LEK, мы сможем работать с несколькими получателями с разными типами ключей.

JSON Web Encryption (JWE)


JSON Web Encryption, также известное как JWE, является еще одним интернет-стандартом IETF и определено в RFC7516. Это более новый стандарт шифрования, чем OpenPGP, поэтому в нем используются более свежие низкоуровневые шифры, предназначенные для удовлетворения более строгих требований к шифрованию.

Если смотреть укрупненно, JWE работает по тому же принципу, что и OpenPGP, поскольку он также поддерживает список получателей и массовую рассылку сообщения, зашифрованного с помощью симметричного ключа, к которому имеет доступ каждый получатель сообщения. Получатели сообщения JWE могут иметь разные типы ключей, такие как ключи RSA, определенные типы ключей эллиптической кривой, предназначенные для шифрования, и симметричные ключи. Поскольку это более новый стандарт, все еще есть возможность расширения JWE для поддержки ключей в аппаратных устройствах, таких как TPM или модули аппаратной защиты (HSM), с использованием интерфейсов PKCS#11 или Key Management and Interoperability Protocol (KMIP). Использование JWE происходит аналогичным OpenPGP образом, если получатели имеют ключи RSA или эллиптические кривые. В будущем мы могли бы расширить его для поддержки симметричных ключей, таких как KMIP внутри HSM.

PKCS#7


PKCS#7, также известный как синтаксис криптографических сообщений (CMS), определен в стандарте IEFT RFC5652. Согласно Википедии о CMS, «его можно использовать для цифровой подписи, дайджеста, аутентификации или шифрования любых форм цифровых данных».

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

Для поддержки ранее описанных технологий шифрования мы расширили документ манифеста, добавив следующую информацию:

  • Сообщения OpenPGP, JWE и PKCS#7 хранятся в карте аннотаций, которая является частью манифеста.
  • В каждом указанном слое содержится по одной карте. Карта аннотаций в основном представляет собой словарь со строками в качестве ключей и строками в качестве значений (пары ключ-значение).


Для поддержки шифрования образов мы определили следующие ключи:

  • org.opencontainers.image.enc.keys.openpgp
  • org.opencontainers.image.enc.keys.jwe
  • org.opencontainers.image.enc.keys.pkcs7


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

Чтобы определить, что слой был зашифрован с помощью LEK, мы расширили существующие медиатипы суффиксом '+encrypted', как показано в следующих примерах:

  • application/vnd.docker.image.rootfs.diff.tar+encrypted
  • application/vnd.docker.image.rootfs.diff.tar.gzip+encrypted


Эти примеры показывают, что слой заархивирован в файле .tar и зашифрован — или оба заархивированы в файле .tar и сжаты в файл .gzip и зашифрованы. На следующем скриншоте показан пример манифеста, который ссылается на зашифрованные слои. Он также показывает карту аннотаций, содержащую зашифрованное сообщение JWE.

5qakctx3w_kasloyjiih6vzsyps.jpeg

Многослойное шифрование с использованием симметричных ключей


Для симметричного шифрования с помощью LEK наша команда выбрала шифр, который поддерживает аутентифицированное шифрование и основан на стандарте шифрования AES со 128- и 256-битными ключами.

Пример реализации: containerd


Мы реализовали наш вариант в новом проекте среды выполнения контейнеров под названием containerd. Его исходный код на golang можно посмотреть, перейдя по ссылке. Docker-демон использует containerd для запуска некоторых своих служб, а у Kubernetes есть плагин для прямого использования containerd. Поэтому мы надеемся, что наши расширения для поддержки зашифрованных образов контейнеров пригодятся обоим.

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

Поддержка алгоритмов аутентифицированного шифрования в Golang принимает байтовый массив в качестве входных данных и выполняет весь этап его шифрования (запечатывания) или дешифрования (открытия), не допуская передачи и добавления дополнительных массивов в поток. Поскольку этот криптографический API требовал загрузки всего уровня в память или изобретения некоторой схемы для изменения вектора инициализации (IV) для каждого блока, мы решили не использовать аутентифицированное шифрование golang с поддержкой связанных данных (AEAD). Вместо этого мы использовали крипто-библиотеку miscreant golang, которая поддерживает AEAD в потоках (блоках) и реализует собственную схему изменения IV в каждом блоке. В нашей реализации мы разбиваем уровень на блоки размером 1 МБ, которые и передаем один за другим для шифрования. Такой подход позволяет снизить объем памяти при использовании аутентифицированного шифра. На стороне дешифрования мы делаем обратное и обращаем внимание на ошибки, возвращаемые функцией Open (), чтобы убедиться, что блоки шифрования не были подделаны.

На уровне выше симметричного шифрования, асимметричные криптографические схемы шифруют LEK уровня и вектор инициализации (IV). Для добавления или удаления криптографических схем мы регистрируем каждую асимметричную криптографическую реализацию. Когда API асимметричного криптографического кода вызывается для шифрования уровня, мы вызываем один за другим зарегистрированные криптографические обработчики, передавая открытые ключи получателей. После того как все ключи получателей используются для шифрования, мы возвращаемся к карте аннотаций с идентификаторами асимметричных криптоалгоритмов в качестве ключей сопоставления и со значениями, содержащими сообщения в кодировке OpenPGP, JWE и PKCS#7. Каждое сообщение содержит упакованные LEK и IV. Карты аннотаций хранятся в документе манифеста, как показано на предыдущем скриншоте.

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

Мы использовали три типа асимметричных схем шифрования для разных типов ключей. Мы используем ключи OpenPGP для шифрования сообщений OpenPGP. PKCS#7, которую мы используем, требует сертификаты x.509 для ключей шифрования. JWE обрабатывает все остальные типы ключей, такие как простые ключи RSA, эллиптические кривые и симметричные ключи. Мы создали прототип расширения для JWE, который позволяет выполнять криптографические операции с использованием ключей, управляемых сервером KMIP.

Среда выполнения containerd включает клиентский инструмент ctr для взаимодействия с ней. Мы расширили ctr, чтобы включить тестирование наших изменений и предоставить доступ для пользователей контейнера. ctr уже реализует подкоманду, которая поддерживает операции с образами, такие как взаимодействие с реестром образов через их извлечение и отправку.

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

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

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

Когда простой (незашифрованный) образ извлекается из реестра, он автоматически распаковывается и разархивируется, чтобы из него можно было сразу создавать контейнеры. Чтобы упростить это действие для зашифрованных образов, мы предлагаем передавать закрытый ключ команде, которая занимается распаковкой образов, чтобы они могли расшифровать слои до распаковки. Если образ зашифрован с помощью нескольких ключей, команде pull можно передать несколько. Такая передача также поддерживается. После успешного извлечения зашифрованного образа из реестра любой пользователь, имеющий доступ к containerd, может создать контейнер из образа. Чтобы подтвердить, что пользователь имеет права на использование образа контейнера, мы предлагаем ему предоставить закрытые ключи, используемые для расшифровки контейнера. Мы используем ключи для проверки авторизации пользователя, могут ли они использоваться для расшифровки LEK каждого зашифрованного уровня, и если это подтверждается, разрешаем запуск контейнера.

Пошаговое руководство шифрования с использованием containerd


В этом разделе мы продемонстрируем шаги шифрования, которые применяются с containderd, используя ctr в командной строке. Мы покажем, как происходит шифрование и дешифрование образа контейнера.

В первую очередь, нужно клонировать репозиторий git containerd/imgcrypt, который является подпроектом и может шифровать/дешифровать образ контейнера. Затем необходимо собрать containerd и запустить его. Чтобы выполнить эти шаги, нужно знать, как настраивается среда разработки golang:

Для imgcrypt требуется использовать containerd версии не ниже 1.3.

Собираем и устанавливаем imgcrypt:

# make
# sudo make install


Запустите containerd с файлом конфигурации, который можно увидеть на примере ниже. Чтобы избежать возникновения конфликта в containerd, используйте директорию /tmp для каталогов. Также соберите containerd версии 1.3 из исходника, но не устанавливайте его.

# cat config.toml
disable_plugins = ["cri"]
root = "/tmp/var/lib/containerd"
state = "/tmp/run/containerd"
[grpc]
  address = "/tmp/run/containerd/containerd.sock"
  uid = 0
  gid = 0
[stream_processors]
    [stream_processors."io.containerd.ocicrypt.decoder.v1.tar.gzip"]
        accepts = ["application/vnd.oci.image.layer.v1.tar+gzip+encrypted"]
        returns = "application/vnd.oci.image.layer.v1.tar+gzip"
        path = "/usr/local/bin/ctd-decoder"
    [stream_processors."io.containerd.ocicrypt.decoder.v1.tar"]
        accepts = ["application/vnd.oci.image.layer.v1.tar+encrypted"]
        returns = "application/vnd.oci.image.layer.v1.tar"
        path = "/usr/local/bin/ctd-decoder"
# sudo ~/src/github.com/containerd/containerd/bin/containerd -c config.toml


Создайте пару ключей RSA с помощью инструмента командной строки openssl и зашифруйте образ:

# openssl genrsa --out mykey.pem
Generating RSA private key, 2048 bit long modulus (2 primes)
...............................................+++++
............................+++++
e is 65537 (0x010001)
# openssl rsa -in mykey.pem -pubout -out mypubkey.pem
writing RSA key
# sudo chmod 0666 /tmp/run/containerd/containerd.sock
# CTR="/usr/local/bin/ctr-enc -a /tmp/run/containerd/containerd.sock"
# $CTR images pull --all-platforms docker.io/library/bash:latest
[...]
# $CTR images layerinfo --platform linux/amd64 docker.io/library/bash:latest
   #                                                                    DIGEST      PLATFORM      SIZE   ENCRYPTION   RECIPIENTS
   0   sha256:9d48c3bd43c520dc2784e868a780e976b207cbf493eaff8c6596eb871cbd9609   linux/amd64   2789669                          
   1   sha256:7dd01fd971d4ec7058c5636a505327b24e5fc8bd7f62816a9d518472bd9b15c0   linux/amd64   3174665                          
   2   sha256:691cfbca522787898c8b37f063dd20e5524e7d103e1a3b298bd2e2b8da54faf5   linux/amd64       340                          
# $CTR images encrypt --recipient jwe:mypubkey.pem --platform linux/amd64 docker.io/library/bash:latest bash.enc:latest
Encrypting docker.io/library/bash:latest to bash.enc:latest
$ $CTR images layerinfo --platform linux/amd64 bash.enc:latest
   #                                                                    DIGEST      PLATFORM      SIZE   ENCRYPTION   RECIPIENTS
   0   sha256:360be141b01f69b25427a9085b36ba8ad7d7a335449013fa6b32c1ecb894ab5b   linux/amd64   2789669          jwe        [jwe]
   1   sha256:ac601e66cdd275ee0e10afead03a2722e153a60982122d2d369880ea54fe82f8   linux/amd64   3174665          jwe        [jwe]
   2   sha256:41e47064fd00424e328915ad2f7f716bd86ea2d0d8315edaf33ecaa6a2464530   linux/amd64       340          jwe        [jwe]


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

# docker pull registry:latest
# docker run -d -p 5000:5000 --restart=always --name registry registry


Отправьте зашифрованный образ в локальный реестр, извлеките его с помощью ctr-enc, а затем запустите образ:

# $CTR images tag bash.enc:latest localhost:5000/bash.enc:latest
# $CTR images push localhost:5000/bash.enc:latest
# $CTR images rm localhost:5000/bash.enc:latest bash.enc:latest
# $CTR images pull localhost:5000/bash.enc:latest
# sudo $CTR run --rm localhost:5000/bash.enc:latest test echo 'Hello World!'
ctr: you are not authorized to use this image: missing private key needed for decryption
# sudo $CTR run --rm --key mykey.pem localhost:5000/bash.enc:latest test echo 'Hello World!'
Hello World!


Заключение


Шифрование образов контейнеров — это хорошее дополнение к их безопасности, оно обеспечивает конфиденциальность данных и целостность образов контейнеров в месте хранения. Предложенная технология основана на общедоступных технологиях шифрования RSA, эллиптической кривой и AES. Она применяет ключи к схемам шифрования более высокого уровня, таким как OpenPGP, JWE и PKCS#7. Если вы умеете работать с OpenPGP, вы можете шифровать образы контейнеров для получателей OpenPGP, используя их адреса электронной почты, в то время как простые ключи RSA и эллиптические кривые используются для шифрования, например, JWE.

© Habrahabr.ru