Day 44 — Asymmetric Crypto & Token Signing
Learning Objectives
- Build a JWT-like token system using ECDSA P-256 — no JWT library needed
- Understand why ECDSA P-256 > RSA-2048 for tokens (smaller, faster, same security)
- Implement the foundation of your API gateway's authentication
Key Concepts
Symmetric (HMAC): same key signs and verifies → key must be shared with verifier
Use for: webhook signing (you control both sides), session cookies
Asymmetric (ECDSA): private key signs, public key verifies → public key shareable
Use for: tokens (anyone can verify, only you can issue)
ECDSA P-256: 256-bit key = ~128-bit security = equivalent to RSA-3072
RSA-2048: use for TLS certs (protocol requirement); prefer ECDSA for tokens
Token structure (custom, no JWT library):
Header: {"alg":"ES256","typ":"TOKEN"}
Claims: {"sub":"user:42","exp":1234567890,"iat":1234567890,"roles":["admin"]}
Signature: ECDSA.Sign(SHA256(base64(header).base64(claims)))
Token: base64url(header).base64url(claims).base64url(signature)
Exercise: ECDSA Token System
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"math/big"
"strings"
"time"
)
// TokenClaims are the payload of a token
type TokenClaims struct {
Subject string `json:"sub"`
IssuedAt int64 `json:"iat"`
ExpiresAt int64 `json:"exp"`
Roles []string `json:"roles"`
}
type TokenService struct {
privateKey *ecdsa.PrivateKey
publicKey *ecdsa.PublicKey
}
func NewTokenService() (*TokenService, error) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil { return nil, err }
return &TokenService{privateKey: key, publicKey: &key.PublicKey}, nil
}
func b64(data []byte) string {
return base64.RawURLEncoding.EncodeToString(data)
}
func unb64(s string) ([]byte, error) {
return base64.RawURLEncoding.DecodeString(s)
}
// Issue signs and returns a token valid for 1 hour
func (ts *TokenService) Issue(subject string, roles []string) (string, error) {
now := time.Now().Unix()
claims := TokenClaims{
Subject: subject,
IssuedAt: now,
ExpiresAt: now + 3600,
Roles: roles,
}
header := b64([]byte(`{"alg":"ES256","typ":"TOKEN"}`))
claimsJSON, err := json.Marshal(claims)
if err != nil { return "", err }
payload := header + "." + b64(claimsJSON)
// Sign SHA256 of the payload
hash := sha256.Sum256([]byte(payload))
r, s, err := ecdsa.Sign(rand.Reader, ts.privateKey, hash[:])
if err != nil { return "", err }
// Encode signature as r||s (each 32 bytes for P-256)
sig := make([]byte, 64)
rBytes := r.Bytes()
sBytes := s.Bytes()
copy(sig[32-len(rBytes):32], rBytes)
copy(sig[64-len(sBytes):64], sBytes)
return payload + "." + b64(sig), nil
}
// Verify validates a token and returns claims
func (ts *TokenService) Verify(token string) (*TokenClaims, error) {
parts := strings.Split(token, ".")
if len(parts) != 3 {
return nil, fmt.Errorf("invalid token format")
}
// Verify signature
payload := parts[0] + "." + parts[1]
hash := sha256.Sum256([]byte(payload))
sigBytes, err := unb64(parts[2])
if err != nil || len(sigBytes) != 64 {
return nil, fmt.Errorf("invalid signature encoding")
}
r := new(big.Int).SetBytes(sigBytes[:32])
s := new(big.Int).SetBytes(sigBytes[32:])
if !ecdsa.Verify(ts.publicKey, hash[:], r, s) {
return nil, fmt.Errorf("invalid signature")
}
// Decode claims
claimsJSON, err := unb64(parts[1])
if err != nil { return nil, fmt.Errorf("invalid claims encoding") }
var claims TokenClaims
if err := json.Unmarshal(claimsJSON, &claims); err != nil {
return nil, fmt.Errorf("invalid claims JSON")
}
// Check expiry
if time.Now().Unix() > claims.ExpiresAt {
return nil, fmt.Errorf("token expired")
}
return &claims, nil
}
func main() {
svc, err := NewTokenService()
if err != nil { fmt.Println("ERROR:", err); return }
// Issue a token
token, err := svc.Issue("user:42", []string{"read", "write"})
if err != nil { fmt.Println("issue error:", err); return }
fmt.Printf("Token: %s\n\n", token[:50]+"...")
// Verify the token
claims, err := svc.Verify(token)
if err != nil { fmt.Println("verify error:", err); return }
fmt.Printf("Verified: subject=%s roles=%v expires=%s\n",
claims.Subject, claims.Roles,
time.Unix(claims.ExpiresAt, 0).Format(time.RFC3339),
)
// Tamper with the token
tampered := token[:len(token)-5] + "XXXXX"
_, err = svc.Verify(tampered)
fmt.Printf("Tampered token: %v\n", err)
}
Real-World Context
This is the token system your API gateway uses in Day 75 and Week 11. ECDSA P-256 tokens are ~60% smaller than RSA-2048 tokens (important for mobile clients and high-frequency API calls). The private key stays on your auth server; the public key can be distributed to all services that need to verify tokens.