sec-jwt-library-rs256
jwt.goThe library uses only Go's standard library (crypto/rsa, crypto/sha256, crypto/x509, encoding/json, etc.) to create and verify RS256-signed JWTs. No third-party dependencies.
Key design decisions:
| Feature | Implementation |
|---|---|
| Signing | RSA-PKCS1-v1_5 with SHA-256 (rsa.SignPKCS1v15 / rsa.VerifyPKCS1v15) |
| Key format | PEM-encoded PKCS1 or PKCS8 RSA private keys |
| Key rotation | Multiple keys stored in a map[string]*KeyPair; kid header identifies which key signed the token |
| Expiry | exp claim checked against time.Now().Unix() |
| Tamper detection | Signature covers base64url(header) + "." + base64url(claims). Any modification invalidates it. |
package jwt
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"strings"
"time"
)
const algorithm = "RS256"
var (
ErrInvalidToken = errors.New("jwt: invalid token")
ErrInvalidSignature = errors.New("jwt: invalid signature")
ErrTokenExpired = errors.New("jwt: token has expired")
ErrUnexpectedSigning = errors.New("jwt: unexpected signing method")
ErrKeyNotFound = errors.New("jwt: key not found")
)
type KeyPair struct {
ID string
PrivateKey *rsa.PrivateKey
PublicKey *rsa.PublicKey
}
type JWT struct {
keys map[string]*KeyPair
activeKeyID string
}
// New creates a JWT instance seeded with one key pair.
func New(keyID string, privateKeyPEM []byte) (*JWT, error) {
j := &JWT{keys: make(map[string]*KeyPair)}
if err := j.AddKey(keyID, privateKeyPEM); err != nil {
return nil, err
}
j.activeKeyID = keyID
return j, nil
}
// AddKey registers a PEM-encoded RSA private key for verification (key rotation).
func (j *JWT) AddKey(keyID string, privateKeyPEM []byte) error {
block, _ := pem.Decode(privateKeyPEM)
if block == nil {
return fmt.Errorf("jwt: failed to decode PEM block")
}
var priv *rsa.PrivateKey
priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
priv2, err2 := x509.ParsePKCS8PrivateKey(block.Bytes)
if err2 != nil {
return fmt.Errorf("jwt: failed to parse private key (PKCS1: %v, PKCS8: %v)", err, err2)
}
var ok bool
priv, ok = priv2.(*rsa.PrivateKey)
if !ok {
return fmt.Errorf("jwt: key is not an RSA private key")
}
}
j.keys[keyID] = &KeyPair{ID: keyID, PrivateKey: priv, PublicKey: &priv.PublicKey}
return nil
}
// SetActiveKey sets the key used for signing new tokens.
func (j *JWT) SetActiveKey(keyID string) error {
if _, ok := j.keys[keyID]; !ok {
return ErrKeyNotFound
}
j.activeKeyID = keyID
return nil
}
// RemoveKey removes a verification key (cannot remove the active signing key).
func (j *JWT) RemoveKey(keyID string) error {
if _, ok := j.keys[keyID]; !ok {
return ErrKeyNotFound
}
if keyID == j.activeKeyID {
return fmt.Errorf("jwt: cannot remove the active signing key")
}
delete(j.keys, keyID)
return nil
}
// Create generates a signed JWT with the given claims and an exp claim.
func (j *JWT) Create(claims map[string]interface{}, expiry time.Duration) (string, error) {
if j.activeKeyID == "" {
return "", fmt.Errorf("jwt: no active signing key")
}
key, ok := j.keys[j.activeKeyID]
if !ok {
return "", ErrKeyNotFound
}
claimsCopy := make(map[string]interface{}, len(claims)+1)
for k, v := range claims {
claimsCopy[k] = v
}
claimsCopy["exp"] = time.Now().Add(expiry).Unix()
return j.compact(claimsCopy, key)
}
// Verify decodes and validates a JWT: checks structure, algorithm, signature, and expiry.
func (j *JWT) Verify(tokenString string) (map[string]interface{}, error) {
tok, err := j.parse(tokenString)
if err != nil {
return nil, err
}
if tok.Header.Alg != algorithm {
return nil, ErrUnexpectedSigning
}
kid := tok.Header.Kid
if kid == "" {
for _, k := range j.keys {
kid = k.ID
break
}
}
key, ok := j.keys[kid]
if !ok {
return nil, ErrKeyNotFound
}
parts := strings.Split(tokenString, ".")
signingInput := parts[0] + "." + parts[1]
if err := verifyRS256(key.PublicKey, []byte(signingInput), tok.Signature); err != nil {
return nil, ErrInvalidSignature
}
if err := verifyTime(tok.Claims); err != nil {
return nil, err
}
return tok.Claims, nil
}
// --- internal helpers -------------------------------------------------------
func base64URLEncode(src []byte) string { return base64.RawURLEncoding.EncodeToString(src) }
func base64URLDecode(s string) ([]byte, error) {
switch len(s) % 4 {
case 2:
s += "=="
case 3:
s += "="
}
return base64.URLEncoding.DecodeString(s)
}
func signRS256(priv *rsa.PrivateKey, data []byte) ([]byte, error) {
h := sha256.Sum256(data)
return rsa.SignPKCS1v15(rand.Reader, priv, crypto.SHA256, h[:])
}
func verifyRS256(pub *rsa.PublicKey, data, sig []byte) error {
h := sha256.Sum256(data)
return rsa.VerifyPKCS1v15(pub, crypto.SHA256, h[:], sig)
}
type header struct {
Alg string `json:"alg"`
Kid string `json:"kid,omitempty"`
Typ string `json:"typ,omitempty"`
}
type jwtToken struct {
Header header
Claims map[string]interface{}
Signature []byte
}
func (j *JWT) compact(claims map[string]interface{}, key *KeyPair) (string, error) {
h := header{Alg: algorithm, Kid: key.ID, Typ: "JWT"}
hdr, _ := json.Marshal(h)
clm, _ := json.Marshal(claims)
hdrB64 := base64URLEncode(hdr)
clmB64 := base64URLEncode(clm)
sig, err := signRS256(key.PrivateKey, []byte(hdrB64+"."+clmB64))
if err != nil {
return "", err
}
return hdrB64 + "." + clmB64 + "." + base64URLEncode(sig), nil
}
func (j *JWT) parse(tokenString string) (*jwtToken, error) {
parts := strings.Split(tokenString, ".")
if len(parts) != 3 {
return nil, ErrInvalidToken
}
hdr, err := base64URLDecode(parts[0])
if err != nil {
return nil, fmt.Errorf("jwt: decode header: %w", err)
}
clm, err := base64URLDecode(parts[1])
if err != nil {
return nil, fmt.Errorf("jwt: decode claims: %w", err)
}
sig, err := base64URLDecode(parts[2])
if err != nil {
return nil, fmt.Errorf("jwt: decode signature: %w", err)
}
var h header
json.Unmarshal(hdr, &h)
var c map[string]interface{}
json.Unmarshal(clm, &c)
return &jwtToken{Header: h, Claims: c, Signature: sig}, nil
}
func verifyTime(claims map[string]interface{}) error {
expClaim, ok := claims["exp"]
if !ok {
return nil
}
var exp int64
switch v := expClaim.(type) {
case float64:
exp = int64(v)
default:
return fmt.Errorf("jwt: invalid exp claim")
}
if time.Now().Unix() > exp {
return ErrTokenExpired
}
return nil
}
All 15 test cases pass (`go test -v -race`): ``` === RUN TestCreateAndVerify — create claims, verify, check fields === RUN TestRejectExpiredToken — negative duration → ErrTokenExpired === RUN TestRejectTamperedToken — altered claims payload → ErrInvalidSignature === RUN TestRejectTamperedHeader — altered header → ErrInvalidSignature === RUN TestRejectTamperedSignature — one-bit flip in sig → ErrInvalidSignature === RUN TestRejectWrongAlgorithm — alg="HS256" → ErrUnexpectedSigning === RUN TestRejectMalformedToken — 5 subtests: empty, 1/2/4 parts, bad base64 === RUN TestKeyRotation — old+new keys, verify both, remove old → ErrKeyNotFound === RUN TestPKCS8Key — PKCS8 PEM key parses and works === RUN TestNoExpiryClaim — token without exp claim is accepted === RUN TestEmptyClaims — nil claims produce a valid token with only exp === RUN TestRemoveActiveKeyFails — active key cannot be removed === RUN TestSetUnknownKey — nonexistent keyID → ErrKeyNotFound === RUN TestVerifyUnknownKid — unknown kid → ErrKeyNotFound === RUN TestTokenExpiredDirectly — pre-set past exp → ErrTokenExpired PASS ok github.com/kara/jwt-rs256 0.915s ``` **Edge cases covered:** - Malformed tokens (empty, wrong number of segments, bad base64) - Algorithm confusion attack (alg != RS256) - Tampered header, claims, and signature - Token with no `exp` claim (allowed per spec) - Expired token (negative duration, pre-set past timestamp) - Key rotation: add new signing key, keep old for verification, remove old key - PKCS1 and PKCS8 private key PEM parsing - Empty/nil claims map - Cannot remove the active signing key
{"model": "deepseek-v4-flash", "problem_class": "sec-jwt-library-rs256", "result": "passed", "tests": 15}