mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 15:41:41 +00:00
66d2af36a7
Rank 8 of the 2026-05-03 deep-research deliverable, commit 1 of 5
(cowork/rank-8-intermediate-ca-hierarchy-prompt.md). Closes the multi-
level CA hierarchy gap for FedRAMP boundary-CA, financial-services
policy-CA, and OT network-CA deployments where regulator-mandated
certificate-policy separation requires multiple layers (root → policy
→ issuing).
This commit lands ONLY the foundation — schema, types, repository
interface, postgres implementation. No service / connector / handler
wiring yet. The 5-commit chain is bisectable: this commit can ship
with no operator-visible behavior change until commits 2-5 wire the
service layer + the local-connector tree-mode + admin API + GUI tree
view + operator runbook. The default value for issuers.hierarchy_mode
is 'single' so every existing operator's behavior is byte-identical
post-migration.
Existing scaffolding REUSED (not redefined):
- internal/crypto/signer.Driver seam — every IntermediateCA carries
a key_driver_id pointing at the signer.Driver instance that owns
its private key. Defense in depth: NEVER persist key bytes in a
row. FileDriver is the production default; future PKCS11Driver /
CloudKMSDriver close the disk-exposure leg via the same seam.
- issuers.id row — the new intermediate_cas FK references it.
Files added:
internal/domain/intermediate_ca.go — IntermediateCA type,
IntermediateCAState
closed enum (active /
retiring / retired),
IsValidIntermediateCAState
+ IsTerminal helpers,
NameConstraint struct
(RFC 5280 §4.2.1.10
permitted+excluded
subtree subset
semantics for service-
layer enforcement),
HierarchyModeSingle /
HierarchyModeTree
constants.
internal/repository/postgres/intermediate_ca.go — IntermediateCARepository
impl: Create (ica-<slug>
ID gen, JSONB +
nullable-column round-
trip, lib/pq 23505 →
ErrAlreadyExists),
Get, ListByIssuer,
ListChildren,
UpdateState,
GetActiveRoot,
WalkAncestry (recursive
CTE — single SQL
round-trip, O(depth)
rows, leaf-first
ordering).
migrations/000028_intermediate_ca_hierarchy.{up,down}.sql
— idempotent schema.
issuers.hierarchy_mode
VARCHAR(20) DEFAULT
'single'. New
intermediate_cas table
with FKs to
issuers / self
(parent_ca_id) +
CHECK constraints
(closed-enum state,
not_after >
not_before, no self-
parent) + 6 indexes
(partial-unique
active root per
issuer, partial-
unique name per
issuer, owning
issuer, parent,
state, expiring).
Files modified:
internal/domain/connector.go — adds Issuer.HierarchyMode field
with full doc comment + JSON tag.
Empty string ≡ single mode for
back-compat.
internal/repository/interfaces.go — adds IntermediateCARepository
interface (7 methods).
Verified locally:
gofmt: clean.
go vet ./internal/domain/... ./internal/repository/...: exit 0.
go build ./internal/domain/... ./internal/repository/...: exit 0.
Out of scope for this commit (lands in commits 2-5):
- service/intermediate_ca.go (CreateRoot / CreateChild / Retire /
LoadHierarchy / AssembleChain + RFC 5280 §4.2.1.9 path-len +
§4.2.1.10 NameConstraints subset enforcement + 9 service tests).
- local connector rewrite + byte-equivalence pin
(TestLocal_HierarchyMode_SingleVsTree_ByteIdentical — the load-
bearing backwards-compat refusal-to-ship test).
- 4 admin-gated handler endpoints + OpenAPI extension + handler tests.
- web/src/pages/IssuerHierarchyPage.tsx.
- docs/intermediate-ca-hierarchy.md sysadmin runbook + connectors.md
row + WORKSPACE-ROADMAP follow-ons.
Reference: cowork/rank-8-intermediate-ca-hierarchy-prompt.md.
69 lines
2.9 KiB
SQL
69 lines
2.9 KiB
SQL
-- 000028_intermediate_ca_hierarchy.up.sql
|
|
-- Rank 8: first-class N-level CA hierarchy management. Closes the
|
|
-- FedRAMP / financial-services / OT-network "policy CA in the middle"
|
|
-- deployment shape. intermediate_cas captures every non-root CA in
|
|
-- the hierarchy with a self-referential parent_ca_id FK; issuers.
|
|
-- hierarchy_mode toggles the new code-path behind a flag.
|
|
--
|
|
-- All operations use IF NOT EXISTS / IF EXISTS so the migration is
|
|
-- idempotent — safe to re-run on every certctl-server boot per the
|
|
-- "Idempotent migrations" architecture decision in CLAUDE.md.
|
|
--
|
|
-- Defense in depth: NEVER persist CA private key bytes. The
|
|
-- key_driver_id column is a reference (filesystem path / KMS key ID
|
|
-- / HSM slot) to the signer.Driver instance that owns the key.
|
|
|
|
ALTER TABLE issuers
|
|
ADD COLUMN IF NOT EXISTS hierarchy_mode VARCHAR(20) NOT NULL DEFAULT 'single';
|
|
|
|
CREATE TABLE IF NOT EXISTS intermediate_cas (
|
|
id TEXT PRIMARY KEY,
|
|
owning_issuer_id TEXT NOT NULL REFERENCES issuers(id) ON DELETE RESTRICT,
|
|
parent_ca_id TEXT REFERENCES intermediate_cas(id) ON DELETE RESTRICT,
|
|
name TEXT NOT NULL,
|
|
subject TEXT NOT NULL,
|
|
state VARCHAR(20) NOT NULL DEFAULT 'active',
|
|
cert_pem TEXT NOT NULL,
|
|
key_driver_id TEXT NOT NULL,
|
|
not_before TIMESTAMPTZ NOT NULL,
|
|
not_after TIMESTAMPTZ NOT NULL,
|
|
path_len_constraint INT,
|
|
name_constraints JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
ocsp_responder_url TEXT,
|
|
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
CONSTRAINT intermediate_ca_state_check CHECK (
|
|
state IN ('active', 'retiring', 'retired')
|
|
),
|
|
CONSTRAINT intermediate_ca_validity_check CHECK (
|
|
not_after > not_before
|
|
),
|
|
CONSTRAINT intermediate_ca_no_self_parent CHECK (
|
|
parent_ca_id IS NULL OR parent_ca_id <> id
|
|
)
|
|
);
|
|
|
|
-- Partial-unique: at most one ACTIVE root per issuer. A root is a row
|
|
-- with parent_ca_id IS NULL (it has no parent in the hierarchy);
|
|
-- multiple retired roots can coexist for audit history.
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_intermediate_ca_active_root_per_issuer
|
|
ON intermediate_cas(owning_issuer_id)
|
|
WHERE parent_ca_id IS NULL AND state = 'active';
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_intermediate_ca_unique_name_per_issuer
|
|
ON intermediate_cas(owning_issuer_id, name);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_intermediate_ca_owning_issuer
|
|
ON intermediate_cas(owning_issuer_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_intermediate_ca_parent
|
|
ON intermediate_cas(parent_ca_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_intermediate_ca_state
|
|
ON intermediate_cas(state);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_intermediate_ca_expiring
|
|
ON intermediate_cas(not_after) WHERE state = 'active';
|