TLS и закрепление сертификатов
Управление версиями TLS
HTTPC по умолчанию требует TLS 1.2+, отклоняя доказанно небезопасные TLS 1.0/1.1. Рекомендуется TLS 1.3 для более сильной прямой секретности и более простого рукопожатия:
cfg := httpc.DefaultConfig()
cfg.Security.MinTLSVersion = tls.VersionTLS12 // по умолчанию
cfg.Security.MaxTLSVersion = tls.VersionTLS13 // по умолчаниюОписание версий
| Версия | Статус | По умолчанию в HTTPC |
|---|---|---|
| TLS 1.0 | Небезопасна, устарела (POODLE/BEAST) | Отклоняется |
| TLS 1.1 | Небезопасна, устарела | Отклоняется |
| TLS 1.2 | Безопасна | Минимальное требование |
| TLS 1.3 | Наиболее безопасна, рекомендуется (принудительная прямая секретность) | Поддерживается |
TIP
Для принудительного использования только TLS 1.3 (более высокая безопасность, более простое рукопожатие) установите MinTLSVersion = tls.VersionTLS13. Обратите внимание: некоторые старые клиенты/прокси могут не поддерживать TLS 1.3 — перед включением убедитесь в совместимости целевого сервиса.
WARNING
После установки Security.TLSConfig значения MinTLSVersion и MaxTLSVersion игнорируются — приоритет у настроек в TLSConfig. Для управления версиями в пользовательском TLSConfig установите TLSConfig.MinVersion / MaxVersion.
Шифрские наборы
Конфигурация по умолчанию допускает только безопасные шифрские наборы (серия ECDHE с принудительной прямой секретностью, AEAD-шифрование):
| Шифрский набор | Описание |
|---|---|
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 | Рекомендуется |
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 | Рекомендуется |
TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305 | Рекомендуется |
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 | Рекомендуется |
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 | Рекомендуется |
TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305 | Рекомендуется |
Шифрские наборы TLS 1.3 согласовываются протоколом автоматически и не управляются полем CipherSuites.
Пользовательская конфигурация TLS
При необходимости тонкого контроля (пользовательский CA, mTLS, фиксация шифрских наборов) установите Security.TLSConfig:
cfg := httpc.DefaultConfig()
cfg.Security.TLSConfig = &tls.Config{
MinVersion: tls.VersionTLS13, // Принудительный TLS 1.3
// Другие пользовательские настройки
}Пользовательский CA-сертификат
При подключении к сервисам с сертификатами внутреннего CA (корпоративный PKI, самоподписанные сертификаты) загрузите пользовательский корневой сертификат:
package main
import (
"crypto/tls"
"crypto/x509"
"log"
"os"
"github.com/cybergodev/httpc"
)
func main() {
caCert, err := os.ReadFile("custom-ca.pem")
if err != nil {
log.Fatal(err)
}
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(caCert) {
log.Fatal("Не удалось разобрать CA-сертификат")
}
cfg := httpc.DefaultConfig()
cfg.Security.TLSConfig = &tls.Config{
RootCAs: caCertPool,
MinVersion: tls.VersionTLS12,
}
client, err := httpc.New(cfg)
if err != nil {
log.Fatal(err)
}
defer func() { _ = client.Close() }()
log.Println("Клиент с пользовательским CA готов")
}Взаимная аутентификация TLS (mTLS)
Когда сервер требует клиентский сертификат (mTLS), настройте поле Certificates:
package main
import (
"crypto/tls"
"log"
"github.com/cybergodev/httpc"
)
func main() {
cert, err := tls.LoadX509KeyPair("client-cert.pem", "client-key.pem")
if err != nil {
log.Fatal(err)
}
cfg := httpc.DefaultConfig()
cfg.Security.TLSConfig = &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS12,
}
client, err := httpc.New(cfg)
if err != nil {
log.Fatal(err)
}
defer func() { _ = client.Close() }()
log.Println("mTLS-клиент готов")
}TIP
mTLS часто применяется в сетях нулевого доверия, внутренней аутентификации сервисных сетей, финансовых API. Сервер идентифицирует вызывающую сторону по клиентскому сертификату без дополнительного токена. При ротации сертификата нужно синхронно обновлять client-cert.pem / client-key.pem.
Закрепление сертификатов
Закрепление сертификатов (Certificate Pinning) добавляет дополнительный уровень проверки поверх стандартной проверки цепочки сертификатов: требует наличия известного закреплённого публичного ключа/сертификата в серверной цепочке. Даже при компрометации доверенного CA или принуждении к выпуску подделанного сертификата злоумышленник не сможет провести атаку «человек посередине» — поскольку публичный ключ его сертификата не соответствует закреплённому значению.
Принцип работы
Стандартная TLS-валидация: клиент доверяет любому сертификату, подписанному доверенным CA. Закрепление сертификатов: клиент дополнительно требует совпадения хеша публичного ключа сертификата с предзаданным значением.
Стандартная проверка: доверие CA → доверие любому сертификату, подписанному CA
Закрепление: доверие CA + публичный ключ сертификата должен совпадать с предзаданным хешемЗакрепление наслаивается поверх стандартной проверки и не требует установки InsecureSkipVerify. HTTPC проверяет сертификат на любом уровне цепочки, поэтому при закреплении промежуточного сертификата оно остаётся действительным и после продления листового сертификата.
Шаги генерации SPKI-хеша
SPKI (SubjectPublicKeyInfo) хеш — наиболее распространённый формат закрепления (стандарт HPKP). Шаги генерации:
Шаг 1: получение сертификата сервера (экспорт из браузера или получение через openssl)
# Получение цепочки сертификатов с сервера
openssl s_client -connect example.com:443 -showcerts < /dev/null 2>/dev/null \
| openssl x509 -outform pem > cert.pemШаг 2: извлечение публичного ключа из сертификата → DER-кодирование → SHA-256 → base64
openssl x509 -in cert.pem -pubkey -noout | \
openssl pkey -pubin -outform der | \
openssl dgst -sha256 -binary | \
openssl enc -base64
# Вывод: YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2fuihg=Разбор трёх шагов:
| Шаг | Фрагмент команды | Назначение |
|---|---|---|
| Извлечение публичного ключа | openssl x509 -pubkey -noout | Извлечение PEM-формата публичного ключа из X.509 сертификата |
| DER-кодирование | openssl pkey -pubin -outform der | Преобразование PEM-ключа в формат DER (PKIX) |
| Хеширование и кодирование | openssl dgst -sha256 -binary | openssl enc -base64 | SHA-256 с последующим base64-кодированием |
TIP
Рекомендуется закреплять SPKI промежуточного сертификата, а не листового. Промежуточный сертификат имеет длительный срок действия (обычно 5-10 лет), при продлении листового сертификата (например, Let's Encrypt 90 дней) промежуточный не меняется — закреплённое значение не нужно часто обновлять.
Закрепление SPKI-хеша (рекомендуется)
NewSPKIHashPinner принимает один или несколько base64-кодированных SHA-256 SPKI-хешей. Указание нескольких хешей поддерживает ротацию ключей — совпадение любого считается успехом:
package main
import (
"crypto/tls"
"log"
"github.com/cybergodev/httpc"
)
func main() {
pinner, err := httpc.NewSPKIHashPinner(
"YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2fuihg=", // Текущий промежуточный сертификат
"C5+lpZ7tcVwmwQIMcRtPbsQtWLABXhQzejna0wHFr8M=", // Резервный (ротация ключей)
)
if err != nil {
log.Fatal(err)
}
cfg := httpc.DefaultConfig()
cfg.Security.MinTLSVersion = tls.VersionTLS12
cfg.Security.CertificatePinner = pinner
client, err := httpc.New(cfg)
if err != nil {
log.Fatal(err)
}
defer func() { _ = client.Close() }()
log.Println("Клиент с закреплением сертификатов готов")
}NewSPKIHashPinner возвращает ошибку при пустом хеше или некорректном base64, выявляя проблемы конфигурации на этапе запуска. Логика проверки: перебор каждого сертификата серверной цепочки на каждом уровне, вычисление SHA-256 его SPKI и сравнение с любым из закреплённых хешей.
TIP
CertificatePinner дополняет стандартную проверку цепочки TLS и не требует установки InsecureSkipVerify. Проверка применяется к сертификату на любом уровне цепочки, поэтому закрепление промежуточного сертификата остаётся действующим после продления листового.
Сравнение трёх конструкторов Pinner
HTTPC предоставляет три конструктора Pinner для разных сценариев:
| Конструктор | Вход | Сценарий применения | Рекомендация |
|---|---|---|---|
NewSPKIHashPinner | base64 SHA-256 SPKI-хеш | Наиболее частый (формат HPKP) | Рекомендуется |
NewPublicKeyPinner | DER-кодированный PKIX публичный ключ | При наличии исходных байтов ключа | Удобно |
NewCertificatePinnerChain | Несколько Pinner | Комбинирование стратегий/смешанная ротация ключей | Продвинутый |
// 1. SPKI-хеш (рекомендуется, наиболее частый)
spkiPinner, err := httpc.NewSPKIHashPinner(
"YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2fuihg=",
)
// 2. DER публичный ключ (уже есть исходные байты ключа, внутренне вычисляется SHA-256)
pubPinner, err := httpc.NewPublicKeyPinner(pubKeyDER1, pubKeyDER2)
// 3. Комбинирование нескольких pinner, принимается любой (смешанные стратегии или ключи ротации разных конструкторов)
chainPinner := httpc.NewCertificatePinnerChain(spkiPinner, pubPinner)
cfg.Security.CertificatePinner = chainPinnerNewCertificatePinnerChain без аргументов возвращает Pinner «отклонять всех» (безопасное умолчание), гарантируя, что пропуск конфигурации не будет тихо пропущен как «разрешить всех».
Пользовательский CertificatePinner
В продвинутых сценариях с необходимостью пользовательской стратегии закрепления (например, закрепление полного сертификата вместо публичного ключа, динамическое получение закреплённых значений из конфигурационного центра) можно напрямую реализовать интерфейс CertificatePinner:
package main
import (
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"errors"
"log"
"github.com/cybergodev/httpc"
)
// fullCertPinner закрепляет SHA-256 полного сертификата (а не SPKI публичного ключа).
type fullCertPinner struct {
pinnedHashes map[string]bool
}
func newFullCertPinner(hashes ...string) *fullCertPinner {
m := make(map[string]bool, len(hashes))
for _, h := range hashes {
m[h] = true
}
return &fullCertPinner{pinnedHashes: m}
}
// Pin реализует интерфейс CertificatePinner, возвращая описание закреплённого значения (для логов/отладки).
func (p *fullCertPinner) Pin() string {
return "full-cert-pinner"
}
// VerifyPeerCertificate реализует интерфейс CertificatePinner.
// Возврат nil означает принятие, не-nil отклоняет рукопожатие.
func (p *fullCertPinner) VerifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
if len(verifiedChains) == 0 || len(verifiedChains[0]) == 0 {
return errors.New("certificate pinning failed: empty chain")
}
for _, cert := range verifiedChains[0] {
sum := sha256.Sum256(cert.Raw)
hash := base64.StdEncoding.EncodeToString(sum[:])
if p.pinnedHashes[hash] {
return nil // Совпадение, принято
}
}
return errors.New("certificate pinning failed: no matching certificate")
}
func main() {
// Закрепление SHA-256 полного листового сертификата (при продлении сертификата значение нужно обновить)
pinner := newFullCertPinner(
"wert6uY/PCq3yAAbZA/wtqfzfQsTwmxnfv6I3vRz1XQ=", // SHA-256 от cert.Raw
)
cfg := httpc.DefaultConfig()
cfg.Security.TLSConfig = &tls.Config{MinVersion: tls.VersionTLS12}
cfg.Security.CertificatePinner = pinner
client, err := httpc.New(cfg)
if err != nil {
log.Fatal(err)
}
defer func() { _ = client.Close() }()
log.Println("Клиент с пользовательским закреплением сертификатов готов")
}WARNING
При реализации пользовательского CertificatePinner обязательно наслаивайте проверку закрепления поверх стандартной проверки цепочки сертификатов (verifiedChains). Никогда не заменяйте им стандартную проверку — всегда держите InsecureSkipVerify = false.
Множественные хеши и ротация ключей
NewSPKIHashPinner и NewPublicKeyPinner принимают несколько значений — совпадение любого считается успехом. Это ключ к ротации ключей: одновременно закрепляются старый и новый публичные ключи, в период ротации проходят как старые, так и новые сертификаты — переключение без остановки.
Процесс ротации:
- Сервер генерирует новую пару ключей и выпускает новый сертификат
- Клиент обновляет закреплённые значения, одновременно сохраняя старый и новый хеши:go
pinner, _ := httpc.NewSPKIHashPinner( "OLD_HASH...", // Старый ключ (сохранять в период ротации) "NEW_HASH...", // Новый ключ (скоро будет включён) ) - Развёртывание клиента, подтверждение прохождения как старого, так и нового сертификата
- Сервер переключается на новый сертификат
- По истечении периода наблюдения без сбоев старый хеш удаляется из закреплённых значений
DANGER
Сбой закрепления (несовпадение закреплённых значений) приведёт к полному отказу соединения без возможности автоматического восстановления. Обязательно:
- Всегда сохраняйте хотя бы один резервный закреплённый ключ на случай повреждения основного
- Применяйте стратегию ротации «сначала добавить новый, затем удалить старый» с двойным окном
- Настройте мониторинг с быстрым откатом клиентской конфигурации при сбоях закрепления
Закрепление сертификатов и Let's Encrypt
Сертификаты Let's Encrypt действительны только 90 дней и часто продляются. Прямое закрепление листового сертификата потребует обновления закреплённого значения каждые 90 дней — высокие расходы на поддержку. Рекомендуемая стратегия:
| Объект закрепления | Срок действия | Влияние продления | Рекомендация |
|---|---|---|---|
| Листовой сертификат | 90 дней | Обновление закреплённого значения при каждом продлении | Низкая |
| Промежуточный сертификат Let's Encrypt | Около 5 лет | Обновление только при ротации промежуточного (раз в несколько лет) | Рекомендуется |
| Публичный ключ вашего домена (при самохостинге) | Под вашим контролем | Обновление только при вашей активной ротации ключа | По обстоятельствам |
Промежуточные сертификаты Let's Encrypt (R3, R10, R11 и др.) действительны несколько лет — закрепление SPKI промежуточного сертификата обеспечивает баланс безопасности и низких расходов на поддержку. При ротации промежуточного сертификата Let's Encrypt объявляет об этом заранее, позволяя спокойно обновить закреплённое значение.
// Закрепление промежуточного сертификата Let's Encrypt (рекомендуется)
pinner, err := httpc.NewSPKIHashPinner(
"YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2fuihg=", // Текущий промежуточный сертификат
"C5+lpZ7tcVwmwQIMcRtPbsQtWLABXhQzejna0wHFr8M=", // Резервный/следующий промежуточный сертификат
)TIP
Публичный ключ промежуточного сертификата Let's Encrypt при ротации обычно остаётся неизменным (выпускается только новый сертификат, ключ переиспользуется). Закрепление SPKI публичного ключа стабильнее, чем закрепление полного промежуточного сертификата — при неизменном ключе SPKI-хеш не меняется.
Соображения безопасности закрепления сертификатов
| Риск | Последствие | Меры снижения |
|---|---|---|
| Истечение/несовпадение закреплённого значения | Полный отказ соединения | Множественные хеши + резервные ключи + мониторинг |
| Потеря резервного ключа | Невозможность ротации, блокировка | Офлайн-резервное копирование нескольких пар ключей |
| Хардкод закреплённых значений | Обновление требует релиза | Динамическая загрузка из конфигурационного центра (пользовательский Pinner) |
| Закрепление только одного уровня | Единая точка отказа | Закрепление нескольких уровней (листовой + промежуточный) |
WARNING
Закрепление сертификатов — «обоюдоострый меч»: значительно повышает защиту от MITM/компрометации CA, но сбой закрепления приводит к жёсткому отказу. Перед применением убедитесь:
- Наличие надёжного механизма обновления и распространения закреплённых значений
- Мониторинг частоты сбоев закрепления с настройкой оповещений
- Наличие аварийного переключателя «отключить закрепление» (хотя это снижает безопасность)
Сравнение стратегий закрепления
| Стратегия | Безопасность | Стоимость поддержки | Рекомендуемый сценарий |
|---|---|---|---|
| Закрепление корневого сертификата | Низкая | Низкая | Только защита от подмены, диапазон CA слишком широк |
| Закрепление промежуточного сертификата | Средняя | Средняя | Рекомендуется, баланс безопасности и поддержки |
| Закрепление листового сертификата | Высокая | Высокая | Высокая безопасность, контролируемые сертификаты |
| Закрепление нескольких уровней | Высокая | Средняя | Наилучший вариант, многоуровневая избыточность |
InsecureSkipVerify
InsecureSkipVerify пропускает всю проверку цепочки TLS-сертификатов, используется только для тестирования:
// Только для тестирования!
cfg := httpc.TestingConfig()
// InsecureSkipVerify = true → пропуск проверки TLS-сертификатовHTTPC при обнаружении InsecureSkipVerify = true в httpc.New() вне тестовой среды выводит предупреждение в stderr (один раз на процесс). Определение тестовой среды: исполняемый файл с суффиксом .test или установлены переменные окружения GO_TEST / GOTEST=1.
DANGER
InsecureSkipVerify = true делает все меры безопасности TLS недействительными (включая закрепление сертификатов), используйте только в тестовой среде. Никогда не устанавливайте true в продакшене. При необходимости пользовательской логики проверки (например, закрепление полного сертификата) реализуйте интерфейс CertificatePinner, а не пропускайте проверку.
HTTP/2
HTTP/2 включён по умолчанию и доступен только при использовании TLS (h2 через ALPN):
cfg := httpc.DefaultConfig()
cfg.Connection.EnableHTTP2 = false // Отключить HTTP/2 (только HTTP/1.1)TIP
При включении закрепления сертификатов или пользовательского TLSConfig согласование ALPN для HTTP/2 продолжает работать корректно. Для отключения HTTP/2 (например, отладка или совместимость со старыми прокси) установите EnableHTTP2 = false.
Лучшие практики
- Используйте конфигурацию TLS по умолчанию (TLS 1.2+), ручная настройка не требуется
- При закреплении сертификатов закрепляйте SPKI промежуточного сертификата с подготовкой резервных значений
- Множественные хеши поддерживают ротацию ключей — используйте стратегию «сначала добавить новый, затем удалить старый»
- Регулярно обновляйте закреплённые значения синхронно с продлением серверных сертификатов
- Используйте
SecureConfig()как базовую линию безопасности - Никогда не устанавливайте
InsecureSkipVerifyв продакшене - В высокобезопасных сценариях закрепляйте многоуровневые сертификаты (листовой + промежуточный) для избыточности
Что дальше
- Защита от SSRF — конфигурация безопасности SSRF
- Обзор безопасности — обзор функций безопасности
- Config API — справочник SecurityConfig