Fix ECDH zeroization, add audit logging, and remediate high findings

- Fix #61: handleRotateKey and handleDeleteUser now zeroize stored
  privBytes instead of calling Bytes() (which returns a copy). New
  state populates privBytes; old references nil'd for GC.
- Add audit logging subsystem (internal/audit) with structured event
  recording for cryptographic operations.
- Add audit log engine spec (engines/auditlog.md).
- Add ValidateName checks across all engines for path traversal (#48).
- Update AUDIT.md: all High findings resolved (0 open).
- Add REMEDIATION.md with detailed remediation tracking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-17 14:04:39 -07:00
parent b33d1f99a0
commit 5c5d7e184e
24 changed files with 1699 additions and 72 deletions

View File

@@ -596,6 +596,9 @@ func (e *CAEngine) handleGetChain(_ context.Context, req *engine.Request) (*engi
if issuerName == "" {
issuerName = req.Path
}
if err := engine.ValidateName(issuerName); err != nil {
return nil, err
}
chain, err := e.GetChainPEM(issuerName)
if err != nil {
@@ -610,6 +613,9 @@ func (e *CAEngine) handleGetChain(_ context.Context, req *engine.Request) (*engi
func (e *CAEngine) handleGetIssuer(_ context.Context, req *engine.Request) (*engine.Response, error) {
name := req.Path
if err := engine.ValidateName(name); err != nil {
return nil, err
}
certPEM, err := e.GetIssuerCertPEM(name)
if err != nil {
@@ -698,6 +704,7 @@ func (e *CAEngine) handleCreateIssuer(ctx context.Context, req *engine.Request)
Expiry: expiry,
}
e.setProfileAIA(&profile)
issuerCert, err := profile.SignRequest(e.rootCert, csr, e.rootKey)
if err != nil {
return nil, fmt.Errorf("ca: sign issuer cert: %w", err)
@@ -757,6 +764,9 @@ func (e *CAEngine) handleDeleteIssuer(ctx context.Context, req *engine.Request)
if name == "" {
name = req.Path
}
if err := engine.ValidateName(name); err != nil {
return nil, err
}
e.mu.Lock()
defer e.mu.Unlock()
@@ -830,6 +840,9 @@ func (e *CAEngine) handleIssue(ctx context.Context, req *engine.Request) (*engin
if issuerName == "" {
return nil, fmt.Errorf("ca: issuer name is required")
}
if err := engine.ValidateName(issuerName); err != nil {
return nil, err
}
profileName, _ := req.Data["profile"].(string)
if profileName == "" {
@@ -922,6 +935,7 @@ func (e *CAEngine) handleIssue(ctx context.Context, req *engine.Request) (*engin
return nil, fmt.Errorf("ca: create leaf CSR: %w", err)
}
e.setProfileAIA(&profile)
leafCert, err := profile.SignRequest(is.cert, csr, is.key)
if err != nil {
return nil, fmt.Errorf("ca: sign leaf cert: %w", err)
@@ -1171,6 +1185,7 @@ func (e *CAEngine) handleRenew(ctx context.Context, req *engine.Request) (*engin
return nil, fmt.Errorf("ca: create renewal CSR: %w", err)
}
e.setProfileAIA(&profile)
newCert, err := profile.SignRequest(is.cert, csr, is.key)
if err != nil {
return nil, fmt.Errorf("ca: sign renewal cert: %w", err)
@@ -1238,6 +1253,9 @@ func (e *CAEngine) handleSignCSR(ctx context.Context, req *engine.Request) (*eng
if issuerName == "" {
return nil, fmt.Errorf("ca: issuer name is required")
}
if err := engine.ValidateName(issuerName); err != nil {
return nil, err
}
csrPEM, _ := req.Data["csr_pem"].(string)
if csrPEM == "" {
@@ -1293,6 +1311,7 @@ func (e *CAEngine) handleSignCSR(ctx context.Context, req *engine.Request) (*eng
}
}
e.setProfileAIA(&profile)
leafCert, err := profile.SignRequest(is.cert, csr, is.key)
if err != nil {
return nil, fmt.Errorf("ca: sign CSR: %w", err)
@@ -1436,6 +1455,20 @@ func (e *CAEngine) handleDeleteCert(ctx context.Context, req *engine.Request) (*
// --- Helpers ---
// setProfileAIA populates the AIA (Authority Information Access) extension
// URLs on the profile if external_url is configured. This allows clients
// to discover the issuing CA certificate for chain building.
func (e *CAEngine) setProfileAIA(profile *certgen.Profile) {
if e.config.ExternalURL == "" {
return
}
base := strings.TrimSuffix(e.config.ExternalURL, "/")
mount := e.mountName()
profile.IssuingCertificateURL = []string{
base + "/v1/pki/" + mount + "/ca/chain",
}
}
func defaultCAConfig() *CAConfig {
return &CAConfig{
Organization: "Metacircular",
@@ -1461,6 +1494,9 @@ func mapToCAConfig(m map[string]interface{}, cfg *CAConfig) error {
if v, ok := m["root_expiry"].(string); ok {
cfg.RootExpiry = v
}
if v, ok := m["external_url"].(string); ok {
cfg.ExternalURL = v
}
return nil
}