Назад к блогу

Подделка 1024-битных RSA-подписей и шифрование в HTML: что именно ломается

Подделка 1024-битных RSA-подписей и шифрование в HTML: что именно ломается

Разбираем, как на самом деле работают схемы RSA из RFC 8017 — от базовых примитивов до кодирования перед подписью и шифрованием. Оказывается, «RSA» — это не одна операция, а целое семейство схем с разными гарантиями стойкости, где уязвимости чаще прячутся не в математике, а в порядке проверок и обработке ошибок. Материал будет полезен тем, кто хочет понять, почему одни схемы доказуемо надёжны, а другие — лишь предположительно, и какие комбинации ключей использовать не стоит.

Разбираем, как устроены примитивы и схемы RSA по RFC 8017 — от возведения в степень до кодирования перед подписью. Вопрос нетривиален тем, что «RSA» — это не одна операция, а семейство примитивов и схем с разными гарантиями: одни доказуемо стойкие, другие лишь предположительно, а уязвимости прячутся не в математике, а в порядке проверок и в том, как приложение сообщает об ошибках.

Что именно определено и что рекомендовано

RFC 8017 определяет две схемы шифрования: RSAES-OAEP и RSAES-PKCS1-v1_5. Для новых приложений требуется поддерживать RSAES-OAEP; RSAES-PKCS1-v1_5 включена только для совместимости с существующими приложениями.

Также определены две схемы подписи: RSASSA-PSS и RSASSA-PKCS1-v1_5. Совместное использование одной пары ключей RSA в разных схемах не рекомендуется для новых приложений. В частности, не рекомендуется применять одну пару и в RSASSA-PSS, и в RSASSA-PKCS1-v1_5: тогда доказательство безопасности RSASSA-PSS перестаёт быть достаточным. Не рекомендуется и совместное применение RSAES-OAEP с RSAES-PKCS1-v1_5: хотя RSAES-OAEP сам по себе атаке противостоит, противник может использовать слабость реализации RSAES-PKCS1-v1_5.

Примитивы RSA по шагам

Все схемы опираются на четыре примитива, которые различаются только назначением и именами аргументов.

RSAEP — примитив шифрования. Принимает открытый ключ (n, e) — модуль и открытый показатель — и представитель сообщения. Первым шагом проверяется диапазон: если m не между 0 и n−1, выводится ошибка message representative out of range и работа прекращается. Если проверка пройдена, вычисляется:

c = m^e mod n

Результат — представитель шифртекста.

RSADP — обратная операция закрытым ключом. Принимает ключ K и представитель шифртекста c, целое между 0 и n−1. Сначала проверяется диапазон c: иначе ciphertext representative out of range и остановка. Дальше поведение зависит от формы ключа. Если K задан как пара (n, d), то m = c^d mod n. Если K задан как набор (p, q, dP, dQ, qInv) и, возможно, с дополнительными тройками (r_i, d_i, t_i), вычисления идут по китайской теореме об остатках: сначала m_1 = c^dP mod p и m_2 = c^dQ mod q, при u > 2 — ещё m_i = c^(d_i) mod r_i для i = 3..u. Затем h = (m_1 − m_2) qInv mod p и m = m_2 + q h; при u > 2 добавляется итеративное восстановление по дополнительным простым. В конце выводится m.

Согласованность CRT-коэффициентов задаётся в определении ключа: dP и dQ — положительные целые меньше p и q, удовлетворяющие edP == 1 (mod (p−1)) и edQ == 1 (mod (q−1)); qInv — положительное целое меньше p, удовлетворяющее q*qInv == 1 (mod p).

RSASP1 и RSAVP1 — примитивы подписи. RSASP1 принимает закрытый ключ K и представитель сообщения m (0..n−1), выдаёт представитель подписи s (0..n−1), а при выходе m за диапазон возвращает message representative out of range. RSAVP1 принимает открытый ключ (n, e) и представитель подписи s (0..n−1), выдаёт представитель сообщения m (0..n−1), а при выходе s за диапазон возвращает signature representative out of range.

Схемы шифрования: OAEP и PKCS#1 v1.5

RSAES-OAEP-ENCRYPT

Сначала проверяются длины: если длина метки L превышает предел хеш-функции — ошибка label too long; если mLen > k − 2hLen − 2 — ошибка message too long. Здесь k — длина модуля в октетах, hLen — длина вывода хеш-функции.

