JEP draft: Security Providers Filter
Фильтр провайдеров безопасности
| Authors | Martin Balao Alonso, Francisco Ferrari Bihurriet |
| Ответственный | Martin Balao Alonso |
| Тип | Feature |
| Область | JDK |
| Статус | Draft |
| Компонент | security-libs / javax.crypto |
| Трудоёмкость | M |
| Длительность | M |
| Создан | 2024/02/08 16:55 |
| Обновлён | 2024/12/19 20:46 |
| Задача | 8325511 |
Аннотация
Расширить Java Cryptographic Architecture (JCA) механизмом фильтрации, который позволяет настраивать, какие сервисы установленных провайдеров безопасности включены во время выполнения.
Цели
Основная цель этого предложения — реализовать механизм фильтрации, который ограничивает, какие сервисы установленных провайдеров безопасности можно использовать в JCA API getInstance (Cipher, Signature, Mac, KeyFactory и т. д.). Сервис, который фильтр не разрешает, не должен быть доступен для использования, как если бы его провайдер безопасности этот сервис не реализовывал.
Механизм фильтрации должен применяться к сервисам как статически установленных провайдеров безопасности (т. е. заданных свойствами безопасности security.provider.<n>), так и динамически установленных (т. е. добавленных с помощью API java.security.Security::addProvider). Провайдеры безопасности OpenJDK и сторонние провайдеры не должны различаться.
Фильтр сервисов должен настраиваться свойством безопасности, которое можно переопределить системным свойством с тем же именем. Синтаксис этого свойства должен обладать следующими характеристиками: 1) идентифицировать сервисы по имени провайдера, типу сервиса и имени или псевдониму алгоритма; 2) содержать конструкции для выбора нескольких сервисов одним правилом (например, всех сервисов определённого типа); 3) позволять задавать действия правил в стиле allow или deny; 4) быть кратким, простым и однозначным. Поскольку эта возможность критична для безопасности, любая синтаксическая ошибка в значении фильтра должна быть фатальной. Чтобы упростить поиск и устранение проблем, нужно реализовать журналирование ошибок и диагностическое журналирование.
Что не является целью
Если фильтр не разрешает сервис, зависящая от него функциональность приложения или библиотеки перестанет работать. Может быть затронута и функциональность сервиса, который использует отключённый сервис как составную часть. Целью этого предложения не является механизм, предупреждающий о зависимой функциональности, которую может затронуть заданное значение фильтра. Кроме того, значение фильтра, введённое пользователем, может содержать избыточные или лишние правила. Такие правила не будут автоматически обнаруживаться или удаляться. Ожидается, что пользователи этой возможности сами оценят и проверят последствия отключения сервиса и при этом обеспечат внутреннюю согласованность заданного значения фильтра.
Это предложение не ставит целью стандартизировать имена алгоритмов в OpenJDK и сторонних провайдерах безопасности. Чтобы задать значение фильтра, нужно знать сервисы установленных провайдеров безопасности и имена или псевдонимы их алгоритмов. Важно учитывать, что провайдеры безопасности могут называть один и тот же алгоритм немного по-разному или не определять одинаковые псевдонимы.
Целью не является фильтрация по клиентам сервисов (т. е. разрешение или запрет сервиса в зависимости от того, какой класс, пакет или модуль пытается его использовать). Кроме того, пока в рамки предложения не входит идентификация сервисов точнее, чем по имени или псевдониму алгоритма (например, по размеру ключа или другим параметрам алгоритма). Тем не менее в будущем могут быть рассмотрены расширения этого предложения, и для этого в синтаксисе фильтра будут зарезервированы такие символы, как : и ,.
В рамки предложения не входит API для динамического задания значений фильтра из кода на Java, подобно тому как java.io.ObjectOutputStream::setObjectInputFilter делает это для фильтров сериализации. Более того, фильтры должны быть неизменяемыми. Целью не является механизм повторной инициализации или изменения значения фильтра во время выполнения.
Фильтр не является фреймворком, помогающим провайдерам безопасности выполнить требования сертификации FIPS. Провайдер безопасности должен регистрировать только те алгоритмы, для которых он сертифицирован, с соответствующими параметрами, и не должен делегировать это решение фильтру или выносить его вовне.
Критерии успеха
Хотя все перечисленные цели этой возможности либо бинарные, либо качественные, всё же важно определить численно измеримые метрики, которые гарантируют, что в JCA API не появится регрессий производительности. Для успеха необходимо выполнить следующие метрики производительности:
-
Пустой фильтр не должен вызывать никакой регрессии производительности. Такой фильтр, настроенный в OpenJDK по умолчанию, не блокировал бы ни один сервис.
-
Непустой фильтр может вызывать пренебрежимо малое снижение производительности при инициализации JCA и при регистрации провайдера, но не должен никак влиять на использование сервиса. Ожидаемое снижение при инициализации JCA связано с разбором фильтра и должно иметь порядок O(n), где n — число символов в значении фильтра. При регистрации провайдера снижение производительности ожидается из-за проверки сервисов по фильтру. Эта проверка должна выполняться только один раз для каждого сервиса.
Мотивация
Текущие возможности настройки JCA позволяют пользователям устанавливать провайдеры безопасности статически или динамически, задавать их приоритет в упорядоченном списке и даже обходить этот порядок для отдельных сервисов. Однако выбрать, какие именно сервисы приносят с собой установленные провайдеры безопасности, нельзя: решение принимается по принципу всё или ничего. Исторически провайдеры безопасности объединяют сервисы разных типов. Например, провайдер безопасности SUN содержит сервисы следующих типов: SecureRandom, Signature, KeyPairGenerator, AlgorithmParameterGenerator, AlgorithmParameters, KeyFactory, MessageDigest, CertificateFactory, KeyStore, CertStore, Policy, Configuration, CertPathBuilder и CertPathValidator. Такая негибкость настройки провайдеров мешает в сценариях, где требуется соответствие политикам.
Соответствие FIPS 140
Для соответствия FIPS 140 криптографические операции должны выполняться в модуле, сертифицированном по FIPS. В OpenJDK провайдер безопасности SunPKCS11 можно настроить на аппаратный или программный модуль, сертифицированный по FIPS, например NSS Software Token. Сторонние провайдеры безопасности, такие как Bouncy Castle, тоже могут быть сертифицированы по FIPS.
Провайдеры безопасности, входящие в OpenJDK (SUN, SunJCE, SunEC и т. д.), не сертифицированы по FIPS, но всё равно могут быть нужны для поддержки сертификатов X.509, TLS или другой некриптографической функциональности. Проблема в том, что они приносят с собой криптографию, не соответствующую FIPS, и её случайное использование нарушило бы соответствие в целом.
Упорядочить установленные провайдеры безопасности по убыванию приоритета недостаточно из-за схемы отката: если алгоритм не найден в более приоритетном провайдере, он будет искаться в менее приоритетном. Для соответствия FIPS 140 было бы полезно, если бы применение политики на уровне провайдеров безопасности гарантировало отключение несоответствующих криптографических сервисов.
Криптографические политики
Со временем криптографические алгоритмы могут ослабевать или становиться непригодными, что ставит под угрозу информационную безопасность и соответствие нормативным требованиям. Поэтому большинство организаций периодически пересматривают и применяют политики, определяющие, какие криптографические алгоритмы разрешено использовать.
Хотя в OpenJDK есть свойства безопасности, ограничивающие криптографические алгоритмы для TLS, проверки цепочки сертификатов и подписи JAR, эти ограничения не действуют для JCA API (Cipher, Signature, Mac и т. д.). Поэтому установленный провайдер безопасности может принести сервис с алгоритмом, нарушающим заданную политику, и сделать его доступным приложению.
Предлагаемый механизм фильтрации позволяет применять криптографические политики во всех JCA API, задавая алгоритмы, которые должны быть заблокированы или разрешены. В любом случае системный администратор может легко применять и обновлять криптографическую политику по своему усмотрению.
В частности, основные дистрибутивы Linux смогут ещё шире согласовать настройки с crypto-policies и применять эти политики ко всем JCA API в OpenJDK. Crypto-policies — это пакет с курируемыми списками криптографических алгоритмов для профилей безопасности, которые можно задать глобально в операционной системе. Профили безопасности дают разные уровни усиления защиты: с уклоном в обратную совместимость, для соответствия нормативам, таким как FIPS, для безопасных настроек по умолчанию или с учётом будущих требований. Для каждого из этих профилей было бы своё значение фильтра в соответствии с алгоритмами, которые должны быть разрешены или заблокированы.
Оценка безопасности для Checkpoint/Restore (снимок и восстановление процесса) In Userspace (CRIU)
В сценариях CRIU снимок процесса JVM делается один раз и возобновляется многократно. Повторное использование криптографических псевдослучайных чисел, секретов или ключей может ослабить или нарушить безопасность системы. Предлагаемый механизм фильтрации помог бы применить криптографическую политику, отключающую генераторы случайных значений и генераторы ключей, а затем проверить, возникали ли какие-либо проблемы при создании снимка. Если проблем не было, разумно предположить, что приложение безопасно использовать с CRIU.
Совместимость, производительность и другие политики
Для совместимости может потребоваться обязательное использование стандартных алгоритмов. Например, для совместимости с другими системами может быть необходимо хранить ключи и сертификаты в формате PKCS #12. Предлагаемый механизм фильтрации можно использовать для применения политик такого типа.
Производительность тоже может быть причиной для применения политики. Если один провайдер безопасности значительно быстрее других, политика может требовать, чтобы все операции определённого типа выполнялись им. Если самый быстрый провайдер не поддерживает алгоритм, предлагаемый механизм фильтрации предотвратит незаметный откат к более медленной реализации.
В более общем случае организация может контролировать, какие провайдеры безопасности, типы сервисов и алгоритмы используются, в соответствии с заданной политикой или критериями. Это предложение не ограничивает политики фиксированными случаями, а позволяет гибко настраивать их под конкретные потребности.
Подводя итог, все описанные выше сценарии могут выиграть от этого предложения в плане безопасности, соответствия требованиям, производительности и совместимости. Общее у этих сценариев — применение заданной политики во всех компонентах JCA для каждого приложения, работающего в конкретной установке JDK. Механизм фильтрации задуман как гибкий и мощный инструмент и для системных администраторов, и для сборщиков пакетов. Поэтому он требует среднего уровня понимания JCA, установленных провайдеров безопасности и его собственной документации.
Применение политики может иметь для приложения два возможных и предусмотренных результата: 1) автоматическое соблюдение политики — например, переход на разрешённый алгоритм — или 2) выброс ошибки, чтобы исправление было выполнено вручную, — например, исключение java.security.NoSuchAlgorithmException. В любом случае риск случайного нарушения политики должен быть снижен.
Добиться такого же уровня применения политик без предлагаемого механизма фильтрации было бы сложнее, при этом легче допустить ошибку, а в некоторых случаях это было бы и вовсе невозможно. Для этого потребовался бы подход «открытого ящика»: аудит исходного кода, конфигураций и сформированных журналов каждого приложения, работающего в установке JDK, чтобы выявить несоответствующие случаи использования JCA. Преимущество автоматической адаптации к политике, описанное выше как результат № 1, было бы недоступно.
Описание
Мы предлагаем настраиваемый фильтр, который включает или отключает сервисы, реализованные установленными провайдерами безопасности. Фильтр инициализируется вместе с классами провайдеров безопасности. Для этого используется значение свойства безопасности jdk.security.providers.filter или одноимённого системного свойства. Если передано системное свойство, оно имеет приоритет над свойством безопасности. Оба свойства можно изменить во время выполнения, поэтому до инициализации фильтра код может повлиять на конфигурацию. Однако после инициализации фильтра последующие изменения свойств во время выполнения не приводят к его сбросу. Если фильтр не задан или задан пустой строкой, фильтрация отключена: разрешены все сервисы.
Фильтр применяется к сервисам, которые реализованы как провайдерами OpenJDK, так и сторонними провайдерами, установленными статически через свойство безопасности security.provider.<n> или динамически через API java.security.Security::addProvider. Фильтрация выполняется по многоуровневой стратегии и может блокировать сервисы на разных уровнях. Стратегия рассчитана на то, чтобы с максимальной эффективностью охватить как обычные, так и особые случаи. Для большинства провайдеров — в том числе для провайдеров OpenJDK — сервисы фильтруются в методах java.security.Provider::put или java.security.Provider::putService и никогда не возвращаются в java.security.Provider::getService или java.security.Provider::getServices. Для сторонних провайдеров, которые переопределяют java.security.Provider::getService или java.security.Provider::getServices и возвращают сервисы, не проверенные фильтром или проверенные и не разрешённые, фильтр применяется повторно в java.security.Provider.Service::newInstance. В редких случаях сторонний провайдер может переопределить java.security.Provider.Service::newInstance и вернуть непроверенную реализацию сервиса (SPI). В качестве последнего уровня защиты сервисы проверяются в API ::getInstance. Чтобы последний уровень защиты работал, тип сервиса должен быть одним из доступных в JDK (Cipher, Signature, Mac и т. д.). Сторонние типы сервисов этой проверкой воспользоваться не могут, так как у фильтра нет публичного API.
Каждый сервис проверяется фильтром только один раз за время своего существования, до использования. Сервис, отклонённый фильтром, нельзя использовать ни в одном из API JCA, как если бы его провайдер безопасности его не реализовывал. В зависимости от доступности сервисов попытка использовать заблокированный сервис может привести к исключению java.security.NoSuchAlgorithmException или к возврату другой реализации того же алгоритма в соответствии с порядком предпочтения установленных провайдеров безопасности.
В OpenJDK есть свойства безопасности для отключения алгоритмов в подсистемах TLS, подписи JAR и проверки пути сертификатов (jdk.tls.disabledAlgorithms, jdk.jar.disabledAlgorithms и jdk.certpath.disabledAlgorithms соответственно). Этот существующий механизм действует поверх предлагаемого фильтра. Поэтому, чтобы алгоритм был доступен, его должны разрешать и существующий механизм, и предлагаемый фильтр.
Сервис идентифицируется сочетанием провайдера безопасности, типа сервиса и имени алгоритма. Вместо имени алгоритма можно при желании использовать псевдоним алгоритма. Фильтр состоит из последовательности шаблонов, которые идентифицируют сервис по критерию совпадения — мы рассмотрим его ниже — и указывают действие: разрешить или запретить проверяемый сервис.
Синтаксис фильтра следующий:
pattern-1; pattern-2; ...; pattern-n
Перед каждым шаблоном в последовательности может стоять символ '!' (например, ! pattern-1). Пробелы между шаблонами, префиксами шаблонов ('!') и разделителями шаблонов (';') не имеют значения. Сервис проверяется по фильтру слева направо. Если сервис совпадает с одним из шаблонов последовательности, принимается решение об авторизации: если перед шаблоном стоит символ '!', сервис запрещается, иначе разрешается. Если ни один шаблон не совпал, по умолчанию сервис запрещается. После принятия решения оставшиеся шаблоны не рассматриваются.
Синтаксис каждого шаблона имеет одну из следующих форм:
1. security-provider
2. security-provider.service-type
3.a. security-provider.service-type.algorithm-name
3.b. security-provider.service-type.algorithm-alias
3.c. security-provider.Cipher.transformation
3.d. security-provider.Cipher.transformation-alias
В форме № 1 для успешного совпадения достаточно, чтобы имя провайдера безопасности было равно security-provider. В форме № 2 тип сервиса также должен быть равен service-type. В форме № 3.a алгоритм сервиса также должен быть равен algorithm-name. В форме № 3.b помимо требований формы № 2 достаточно, чтобы один из псевдонимов сервиса совпал с algorithm-alias. Форма № 3.c похожа на форму № 3.a, но применяется к преобразованиям шифра из нескольких компонентов (algorithm/mode/padding). Форма № 3.d эквивалентна № 3.c, но ищет совпадение с псевдонимом преобразования (algorithm-alias/mode/padding). Во всех случаях имена в шаблонах и имена сервисов должны состоять из допустимых символов и не могут быть пустыми. Совпадение с шаблонами всегда проверяется без учёта регистра.
Символы '\n' и '\0' в шаблоне недопустимы. Символ '.' служит разделителем между уровнями: провайдером безопасности, типом сервиса, именем алгоритма или псевдонимом алгоритма. Следующие символы, если они входят в один из перечисленных уровней, нужно экранировать, поставив перед ними символ '\': '!', '*', ' ' (пробел), '.', ';', '\', ':' и ','. Экранирование любого другого символа ни на что не влияет, кроме того что символ '\' молча отбрасывается.
Стоит отметить, что эти правила экранирования относятся к значению фильтра в том виде, в каком оно читается в API java.security.Security::getProperty и java.lang.System::getProperty: в зависимости от способа передачи значения фильтра может потребоваться дополнительное экранирование. Например, в свойствах безопасности символы '\' должны быть экранированы. Поэтому, чтобы совпасть с провайдером с именем abc\123, шаблон при передаче в виде свойства безопасности нужно экранировать как abc\\\\123.
Помимо экранированных последовательностей символов, имена в шаблонах могут содержать подстановочные знаки '*', обозначающие ноль или более повторений любого символа. Подстановочные знаки работают в жадном режиме: пытаются захватить как можно больше символов и при необходимости отступают.
Если у сервиса есть псевдонимы, имя его алгоритма и каждый из псевдонимов проверяются по фильтру независимо. Заметим, что провайдер безопасности и тип сервиса во всех этих проверках одни и те же. Из полученного набора решений об авторизации — которые могут противоречить друг другу — наивысший приоритет имеет и в итоге действует решение самого левого шаблона в фильтре. Эта стратегия эквивалентна такому изменению проверки сервиса по каждому шаблону, при котором перебирается каждый псевдоним (помимо имени алгоритма) и проверка останавливается, как только для одного из них принято решение.
Для диагностики можно включить отладочный журнал фильтра системным свойством java.security.debug=jca и искать сообщения с префиксом ProvidersFilter. Чтобы вывести список сервисов, разрешённых и не разрешённых фильтром, для каждого установленного провайдера безопасности, запустите java с аргументом -XshowSettings:security:providers. Если значение фильтра синтаксически некорректно, сообщение выброшенного исключения указывает точное место в шаблоне, которое не удалось разобрать.
Согласованность провайдеров безопасности при блокировке базовых сервисов
Предполагается, что приложения и библиотеки получают экземпляры сервисов через API JCA, а не создают напрямую экземпляры классов их реализации, которые часто являются закрытыми. Если бы происходило второе, фильтр не смог бы заблокировать сервис.
Реализации сервиса может потребоваться другой сервис в качестве базового компонента. Например, сервис Signature для алгоритма SHA256withECDSA может использовать сервис MessageDigest для алгоритма SHA256. Однако сервис SHA256withECDSA другого провайдера может обрабатывать эту зависимость, напрямую создавая экземпляр класса реализации базового компонента, а не обращаясь к JCA.
В результате блокировка базового сервиса может по-разному влиять на зависимые сервисы у разных провайдеров безопасности. Например, блокировка сервиса MessageDigest SHA256 может привести к тому, что сервис Signature SHA256withECDSA перестанет работать у провайдера, который получал базовый сервис через JCA, но не затронет провайдера, который создаёт экземпляр класса его реализации.
Чтобы снизить риск путаницы, будут подготовлены документация и рекомендации о том, как написать действующий фильтр независимо от провайдера безопасности. Возвращаясь к предыдущему примеру: не следует считать, что фильтр, блокирующий сервис MessageDigest SHA256, как-либо влияет на сервис Signature SHA256withECDSA: если сервис Signature SHA256withECDSA нужно заблокировать, в фильтре должно быть правило для него. С другой стороны, блокировка MessageDigest SHA256 может привести к тому, что перестанут работать другие функции — даже помимо Signature SHA256withECDSA.
JCA Cipher API и преобразования
Cipher API ведёт себя не так, как другие API JCA. Имена алгоритмов, которые называются преобразованиями, имеют вид algorithm (один компонент) или algorithm/mode/padding (несколько компонентов). В форме из нескольких компонентов режим или дополнение могут быть пустыми, но не оба сразу. Если оба компонента пусты, вместо этого используется форма из одного компонента (т. е. algorithm// обрабатывается как algorithm). Преобразования из нескольких компонентов требуют особого внимания, так как при определении фильтра они могут стать источником путаницы.
При поиске сервисов, поддерживающих преобразование из нескольких компонентов, в качестве алгоритма или псевдонима сервиса проверяются четыре возможных производных варианта: algorithm/mode/padding, algorithm/mode, algorithm//padding и algorithm. Например, если в вызове Cipher::getInstance указано преобразование AES/CBC/PKCS5Padding, с помощью API Provider::getService ищутся сервисы с алгоритмом или псевдонимами AES/CBC/PKCS5Padding, AES/CBC, AES//PKCS5Padding и AES. Сервис может поддерживать преобразование, даже если имя его алгоритма и псевдонимы не совпадают с преобразованием в точности, как в последних трёх случаях предыдущего примера. В этих случаях Cipher API выполняет дополнительные проверки, чтобы определить, действительно ли сервис поддерживает преобразование.
Чтобы показать, почему предыдущий случай может запутать, предположим, что преобразование AES/CBC/PKCS5Padding нужно разрешить, а всё остальное заблокировать. Естественным значением фильтра было бы *.Cipher.AES/CBC/PKCS5Padding; !*. Один из кандидатов на поддержку этого преобразования — сервис Cipher AES провайдера SunJCE. Однако этот фильтр такой сервис не разрешил бы. Чтобы решить эту проблему, нужно реализовать особую обработку преобразований шифра. Эта обработка применяется только в случаях, когда преобразование состоит из нескольких компонентов и отличается от алгоритма и псевдонимов сервиса.
Сервис-кандидат на поддержку преобразования нужно проверять по фильтру на основе преобразования, а не его алгоритма или псевдонимов. При этом провайдер и тип сервиса-кандидата для проверки верны. Также нужно проанализировать возможные псевдонимы преобразования. Псевдонимы преобразования строятся так: перебираются алгоритм и псевдонимы сервиса, из каждого после разбиения по '/' берётся первый компонент, и к нему добавляются режим и дополнение преобразования.
Например, в вызове Cipher::getInstance("AES/CBC/PKCS5Padding") сервис Cipher AES провайдера SunJCE является кандидатом на поддержку преобразования. При проверке по фильтру рассматриваются следующие преобразование и псевдонимы преобразования:
- AES/CBC/PKCS5Padding
- OID.2.16.840.1.101.3.4.1/CBC/PKCS5Padding
- 2.16.840.1.101.3.4.1/CBC/PKCS5Padding
№ 1 — это преобразование; оно получается разбиением алгоритма сервиса по '/', выделением его первого компонента («AES») и добавлением режима и дополнения преобразования. № 2 и № 3 — псевдонимы преобразования на основе псевдонимов сервиса; они получаются разбиением каждого псевдонима по '/', выделением его первого компонента и добавлением режима и дополнения преобразования. Сервис был бы разрешён по преобразованию № 1.
Преобразование № 1 и его псевдонимы № 2 и № 3 — эквивалентные аргументы для вызова Cipher::getInstance("transformation"): возвращается один и тот же сервис Cipher AES провайдера SunJCE. Фильтр поддерживает псевдонимы преобразований так же, как обычные псевдонимы (не относящиеся к преобразованиям).
Примеры правильно заданных значений фильтра
Разрешить все провайдеры безопасности, типы сервисов и алгоритмы:
jdk.security.providers.filter=
или
jdk.security.providers.filter=*
или
jdk.security.providers.filter=*.*
или
jdk.security.providers.filter=*.*.*
--
Разрешить все сервисы, кроме реализации MessageDigest алгоритма MD5 провайдера SUN:
jdk.security.providers.filter=!SUN.MessageDigest.MD5; *
--
Разрешить все сервисы, кроме реализаций MessageDigest алгоритма MD5, независимо от провайдера безопасности:
jdk.security.providers.filter=!*.MessageDigest.MD5; *
--
Разрешить все сервисы, кроме тех, в которых используются алгоритмы MD2 или MD5, независимо от провайдера безопасности, типа сервиса и точного имени алгоритма:
jdk.security.providers.filter=!*.*.*MD2*; !*.*.*MD5*; *
Обратите внимание на подстановочные символы в начале и в конце имён алгоритмов в этом примере. Они нужны, чтобы расширить область сопоставления и включить в неё, среди прочего, HmacMD5, MD5withRSA и PBEWithMD5AndDES.
--
Разрешить все сервисы SunJCE, кроме преобразований шифра AES в режиме ECB, независимо от схемы дополнения.
jdk.security.providers.filter=!SunJCE.Cipher.AES*/ECB/*; !SunJCE.Cipher.AES; SunJCE
Обратите внимание, что в этом примере первый шаблон, блокирующий режим ECB, соответствует преобразованиям, начинающимся с AES, AES_128, AES_192 и AES_256. Кроме того, алгоритм AES нужно заблокировать вторым шаблоном, потому что сервис AES провайдера SunJCE по умолчанию использует режим ECB. Алгоритмы AES_128, AES_192 и AES_256 без режима и дополнения SunJCE не регистрирует, и особой обработки они не требуют. Блокировать AES*//* не нужно, потому что режим в виде пустой строки всё равно не поддерживается.
Другие провайдеры могут задавать другие имена алгоритмов, другие режимы по умолчанию и даже поддерживать режим в виде пустой строки. Перед написанием фильтра рекомендуется изучить каждый провайдер. С учётом сказанного, в качестве отправной точки для блокировки всех преобразований AES в режиме ECB независимо от провайдера, возможно, стоит рассмотреть следующее значение фильтра:
jdk.security.providers.filter=!*.Cipher.AES*/ECB/*; !*.Cipher.AES*//*; *.Cipher.AES*/*/*; !*.Cipher.AES*; *
--
Разрешить все сервисы, кроме реализации Cipher алгоритма RC4 провайдера SunJCE:
jdk.security.providers.filter=!SunJCE.Cipher.ARCFOUR; *
или
jdk.security.providers.filter=!SunJCE.Cipher.RC4; *
или
jdk.security.providers.filter=!SunJCE.Cipher.1\.2\.840\.113549\.3\.4; *
Обратите внимание, что в этом примере для одной и той же цели можно использовать как имя алгоритма, так и любой из псевдонимов.
--
Разрешить только провайдер безопасности SUN со всеми его типами сервисов и алгоритмами. Сервисы, реализованные другими провайдерами безопасности, должны быть отключены.
jdk.security.providers.filter=SUN
или
jdk.security.providers.filter=SUN; !*
Обратите внимание, что в первом значении этого примера действует неявное правило блокировки всего, что не соответствует ни одному шаблону.
--
Разрешить только провайдер безопасности SUN со всеми его типами сервисов и алгоритмами, кроме MessageDigest. Сервисы, реализованные другими провайдерами безопасности, должны быть отключены.
jdk.security.providers.filter=!SUN.MessageDigest; SUN
Обратите внимание, что в этом примере более конкретный шаблон с провайдером безопасности SUN расположен левее самого общего. Иначе общий шаблон примет решение для всех сервисов, реализованных SUN, включая MessageDigest, и конкретный шаблон будет проигнорирован.
Примеры неправильно заданных значений фильтра
Разрешить все сервисы, кроме алгоритма HmacMD5, независимо от провайдера безопасности и типа сервиса:
jdk.security.providers.filter=*; !*.*.HmacMD5
Это неправильно, потому что правило "*" соответствует любому сервису и разрешает его, а правило, блокирующее HmacMD5, всегда игнорируется. Правильное значение фильтра для этого примера было бы таким: !*.*.HmacMD5; *
--
Разрешить все сервисы, реализованные SUN, кроме MessageDigest. Сервисы, реализованные другими провайдерами безопасности, должны быть отключены.
jdk.security.providers.filter=!SUN.MessageDigest
Хотя и сервисы MessageDigest провайдера SUN, и сервисы, реализованные другими провайдерами безопасности, отключены, остальные сервисы SUN (не MessageDigest) не разрешены. Правильное значение фильтра для этого примера было бы !SUN.MessageDigest; SUN
--
Разрешить все сервисы, реализованные провайдером безопасности SunPKCS11. Сервисы, реализованные другими провайдерами безопасности, должны быть отключены.
jdk.security.providers.filter=SunPKCS11
Это неправильно, потому что провайдер SunPKCS11 нужно указывать по его имени, а не по классу. Допустимое имя имело бы вид SunPKCS11-SomeName. Правильное значение фильтра для этого примера было бы SunPKCS11-SomeName or SunPKCS11-*
Альтернативы
Перегруппировка провайдеров безопасности по типам сервисов
Идея этой альтернативы — перегруппировать провайдеры безопасности так, чтобы сервисы разных типов не смешивались друг с другом. Такая новая группировка позволила бы выбирать сервисы с точностью до типа при установке провайдера безопасности. Например, чтобы решить задачу FIPS, описанную в разделе Мотивация, провайдер безопасности без сертификации FIPS, обеспечивающий поддержку сертификатов X.509, устанавливался бы без того, чтобы в том же пакете поставлялась криптография без сертификации FIPS.
Вот некоторые из недостатков, выявленных у этой альтернативы:
-
Перегруппировка провайдеров безопасности OpenJDK серьёзно нарушила бы совместимость. Приложения, опираясь на публичную документацию JCA, могут ожидать, что сервис находится в определённом провайдере безопасности. При вызове API JCA может передаваться провайдер безопасности, реализующий алгоритм. Например, вызов
Signature.getInstance("SHA256withDSA", "SUN")ожидает, что провайдер безопасности SUN реализует сервис Signature для алгоритма SHA256withDSA. Такой вызов перестал бы работать, если бы сервисы Signature были перенесены из SUN в другой провайдер безопасности. -
Навязать изменения сторонним провайдерам безопасности невозможно, что снижает эффективность и охват этого подхода.
-
Даже если перегруппировать провайдеры безопасности по типам сервисов, добиться точности на уровне алгоритмов невозможно. Это не позволило бы поддержать некоторые сценарии, описанные в разделе Мотивация.
Затенение сервисов
В этом подходе мы исследовали фильтрацию сервисов путём их затенения с помощью фиктивного провайдера безопасности. Были намечены два варианта этой идеи:
Вариант 1
Фиктивный провайдер устанавливался бы первым в порядке предпочтения. Типы сервисов и алгоритмы, которые нужно заблокировать, фиктивный провайдер регистрировал бы у себя и возвращал бы экземпляры, которые при использовании выбрасывают исключение. Такая стратегия не позволила бы вернуть и использовать настоящий сервис. Если сервис нужно разрешить, фиктивный провайдер не возвращает экземпляр, и резервный механизм получает сервис от настоящего провайдера (если он доступен).
Вот некоторые из недостатков, выявленных у этого варианта:
-
Было бы невозможно блокировать сервисы, когда их провайдер безопасности передаётся в вызове API JCA. Кроме того, нет возможности блокировать сервисы в зависимости от их провайдера безопасности.
-
Исключения выбрасывались бы отложенно, из-за чего приложениям было бы трудно обрабатывать сбои и искать альтернативные сервисы. После вызова JCA ожидается, что возвращённый экземпляр типа сервиса работает. Любые изменения в этом отношении могут повлиять на совместимость.
-
Применять политики, блокирующие все алгоритмы заданного типа, было бы сложно, потому что для этого требуется исчерпывающий перечень всех алгоритмов. Политики типа списка разрешённых тоже потребовали бы исчерпывающего перечня всех алгоритмов и типов сервисов, потому что этот вариант может только блокировать сервисы.
Вариант 2
В качестве альтернативы фиктивный провайдер может подменять провайдеры из списка установленных, сохраняя ссылки на них. Переопределив java.security.Provider::getService, фиктивный провайдер может перехватывать все запросы типов сервисов и алгоритмов и возвращать либо null, либо настоящий сервис в соответствии со своей внутренней логикой фильтрации. Иными словами, фильтр был бы реализован внутри провайдера безопасности.
Вот некоторые из недостатков, выявленных у этого варианта:
-
Приложение может динамически установить провайдер безопасности, стоящий в порядке предпочтения выше фиктивного провайдера. В этом случае фиктивный провайдер не смог бы подменить новый провайдер и фильтровать его сервисы.
-
Этот подход очень сложен, а инфраструктура провайдеров безопасности не рассчитана на такое использование.
Только предпочтительные алгоритмы
В этой альтернативе вводится новое логическое свойство безопасности, позволяющее при необходимости переопределить семантику существующего свойства jdk.security.provider.preferred. При новой семантике разрешены были бы только предпочтительные сервисы. Подходящим именем для нового свойства было бы jdk.security.provider.preferredOnly.
Вот некоторые из недостатков, выявленных у этой альтернативы:
-
Перегруженная семантика свойства может запутать пользователей. Прецедентов такого поведения у свойств безопасности нет.
-
Текущий синтаксис предпочтительных сервисов не позволяет включать несколько сервисов в одно правило, из-за чего при перечислении большого числа сервисов он получается очень многословным и подверженным ошибкам.
-
С текущим синтаксисом предпочтительных сервисов невозможно было бы реализовать для блокировки сервисов стратегию списка запрещённых.
-
Синтаксис можно расширить, но сделать это так, чтобы он был согласованным и осмысленным для обеих семантик, было бы трудно.
Альтернативные варианты синтаксиса фильтра провайдеров безопасности
Предлагаемый синтаксис фильтра основан на существующих свойствах безопасности, таких как фильтр сериализации объектов, фильтр RMI Registry, фильтры JNDI (глобальный, LDAP и RMI) и список предпочтительных провайдеров. Хотя для фильтрации сервисов JCA нужны изменения, специфичные для этой предметной области, многие зарезервированные символы и конструкции сохраняют свою семантику: список шаблонов, упорядоченный слева направо, разделение шаблонов с помощью ';', использование '!' для действий отклонения, наличие подстановочных символов '*' для сопоставления шаблонов, указание сервисов последовательностями service-type.algorithm или cipher.transformation и, в более широком смысле, идея сопоставления со всеми сервисами, если уровни типа сервиса или алгоритма не указаны. Это решение обосновано тем, что оно опирается на знакомство пользователей с существующими грамматиками фильтрации в OpenJDK и упрощает освоение. Вместе с тем предметная область сервисов JCA достаточно отличается от других фильтров, основанных на классах, чтобы пользователи ожидали от неё своей специфики.
Тем не менее были исследованы и альтернативные варианты синтаксиса фильтра. Эта работа шла в направлении большей многословности шаблонов, чтобы контекст каждого компонента фильтра был задан явно. Например, шаблону вроде !SUN.MessageDigest.MD5; !*.MessageDigest.SHA*; * в предлагаемом синтаксисе в альтернативном синтаксисе мог бы соответствовать { provider: SUN; service-type: MessageDigest; algorithm: MD5; decision: reject } and { provider: ALL; service-type: MessageDigest; algorithm: { startsWith: SHA }; decision: reject } and { provider: ALL; service-type: ALL; algorithm: ALL; decision: allow }. Между очень компактным и полностью многословным синтаксисом можно выстроить практически бесконечное число вариантов и компромиссов, но оказалось, что все они обладают одинаковой выразительной силой.
У исследованных альтернативных вариантов синтаксиса выявлены следующие недостатки:
-
Основные источники сложности фильтра не устраняются. Для определения фильтра по-прежнему нужно было бы разобраться в сопоставлении сервисов по шаблонам, учитывать правила шаблонов, зависящие от порядка, и хорошо понимать как JCA, так и установленные провайдеры. Высокоуровневые действия, такие как блокировка всех использований алгоритма или блокировка режима шифрования, по-прежнему могут состоять из нескольких низкоуровневых шаблонов, даже с чередованием действий разрешения и отклонения, но их было бы труднее представить наглядно. Преимущества в основном носят второстепенный характер.
-
Из-за дополнительной многословности фильтры становятся длиннее и труднее читаются, если не использовать правильные отступы. Особенно это затронуло бы значения фильтров, передаваемые как системные свойства. При таком подходе, вероятно, потребовалась бы поддержка определения фильтров во внешних файлах.
-
Пользователи не увидят сходства с существующими фильтрами безопасности OpenJDK, и им пришлось бы изучать никак не связанный с ними язык. Язык с большим числом зарезервированных слов, конструкций и правил потенциально может повысить сложность, потребовать более долгого освоения и оставить простор для разных толкований, неоднозначности или несогласованности.
Чтобы проиллюстрировать это, расширим предложенный ранее синтаксис новой конструкцией для поддержки преобразований Cipher. Блокировка алгоритма AES с режимом ECB и дополнением PKCS #5 могла бы выглядеть так:
{ provider: ALL; service-type: Cipher; transformation: { algorithm: AES; mode: ECB; padding: PKCS5Padding }; decision: reject }. На первый взгляд этот пример выглядит простым, но при более внимательном рассмотрении возникают вопросы: можно ли использовать блоки преобразования и алгоритма на одном уровне вложенности? Считается ли, что режим и дополнение соответствуют всем значениям, если они не указаны в блоке преобразования? Что произойдёт, если блок преобразования добавлен, а сервис не относится к типу Cipher? Или если тип сервиса — Cipher и используется блок алгоритма?Более компактный синтаксис с плоской интерпретацией (например, простое сопоставление текста), единообразный на каждом уровне (провайдер, тип сервиса и либо алгоритм, либо преобразование), помогает снизить риск неожиданных взаимодействий между конструкциями языка.
Фильтрация только на уровне API ::getInstance
В качестве альтернативы мы рассмотрели упрощение многоуровневого механизма фильтрации: убрать проверки java.security.Provider::put, java.security.Provider::putService и java.security.Provider.Service::newInstance и оставить только проверки ::getInstance. Такой подход потребовал бы более компактной реализации, но уступает предложенному в эффективности и согласованности. На практике сторонние типы сервисов окажутся за пределами действия фильтра, или же для решения этой проблемы фильтру потребуется публичный API. Во втором случае понадобилось бы содействие сторонних библиотек, что оставляет простор для несогласованного поведения.
Чтобы получить мощное, гибкое и долгосрочное решение для фильтра, мы склоняемся к стратегии глубокой фильтрации.
Использование существующего API java.security.Provider::configure для настройки провайдеров безопасности
Провайдеры безопасности могут по желанию реализовать API java.security.Provider::configure, чтобы получать конфигурацию инициализации в виде строк. Сейчас в библиотеках JDK этот API реализует только провайдер безопасности SunPKCS11, который с его помощью получает конфигурацию для нижележащей библиотеки PKCS #11. В этой альтернативе мы рассматриваем использование этого API для настройки фильтра вместо добавления свойств безопасности и системных свойств.
У этого подхода несколько недостатков. Приведённый ниже список не претендует на полноту, но его достаточно, чтобы отказаться от этого подхода:
-
Хотя в фильтре можно было бы обойтись без части шаблона, задающей провайдера, выигрыш в простоте ничтожен, поскольку спецификация синтаксиса всё равно потребовалась бы для компонентов типа сервиса, алгоритма и псевдонима алгоритма.
-
Для SunPKCS11 и для сторонних провайдеров безопасности, которые сейчас реализуют
java.security.Provider::configure, любое требование к строке конфигурации, получаемой через параметр, нарушило бы совместимость. Текущий публичный API никак не регламентирует значение параметраconfigArg. -
Сторонним провайдерам безопасности, которые переопределяют
java.security.Provider::configure, пришлось бы намеренно выполнять действия по фильтрации. Без публичного API фильтра это, вероятно, привело бы к шаблонному коду. В любом случае это открывает путь к несогласованности и рискам безопасности. -
Пострадала бы возможность фильтрации независимо от провайдера безопасности, поскольку конфигурация задавалась бы для каждого провайдера безопасности отдельно. При статической конфигурации пользователю пришлось бы писать значения фильтра для каждого установленного провайдера безопасности. При динамической конфигурации это означает вызов
::configureдля каждого экземпляра провайдера. В любом случае это привело бы к дублированию значений фильтра и повысило бы риск ошибок конфигурации. Например, блокировку алгоритма AES пришлось бы указывать для SunJCE, для SunPKCS11 и, возможно, для сторонних провайдеров безопасности. Более того, статическая или динамическая установка новых провайдеров безопасности потребовала бы явной настройки.
Тестирование
Для проверки реализации этого предложения будет разработана тестовая обвязка, способная запускать процессы JVM со значениями фильтра, переданными как свойство безопасности. На основе этой обвязки будут реализованы следующие категории тестов:
-
Фильтрация сервисов по списку разрешённых по провайдеру безопасности, типу сервиса, алгоритму или псевдониму.
-
Фильтрация сервисов по списку запрещённых по провайдеру безопасности, типу сервиса, алгоритму или псевдониму.
-
Фильтрация сервисов с несколькими псевдонимами.
-
Фильтрация сервисов по преобразованию и псевдонимам преобразований.
-
Фильтрация сервисов, реализованных статически и динамически установленными провайдерами безопасности.
-
Фильтрация сервисов, реализованных провайдерами безопасности OpenJDK и сторонними провайдерами безопасности.
-
Значения фильтра, требующие экранирования символов или содержащие подстановочные знаки.
-
Значения фильтра с некорректным синтаксисом.
Кроме того, следует провести тестирование производительности в соответствии с разделом Критерии успеха.
Риски и допущения
Предполагается, что перед написанием значения фильтра будет проведён тщательный анализ сервисов, реализованных установленными провайдерами безопасности. Хотя провайдеры документируют все поддерживаемые типы сервисов и алгоритмы, чтобы разработчики приложений могли ими пользоваться, эта информация не обязательно собрана в одном стандарте или репозитории. Более того, провайдеры могут называть одни и те же алгоритмы немного по-разному, определять собственные псевдонимы, поддерживать разные режимы и дополнения для преобразований шифров или реализовывать сервисы, которым для работы нужны другие сервисы. При обновлении библиотеки провайдера безопасности могут появиться новые сервисы, измениться существующие и быть удалены устаревшие. Поэтому значения фильтра следует подбирать под каждое окружение после всестороннего анализа и проверять надлежащим тестированием. Перенос значений фильтра на другие развёртывания JDK без тщательной оценки может создать риск несоответствия политикам.
Предлагаемый для фильтров язык стремится к выразительности, но наследует часть низкоуровневых сложностей языков шаблонов и привносит новые. К ним относятся сопоставление по псевдорегулярным выражениям с жадными подстановочными знаками и escape-последовательностями символов, шаблоны, зависящие от позиции, семантика списков разрешённых и запрещённых, вычисление псевдонимов сервисов и особая обработка преобразований шифров. Среди всех настроек безопасности в OpenJDK фильтры относятся скорее к продвинутым и требуют знаний не только их синтаксиса и семантики, но и, в более общем смысле, Java Cryptographic Architecture. Чтобы снять эту проблему, настоящее предложение служит авторитетной спецификацией Security Providers Filter и находится в открытом доступе. Подробную документацию по JCA можно найти в Security Developer’s Guide.
Последствия неправильно определённого фильтра сводятся к следующим случаям: 1) сервис, который должен был быть разрешён, разрешён не был, и 2) сервис, который должен был быть заблокирован, заблокирован не был. Первый случай, пожалуй, менее проблематичен, поскольку отсутствие сервиса, скорее всего, приведёт к неработающей или недоступной функциональности, которую можно обнаружить при тестировании приложения. Второй случай может нарушить соответствие политикам, и его может быть труднее заметить. Чтобы снизить эти риски, предложение включает механизмы отладки фильтра и журналы, которые помогают автору фильтра понять, к чему приводит то или иное значение. К этим механизмам отладки относятся сообщения журнала при разборе фильтра, сообщения журнала при проверке сервисов по фильтру, сообщения исключений с указанием синтаксических ошибок и перечень разрешённых и заблокированных сервисов для каждого установленного провайдера. Подробнее об этих механизмах см. в разделе Описание.