Алгоритмы подписи
CyberGo JWT поддерживает 4 типа, всего 12 алгоритмов подписи, охватывающих все сценарии от монолитных приложений до микросервисной архитектуры.
Обзор алгоритмов
| Тип | Алгоритмы | Тип ключа | Сценарий использования |
|---|---|---|---|
| HMAC | HS256 / HS384 / HS512 | Симметричный ключ | Монолитные приложения, простые сервисы |
| RSA | RS256 / RS384 / RS512 | Публичный/приватный ключ | Микросервисы, проверка на нескольких сервисах |
| RSA-PSS | PS256 / PS384 / PS512 | Публичный/приватный ключ | Микросервисы (рекомендуемая замена RSA) |
| ECDSA | ES256 / ES384 / ES512 | Публичный/приватный ключ | Высокопроизводительные микросервисы |
HMAC (симметричный ключ)
HMAC использует один и тот же ключ для подписи и проверки — это самое простое решение.
Требования к ключу
HMAC-ключ должен пройти две проверки в validateSigningKey:
- Проверка длины: при
len(SecretKey) < 32возвращаетсяErrInvalidSecretKey, сообщение об ошибке содержит фактическую длину в байтах, например"minimum 32 bytes required, got 16" - Проверка энтропии: через
internal.IsWeakKeyобнаруживаются низкоэнтропийные ключи, следующие шаблоны отклоняются:- Полностью одинаковые символы (например,
"aaaaaaaa...") - Повторяющиеся короткие шаблоны (например,
"abcabcabc...") - Последовательные возрастающие/убывающие последовательности (например,
"abcdefgh...") - Распространённые слабые пароли и их вариации (например,
"password","qwerty")
- Полностью одинаковые символы (например,
Слабые ключи будут отклонены
Не используйте «повторяющиеся символы», «последовательности», «словарные слова» и другие легко угадываемые ключи. Даже если длина достигает 32 байт, низкоэнтропийный ключ будет отклонён на этапе инициализации jwt.New с возвратом ErrInvalidSecretKey.
В production следует генерировать ключи из криптографически безопасного случайного источника:
package main
import (
"crypto/rand"
"encoding/base64"
"fmt"
"log"
"github.com/cybergodev/jwt"
)
func main() {
// Генерация 32-байтного случайного ключа с помощью crypto/rand
raw := make([]byte, 32)
if _, err := rand.Read(raw); err != nil {
log.Fatal(err)
}
// Кодирование в base64 для хранения и передачи
secret := base64.StdEncoding.EncodeToString(raw)
cfg := jwt.DefaultConfig()
cfg.SecretKey = secret
processor, err := jwt.New(cfg)
if err != nil {
log.Fatal(err)
}
defer processor.Close()
fmt.Println("HMAC-ключ готов, длина (байт):", len(secret)) // Вывод: HMAC-ключ готов, длина (байт): 44
}Использование
cfg := jwt.DefaultConfig()
cfg.SecretKey = "hmac-key-that-has-at-least-32-bytes!"
cfg.SigningMethod = jwt.SigningMethodHS256 // Значение по умолчанию, можно опуститьВыбор алгоритма
| Константа | Алгоритм | Описание |
|---|---|---|
SigningMethodHS256 | HMAC-SHA256 | Рекомендуется, баланс производительности и безопасности |
SigningMethodHS384 | HMAC-SHA384 | Более высокая безопасность |
SigningMethodHS512 | HMAC-SHA512 | Максимальная безопасность |
Рекомендация
Для большинства сценариев достаточно HS256. Рекомендуется использовать криптографически безопасный случайный ключ длиной не менее 32 байт.
RSA (асимметричный ключ)
RSA использует приватный ключ для подписи и публичный для проверки. Подходит для сценариев, где проверяющей стороне не нужно владеть приватным ключом.
Использование
cfg := jwt.DefaultConfig()
cfg.SigningMethod = jwt.SigningMethodRS256
cfg.SigningKey = rsaPrivateKey // *rsa.PrivateKey
cfg.VerificationKey = rsaPublicKey // *rsa.PublicKey (необязательно)Ключ проверки
VerificationKey необязателен. Если не установлен, библиотека использует SigningKey для проверки (извлекает публичный ключ из приватного).
Генерация ключей
// Генерация 2048-битной пары RSA-ключей (библиотека требует минимум 2048 бит, иначе ErrInvalidSecretKey)
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
log.Fatal(err)
}
publicKey := &privateKey.PublicKeyВыбор алгоритма
| Константа | Алгоритм | Описание |
|---|---|---|
SigningMethodRS256 | RSA-SHA256 | Рекомендуется |
SigningMethodRS384 | RSA-SHA384 | Более высокая безопасность |
SigningMethodRS512 | RSA-SHA512 | Максимальная безопасность |
Совместное использование ключей с RSA-PSS
RS256/RS384/RS512 и PS256/PS384/PS512 используют одинаковые типы ключей (*rsa.PrivateKey / *rsa.PublicKey) и одинаковую логику проверки, ключи взаимозаменяемы. При миграции с RSA на RSA-PSS повторная генерация ключей не требуется.
RSA-PSS (асимметричный ключ, рекомендуется вместо RSA)
RSA-PSS — улучшенная схема подписи RSA, использующая заполнение вероятностной схемы подписи (PSS). Безопаснее PKCS#1 v1.5. Ключи те же, что и для RSA.
Использование
cfg := jwt.DefaultConfig()
cfg.SigningMethod = jwt.SigningMethodPS256
cfg.SigningKey = rsaPrivateKey // *rsa.PrivateKey (совместим с ключами RSA)
cfg.VerificationKey = rsaPublicKey // *rsa.PublicKey (необязательно)Рекомендуемая замена
RSA-PSS безопаснее RSA PKCS#1 v1.5. Рекомендуется использовать алгоритмы RSA-PSS в новых проектах. Ключи полностью идентичны RSA, дополнительная генерация не требуется.
Выбор алгоритма
| Константа | Алгоритм | Описание |
|---|---|---|
SigningMethodPS256 | RSA-PSS-SHA256 | Рекомендуется |
SigningMethodPS384 | RSA-PSS-SHA384 | Более высокая безопасность |
SigningMethodPS512 | RSA-PSS-SHA512 | Максимальная безопасность |
ECDSA (эллиптическая кривая)
ECDSA — также асимметричный алгоритм, но с более короткими ключами и лучшей производительностью.
Использование
cfg := jwt.DefaultConfig()
cfg.SigningMethod = jwt.SigningMethodES256
cfg.SigningKey = ecdsaPrivateKey // *ecdsa.PrivateKey
cfg.VerificationKey = ecdsaPublicKey // *ecdsa.PublicKey (необязательно)Генерация ключей
// Генерация пары ключей на кривой P-256
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
log.Fatal(err)
}
publicKey := &privateKey.PublicKeyВыбор алгоритма
| Константа | Алгоритм | Кривая | Описание |
|---|---|---|---|
SigningMethodES256 | ECDSA-SHA256 | P-256 | Рекомендуется |
SigningMethodES384 | ECDSA-SHA384 | P-384 | Более высокая безопасность |
SigningMethodES512 | ECDSA-SHA512 | P-521 | Максимальная безопасность |
Соответствие кривых
Алгоритм и кривая должны строго соответствовать друг другу; при инициализации производится принудительная проверка (см. исходный код validateECDSACurve):
| Алгоритм | Обязательная кривая | Способ генерации |
|---|---|---|
| ES256 | P-256 | elliptic.P256() |
| ES384 | P-384 | elliptic.P384() |
| ES512 | P-521 | elliptic.P521() |
ES512 использует P-521, а не P-512
Кривая, соответствующая ES512, — P-521 (обратите внимание: 521, а не 512). Это распространённая ошибка — число 512 легко наводит на мысль, что кривая тоже P-512, но в стандартной библиотеке Go P512 не существует, старшая кривая — elliptic.P521(). При несоответствии кривой возвращается ErrInvalidSecretKey.
Режим разделения ключей
В микросервисной архитектуре часто требуется разделить возможность подписи (выпуск токенов) и возможность проверки (проверка токенов), следуя принципу минимальных привилегий:
| Роль сервиса | Ключ | Обязанности |
|---|---|---|
| Сервис аутентификации | Приватный ключ (SigningKey) | Выпуск токенов доступа после успешного входа |
| API-сервис | Публичный ключ (VerificationKey) | Проверка подписи токена, не участвует в выпуске |
Сервис аутентификации содержит приватный ключ и отвечает за выпуск, API-сервис проверяет токен через публичный ключ. Даже если в конфигурации API-сервиса записан SigningKey (текущий API требует, чтобы это поле было непустым), при установленном VerificationKey для проверки используется именно публичный ключ.
Приоритет VerificationKey
После установки VerificationKey процесс проверки использует этот публичный ключ, а не извлечённый из SigningKey. Это позволяет API-сервису явно управлять ключом проверки и подходит для сценариев, где ключ проверки и ключ подписи распределяются раздельно.
Сервис аутентификации (выпуск токенов):
authCfg := jwt.DefaultConfig()
authCfg.SigningMethod = jwt.SigningMethodRS256
authCfg.SigningKey = rsaPrivateKey // *rsa.PrivateKey, для подписи
authCfg.VerificationKey = &rsaPrivateKey.PublicKeyAPI-сервис (только проверка):
apiCfg := jwt.DefaultConfig()
apiCfg.SigningMethod = jwt.SigningMethodRS256
apiCfg.SigningKey = rsaPrivateKey // Текущий API требует SigningKey непустым
apiCfg.VerificationKey = rsaPublicKey // *rsa.PublicKey, фактически используется при проверкеПредупреждение
Processor, настроенный только на проверку, не должен вызывать Create / CreateRefresh (подпись требует приватного ключа). Полный пример межсервисного взаимодействия см. в Продвинутые примеры.
Как выбрать
Монолитное приложение ──────→ HMAC
Микросервисы (одна область доверия) ──→ HMAC
Микросервисы (кросс-сервисная проверка) → RSA, RSA-PSS или ECDSA
Приоритет безопасности ─────→ RSA-PSS (замена RSA)
Высокие требования к производительности → ECDSA
Чувствительность к длине ключа ─→ ECDSA| Фактор | HMAC | RSA | RSA-PSS | ECDSA |
|---|---|---|---|---|
| Скорость подписи | Быстрая | Медленнее | Медленнее | Быстрая |
| Скорость проверки | Быстрая | Быстрая | Быстрая | Быстрая |
| Длина ключа | 32+ байта | 2048+ бит | 2048+ бит | 256+ бит |
| Длина подписи | Фиксированная | Длинная (~256 байт) | Длинная (~256 байт) | Короткая (~64 байта) |
| Архитектурная связанность | Тесная | Слабая | Слабая | Слабая |
| Безопасность | Высокая | Высокая | Более высокая | Высокая |
Лучшие практики управления ключами
Внедрение через переменные окружения
Передавайте ключи через переменные окружения, чтобы избежать хардкода в исходном коде:
package main
import (
"fmt"
"os"
"github.com/cybergodev/jwt"
)
func main() {
secret := os.Getenv("JWT_SECRET_KEY")
cfg := jwt.DefaultConfig()
cfg.SecretKey = secret
processor, err := jwt.New(cfg)
if err != nil {
fmt.Println("Недействительный ключ:", err)
return
}
defer processor.Close()
fmt.Println("Processor готов") // Вывод: Processor готов
}Загрузка RSA-ключей из PEM-файла
В production асимметричные ключи обычно хранятся в PEM-файлах и загружаются при запуске с помощью crypto/x509:
package main
import (
"crypto/x509"
"encoding/pem"
"fmt"
"os"
"github.com/cybergodev/jwt"
)
func main() {
// Чтение PEM-файла приватного ключа
keyData, err := os.ReadFile("private_key.pem")
if err != nil {
fmt.Println("Ошибка чтения приватного ключа:", err)
return
}
block, _ := pem.Decode(keyData)
if block == nil {
fmt.Println("Ошибка декодирования PEM")
return
}
privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
fmt.Println("Ошибка разбора приватного ключа:", err)
return
}
cfg := jwt.DefaultConfig()
cfg.SigningMethod = jwt.SigningMethodRS256
cfg.SigningKey = privateKey
processor, err := jwt.New(cfg)
if err != nil {
fmt.Println("Ошибка инициализации:", err)
return
}
defer processor.Close()
fmt.Println("RSA-ключ загружен из PEM") // Вывод: RSA-ключ загружен из PEM
}Загрузка публичного ключа из PEM
PEM-файлы публичного ключа разбираются через x509.ParsePKIXPublicKey, возвращаемое значение имеет тип any и требует приведения типа к *rsa.PublicKey или *ecdsa.PublicKey. Полный пример см. в Продвинутые примеры.
Ротация ключей
Рекомендации по ротации
- Регулярно ротируйте ключи подписи (рекомендуется каждые 3–6 месяцев)
- В период параллельного действия старых и новых ключей проверяющая сторона одновременно принимает оба публичных ключа
- Используйте заголовок
kid(Key ID) для идентификации текущей версии ключа, облегчая градуальное переключение - После завершения ротации отзывайте старые ключи и проверяйте, нужно ли обновить чёрный список
Меры безопасности
Запрещено
- Не храните ключи в коде (hardcode)
- Не используйте слабые ключи (только цифры, повторяющиеся символы и т.д.)
- Не используйте алгоритм
none(библиотека автоматически отклоняет) - HMAC-ключ не должен быть короче 32 байт
Лучшие практики
- Используйте переменные окружения или сервисы управления ключами для хранения ключей
- Регулярно ротируйте ключи подписи
- В производственной среде рекомендуется использовать RSA или ECDSA
- RSA-ключи рекомендуется использовать размером от 2048 бит
Дальнейшие шаги
- Пользовательские Claims — определение бизнес-полей
- Справочник API → Функции пакета — полные сигнатуры API
- Базовые примеры — полный пример HMAC