Skip to content

Алгоритмы подписи

CyberGo JWT поддерживает 4 типа, всего 12 алгоритмов подписи, охватывающих все сценарии от монолитных приложений до микросервисной архитектуры.

Обзор алгоритмов

ТипАлгоритмыТип ключаСценарий использования
HMACHS256 / HS384 / HS512Симметричный ключМонолитные приложения, простые сервисы
RSARS256 / RS384 / RS512Публичный/приватный ключМикросервисы, проверка на нескольких сервисах
RSA-PSSPS256 / PS384 / PS512Публичный/приватный ключМикросервисы (рекомендуемая замена RSA)
ECDSAES256 / 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 следует генерировать ключи из криптографически безопасного случайного источника:

go
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
}

Использование

go
cfg := jwt.DefaultConfig()
cfg.SecretKey = "hmac-key-that-has-at-least-32-bytes!"
cfg.SigningMethod = jwt.SigningMethodHS256 // Значение по умолчанию, можно опустить

Выбор алгоритма

КонстантаАлгоритмОписание
SigningMethodHS256HMAC-SHA256Рекомендуется, баланс производительности и безопасности
SigningMethodHS384HMAC-SHA384Более высокая безопасность
SigningMethodHS512HMAC-SHA512Максимальная безопасность

Рекомендация

Для большинства сценариев достаточно HS256. Рекомендуется использовать криптографически безопасный случайный ключ длиной не менее 32 байт.

RSA (асимметричный ключ)

RSA использует приватный ключ для подписи и публичный для проверки. Подходит для сценариев, где проверяющей стороне не нужно владеть приватным ключом.

Использование

go
cfg := jwt.DefaultConfig()
cfg.SigningMethod = jwt.SigningMethodRS256
cfg.SigningKey = rsaPrivateKey        // *rsa.PrivateKey
cfg.VerificationKey = rsaPublicKey    // *rsa.PublicKey (необязательно)

Ключ проверки

VerificationKey необязателен. Если не установлен, библиотека использует SigningKey для проверки (извлекает публичный ключ из приватного).

Генерация ключей

go
// Генерация 2048-битной пары RSA-ключей (библиотека требует минимум 2048 бит, иначе ErrInvalidSecretKey)
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
    log.Fatal(err)
}
publicKey := &privateKey.PublicKey

Выбор алгоритма

КонстантаАлгоритмОписание
SigningMethodRS256RSA-SHA256Рекомендуется
SigningMethodRS384RSA-SHA384Более высокая безопасность
SigningMethodRS512RSA-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.

Использование

go
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, дополнительная генерация не требуется.

Выбор алгоритма

КонстантаАлгоритмОписание
SigningMethodPS256RSA-PSS-SHA256Рекомендуется
SigningMethodPS384RSA-PSS-SHA384Более высокая безопасность
SigningMethodPS512RSA-PSS-SHA512Максимальная безопасность

ECDSA (эллиптическая кривая)

ECDSA — также асимметричный алгоритм, но с более короткими ключами и лучшей производительностью.

Использование

go
cfg := jwt.DefaultConfig()
cfg.SigningMethod = jwt.SigningMethodES256
cfg.SigningKey = ecdsaPrivateKey      // *ecdsa.PrivateKey
cfg.VerificationKey = ecdsaPublicKey  // *ecdsa.PublicKey (необязательно)

Генерация ключей

go
// Генерация пары ключей на кривой P-256
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
    log.Fatal(err)
}
publicKey := &privateKey.PublicKey

Выбор алгоритма

КонстантаАлгоритмКриваяОписание
SigningMethodES256ECDSA-SHA256P-256Рекомендуется
SigningMethodES384ECDSA-SHA384P-384Более высокая безопасность
SigningMethodES512ECDSA-SHA512P-521Максимальная безопасность

Соответствие кривых

Алгоритм и кривая должны строго соответствовать друг другу; при инициализации производится принудительная проверка (см. исходный код validateECDSACurve):

АлгоритмОбязательная криваяСпособ генерации
ES256P-256elliptic.P256()
ES384P-384elliptic.P384()
ES512P-521elliptic.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-сервису явно управлять ключом проверки и подходит для сценариев, где ключ проверки и ключ подписи распределяются раздельно.

Сервис аутентификации (выпуск токенов):

go
authCfg := jwt.DefaultConfig()
authCfg.SigningMethod = jwt.SigningMethodRS256
authCfg.SigningKey = rsaPrivateKey           // *rsa.PrivateKey, для подписи
authCfg.VerificationKey = &rsaPrivateKey.PublicKey

API-сервис (только проверка):

go
apiCfg := jwt.DefaultConfig()
apiCfg.SigningMethod = jwt.SigningMethodRS256
apiCfg.SigningKey = rsaPrivateKey            // Текущий API требует SigningKey непустым
apiCfg.VerificationKey = rsaPublicKey        // *rsa.PublicKey, фактически используется при проверке

Предупреждение

Processor, настроенный только на проверку, не должен вызывать Create / CreateRefresh (подпись требует приватного ключа). Полный пример межсервисного взаимодействия см. в Продвинутые примеры.

Как выбрать

text
Монолитное приложение ──────→ HMAC
Микросервисы (одна область доверия) ──→ HMAC
Микросервисы (кросс-сервисная проверка) → RSA, RSA-PSS или ECDSA
Приоритет безопасности ─────→ RSA-PSS (замена RSA)
Высокие требования к производительности → ECDSA
Чувствительность к длине ключа ─→ ECDSA
ФакторHMACRSARSA-PSSECDSA
Скорость подписиБыстраяМедленнееМедленнееБыстрая
Скорость проверкиБыстраяБыстраяБыстраяБыстрая
Длина ключа32+ байта2048+ бит2048+ бит256+ бит
Длина подписиФиксированнаяДлинная (~256 байт)Длинная (~256 байт)Короткая (~64 байта)
Архитектурная связанностьТеснаяСлабаяСлабаяСлабая
БезопасностьВысокаяВысокаяБолее высокаяВысокая

Лучшие практики управления ключами

Внедрение через переменные окружения

Передавайте ключи через переменные окружения, чтобы избежать хардкода в исходном коде:

go
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:

go
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 бит

Дальнейшие шаги