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:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -29,11 +29,13 @@ func GetProfile(name string) (certgen.Profile, bool) {
|
||||
}
|
||||
// Return a copy so callers can modify.
|
||||
cp := certgen.Profile{
|
||||
IsCA: p.IsCA,
|
||||
PathLen: p.PathLen,
|
||||
Expiry: p.Expiry,
|
||||
KeyUse: make([]string, len(p.KeyUse)),
|
||||
ExtKeyUsages: make([]string, len(p.ExtKeyUsages)),
|
||||
IsCA: p.IsCA,
|
||||
PathLen: p.PathLen,
|
||||
Expiry: p.Expiry,
|
||||
KeyUse: make([]string, len(p.KeyUse)),
|
||||
ExtKeyUsages: make([]string, len(p.ExtKeyUsages)),
|
||||
OCSPServer: append([]string(nil), p.OCSPServer...),
|
||||
IssuingCertificateURL: append([]string(nil), p.IssuingCertificateURL...),
|
||||
}
|
||||
copy(cp.KeyUse, p.KeyUse)
|
||||
copy(cp.ExtKeyUsages, p.ExtKeyUsages)
|
||||
|
||||
@@ -10,6 +10,7 @@ type CAConfig struct {
|
||||
Country string `json:"country,omitempty"`
|
||||
KeyAlgorithm string `json:"key_algorithm"`
|
||||
RootExpiry string `json:"root_expiry"`
|
||||
ExternalURL string `json:"external_url,omitempty"`
|
||||
KeySize int `json:"key_size"`
|
||||
}
|
||||
|
||||
|
||||
@@ -588,6 +588,9 @@ func (e *SSHCAEngine) handleUpdateProfile(ctx context.Context, req *engine.Reque
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("sshca: name is required")
|
||||
}
|
||||
if err := engine.ValidateName(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Load existing profile.
|
||||
profile, err := e.loadProfile(ctx, name)
|
||||
@@ -631,6 +634,9 @@ func (e *SSHCAEngine) handleGetProfile(ctx context.Context, req *engine.Request)
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("sshca: name is required")
|
||||
}
|
||||
if err := engine.ValidateName(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
profile, err := e.loadProfile(ctx, name)
|
||||
if err != nil {
|
||||
@@ -697,6 +703,9 @@ func (e *SSHCAEngine) handleDeleteProfile(ctx context.Context, req *engine.Reque
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("sshca: name is required")
|
||||
}
|
||||
if err := engine.ValidateName(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check existence.
|
||||
if _, err := e.barrier.Get(ctx, e.mountPath+"profiles/"+name+".json"); err != nil {
|
||||
|
||||
@@ -450,6 +450,9 @@ func (e *TransitEngine) handleDeleteKey(ctx context.Context, req *engine.Request
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("transit: name is required")
|
||||
}
|
||||
if err := engine.ValidateName(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ks, ok := e.keys[name]
|
||||
if !ok {
|
||||
@@ -498,6 +501,9 @@ func (e *TransitEngine) handleGetKey(_ context.Context, req *engine.Request) (*e
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("transit: name is required")
|
||||
}
|
||||
if err := engine.ValidateName(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ks, ok := e.keys[name]
|
||||
if !ok {
|
||||
@@ -561,6 +567,9 @@ func (e *TransitEngine) handleRotateKey(ctx context.Context, req *engine.Request
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("transit: name is required")
|
||||
}
|
||||
if err := engine.ValidateName(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ks, ok := e.keys[name]
|
||||
if !ok {
|
||||
@@ -638,6 +647,9 @@ func (e *TransitEngine) handleUpdateKeyConfig(ctx context.Context, req *engine.R
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("transit: name is required")
|
||||
}
|
||||
if err := engine.ValidateName(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ks, ok := e.keys[name]
|
||||
if !ok {
|
||||
@@ -684,6 +696,9 @@ func (e *TransitEngine) handleTrimKey(ctx context.Context, req *engine.Request)
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("transit: name is required")
|
||||
}
|
||||
if err := engine.ValidateName(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ks, ok := e.keys[name]
|
||||
if !ok {
|
||||
@@ -1290,6 +1305,9 @@ func (e *TransitEngine) handleGetPublicKey(_ context.Context, req *engine.Reques
|
||||
if keyName == "" {
|
||||
return nil, fmt.Errorf("transit: name is required")
|
||||
}
|
||||
if err := engine.ValidateName(keyName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ks, ok := e.keys[keyName]
|
||||
if !ok {
|
||||
|
||||
@@ -212,6 +212,9 @@ func (e *UserEngine) handleRegister(ctx context.Context, req *engine.Request) (*
|
||||
}
|
||||
|
||||
username := req.CallerInfo.Username
|
||||
if err := engine.ValidateName(username); err != nil {
|
||||
return nil, fmt.Errorf("user: invalid username: %w", err)
|
||||
}
|
||||
e.mu.RLock()
|
||||
if u, ok := e.users[username]; ok {
|
||||
pubB64 := base64.StdEncoding.EncodeToString(u.pubKey.Bytes())
|
||||
@@ -302,6 +305,9 @@ func (e *UserEngine) handleGetPublicKey(_ context.Context, req *engine.Request)
|
||||
if username == "" {
|
||||
return nil, fmt.Errorf("user: username is required")
|
||||
}
|
||||
if err := engine.ValidateName(username); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e.mu.RLock()
|
||||
defer e.mu.RUnlock()
|
||||
@@ -657,14 +663,16 @@ func (e *UserEngine) handleRotateKey(ctx context.Context, req *engine.Request) (
|
||||
return nil, fmt.Errorf("user: rotate key: %w", err)
|
||||
}
|
||||
|
||||
// Zeroize old key.
|
||||
oldRaw := oldState.privKey.Bytes()
|
||||
crypto.Zeroize(oldRaw)
|
||||
// Zeroize old key material and drop reference for GC.
|
||||
crypto.Zeroize(oldState.privBytes)
|
||||
oldState.privKey = nil
|
||||
oldState.privBytes = nil
|
||||
|
||||
// Update in-memory state.
|
||||
e.users[caller] = &userState{
|
||||
privKey: priv,
|
||||
pubKey: priv.PublicKey(),
|
||||
privKey: priv,
|
||||
privBytes: priv.Bytes(),
|
||||
pubKey: priv.PublicKey(),
|
||||
config: &UserKeyConfig{
|
||||
Algorithm: e.config.KeyAlgorithm,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
@@ -692,6 +700,9 @@ func (e *UserEngine) handleDeleteUser(ctx context.Context, req *engine.Request)
|
||||
if username == "" {
|
||||
return nil, fmt.Errorf("user: username is required")
|
||||
}
|
||||
if err := engine.ValidateName(username); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
@@ -701,9 +712,10 @@ func (e *UserEngine) handleDeleteUser(ctx context.Context, req *engine.Request)
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
|
||||
// Zeroize private key.
|
||||
oldRaw := oldState.privKey.Bytes()
|
||||
crypto.Zeroize(oldRaw)
|
||||
// Zeroize private key material and drop reference for GC.
|
||||
crypto.Zeroize(oldState.privBytes)
|
||||
oldState.privKey = nil
|
||||
oldState.privBytes = nil
|
||||
|
||||
// Delete from barrier.
|
||||
prefix := e.mountPath + "users/" + username + "/"
|
||||
|
||||
Reference in New Issue
Block a user