Files
pangolin/server/internal/mtls/ca_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

135 lines
3.2 KiB
Go

package mtls
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"path/filepath"
"testing"
)
// newTestCA creates a CA in the given temporary directory.
func newTestCA(t *testing.T, dir string) *CA {
t.Helper()
ca, err := NewCA(CAConfig{
KeyPath: filepath.Join(dir, "ca.key"),
CertPath: filepath.Join(dir, "ca.crt"),
})
if err != nil {
t.Fatalf("NewCA: %v", err)
}
return ca
}
// newTestCSR generates an ECDSA P-256 key and a CSR PEM for testing.
func newTestCSR(t *testing.T) ([]byte, *ecdsa.PrivateKey) {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("generate CSR key: %v", err)
}
tmpl := &x509.CertificateRequest{
Subject: pkix.Name{CommonName: "ignored-by-signer"},
}
csrDER, err := x509.CreateCertificateRequest(rand.Reader, tmpl, key)
if err != nil {
t.Fatalf("CreateCertificateRequest: %v", err)
}
csrPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrDER})
return csrPEM, key
}
func TestCA_GenerateAndLoad(t *testing.T) {
dir := t.TempDir()
// First call: generate a new CA.
ca1 := newTestCA(t, dir)
if len(ca1.CAPEM()) == 0 {
t.Fatal("CAPEM returned empty")
}
// Second call with same paths: must load from disk without error.
ca2, err := NewCA(CAConfig{
KeyPath: filepath.Join(dir, "ca.key"),
CertPath: filepath.Join(dir, "ca.crt"),
})
if err != nil {
t.Fatalf("NewCA reload: %v", err)
}
// The CA certificates must be byte-identical.
if !bytes.Equal(ca1.CAPEM(), ca2.CAPEM()) {
t.Fatal("reloaded CA cert differs from generated CA cert")
}
}
func TestCA_SignCSR_CNOverride(t *testing.T) {
dir := t.TempDir()
ca := newTestCA(t, dir)
csrPEM, _ := newTestCSR(t)
nodeUUID := "node-11111111-2222-3333-4444-555555555555"
certPEM, err := ca.SignCSR(csrPEM, nodeUUID)
if err != nil {
t.Fatalf("SignCSR: %v", err)
}
block, _ := pem.Decode(certPEM)
if block == nil {
t.Fatal("SignCSR returned empty PEM")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatalf("ParseCertificate: %v", err)
}
// CN must match nodeUUID regardless of what the CSR contained.
if cert.Subject.CommonName != nodeUUID {
t.Errorf("CN = %q; want %q", cert.Subject.CommonName, nodeUUID)
}
}
func TestCA_SignCSR_VerifiesAgainstCAPool(t *testing.T) {
dir := t.TempDir()
ca := newTestCA(t, dir)
csrPEM, _ := newTestCSR(t)
certPEM, err := ca.SignCSR(csrPEM, "node-test")
if err != nil {
t.Fatalf("SignCSR: %v", err)
}
block, _ := pem.Decode(certPEM)
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatalf("ParseCertificate: %v", err)
}
// Build a pool with only our CA and verify the issued cert against it.
pool := x509.NewCertPool()
pool.AddCert(ca.CACert())
_, err = cert.Verify(x509.VerifyOptions{
Roots: pool,
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
})
if err != nil {
t.Errorf("cert.Verify: %v", err)
}
}
func TestCA_SignCSR_RejectsInvalidPEM(t *testing.T) {
dir := t.TempDir()
ca := newTestCA(t, dir)
_, err := ca.SignCSR([]byte("not-a-pem"), "any-uuid")
if err == nil {
t.Fatal("expected error for invalid CSR PEM, got nil")
}
}