Files
pangolin/server/internal/mtls/bootstrap_test.go
T
wangjia 73fd281bb5 feat(mtls): implement mTLS/CA + bootstrap token framework [tsk_FUQws_DMIcXa]
ECDSA P-256 self-signed CA with disk persistence (load-or-generate),
CSR signing (CN=nodeUUID, 90d validity, EKU=ClientAuth), one-time
bootstrap tokens via Redis GETDEL (15min TTL), CRL revocation with
Redis SET + DB interface, gRPC unary+stream interceptors that extract
CN from verified TLS chains (Enroll whitelisted, others require cert),
and NewServerTLSConfig (VerifyClientCertIfGiven + TLS 1.3 + CRL hook).

Frozen API: SignCSR / CAPEM / IssueToken / ConsumeToken / Revoke /
            NewServerTLSConfig / UnaryServerInterceptor / NodeUUIDFromContext

Tests cover: CA sign+verify, token one-time guarantee, TTL expiry,
             revocation rejection, interceptor whitelist (5 categories).
             Redis layer backed by miniredis in tests.

Run setup.sh from server/ to fetch deps and verify tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 01:44:21 +08:00

100 lines
2.5 KiB
Go

package mtls
import (
"context"
"strings"
"testing"
"time"
"github.com/alicebob/miniredis/v2"
"github.com/redis/go-redis/v9"
)
func newTestBootstrapManager(t *testing.T) (*BootstrapTokenManager, *miniredis.Miniredis) {
t.Helper()
mr := miniredis.RunT(t)
rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
t.Cleanup(func() { rdb.Close() })
return NewBootstrapTokenManager(rdb), mr
}
func TestBootstrap_IssueToken_Format(t *testing.T) {
mgr, _ := newTestBootstrapManager(t)
token, err := mgr.IssueToken(context.Background(), "node-abc")
if err != nil {
t.Fatalf("IssueToken: %v", err)
}
if len(token) != 64 {
t.Errorf("token length = %d; want 64 hex chars", len(token))
}
if strings.ContainsAny(token, "ghijklmnopqrstuvwxyz !@#") {
t.Errorf("token contains non-hex chars: %q", token)
}
}
func TestBootstrap_ConsumeToken_OnceOnly(t *testing.T) {
mgr, _ := newTestBootstrapManager(t)
ctx := context.Background()
const nodeUUID = "node-11111111-2222-3333-4444-555555555555"
token, err := mgr.IssueToken(ctx, nodeUUID)
if err != nil {
t.Fatalf("IssueToken: %v", err)
}
// First consume: must succeed.
got, err := mgr.ConsumeToken(ctx, token)
if err != nil {
t.Fatalf("ConsumeToken (first): %v", err)
}
if got != nodeUUID {
t.Errorf("ConsumeToken returned %q; want %q", got, nodeUUID)
}
// Second consume: must fail (one-time guarantee).
_, err = mgr.ConsumeToken(ctx, token)
if err == nil {
t.Fatal("ConsumeToken (second): expected error for already-consumed token, got nil")
}
}
func TestBootstrap_ConsumeToken_NeverIssued(t *testing.T) {
mgr, _ := newTestBootstrapManager(t)
_, err := mgr.ConsumeToken(context.Background(), "deadbeefdeadbeef")
if err == nil {
t.Fatal("expected error for non-existent token, got nil")
}
}
func TestBootstrap_ConsumeToken_ExpiredTTL(t *testing.T) {
mgr, mr := newTestBootstrapManager(t)
ctx := context.Background()
token, err := mgr.IssueToken(ctx, "node-expired")
if err != nil {
t.Fatalf("IssueToken: %v", err)
}
// Advance miniredis clock past the 15-minute TTL.
mr.FastForward(bootstrapTokenTTL + time.Second)
_, err = mgr.ConsumeToken(ctx, token)
if err == nil {
t.Fatal("expected error after TTL expiry, got nil")
}
}
func TestBootstrap_IssueToken_Uniqueness(t *testing.T) {
mgr, _ := newTestBootstrapManager(t)
ctx := context.Background()
t1, _ := mgr.IssueToken(ctx, "node-A")
t2, _ := mgr.IssueToken(ctx, "node-B")
if t1 == t2 {
t.Error("IssueToken returned duplicate tokens for different nodes")
}
}