Затем в EME-OAEP: если L не задана, она считается пустой строкой, и вычисляется lHash = Hash(L) длиной hLen. Генерируется строка дополнения PS из k − mLen − 2hLen − 2 нулевых октетов (может быть нулевой длины). Формируется блок данных DB = lHash || PS || 0x01 || M длиной k − hLen − 1. Генерируется случайный seed длиной hLen, и через MGF1 вычисляются маски: dbMask = MGF(seed, k − hLen − 1), maskedDB = DB xor dbMask, seedMask = MGF(maskedDB, hLen), maskedSeed = seed xor seedMask. Итог — EM = 0x00 || maskedSeed || maskedDB длиной k октетов.

Метка L ассоциируется с сообщением; по умолчанию её значение — пустая строка. В этой версии PKCS #1 L — пустая строка, другие применения метки вне области документа.

RSAES-OAEP-DECRYPT

Принимает закрытый ключ K, шифртекст C длины k и необязательную метку L. Сначала проверки длин: если длина L превышает предел хеш-функции, если длина C не равна k октетам или если k < 2hLen + 2 — выводится decryption error и работа прекращается. Затем C преобразуется в целое c = OS2IP(C), применяется RSADP(K, c); если RSADP сообщает ciphertext representative out of range (c >= n), снова выводится decryption error.

Полученное m преобразуется в EM = I2OSP(m, k), после чего идёт декодирование EME-OAEP: если L не задана, берётся пустая строка, вычисляется lHash = Hash(L), EM разделяется на Y, maskedSeed длины hLen и maskedDB длины k − hLen − 1. Из maskedDB через MGF получается seedMask, из maskedSeed xor seedMask — seed, из seed через MGF — dbMask, и DB = maskedDB xor dbMask. DB разделяется как lHash' || PS || 0x01 || M; если нет октета 0x01, отделяющего PS от M, если lHash не равен lHash' или если Y ненулевой — decryption error и остановка.

Ключевое требование: противник не должен различать разные условия ошибки на этом шаге — ни по сообщению об ошибке, ни по времени, и вообще не должен узнавать частичную информацию об EM. Иначе он получит полезные сведения о расшифровке C, что ведёт к атаке с выбранным шифртекстом, подобной атаке Мангера.

RSAES-PKCS1-v1_5

При шифровании первым шагом проверяется длина: если mLen > k − 11, где k — длина модуля n в октетах, операция завершается ошибкой message too long. Затем генерируется строка PS длины k − mLen − 3 из псевдослучайных ненулевых октетов, причём её длина не меньше восьми октетов, и формируется кодированное сообщение:

EM = 0x00 || 0x02 || PS || 0x00 || M

Далее EM преобразуется в целое m = OS2IP(EM), применяется RSAEP с открытым ключом (n, e), и результат c преобразуется в шифртекст C = I2OSP(c, k).

При расшифровании сначала проверяется, что длина C равна k октетам и что k не меньше 11, иначе decryption error. После RSADP и преобразования EM = I2OSP(m, k) проверяется, что первый октет EM равен 0x00, второй равен 0x02, что существует октет 0x00, отделяющий PS от M, и что длина PS не меньше 8 октетов; при нарушении любого из условий — decryption error.

Почему RSAES-PKCS1-v1_5 уязвима

Можно создавать корректные шифртексты RSAES-PKCS1-v1_5, не зная соответствующих открытых текстов, с разумной вероятностью успеха. Это используется в атаке с выбором шифртекста, как показано в работе Bleichenbacher. При декодировании EME-PKCS1-v1_5 проверяется формат EM = 0x00 || 0x02 || PS || 0x00 || M, и при несоответствии выводится decryption error. Если противник может различать разные условия ошибки по сообщению или по времени, он получает полезную информацию о расшифровании C, что ведёт к усиленной версии атаки Bleichenbacher.

RFC 8017 рекомендует легко реализуемые контрмеры: добавление структуры к кодируемым данным, строгую проверку соответствия PKCS #1 v1.5 (и другой избыточности) в расшифрованных сообщениях и объединение сообщений об ошибках в клиент-серверном протоколе на основе PKCS #1 v1.5. Отдельно указано: нужно следить, чтобы противник не различал условия ошибки — ни по сообщению, ни по времени.

Схемы подписи: PSS и PKCS#1 v1.5

EMSA-PSS-ENCODE

