Skip to content

Signing Algorithms

CyberGo JWT supports 4 categories with 12 signing algorithms, covering scenarios from monolithic apps to microservice architectures.

Algorithm Overview

TypeAlgorithmsKey TypeUse Case
HMACHS256 / HS384 / HS512Symmetric keySingle apps, simple services
RSARS256 / RS384 / RS512Public/Private keyMicroservices, multi-service validation
RSA-PSSPS256 / PS384 / PS512Public/Private keyMicroservices (recommended over RSA)
ECDSAES256 / ES384 / ES512Public/Private keyHigh-performance microservices

HMAC (Symmetric Key)

HMAC uses the same key for signing and verification — the simplest approach.

Key Requirements

An HMAC key must pass two checks performed by validateSigningKey:

  • Length check: when len(SecretKey) < 32, returns ErrInvalidSecretKey; the error message includes the actual byte length, e.g. "minimum 32 bytes required, got 16".
  • Entropy check: low-entropy keys are detected via internal.IsWeakKey. The following patterns are rejected:
    • All identical characters (e.g. "aaaaaaaa...")
    • Repeated short patterns (e.g. "abcabcabc...")
    • Sequential ascending/descending sequences (e.g. "abcdefgh...")
    • Common weak passwords and their variants (e.g. "password", "qwerty")

Weak Keys Are Rejected

Do not use easily guessable keys such as "repeated characters", "sequential sequences", or "dictionary words". Even if the length reaches 32 bytes, a low-entropy key is still rejected during jwt.New initialization with ErrInvalidSecretKey.

In production, generate keys from a cryptographically secure random source:

go
package main

import (
    "crypto/rand"
    "encoding/base64"
    "fmt"
    "log"

    "github.com/cybergodev/jwt"
)

func main() {
    // Generate a 32-byte random key using crypto/rand
    raw := make([]byte, 32)
    if _, err := rand.Read(raw); err != nil {
        log.Fatal(err)
    }
    // Store and pass it base64-encoded
    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 key ready, length (bytes):", len(secret)) // Output: HMAC key ready, length (bytes): 44
}

Usage

go
cfg := jwt.DefaultConfig()
cfg.SecretKey = "hmac-key-that-has-at-least-32-bytes!"
cfg.SigningMethod = jwt.SigningMethodHS256 // default, can be omitted

Algorithm Selection

ConstantAlgorithmDescription
SigningMethodHS256HMAC-SHA256Recommended, balanced performance and security
SigningMethodHS384HMAC-SHA384Higher security
SigningMethodHS512HMAC-SHA512Highest security

Recommendation

HS256 is sufficient for most scenarios. Keys should be generated using cryptographically secure random methods, at least 32 bytes.

RSA (Asymmetric Key)

RSA uses a private key for signing and a public key for verification. Suitable when the verifying party doesn't need the private key.

Usage

go
cfg := jwt.DefaultConfig()
cfg.SigningMethod = jwt.SigningMethodRS256
cfg.SigningKey = rsaPrivateKey        // *rsa.PrivateKey
cfg.VerificationKey = rsaPublicKey    // *rsa.PublicKey (optional)

Verification Key

VerificationKey is optional. When not set, the library uses SigningKey for verification (internally extracts the public key from the private key).

Key Generation

go
// Generate 2048-bit RSA key pair (the library requires at least 2048 bits, otherwise returns ErrInvalidSecretKey)
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
    log.Fatal(err)
}
publicKey := &privateKey.PublicKey

Algorithm Selection

ConstantAlgorithmDescription
SigningMethodRS256RSA-SHA256Recommended
SigningMethodRS384RSA-SHA384Higher security
SigningMethodRS512RSA-SHA512Highest security

Sharing Keys with RSA-PSS

RS256/RS384/RS512 and PS256/PS384/PS512 use the same key types (*rsa.PrivateKey / *rsa.PublicKey) and the same verification logic, so the keys are interchangeable. Migrating from RSA to RSA-PSS requires no key regeneration.

RSA-PSS is an improved RSA signing scheme using Probabilistic Signature Scheme (PSS) padding, offering better security than PKCS#1 v1.5. Uses the same keys as RSA.

Usage

go
cfg := jwt.DefaultConfig()
cfg.SigningMethod = jwt.SigningMethodPS256
cfg.SigningKey = rsaPrivateKey        // *rsa.PrivateKey (same key as RSA)
cfg.VerificationKey = rsaPublicKey    // *rsa.PublicKey (optional)

Recommended Replacement

RSA-PSS is more secure than RSA PKCS#1 v1.5. New projects should prefer RSA-PSS algorithms. Keys are identical to RSA — no additional key generation needed.

Algorithm Selection

ConstantAlgorithmDescription
SigningMethodPS256RSA-PSS-SHA256Recommended
SigningMethodPS384RSA-PSS-SHA384Higher security
SigningMethodPS512RSA-PSS-SHA512Highest security

ECDSA (Elliptic Curve)

ECDSA is also asymmetric but with shorter keys and better performance.

Usage

go
cfg := jwt.DefaultConfig()
cfg.SigningMethod = jwt.SigningMethodES256
cfg.SigningKey = ecdsaPrivateKey      // *ecdsa.PrivateKey
cfg.VerificationKey = ecdsaPublicKey  // *ecdsa.PublicKey (optional)

Key Generation

go
// Generate P-256 curve key pair
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
    log.Fatal(err)
}
publicKey := &privateKey.PublicKey

