Repository navigation
Make recipient authorization explicit; data commands fail closed on drift - #5
Merged
Merged
Conversation
…n drift Seal and resolve no longer consume recipient drift: seal refuses keylessly before any effect, resolve preserves the ours-side recipient set and reports remaining drift. The new gitveil recipient add/remove commands are the only authorization entry points; they require every effective per-file recipient change to be named on the command line, rejecting injected manifest edits by printing the unexplained recipient. A first seal under a policy additionally requires every existing ciphertext of that policy to be aligned. The manifest publishes before file convergence, so any mid-run failure leaves refusable drift that an idempotent rerun converges.
…, honest output recipient add/remove now converge each envelope only in their own direction (envelope union/minus the named recipients), so mixed drift converges as two sequential one-sided commands instead of deadlocking; recipients are positional arguments. resolve encrypts merged content to the intersection of the ours envelope and the policy, rotating the data key so removed recipients never receive merged values, and refuses a disjoint policy. Policy-level output renders after per-file effects and only for published state; partial failures are called out and a faulted removal no longer claims an effective revocation. The all-failed preflight path keeps per-file reports and withholds the manifest. The manifest read-modify-republish boundary moved to manifest_document, and ciphertext writes share the workspace write policy.
Add integration coverage for policy-scoped authorization (explicit --policy selection, other policies byte-untouched, mandatory selector with several policies), per-file reporting of an unparseable ciphertext while the rest of the policy converges, the emptied-envelope removal refusal, and the resolve case that rotates and reports a pending addition in one run. Add a unit test for the concurrency check in manifest publication and assign manifest_document unit tests to the host suite. Reword the emptied-envelope error so the positional CLI cannot misread it.
Merged
Sukitly
added a commit
that referenced
this pull request
Aug 24, 2026
## Summary Prepares the v0.2.0 release per `docs/releasing.md`: - `Cargo.toml` / `Cargo.lock`: version 0.1.0 -> 0.2.0. - README: pinned installer download examples now reference `v0.2.0` (valid once the release is published). 0.2.0 is a minor bump carrying the explicit recipient authorization model (#5): `gitveil recipient add/remove` are the only authorization entry points, data commands fail closed on recipient drift, and `resolve` never extends access. Breaking relative to 0.1.0: hand-editing the manifest and running `seal` no longer performs rewrap or rotation. Because this PR touches `Cargo.toml`/`Cargo.lock`, the release workflow pull-request path also builds all four native target archives as a pre-release rehearsal. ## After merge ```bash git switch main && git pull --ff-only origin main git tag -a v0.2.0 -m "Gitveil v0.2.0" git push origin v0.2.0 ``` Then approve the `release` environment in Actions, verify the draft assets (`SHA256SUMS` and attestations), and publish the draft through the GitHub Releases interface.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Authorization changes are decoupled from data commands. Previously,
sealandresolvesilently consumed recipient drift between the manifest policy and an envelope: anyone who could edit.gitveilrc.json(a merged PR, a compromised write path) could inject their public recipient, and the next routinesealby an identity holder would wrap the file data key to it — a confused-deputy grant with no review point.New authorization model
gitveil recipient add AGE_RECIPIENT...andgitveil recipient remove AGE_RECIPIENT...are the only entry points that change who can decrypt. The command line names every recipient whose access changes; recipients are positional arguments (--policyoptional, inferred when exactly one policy exists).addtargetsenvelope ∪ named(never removes anyone),removetargetsenvelope ∖ named(never grants anyone). A tampered manifest entry is therefore never wrapped or revoked until someone names it explicitly; it surfaces as residual drift, printed with the full recipient value plus anext:action line. Mixed drift converges as two sequential one-sided commands in either order.addrewraps via SOPSupdatekeyswith byte-stable encrypted leaves;removerotates the data key and re-encrypts, so removed identities cannot decrypt later versions. Authorization commands never read or write workspace plaintext.sealrefuses and an idempotent rerun converges. When no envelope can be decrypted (no identity), nothing is published and every affected file is reported.Data commands never extend access
sealfails closed (keylessly, before any decryption) on any recipient drift and points to the recipient commands. A first seal of a new file additionally requires every existing ciphertext under the same policy to be aligned, so a tampered policy cannot capture new files while old ones show drift.resolveencrypts merged content to the intersection of the ours-side envelope set and the policy: a policy-removed recipient never receives merged (theirs-side) values — the data key rotates to exclude it — and no new recipient is granted anything. A disjoint policy makes resolve refuse with zero side effects.Internal changes
src/recipients/plan.rs(registered in the functional-core check) owns the per-direction convergence targets, residual-drift computation, and rejection rules.src/manifest_document.rs, shared byconfigureandrecipients; ciphertext write policy is single-sourced inworkspace::write_ciphertext_file.Breaking change
The previously documented flows "edit the manifest, then run
gitveil sealto rewrap/rotate" no longer perform authorization;sealnow refuses with guidance.SealOutcome/resolveoutput labels changed accordingly. No compatibility path is provided by design: the security invariant requires a single authorization entry point.Tests
EmptiesEnvelope/EmptyPolicy/RemoveNotMember/duplicate rejections.tests/integration_recipients.rs(real SOPS, real decryption assertions): add rewrap with byte-stable leaves and second-identity decryption; remove rotation with real exclusion of the removed identity; injected manifest recipients never wrapped and surfaced in full; mixed drift end-to-end (removethenadd, granted identity decrypts, revoked identity does not); faulted rewrap and faulted rotation leave refusable drift, never claim effective revocation, and converge on rerun; all-failed preflight reports every file and withholds the manifest; argument validation; multi-file and predeclared-entry convergence.integration_seal_open.rs: drift refusal in both directions (keyless), content edits cannot bypass the refusal, first-seal policy cross-check, aligned seal without an identity leaves plaintext/ciphertext/baseline unchanged.integration_resolve.rs: intersection narrowing with real exclusion of the removed identity, disjoint-policy refusal with zero side effects, addition-drift preservation with drift reporting.docs/getting-started.md.Checks run locally
cargo fmt --check,cargo clippy --all-targets(zero warnings)scripts/check-functional-core.py(25 modules),scripts/check-doc-links.pycargo nextest run -E 'not (binary(package_contract) | binary(release_installer_contract))': 272/272 passedNot run locally:
package_contractandrelease_installer_contract(release-build suites; packaging is untouched) — covered by theLinux/macOSCI gates.