Параметризуется выбором хеш-функции, функции генерации маски и длины соли. Принимает сообщение M и максимальную битовую длину emBits — не менее 8hLen + 8sLen + 9.

Сначала проверяется, что длина M не превышает ограничение хеша (иначе message too long), затем вычисляется mHash = Hash(M) длиной hLen. Если emLen = ceil(emBits/8) меньше hLen + sLen + 2 — ошибка encoding error. Генерируется случайная соль длины sLen (при sLen = 0 — пустая строка), и формируется M' = восемь нулевых октетов || mHash || salt длиной 8 + hLen + sLen. Затем H = Hash(M'), и DB = PS || 0x01 || salt, где PS — строка нулей длины emLen − sLen − hLen − 2 (может быть нулевой). Маска dbMask = MGF(H, emLen − hLen − 1), maskedDB = DB xor dbMask, после чего левые 8*emLen − emBits бит самого левого октета maskedDB обнуляются. Итоговое закодированное сообщение имеет вид:

EM = maskedDB || H || 0xbc

Типичные длины соли в октетах — hLen и 0.

EMSA-PSS-VERIFY

Принимает M, EM длины emLen = ceil(emBits/8) и emBits, возвращает consistent или inconsistent. Если длина M превышает предел хеш-функции — сразу inconsistent. Вычисляется mHash = Hash(M). Если emLen < hLen + sLen + 2 — inconsistent. Если самый правый октет EM не равен 0xbc — inconsistent.

Из EM выделяются maskedDB (левые emLen − hLen − 1 октетов) и H (следующие hLen октетов); проверяется, что левые 8emLen − emBits бит самого левого октета maskedDB равны нулю. Затем dbMask = MGF(H, emLen − hLen − 1), DB = maskedDB xor dbMask, и левые 8emLen − emBits бит самого левого октета DB обнуляются. Проверяется, что левые emLen − hLen − sLen − 2 октетов DB равны нулю, а октет на позиции emLen − hLen − sLen − 1 равен 0x01 — иначе inconsistent. Соль берётся как последние sLen октетов DB. Наконец, вычисляется H' = Hash(M') для M' = восемь нулевых октетов || mHash || salt; если H = H' — consistent, иначе inconsistent.

EMSA-PKCS1-v1_5

Формирует значение из фиксированного префикса для выбранной хеш-функции и хеш-значения H. Для девяти хеш-функций (MD2, MD5, SHA-1, SHA-224, SHA-256, SHA-384, SHA-512, SHA-512/224, SHA-512/256) заданы точные байтовые префиксы. Например, для SHA-256 префикс равен 30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20, после которого идёт H. Каждый префикс включает OID хеш-функции, параметр NULL и длину хеш-значения. Схема детерминирована, потому что хеш-функции детерминированы: выход полностью определяется входом. Поэтому проверка может просто повторить кодирование и сравнить результат с ранее полученным закодированным сообщением.

Почему PSS предпочтительнее

RSASSA-PSS использует рандомизированную схему кодирования EMSA-PSS с солью — случайным значением, добавляемым к хешу сообщения. Это делает подпись вероятностной и обеспечивает доказуемую стойкость: сложность подделки подписи напрямую связывается со сложностью инвертирования RSA-функции. При проверке извлекаются соль и хеш, и проверяющий убеждается, что хеш, соль и сообщение согласованы.

RSASSA-PKCS1-v1_5 использует детерминированное кодирование EMSA-PKCS1-v1_5, и его стойкость лишь предполагается. В EMSA-PKCS1-v1_5 идентификатор хеш-функции встроен в кодированное сообщение, что вынуждает противника искать коллизии именно выбранной хеш-функции, но доказуемой стойкости это не даёт. Рекомендуется постепенный переход от EMSA-PKCS1-v1_5 к EMSA-PSS как предосторожность против будущих разработок.

MGF1 и хеш-функции

MGF1 принимает seed mgfSeed и желаемую длину маски maskLen в октетах. Если maskLen > 2^32 hLen, выводится mask too long и работа прекращается. Затем T инициализируется пустой строкой, и для counter от 0 до ceil(maskLen / hLen) − 1 выполняется цикл: counter преобразуется в 4-октетную строку C = I2OSP(counter, 4), после чего к T присоединяется Hash(mgfSeed || C). В конце выводятся первые maskLen октетов T.