Algorithm Selection

ConstantAlgorithmCurveDescription
SigningMethodES256ECDSA-SHA256P-256Recommended
SigningMethodES384ECDSA-SHA384P-384Higher security
SigningMethodES512ECDSA-SHA512P-521Highest security

Curve Matching

Algorithms and curves must correspond strictly; this is enforced at initialization (see validateECDSACurve in the source):

AlgorithmRequired CurveHow to Generate
ES256P-256elliptic.P256()
ES384P-384elliptic.P384()
ES512P-521elliptic.P521()

ES512 Uses P-521, Not P-512

The curve corresponding to ES512 is P-521 (note: 521, not 512). This is a common mistake — the number 512 misleadingly suggests the curve is also P-512, but the Go standard library has no P512; the highest curve is elliptic.P521(). A curve mismatch returns ErrInvalidSecretKey.

Key Separation Mode

In a microservice architecture, you typically separate the signing capability (issuing tokens) from the verification capability (validating tokens), following the principle of least privilege:

Service RoleHeld KeyResponsibility
Auth servicePrivate key (SigningKey)Issues access tokens after successful login
API servicePublic key (VerificationKey)Validates token signatures, does not participate in issuance

The auth service holds the private key for signing, and API services verify tokens via the public key. Even if the API service's config also includes SigningKey (the current API requires this field to be non-empty), as long as VerificationKey is set, that public key is used for verification.

VerificationKey Takes Precedence

Once VerificationKey is set, the verification flow uses that public key rather than the one extracted from SigningKey. This lets an API service explicitly control the verification key, suitable for scenarios where the verification key and signing key are distributed separately.

Auth service (issuing tokens):

go
authCfg := jwt.DefaultConfig()
authCfg.SigningMethod = jwt.SigningMethodRS256
authCfg.SigningKey = rsaPrivateKey           // *rsa.PrivateKey, used for signing
authCfg.VerificationKey = &rsaPrivateKey.PublicKey

API service (verification only):

go
apiCfg := jwt.DefaultConfig()
apiCfg.SigningMethod = jwt.SigningMethodRS256
apiCfg.SigningKey = rsaPrivateKey            // Current API requires SigningKey to be non-empty
apiCfg.VerificationKey = rsaPublicKey        // *rsa.PublicKey, actually used for verification

WARNING

A verification-only Processor must not call Create / CreateRefresh (signing requires the private key). For a complete cross-service example, see Advanced Examples.

How to Choose

text
Monolithic app ──────────→ HMAC
Microservices (same trust domain) ──→ HMAC
Microservices (cross-service verification) → RSA, RSA-PSS, or ECDSA
Security priority ────────→ RSA-PSS (replaces RSA)
High performance ─────────→ ECDSA
Short key length ─────────→ ECDSA
FactorHMACRSARSA-PSSECDSA
Sign speedFastSlowerSlowerFast
Verify speedFastFastFastFast
Key length32+ bytes2048+ bits2048+ bits256+ bits
Signature lengthFixedLong (~256 bytes)Long (~256 bytes)Short (~64 bytes)
Architecture couplingTightLooseLooseLoose
SecurityHighHighHigherHigh

Key Management Best Practices

Environment Variable Injection

Pass keys via environment variables to avoid hardcoding them in source code:

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("Invalid key:", err)
        return
    }
    defer processor.Close()
    fmt.Println("Processor ready") // Output: Processor ready
}

Loading an RSA Key from a PEM File

In production, asymmetric keys are usually stored as PEM files and parsed at startup using crypto/x509:

go
package main

import (
    "crypto/x509"
    "encoding/pem"
    "fmt"
    "os"

    "github.com/cybergodev/jwt"
)

func main() {
    // Read the private key PEM file
    keyData, err := os.ReadFile("private_key.pem")
    if err != nil {
        fmt.Println("Failed to read private key:", err)
        return
    }

    block, _ := pem.Decode(keyData)
    if block == nil {
        fmt.Println("PEM decode failed")
        return
    }

    privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
    if err != nil {
        fmt.Println("Failed to parse private key:", err)
        return
    }

    cfg := jwt.DefaultConfig()
    cfg.SigningMethod = jwt.SigningMethodRS256
    cfg.SigningKey = privateKey
    processor, err := jwt.New(cfg)
    if err != nil {
        fmt.Println("Initialization failed:", err)
        return
    }
    defer processor.Close()
    fmt.Println("RSA key loaded from PEM") // Output: RSA key loaded from PEM
}

Loading a Public Key from PEM

A public key PEM file is parsed with x509.ParsePKIXPublicKey, which returns any and must be type-asserted to *rsa.PublicKey or *ecdsa.PublicKey. For a complete example, see Advanced Examples.

Key Rotation

Rotation Recommendations

  • Rotate signing keys regularly (recommended every 3–6 months)
  • During the overlap period of old and new keys, the verifying side should accept both public keys
  • Use a kid (Key ID) header to identify the current key version, easing gradual rollout
  • After rotation completes, revoke the old key and check whether the blacklist needs updating

Security Notes

Prohibited

  • Never hardcode keys in source code
  • Never use weak keys (pure digits, repeated characters, etc.)
  • Never use the none algorithm (automatically rejected by this library)
  • HMAC keys must not be shorter than 32 bytes

Best Practices

  • Use environment variables or key management services
  • Rotate signing keys regularly
  • Use RSA or ECDSA in production
  • RSA keys should be 2048 bits or larger

Next Steps