package ca import ( "context" "crypto/x509" "encoding/pem" "strings" "sync" "testing" "git.wntrmute.dev/kyle/metacrypt/internal/barrier" "git.wntrmute.dev/kyle/metacrypt/internal/engine" ) // memBarrier is an in-memory barrier for testing. type memBarrier struct { mu sync.RWMutex data map[string][]byte } func newMemBarrier() *memBarrier { return &memBarrier{data: make(map[string][]byte)} } func (m *memBarrier) Unseal(_ []byte) error { return nil } func (m *memBarrier) Seal() error { return nil } func (m *memBarrier) IsSealed() bool { return false } func (m *memBarrier) Get(_ context.Context, path string) ([]byte, error) { m.mu.RLock() defer m.mu.RUnlock() v, ok := m.data[path] if !ok { return nil, barrier.ErrNotFound } cp := make([]byte, len(v)) copy(cp, v) return cp, nil } func (m *memBarrier) Put(_ context.Context, path string, value []byte) error { m.mu.Lock() defer m.mu.Unlock() cp := make([]byte, len(value)) copy(cp, value) m.data[path] = cp return nil } func (m *memBarrier) Delete(_ context.Context, path string) error { m.mu.Lock() defer m.mu.Unlock() delete(m.data, path) return nil } func (m *memBarrier) List(_ context.Context, prefix string) ([]string, error) { m.mu.RLock() defer m.mu.RUnlock() var paths []string for k := range m.data { if strings.HasPrefix(k, prefix) { paths = append(paths, strings.TrimPrefix(k, prefix)) } } return paths, nil } func adminCaller() *engine.CallerInfo { return &engine.CallerInfo{Username: "admin", Roles: []string{"admin"}, IsAdmin: true} } func userCaller() *engine.CallerInfo { return &engine.CallerInfo{Username: "user", Roles: []string{"user"}, IsAdmin: false} } func setupEngine(t *testing.T) (*CAEngine, *memBarrier) { t.Helper() b := newMemBarrier() eng := NewCAEngine().(*CAEngine) ctx := context.Background() config := map[string]interface{}{ "organization": "TestOrg", "key_algorithm": "ecdsa", "key_size": float64(256), "root_expiry": "87600h", } if err := eng.Initialize(ctx, b, "engine/ca/test/", config); err != nil { t.Fatalf("Initialize: %v", err) } return eng, b } func TestInitializeGeneratesRootCA(t *testing.T) { eng, _ := setupEngine(t) if eng.rootCert == nil { t.Fatal("root cert is nil") } if eng.rootKey == nil { t.Fatal("root key is nil") } if !eng.rootCert.IsCA { t.Error("root cert is not a CA") } if eng.rootCert.Subject.CommonName != "TestOrg Root CA" { t.Errorf("root CN: got %q, want %q", eng.rootCert.Subject.CommonName, "TestOrg Root CA") } if eng.rootCert.MaxPathLen != 1 { t.Errorf("root MaxPathLen: got %d, want 1", eng.rootCert.MaxPathLen) } } func TestUnsealSealLifecycle(t *testing.T) { eng, b := setupEngine(t) mountPath := "engine/ca/test/" // Seal and verify state is cleared. if err := eng.Seal(); err != nil { t.Fatalf("Seal: %v", err) } if eng.rootCert != nil { t.Error("rootCert should be nil after seal") } if eng.rootKey != nil { t.Error("rootKey should be nil after seal") } // Unseal and verify state is restored. ctx := context.Background() if err := eng.Unseal(ctx, b, mountPath); err != nil { t.Fatalf("Unseal: %v", err) } if eng.rootCert == nil { t.Error("rootCert should be non-nil after unseal") } if eng.rootKey == nil { t.Error("rootKey should be non-nil after unseal") } if !eng.rootCert.IsCA { t.Error("root cert should be CA after unseal") } } func TestCreateIssuer(t *testing.T) { eng, _ := setupEngine(t) ctx := context.Background() req := &engine.Request{ Operation: "create-issuer", CallerInfo: adminCaller(), Data: map[string]interface{}{ "name": "infra", }, } resp, err := eng.HandleRequest(ctx, req) if err != nil { t.Fatalf("create-issuer: %v", err) } if resp.Data["name"] != "infra" { t.Errorf("issuer name: got %v, want %q", resp.Data["name"], "infra") } // Verify the issuer cert is an intermediate CA signed by root. certPEM := resp.Data["cert_pem"].(string) block, _ := pem.Decode([]byte(certPEM)) if block == nil { t.Fatal("failed to decode issuer cert PEM") } issuerCert, err := x509.ParseCertificate(block.Bytes) if err != nil { t.Fatalf("parse issuer cert: %v", err) } if !issuerCert.IsCA { t.Error("issuer cert should be a CA") } // MaxPathLen 0 with MaxPathLenZero=false parses as -1 in Go's x509. // Either 0 or -1 is acceptable for a path-length-constrained intermediate. if issuerCert.MaxPathLen > 0 { t.Errorf("issuer MaxPathLen: got %d, want 0 or -1", issuerCert.MaxPathLen) } if issuerCert.Subject.CommonName != "infra" { t.Errorf("issuer CN: got %q, want %q", issuerCert.Subject.CommonName, "infra") } // Verify issuer is in memory. if _, ok := eng.issuers["infra"]; !ok { t.Error("issuer not found in memory") } } func TestCreateIssuerRejectsNonAdmin(t *testing.T) { eng, _ := setupEngine(t) ctx := context.Background() req := &engine.Request{ Operation: "create-issuer", CallerInfo: userCaller(), Data: map[string]interface{}{ "name": "infra", }, } _, err := eng.HandleRequest(ctx, req) if err == nil { t.Fatal("expected error for non-admin create-issuer") } if err != ErrForbidden { t.Errorf("expected ErrForbidden, got: %v", err) } } func TestCreateIssuerRejectsNilCallerInfo(t *testing.T) { eng, _ := setupEngine(t) ctx := context.Background() req := &engine.Request{ Operation: "create-issuer", Data: map[string]interface{}{ "name": "infra", }, } _, err := eng.HandleRequest(ctx, req) if err != ErrUnauthorized { t.Errorf("expected ErrUnauthorized, got: %v", err) } } func TestIssueCertificate(t *testing.T) { eng, _ := setupEngine(t) ctx := context.Background() // Create an issuer first. _, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "create-issuer", CallerInfo: adminCaller(), Data: map[string]interface{}{"name": "infra"}, }) if err != nil { t.Fatalf("create-issuer: %v", err) } // Issue a certificate. resp, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "issue", Path: "infra", CallerInfo: userCaller(), Data: map[string]interface{}{ "issuer": "infra", "common_name": "web.example.com", "profile": "server", "dns_names": []interface{}{"web.example.com", "www.example.com"}, }, }) if err != nil { t.Fatalf("issue: %v", err) } if resp.Data["cn"] != "web.example.com" { t.Errorf("cn: got %v", resp.Data["cn"]) } if resp.Data["serial"] == nil || resp.Data["serial"] == "" { t.Error("serial should not be empty") } if resp.Data["cert_pem"] == nil { t.Error("cert_pem should not be nil") } if resp.Data["key_pem"] == nil { t.Error("key_pem should not be nil") } if resp.Data["chain_pem"] == nil { t.Error("chain_pem should not be nil") } // Verify the leaf cert. certPEM := resp.Data["cert_pem"].(string) block, _ := pem.Decode([]byte(certPEM)) leafCert, err := x509.ParseCertificate(block.Bytes) if err != nil { t.Fatalf("parse leaf cert: %v", err) } if leafCert.IsCA { t.Error("leaf cert should not be a CA") } if leafCert.Subject.CommonName != "web.example.com" { t.Errorf("leaf CN: got %q", leafCert.Subject.CommonName) } if len(leafCert.DNSNames) != 2 { t.Errorf("leaf DNSNames: got %v", leafCert.DNSNames) } } func TestIssueCertificateWithOverrides(t *testing.T) { eng, _ := setupEngine(t) ctx := context.Background() _, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "create-issuer", CallerInfo: adminCaller(), Data: map[string]interface{}{"name": "infra"}, }) if err != nil { t.Fatalf("create-issuer: %v", err) } // Issue with custom TTL and key usages. resp, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "issue", CallerInfo: userCaller(), Data: map[string]interface{}{ "issuer": "infra", "common_name": "peer.example.com", "profile": "peer", "ttl": "720h", "key_usages": []interface{}{"digital signature"}, "ext_key_usages": []interface{}{"client auth"}, }, }) if err != nil { t.Fatalf("issue with overrides: %v", err) } certPEM := resp.Data["cert_pem"].(string) block, _ := pem.Decode([]byte(certPEM)) leafCert, err := x509.ParseCertificate(block.Bytes) if err != nil { t.Fatalf("parse leaf: %v", err) } // Verify client auth EKU. hasClientAuth := false for _, eku := range leafCert.ExtKeyUsage { if eku == x509.ExtKeyUsageClientAuth { hasClientAuth = true } } if !hasClientAuth { t.Error("expected client auth EKU") } } func TestIssueRejectsNilCallerInfo(t *testing.T) { eng, _ := setupEngine(t) ctx := context.Background() _, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "create-issuer", CallerInfo: adminCaller(), Data: map[string]interface{}{"name": "infra"}, }) if err != nil { t.Fatalf("create-issuer: %v", err) } _, err = eng.HandleRequest(ctx, &engine.Request{ Operation: "issue", Data: map[string]interface{}{ "issuer": "infra", "common_name": "test.example.com", }, }) if err != ErrUnauthorized { t.Errorf("expected ErrUnauthorized, got: %v", err) } } func TestPrivateKeyNotStoredInBarrier(t *testing.T) { eng, b := setupEngine(t) ctx := context.Background() _, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "create-issuer", CallerInfo: adminCaller(), Data: map[string]interface{}{"name": "infra"}, }) if err != nil { t.Fatalf("create-issuer: %v", err) } resp, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "issue", CallerInfo: userCaller(), Data: map[string]interface{}{ "issuer": "infra", "common_name": "test.example.com", "profile": "server", }, }) if err != nil { t.Fatalf("issue: %v", err) } serial := resp.Data["serial"].(string) // Check that the cert record does not contain a private key. recordData, err := b.Get(ctx, "engine/ca/test/certs/"+serial+".json") if err != nil { t.Fatalf("get cert record: %v", err) } if strings.Contains(string(recordData), "PRIVATE KEY") { t.Error("cert record should not contain private key") } } func TestRenewCertificate(t *testing.T) { eng, _ := setupEngine(t) ctx := context.Background() _, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "create-issuer", CallerInfo: adminCaller(), Data: map[string]interface{}{"name": "infra"}, }) if err != nil { t.Fatalf("create-issuer: %v", err) } // Issue original cert. issueResp, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "issue", CallerInfo: userCaller(), Data: map[string]interface{}{ "issuer": "infra", "common_name": "renew.example.com", "profile": "server", "dns_names": []interface{}{"renew.example.com"}, }, }) if err != nil { t.Fatalf("issue: %v", err) } origSerial := issueResp.Data["serial"].(string) // Renew. renewResp, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "renew", CallerInfo: userCaller(), Data: map[string]interface{}{ "serial": origSerial, }, }) if err != nil { t.Fatalf("renew: %v", err) } newSerial := renewResp.Data["serial"].(string) if newSerial == origSerial { t.Error("renewed cert should have different serial") } if renewResp.Data["cn"] != "renew.example.com" { t.Errorf("renewed CN: got %v", renewResp.Data["cn"]) } if renewResp.Data["cert_pem"] == nil { t.Error("renewed cert_pem should not be nil") } if renewResp.Data["key_pem"] == nil { t.Error("renewed key_pem should not be nil") } } func TestGetAndListCerts(t *testing.T) { eng, _ := setupEngine(t) ctx := context.Background() _, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "create-issuer", CallerInfo: adminCaller(), Data: map[string]interface{}{"name": "infra"}, }) if err != nil { t.Fatalf("create-issuer: %v", err) } // Issue two certs. for _, cn := range []string{"a.example.com", "b.example.com"} { _, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "issue", CallerInfo: userCaller(), Data: map[string]interface{}{ "issuer": "infra", "common_name": cn, "profile": "server", }, }) if err != nil { t.Fatalf("issue %s: %v", cn, err) } } // List certs. listResp, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "list-certs", CallerInfo: userCaller(), }) if err != nil { t.Fatalf("list-certs: %v", err) } certs, ok := listResp.Data["certs"].([]map[string]interface{}) if !ok { t.Fatalf("certs type: %T", listResp.Data["certs"]) } if len(certs) != 2 { t.Errorf("expected 2 certs, got %d", len(certs)) } // Get a specific cert. serial := certs[0]["serial"].(string) getResp, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "get-cert", CallerInfo: userCaller(), Data: map[string]interface{}{ "serial": serial, }, }) if err != nil { t.Fatalf("get-cert: %v", err) } if getResp.Data["serial"] != serial { t.Errorf("get-cert serial: got %v, want %v", getResp.Data["serial"], serial) } } func TestUnsealRestoresIssuers(t *testing.T) { eng, b := setupEngine(t) ctx := context.Background() mountPath := "engine/ca/test/" // Create issuer. _, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "create-issuer", CallerInfo: adminCaller(), Data: map[string]interface{}{"name": "infra"}, }) if err != nil { t.Fatalf("create-issuer: %v", err) } // Seal. eng.Seal() // Unseal. if err := eng.Unseal(ctx, b, mountPath); err != nil { t.Fatalf("Unseal: %v", err) } // Verify issuer was restored. if _, ok := eng.issuers["infra"]; !ok { t.Error("issuer 'infra' not restored after unseal") } // Verify we can issue from the restored issuer. _, err = eng.HandleRequest(ctx, &engine.Request{ Operation: "issue", CallerInfo: userCaller(), Data: map[string]interface{}{ "issuer": "infra", "common_name": "after-unseal.example.com", "profile": "server", }, }) if err != nil { t.Fatalf("issue after unseal: %v", err) } } func TestDeleteIssuer(t *testing.T) { eng, _ := setupEngine(t) ctx := context.Background() _, err := eng.HandleRequest(ctx, &engine.Request{ Operation: "create-issuer", CallerInfo: adminCaller(), Data: map[string]interface{}{"name": "infra"}, }) if err != nil { t.Fatalf("create-issuer: %v", err) } _, err = eng.HandleRequest(ctx, &engine.Request{ Operation: "delete-issuer", CallerInfo: adminCaller(), Data: map[string]interface{}{"name": "infra"}, }) if err != nil { t.Fatalf("delete-issuer: %v", err) } if _, ok := eng.issuers["infra"]; ok { t.Error("issuer should be deleted") } } func TestPublicMethods(t *testing.T) { eng, _ := setupEngine(t) ctx := context.Background() // Test GetRootCertPEM. rootPEM, err := eng.GetRootCertPEM() if err != nil { t.Fatalf("GetRootCertPEM: %v", err) } block, _ := pem.Decode(rootPEM) if block == nil { t.Fatal("failed to decode root PEM") } // Create issuer for chain/issuer tests. _, err = eng.HandleRequest(ctx, &engine.Request{ Operation: "create-issuer", CallerInfo: adminCaller(), Data: map[string]interface{}{"name": "infra"}, }) if err != nil { t.Fatalf("create-issuer: %v", err) } // Test GetIssuerCertPEM. issuerPEM, err := eng.GetIssuerCertPEM("infra") if err != nil { t.Fatalf("GetIssuerCertPEM: %v", err) } block, _ = pem.Decode(issuerPEM) if block == nil { t.Fatal("failed to decode issuer PEM") } // Test GetChainPEM. chainPEM, err := eng.GetChainPEM("infra") if err != nil { t.Fatalf("GetChainPEM: %v", err) } // Chain should contain two certificates. certCount := strings.Count(string(chainPEM), "BEGIN CERTIFICATE") if certCount != 2 { t.Errorf("chain should contain 2 certs, got %d", certCount) } // Test nonexistent issuer. _, err = eng.GetIssuerCertPEM("nonexistent") if err != ErrIssuerNotFound { t.Errorf("expected ErrIssuerNotFound, got: %v", err) } }