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

★ Подписывайтесь на канал и проходите курсы.