Files
incredigo/internal/rotate/noop.go
T
leetcrypt 59af53dcc0 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>
2026-06-18 14:48:24 -07:00

81 lines
2.8 KiB
Go

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...)
}