73fd281bb5
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>
135 lines
3.2 KiB
Go
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")
|
|
}
|
|
}
|