RFC 8017 перечисляет девять хеш-функций: MD2, MD5, SHA-1, SHA-224, SHA-256, SHA-384, SHA-512, SHA-512/224 и SHA-512/256. MD2 и MD5 объявлены криптографически небезопасными и должны быть удалены из существующих приложений; эта версия стандарта поддерживает их только для обратной совместимости. Для новых приложений рекомендованы только SHA-224, SHA-256, SHA-384, SHA-512, SHA-512/224 и SHA-512/256. MD2, MD5 и SHA-1 рекомендованы только для совместимости с существующими приложениями на основе PKCS #1 v1.5. Для RSAES-OAEP и EMSA-PSS рекомендованы SHA-1, SHA-224, SHA-256, SHA-384, SHA-512, SHA-512/224 и SHA-512/256. Для EMSA-PKCS1-v1_5 рекомендованы SHA-224, SHA-256, SHA-384, SHA-512, SHA-512/224 и SHA-512/256.

Форматы ключей и ASN.1

RSAPublicKey — ASN.1 SEQUENCE из двух обязательных INTEGER: modulus (модуль n) и publicExponent (открытая экспонента e).

RSAPrivateKey — SEQUENCE, где обязательны version, modulus, publicExponent, privateExponent, prime1, prime2 и coefficient, а otherPrimeInfos помечен OPTIONAL. version — INTEGER с именованными значениями two-prime(0) и multi(1); он должен быть 0, если multi-prime не используется, и 1 при multi-prime. coefficient — обратный элемент q по модулю p. otherPrimeInfos содержит сведения о дополнительных простых r_3, ..., r_u по порядку; он должен быть опущен при version 0 и содержать минимум один OtherPrimeInfo при version 1. Каждый OtherPrimeInfo — SEQUENCE из prime (простой множитель r_i, i >= 3), exponent (d_i = d mod (r_i − 1)) и coefficient (CRT-коэффициент t_i = (r_1 r_2 ... * r_(i−1))^(−1) mod r_i).

OID и параметры AlgorithmIdentifier:

СхемаOIDПараметры
rsaEncryption (RSAES-PKCS1-v1_5){ pkcs-1 1 }NULL
id-RSAES-OAEP{ pkcs-1 7 }RSAES-OAEP-params
id-RSASSA-PSS{ pkcs-1 10 }RSASSA-PSS-params

Для схем подписи PKCS#1 v1.5 OID выбирается по хеш-алгоритму: md2WithRSAEncryption {pkcs-1 2}, md5WithRSAEncryption {pkcs-1 4}, sha1WithRSAEncryption {pkcs-1 5}, sha224WithRSAEncryption {pkcs-1 14}, sha256WithRSAEncryption {pkcs-1 11}, sha384WithRSAEncryption {pkcs-1 12}, sha512WithRSAEncryption {pkcs-1 13}, sha512-224WithRSAEncryption {pkcs-1 15}, sha512-256WithRSAEncryption {pkcs-1 16}. Для каждого из них параметры AlgorithmIdentifier обязаны быть NULL.

Что из этого следует на практике

  • Схема — не примитив. RSAEP/RSADP/RSASP1/RSAVP1 различаются только назначением и именами аргументов; вся защита от атак лежит в кодировании перед примитивом и в порядке проверок при декодировании. Слабость реализации RSAES-PKCS1-v1_5 может подорвать RSAES-OAEP при общей паре ключей.
  • Ошибки расшифрования нельзя различать. И для OAEP, и для PKCS#1 v1.5 RFC прямо требует, чтобы противник не отличал условия ошибки ни по сообщению, ни по времени. Иначе — атака с выбранным шифртекстом (Мангер для OAEP, усиленная Bleichenbacher для v1.5).
  • PSS даёт доказуемую стойкость, v1.5 — предположительную. Рандомизация солью в PSS связывает сложность подделки со сложностью инвертирования RSA; в v1.5 такой гарантии нет, и RFC рекомендует постепенный переход.
  • MD2 и MD5 непригодны. Они объявлены криптографически небезопасными и должны быть удалены; для новых приложений рекомендованы только SHA-2 семейства.
  • Одна пара ключей — одна схема. Совместное использование пары в PSS и v1.5 или в OAEP и v1.5 не рекомендуется: доказательства безопасности перестают работать.
  • Детерминированность v1.5 упрощает проверку. Поскольку кодирование детерминировано, верификация может повторить кодирование и сравнить результат — но это же свойство не даёт PSS-подобных гарантий.

Источники

Похожее