Implement CA/PKI engine with two-tier X.509 certificate issuance
Add the first concrete engine implementation: a CA (PKI) engine that generates
a self-signed root CA at mount time, issues scoped intermediate CAs ("issuers"),
and signs leaf certificates using configurable profiles (server, client, peer).
Engine framework updates:
- Add CallerInfo struct for auth context in engine requests
- Add config parameter to Engine.Initialize for mount-time configuration
- Export Mount.Engine field; add GetEngine/GetMount on Registry
CA engine (internal/engine/ca/):
- Two-tier PKI: root CA → issuers → leaf certificates
- 10 operations: get-root, get-chain, get-issuer, create/delete/list issuers,
issue, get-cert, list-certs, renew
- Certificate profiles with user-overridable TTL, key usages, and key algorithm
- Private keys never stored in barrier; zeroized from memory on seal
- Supports ECDSA, RSA, and Ed25519 key types via goutils/certlib/certgen
Server routes:
- Wire up engine mount/request handlers (replace Phase 1 stubs)
- Add public PKI routes (/v1/pki/{mount}/ca, /ca/chain, /issuer/{name})
for unauthenticated TLS trust bootstrapping
Also includes: ARCHITECTURE.md, deploy config updates, operational tooling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
649
internal/engine/ca/ca_test.go
Normal file
649
internal/engine/ca/ca_test.go
Normal file
@@ -0,0 +1,649 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user