59af53dcc0
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>
81 lines
2.8 KiB
Go
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...)
|
|
}
|