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. Эта память не будет очищена, пока значение паники доступно. - Адреса указателей могут попасть в буферы данных, которые использует сборщик мусора. Не храните конфиденциальную информацию в указателях.
Пакет в основном предназначен для разработчиков, которые работают с криптографическими библиотеками. Большинству приложений лучше использовать более высокоуровневые библиотеки, которые внутри уже используют secret.Do.
В Go 1.26 пакет runtime/secret объявлен экспериментальным. Чтобы его включить, установите переменную GOEXPERIMENT=runtimesecret при сборке.
Пример
Генерируем сессионный ключ, при этом временный приватный ключ и общий секрет остаются в безопасности благодаря secret.Do:
// DeriveSessionKey performs an ephemeral key exchange to agree on a
// session key. It wraps the sensitive mathematics in secret.Do to
// ensure the ephemeral private key and raw shared secret are
// erased from memory immediately after use.
func DeriveSessionKey(peerPublicKey *ecdh.PublicKey) (*ecdh.PublicKey, []byte, error) {
var pubKey *ecdh.PublicKey
var sessionKey []byte
var err error
// Use secret.Do to contain the sensitive data during the handshake.
// The ephemeral private key and the raw shared secret will be
// wiped out when this function finishes.
secret.Do(func() {
// 1. Generate an ephemeral private key.
// This is highly sensitive; if leaked later, forward secrecy is broken.
privKey, e := ecdh.P256().GenerateKey(rand.Reader)
if e != nil {
err = e
return
}
// 2. Compute the shared secret (ECDH).
// This raw secret is also highly sensitive.
sharedSecret, e := privKey.ECDH(peerPublicKey)
if e != nil {
err = e
return
}
// 3. Derive the final session key (e.g., using HKDF).
// We copy the result out; the inputs (privKey, sharedSecret)
// will be destroyed by secret.Do when they become unreachable.
sessionKey = performHKDF(sharedSecret)
pubKey = privKey.PublicKey()
})
// The session key is returned for use, but the "recipe" to recreate it
// is destroyed. Additionally, because the session key was allocated
// inside the secret block, the runtime will automatically zero it out
// when the application is finished using it.
return pubKey, sessionKey, err
}
Здесь временный приватный ключ и общий секрет по сути являются "опасными отходами" — они нужны для создания итогового сессионного ключа, но их опасно хранить.
Если эти значения останутся в куче, и злоумышленник потом получит доступ к памяти приложения (например, через дамп памяти или уязвимость типа Heartbleed), он сможет использовать эти промежуточные данные, чтобы восстановить сессионный ключ и расшифровать прошлые сеансы.
Завернув вычисления в secret.Do, мы гарантируем, что как только сессионный ключ создан, все "ингредиенты", использованные для его создания, сразу же навсегда удаляются. Это значит, что даже если сервер взломают в будущем, этот конкретный прошлый сеанс нельзя будет раскрыть, что обеспечивает прямую секретность.
Ссылки
𝗣 21865 • 𝗖𝗟 704615 • 👥 Daniel Morsing, Dave Anderson, Filippo Valsorda, Jason A. Donenfeld, Keith Randall, Russ Cox
★ Подписывайтесь на канал и проходите курсы.