feat(cmd): S04-B re-encrypt root scope
What
Implements the re-encrypt --scope=root operator command — root key rotation — the fourth step of the S04-B rotation-tooling plan. Replaces Step 3's reEncryptRootNotImplemented stub with the real action:
- Provider derivation from the decoded key file: an active provider that wraps under the first key, and a single-key provider per retained key whose URI selects the rows still wrapped under it. A retained line whose URI equals the active key's is skipped with a log line (a duplicated key-file line is an authoring mistake, not a rotation input).
- Orientation refusal before any re-wrap, naming both URIs, when the decoded file's first key does not carry
--active-key-uri(a stale mount would otherwise rotate in reverse and still pass verification). The decoded active and retained URIs are logged at startup. - Serial
RootKeyRotator.RotateAllfrom each retained provider to the active provider, with aProgresscallback feeding per-batch liveness logs. - Whole-live-set verification via
ListLiveKeysNotUnderRootKeyURI(<active URI>, 1), run unconditionally (even for a no-retained-keys file): a non-empty result exits 1 naming the first offendingroot_key_uri.
Composition seam growth
The one-shot operator composition is extended for the root scope (the plan's Step 4 file list intentionally omits operator.go): rotationStore embeds crypto.RotationKeyStore so the injected store drives NewRootKeyRotator, and operatorRuntime carries the infrastructure config rather than any key material — the action resolves the root-key secret itself, builds the providers, and zeroizes the decoded keys before the first database round trip, so no other subcommand ever holds root-key bytes. A command-level preflight validates the secret (resolve, decode, wipe) before the composition opens the database pool, so a broken secret is reported without a database round trip.
Spec coverage
Spec: docs/specs/S04-b-encryption-key-rotation-tooling.md, "Root key rotation (--scope=root)".
| Criterion | Test |
|---|---|
| AC 1 — re-wraps every live row onto the active key; re-run processes zero | TestReEncryptRootScope_RewrapsEveryLiveRow |
| AC 2 — single-key file rotates nothing but still verifies (exit 0) | TestReEncryptRootScope_SingleKeyFileVerifies |
| AC 3 — verification empty=success; non-empty exits 1 naming the offender (incl. a URI absent from the file) | TestReEncryptRootScope_StrandedURIFailsVerification, TestReEncryptRootScope_SingleKeyFileVerifiesOffender |
| AC 4 — orientation refusal before any re-wrap naming both URIs; startup URI logging | TestReEncryptRoot_OrientationRefusalNamesBothURIs, TestReEncryptRootScope_OrientationRefusalLeavesRowsUntouched |
| AC 14 — no key material in command-flow logs | TestReEncryptRootScope_RewrapsEveryLiveRow (AC-14 assertion) |
| Root error cases — unwrap failure aborts and persists; duplicate line skipped; transient failure resumes | TestReEncryptRootScope_UnwrapFailureAbortsAndPersists, _DuplicateLineSkipped, _TransientFailureResumes |
Testing
The action is covered end-to-end by cmd/artifact-registry/reencrypt_root_integration_test.go, driven through cmd.Run against a per-test isolated database.
No e2e scenario catalog entry is added or affected: re-encrypt --scope=root is an operator tool with no user-facing request path, so it has no place in the format/protocol e2e catalogs. The operator-facing runbook for the command lands with the documentation step (Step 9) of this plan.
Stacking
Targets sahmed/s04b-step3-commands (Step 3, !1345 (merged)); GitLab retargets this MR down the stack as each predecessor merges. Part of the S04-B rotation-tooling plan.
Related to https://gitlab.com/gitlab-org/ops/artifact-registry/-/issues/496