rotate: nine rotation drivers + verify-before-revoke execute spine
Implements the Rotator interface across all four rotation patterns, each a self-contained one-file driver self-registering via init(): in-place DB password postgres, mysql (3-stmt unprivileged fallback), redis local keypair + propagate sshkey (ed25519), wireguard (clamped curve25519) provider-API token gitea PAT, mullvad device key cloud-key self-identifying aws IAM access key (hand-rolled SigV4, no SDK dep) Spine (execute.go) enforces Hard Rule #2: backup -> rotate -> verify(new) -> store -> re-read+verify -> revoke-old; dryrun.go keeps --execute gated. Each driver ships a table-driven test proving real cutover (old secret stops authenticating) and asserting no secret substring leaks to argv/Meta/errors. Promotes golang.org/x/crypto to a direct dependency. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
package rotate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"incredigo/internal/discover"
|
||||
"incredigo/internal/vault"
|
||||
)
|
||||
|
||||
// NoopRotator is a dry-run Rotator that exercises the full rotation flow —
|
||||
// Detect → Rotate → Verify → RevokeOld — WITHOUT contacting any service or
|
||||
// changing any real credential. It mints a random in-vault "new" secret so the
|
||||
// safety spine (backup → rotate → verify → store → revoke) can be tested
|
||||
// end-to-end (docs/ROTATION.md §6 build-order step 1).
|
||||
//
|
||||
// It is intentionally NOT registered in the global registry: the production
|
||||
// `rotate` command must still report zero drivers and refuse --execute (see
|
||||
// cmd/incredigo and CLAUDE.md hard rules). Wire it up explicitly in tests, or
|
||||
// later behind an opt-in dry-run flag.
|
||||
type NoopRotator struct {
|
||||
mu sync.Mutex
|
||||
revoked []string // identities passed to RevokeOld, for assertions
|
||||
}
|
||||
|
||||
// Name identifies the driver in plans and audit records.
|
||||
func (r *NoopRotator) Name() string { return "noop" }
|
||||
|
||||
// Detect matches every credential — the dry run applies to anything.
|
||||
func (r *NoopRotator) Detect(discover.Credential) bool { return true }
|
||||
|
||||
// Rotate mints a fresh random secret in the vault and returns its handle. It
|
||||
// contacts no provider and leaves the old credential untouched, honoring the
|
||||
// Rotator contract (mint new, never revoke here).
|
||||
func (r *NoopRotator) Rotate(ctx context.Context, c discover.Credential, v *vault.Vault) (*vault.Handle, error) {
|
||||
raw := make([]byte, 32)
|
||||
if _, err := rand.Read(raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
enc := make([]byte, hex.EncodedLen(len(raw)))
|
||||
hex.Encode(enc, raw)
|
||||
for i := range raw { // wipe the random source; Store wipes enc itself
|
||||
raw[i] = 0
|
||||
}
|
||||
return v.Store(enc), nil
|
||||
}
|
||||
|
||||
// Verify confirms the freshly minted secret is in custody and non-empty. A real
|
||||
// driver would authenticate it against the service; the noop only checks that
|
||||
// the new value survived rotation, so verify-before-revoke ordering is testable.
|
||||
func (r *NoopRotator) Verify(ctx context.Context, newSecret *vault.Handle, v *vault.Vault) error {
|
||||
buf, err := v.Open(newSecret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if buf.Size() == 0 {
|
||||
return errors.New("noop verify: new secret is empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RevokeOld retires nothing — there is no real provider — but records the
|
||||
// identity so a test can assert revoke ran only after verify. Mirrors the
|
||||
// safety-spine ordering without any destructive effect.
|
||||
func (r *NoopRotator) RevokeOld(ctx context.Context, c discover.Credential, v *vault.Vault) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.revoked = append(r.revoked, c.Identity)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Revoked returns a copy of the identities whose RevokeOld was invoked.
|
||||
func (r *NoopRotator) Revoked() []string {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return append([]string(nil), r.revoked...)
|
||||
}
|
||||
Reference in New Issue
Block a user