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,107 @@
|
||||
package mtls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"testing"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// noopDB satisfies the DB interface without persisting anything.
|
||||
type noopDB struct{}
|
||||
|
||||
func (noopDB) RecordRevocation(_ context.Context, _ string) error { return nil }
|
||||
|
||||
func newTestCRL(t *testing.T) (*CRL, *miniredis.Miniredis) {
|
||||
t.Helper()
|
||||
mr := miniredis.RunT(t)
|
||||
rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
|
||||
t.Cleanup(func() { rdb.Close() })
|
||||
return NewCRL(rdb, noopDB{}), mr
|
||||
}
|
||||
|
||||
func TestCRL_RevokeAndIsRevoked(t *testing.T) {
|
||||
crl, _ := newTestCRL(t)
|
||||
ctx := context.Background()
|
||||
|
||||
const nodeUUID = "node-revoked-uuid"
|
||||
|
||||
if crl.IsRevoked(nodeUUID) {
|
||||
t.Fatal("IsRevoked = true before Revoke; want false")
|
||||
}
|
||||
|
||||
if err := crl.Revoke(ctx, nodeUUID); err != nil {
|
||||
t.Fatalf("Revoke: %v", err)
|
||||
}
|
||||
|
||||
if !crl.IsRevoked(nodeUUID) {
|
||||
t.Fatal("IsRevoked = false after Revoke; want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCRL_IsRevoked_UnknownNode(t *testing.T) {
|
||||
crl, _ := newTestCRL(t)
|
||||
|
||||
if crl.IsRevoked("never-revoked") {
|
||||
t.Fatal("IsRevoked = true for unknown node; want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCRL_VerifyPeerCertificate_Revoked(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
ca := newTestCA(t, dir)
|
||||
|
||||
crl, _ := newTestCRL(t)
|
||||
ctx := context.Background()
|
||||
|
||||
nodeUUID := "node-revoked-cert"
|
||||
csrPEM, _ := newTestCSR(t)
|
||||
certPEM, err := ca.SignCSR(csrPEM, nodeUUID)
|
||||
if err != nil {
|
||||
t.Fatalf("SignCSR: %v", err)
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(certPEM)
|
||||
clientCert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseCertificate: %v", err)
|
||||
}
|
||||
|
||||
// Build verified chains as the TLS stack would.
|
||||
pool := x509.NewCertPool()
|
||||
pool.AddCert(ca.CACert())
|
||||
chains, err := clientCert.Verify(x509.VerifyOptions{
|
||||
Roots: pool,
|
||||
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("cert.Verify: %v", err)
|
||||
}
|
||||
|
||||
// Before revocation: VerifyPeerCertificate must pass.
|
||||
if err := crl.VerifyPeerCertificate(nil, chains); err != nil {
|
||||
t.Fatalf("VerifyPeerCertificate before revoke: %v", err)
|
||||
}
|
||||
|
||||
// Revoke the node.
|
||||
if err := crl.Revoke(ctx, nodeUUID); err != nil {
|
||||
t.Fatalf("Revoke: %v", err)
|
||||
}
|
||||
|
||||
// After revocation: VerifyPeerCertificate must reject.
|
||||
if err := crl.VerifyPeerCertificate(nil, chains); err == nil {
|
||||
t.Fatal("VerifyPeerCertificate after revoke: expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCRL_VerifyPeerCertificate_NoClientCert(t *testing.T) {
|
||||
crl, _ := newTestCRL(t)
|
||||
|
||||
// Empty verifiedChains = no client cert presented (e.g. during Enroll).
|
||||
if err := crl.VerifyPeerCertificate(nil, nil); err != nil {
|
||||
t.Errorf("VerifyPeerCertificate with no chains: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user