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>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user