Вопрос по безопасности: что такое тайминговая атака на криптографическую функцию и как писать код, устойчивый к таким атакам; приведите примеры уязвимого и защищённого кода
Кратко — что это и откуда берётся - Тайминговая атака — когда злоумышленник измеряет время работы криптографической операции и по зависимостям времени выводит секретные данные (ключи, MAC, пароли). Формально время может зависеть от секрета: T(input,s)T(\text{input}, s)T(input,s). По изменениям TTT атакующий реконструирует части sss. - Источники утечек: ветвления, ранний выход, разные пути выполнения (branch mispredict), разные обращения в кэш (table lookup vs. cache miss), переменное число итераций, инструкции с переменным временем. Как писать устойчивый код — принципы 1. Делайте операции, время которых не зависит от секретных данных (constant-time). Идея: для двух разных секретов время должно быть (практически) одинаковым: ∀s1,s2 T(input,s1)≈T(input,s2)\forall s_1,s_2\ T(\text{input},s_1)\approx T(\text{input},s_2)∀s1,s2T(input,s1)≈T(input,s2). 2. Избегайте секрет-зависимых ветвлений и ранних возвратов. Не писать «if (secret != x) return;». 3. Не делайте секрет-зависимых обращений в память (lookup по секретному индексу) — это даёт утечку через кэш. 4. Используйте проверенные библиотечные реализации (OpenSSL, libsodium, BoringSSL) и их тайминг-сейф функции (например, CRYPTO_memcmp(), sodium_memcmp(), hmac.compare_digest). 5. Будьте внимательны к компилятору: шаблоны «без ветвлений» можно случайно превратить в ветвления оптимизацией. Тестируйте ассемблер/профилирование; при необходимости используйте volatile/барьеры или атрибуты/интринсики библиотеки. Примеры 1) Уязвимый код (строго ранний выход — уязвим для сравнения MAC / пароля): C: ``` int vulnerable_equal(const unsigned char *a, const unsigned char *b) { // strcmp/memcmp/поэлементный ранний выход — тайминг утечёт for (size_t i = 0; a[i] && b[i]; ++i) { if (a[i] != b[i]) return 0; } return 1; } ``` Пояснение: время зависит от первого различного байта (позиция kkk влияет на время). 2) Защищённый код — сравнение константного времени: C: ``` int constant_time_equal(const unsigned char *a, const unsigned char *b, size_t n) { unsigned char diff = 0; for (size_t i = 0; i < n; ++i) { diff |= a[i] ^ b[i]; // всегда обход всех байт, без раннего выхода } return diff == 0; } ``` Пояснение: независимо от расположения различий выполняется одинаковое число операций. 3) Уязвимый доступ по секретному индексу (lookup leak): C: ``` unsigned char sbox_lookup_vuln(const unsigned char *sbox, size_t idx) { // чтение по секретному idx — кэш-утечка return sbox[idx]; } ``` 4) Константно-временный lookup (паттерн с маской): C: ``` unsigned char sbox_lookup_ct(const unsigned char *sbox, size_t n, size_t idx) { unsigned char out = 0; for (size_t i = 0; i < n; ++i) { // (i == idx) вычисляется без ветвления, затем расширяется в маску 0x00/0xFF unsigned char mask = (unsigned char)(-(int)(i == idx)); out |= sbox[i] & mask; // собираем требуемый байт через побитовую маску } return out; } ``` Замечание: этот приём избегает секрет-зависимых ветвлений, но всё равно читает всю таблицу (поэтому кэш-утечка отсутствует по индексу). Следите за тем, чтобы компилятор не переписал код в ветвления. Дополнительные советы и оговорки - Для криптографии предпочитайте аппаратные реализации (AES-NI) либо библиотеки, уже оптимизированные для constant-time. - Для языков высокого уровня: используйте специализированные функции (в Python: hmac.compare_digest — реализована безопасно в CPython). - Тестируйте устойчивость: профайлеры, измерения времени с большим числом запросов, статический и ассемблерный анализ. - Полная невосприимчивость к таймингу на практике требует аккуратности: сетевые шумы и локальные измерения — факторы; тем не менее, простые ошибки (ранний выход, секретный индекс в таблице) легко эксплуатируются. Коротко: избегайте секрет-зависимых ветвлений и обращений в память; используйте проверенные библиотеки и константно-временные примитивы (пример выше: побайтовая агрегация XOR вместо раннего return).
- Тайминговая атака — когда злоумышленник измеряет время работы криптографической операции и по зависимостям времени выводит секретные данные (ключи, MAC, пароли). Формально время может зависеть от секрета: T(input,s)T(\text{input}, s)T(input,s). По изменениям TTT атакующий реконструирует части sss.
- Источники утечек: ветвления, ранний выход, разные пути выполнения (branch mispredict), разные обращения в кэш (table lookup vs. cache miss), переменное число итераций, инструкции с переменным временем.
Как писать устойчивый код — принципы
1. Делайте операции, время которых не зависит от секретных данных (constant-time).
Идея: для двух разных секретов время должно быть (практически) одинаковым: ∀s1,s2 T(input,s1)≈T(input,s2)\forall s_1,s_2\ T(\text{input},s_1)\approx T(\text{input},s_2)∀s1 ,s2 T(input,s1 )≈T(input,s2 ).
2. Избегайте секрет-зависимых ветвлений и ранних возвратов. Не писать «if (secret != x) return;».
3. Не делайте секрет-зависимых обращений в память (lookup по секретному индексу) — это даёт утечку через кэш.
4. Используйте проверенные библиотечные реализации (OpenSSL, libsodium, BoringSSL) и их тайминг-сейф функции (например, CRYPTO_memcmp(), sodium_memcmp(), hmac.compare_digest).
5. Будьте внимательны к компилятору: шаблоны «без ветвлений» можно случайно превратить в ветвления оптимизацией. Тестируйте ассемблер/профилирование; при необходимости используйте volatile/барьеры или атрибуты/интринсики библиотеки.
Примеры
1) Уязвимый код (строго ранний выход — уязвим для сравнения MAC / пароля):
C:
```
int vulnerable_equal(const unsigned char *a, const unsigned char *b) {
// strcmp/memcmp/поэлементный ранний выход — тайминг утечёт
for (size_t i = 0; a[i] && b[i]; ++i) {
if (a[i] != b[i]) return 0;
}
return 1;
}
```
Пояснение: время зависит от первого различного байта (позиция kkk влияет на время).
2) Защищённый код — сравнение константного времени:
C:
```
int constant_time_equal(const unsigned char *a, const unsigned char *b, size_t n) {
unsigned char diff = 0;
for (size_t i = 0; i < n; ++i) {
diff |= a[i] ^ b[i]; // всегда обход всех байт, без раннего выхода
}
return diff == 0;
}
```
Пояснение: независимо от расположения различий выполняется одинаковое число операций.
3) Уязвимый доступ по секретному индексу (lookup leak):
C:
```
unsigned char sbox_lookup_vuln(const unsigned char *sbox, size_t idx) {
// чтение по секретному idx — кэш-утечка
return sbox[idx];
}
```
4) Константно-временный lookup (паттерн с маской):
C:
```
unsigned char sbox_lookup_ct(const unsigned char *sbox, size_t n, size_t idx) {
unsigned char out = 0;
for (size_t i = 0; i < n; ++i) {
// (i == idx) вычисляется без ветвления, затем расширяется в маску 0x00/0xFF
unsigned char mask = (unsigned char)(-(int)(i == idx));
out |= sbox[i] & mask; // собираем требуемый байт через побитовую маску
}
return out;
}
```
Замечание: этот приём избегает секрет-зависимых ветвлений, но всё равно читает всю таблицу (поэтому кэш-утечка отсутствует по индексу). Следите за тем, чтобы компилятор не переписал код в ветвления.
Дополнительные советы и оговорки
- Для криптографии предпочитайте аппаратные реализации (AES-NI) либо библиотеки, уже оптимизированные для constant-time.
- Для языков высокого уровня: используйте специализированные функции (в Python: hmac.compare_digest — реализована безопасно в CPython).
- Тестируйте устойчивость: профайлеры, измерения времени с большим числом запросов, статический и ассемблерный анализ.
- Полная невосприимчивость к таймингу на практике требует аккуратности: сетевые шумы и локальные измерения — факторы; тем не менее, простые ошибки (ранний выход, секретный индекс в таблице) легко эксплуатируются.
Коротко: избегайте секрет-зависимых ветвлений и обращений в память; используйте проверенные библиотеки и константно-временные примитивы (пример выше: побайтовая агрегация XOR вместо раннего return).