Frontend Labs
⋯

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.


Generated from week6.md · 8 Oct 2026 16:36