Go-фича: Защита секретов
Часть серии Принято! В ней простыми словами объясняются новые фичи Go.
Автоматически очищать использованную память, чтобы предотвратить утечку секретных данных.
Версия 1.26 • Станд. библиотека • Мелочь
Что
Новый пакет runtime/secret позволяет запускать функцию в защищенном режиме (secret mode). После завершения функции все использованные ей регистры и стек сразу очищаются (затираются нулями). Выделенная функцией память в куче очищается после того, как сборщик мусора решит, что она недостижима (unreachable).
secret.Do(func() {
// Сгенерировть сессионный ключ
// и зашифровать им данные.
})
Это помогает убедиться, что конфиденциальная информация не хранится в памяти дольше, чем нужно, и снижает риск того, что злоумышленники получат к ней доступ.
Пакет экспериментальный и предназначен в основном для разработчиков криптографических библиотек, а не для разработчиков приложений.
Зачем
Криптографические протоколы, такие как WireGuard или TLS, обладают свойством под названием "прямая секретность" (forward secrecy). Это значит, что даже если злоумышленник получит доступ к долгосрочным секретам (например, к приватному ключу в TLS), он все равно не сможет расшифровать прошлые сессии. Для этого сессионные ключи (которые используются для шифрования и расшифровки данных во время конкретной сессии) должны удаляться из памяти после использования. Если нет надежного способа очистить эту память, ключи могут остаться там надолго, и тогда прямая секретность нарушится.
В Go память управляется рантаймом, и нет гарантии, когда и как она очищается. Конфиденциальные данные могут оставаться в куче или на стеке, и их можно получить через дампы памяти или атаки на память. Разработчикам часто приходится использовать ненадежные "хаки" с рефлексией, чтобы попытаться обнулить внутренние буферы в стандартных криптографических библиотеках. Даже так часть данных все равно может остаться в памяти, где разработчик не может их удалить или контролировать.
Решение — создать рантайм-механизм, который автоматически очищает всю временную память, задействованную во время операций над секретными данными. Так разработчикам библиотек будет проще писать безопасный код без всяких костылей.
Как
Добавить пакет runtime/secret с функциями Do и Enabled:
// Do invokes f.
//
// Do ensures that any temporary storage used by f is erased in a
// timely manner. (In this context, "f" is shorthand for the
// entire call tree initiated by f.)
// - Any registers used by f are erased before Do returns.
// - Any stack used by f is erased before Do returns.
// - Any heap allocation done by f is erased as soon as the garbage
// collector realizes that it is no longer reachable.
// - Do works even if f panics or calls runtime.Goexit. As part of
// that, any panic raised by f will appear as if it originates from
// Do itself.
func Do(f func())
// Enabled reports whether Do appears anywhere on the call stack.
func Enabled() bool
Текущее решение имеет несколько ограничений:
- Работает только на linux/amd64 и linux/arm64. На других платформах функция
Doпросто вызываетfнапрямую. - Защита не распространяется на глобальные переменные, которые изменяет
f. - Если внутри
fзапускается новая горутина, произойдет паника. - Если
fвызываетruntime.Goexit, очистка откладывается до выполнения всех отложенных функций. - Данные в куче очищаются только если ➊ на них больше нет ссылок, и ➋ сборщик мусора заметил это. Первый пункт зависит от программы, второй — от того, когда рантайм решит провести сборку мусора.
- Если в
fпроисходит паника, значение паники может содержать ссылки на память, выделенную внутриf. Эта память не будет очищена, пока значение паники доступно. - Адреса указателей могут попасть в буферы данных, которые использует сборщик мусора. Не храните конфиденциальную информацию в указателях.
Последний пункт может быть не сразу понятен, поэтому вот пример. Если смещение в массиве само по себе является секретом (у вас есть массив data, и секретный ключ всегда начинается с data[100]), не создавайте указатель на это место (не делайте указатель p на &data[100]). Иначе сборщик мусора может сохранить этот указатель (ему нужно знать обо всех активных указателях для работы). Если кто-то получит доступ к памяти сборщика мусора, ваше секретное смещение может быть раскрыто.
Пакет в основном предназначен для разработчиков, которые работают с криптографическими библиотеками. Большинству приложений лучше использовать более высокоуровневые библиотеки, которые внутри уже используют secret.Do.
В Go 1.26 пакет runtime/secret объявлен экспериментальным. Чтобы его включить, установите переменную GOEXPERIMENT=runtimesecret при сборке.
Пример
Используем secret.Do, чтобы сгенерировать сессионный ключ и зашифровать сообщение с помощью AES-GCM:
// Encrypt создает временный ключ и шифрует сообщение.
// Вся чувствительная операция обернута в secret.Do,
// чтобы ключ и внутреннее состояние AES были удалены из памяти.
func Encrypt(message []byte) ([]byte, error) {
var ciphertext []byte
var encErr error
secret.Do(func() {
// 1. Сгенерировать временный ключ длиной 32 байта.
// Эта операция защищена с помощью secret.Do.
key := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, key); err != nil {
encErr = err
return
}
// 2. Создаем шифр (раундовые ключи).
// Эта структура тоже защищена.
block, err := aes.NewCipher(key)
if err != nil {
encErr = err
return
}
gcm, err := cipher.NewGCM(block)
if err != nil {
encErr = err
return
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
encErr = err
return
}
// 3. Запечатываем данные.
// Из замыкания выходит только зашифрованный текст.
ciphertext = gcm.Seal(nonce, nonce, message, nil)
})
return ciphertext, encErr
}
Здесь secret.Do защищает не только сам ключ, но и структуру cipher.Block, которая создается внутри функции и содержит расширенные ключи.
Ссылки
𝗣 21865 • 𝗖𝗟 704615 • 👥 Daniel Morsing, Dave Anderson, Filippo Valsorda, Jason A. Donenfeld, Keith Randall, Russ Cox
★ Подписывайтесь на канал и проходите курсы.