806 lines
20 KiB
Go
806 lines
20 KiB
Go
package ca
|
|
|
|
import (
|
|
"context"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"errors"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"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 {
|
|
data map[string][]byte
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
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) //nolint:errcheck
|
|
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 TestInitializeWithImportedRoot(t *testing.T) {
|
|
// First, generate a root CA to use as the import source.
|
|
srcEng, _ := setupEngine(t)
|
|
rootPEM, err := srcEng.GetRootCertPEM()
|
|
if err != nil {
|
|
t.Fatalf("GetRootCertPEM: %v", err)
|
|
}
|
|
// Get the key PEM from barrier.
|
|
srcKeyPEM, err := srcEng.barrier.Get(context.Background(), srcEng.mountPath+"root/key.pem")
|
|
if err != nil {
|
|
t.Fatalf("get root key: %v", err)
|
|
}
|
|
|
|
// Now initialize a new engine with the imported root.
|
|
b := newMemBarrier()
|
|
eng := NewCAEngine().(*CAEngine) //nolint:errcheck
|
|
ctx := context.Background()
|
|
|
|
config := map[string]interface{}{
|
|
"organization": "ImportOrg",
|
|
"root_cert_pem": string(rootPEM),
|
|
"root_key_pem": string(srcKeyPEM),
|
|
}
|
|
|
|
if err := eng.Initialize(ctx, b, "engine/ca/imported/", config); err != nil {
|
|
t.Fatalf("Initialize with import: %v", err)
|
|
}
|
|
|
|
if eng.rootCert == nil {
|
|
t.Fatal("root cert is nil after import")
|
|
}
|
|
if !eng.rootCert.IsCA {
|
|
t.Error("imported root is not a CA")
|
|
}
|
|
// The CN should be from the original cert, not the new org.
|
|
if eng.rootCert.Subject.CommonName != "TestOrg Root CA" {
|
|
t.Errorf("imported root CN: got %q, want %q", eng.rootCert.Subject.CommonName, "TestOrg Root CA")
|
|
}
|
|
|
|
// Verify we can create issuers and issue certs from the imported root.
|
|
_, err = eng.HandleRequest(ctx, &engine.Request{
|
|
Operation: "create-issuer",
|
|
CallerInfo: adminCaller(),
|
|
Data: map[string]interface{}{"name": "infra"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create-issuer from imported root: %v", err)
|
|
}
|
|
|
|
_, err = eng.HandleRequest(ctx, &engine.Request{
|
|
Operation: "issue",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{
|
|
"issuer": "infra",
|
|
"common_name": "imported.example.com",
|
|
"profile": "server",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("issue from imported root: %v", err)
|
|
}
|
|
}
|
|
|
|
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) //nolint:errcheck
|
|
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 !errors.Is(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 !errors.Is(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) //nolint:errcheck
|
|
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) //nolint:errcheck
|
|
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 !errors.Is(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) //nolint:errcheck
|
|
|
|
// 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) //nolint:errcheck
|
|
|
|
// 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) //nolint:errcheck
|
|
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) //nolint:errcheck
|
|
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 TestImportRootRejectsValidRoot(t *testing.T) {
|
|
eng, _ := setupEngine(t)
|
|
ctx := context.Background()
|
|
|
|
// Generate a new root to try importing.
|
|
other, _ := setupEngine(t)
|
|
otherPEM, _ := other.GetRootCertPEM()
|
|
otherKeyPEM, _ := other.barrier.Get(ctx, other.mountPath+"root/key.pem")
|
|
|
|
_, err := eng.HandleRequest(ctx, &engine.Request{
|
|
Operation: "import-root",
|
|
CallerInfo: adminCaller(),
|
|
Data: map[string]interface{}{
|
|
"cert_pem": string(otherPEM),
|
|
"key_pem": string(otherKeyPEM),
|
|
},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error when importing over a valid root")
|
|
}
|
|
if !strings.Contains(err.Error(), "still valid") {
|
|
t.Errorf("expected 'still valid' error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestImportRootReplacesExpiredRoot(t *testing.T) {
|
|
eng, _ := setupEngine(t)
|
|
ctx := context.Background()
|
|
|
|
// Simulate an expired root by setting NotAfter to the past.
|
|
eng.mu.Lock()
|
|
eng.rootCert.NotAfter = time.Now().Add(-1 * time.Hour)
|
|
eng.mu.Unlock()
|
|
|
|
// Generate a new root to import.
|
|
other, _ := setupEngine(t)
|
|
newPEM, _ := other.GetRootCertPEM()
|
|
newKeyPEM, _ := other.barrier.Get(ctx, other.mountPath+"root/key.pem")
|
|
|
|
resp, err := eng.HandleRequest(ctx, &engine.Request{
|
|
Operation: "import-root",
|
|
CallerInfo: adminCaller(),
|
|
Data: map[string]interface{}{
|
|
"cert_pem": string(newPEM),
|
|
"key_pem": string(newKeyPEM),
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("import-root: %v", err)
|
|
}
|
|
if resp.Data["cn"] == nil {
|
|
t.Error("expected cn in response")
|
|
}
|
|
|
|
// Verify the root was replaced.
|
|
rootPEM, err := eng.GetRootCertPEM()
|
|
if err != nil {
|
|
t.Fatalf("GetRootCertPEM: %v", err)
|
|
}
|
|
if string(rootPEM) != string(newPEM) {
|
|
t.Error("root cert was not replaced")
|
|
}
|
|
|
|
// Verify we can still create issuers with the new root.
|
|
_, err = eng.HandleRequest(ctx, &engine.Request{
|
|
Operation: "create-issuer",
|
|
CallerInfo: adminCaller(),
|
|
Data: map[string]interface{}{"name": "new-issuer"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create-issuer after import: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestImportRootRequiresAdmin(t *testing.T) {
|
|
eng, _ := setupEngine(t)
|
|
ctx := context.Background()
|
|
|
|
_, err := eng.HandleRequest(ctx, &engine.Request{
|
|
Operation: "import-root",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{
|
|
"cert_pem": "fake",
|
|
"key_pem": "fake",
|
|
},
|
|
})
|
|
if !errors.Is(err, ErrForbidden) {
|
|
t.Errorf("expected ErrForbidden, got: %v", err)
|
|
}
|
|
}
|
|
|
|
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 !errors.Is(err, ErrIssuerNotFound) {
|
|
t.Errorf("expected ErrIssuerNotFound, got: %v", err)
|
|
}
|
|
}
|