Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Inspect the existing implementation and tests before proposing changes. Do not i
- Gitveil is one Rust 2024 package with a library and a thin binary.
- Supported production hosts are macOS and Linux.
- The Rust version is pinned in `rust-toolchain.toml`.
- The compatible SOPS build and artifact checksums are declared in `scripts/sops_artifacts.py`.
- Compatible SOPS and age-keygen builds and artifact checksums are declared in `scripts/sops_artifacts.py` and `scripts/age_artifacts.py`.
- `Cargo.toml` intentionally has `publish = false`; do not publish the crate or create releases unless explicitly requested.
- Keep all tracked text, code comments, tests, commit messages, and pull request content in English.
- Do not add private keys, plaintext secrets, local absolute paths, generated build output, unpublished planning material, or tool transcripts.
Expand All @@ -32,8 +32,8 @@ Keep pure policy and transformation code separate from external effects.
- `scripts/check-architecture.py` is the executable declaration of modules that belong to the functional core. Add new pure modules to that declaration rather than bypassing the check.
- `src/source/` owns typed and lossless source parsing and rendering.
- `src/semantic/` owns pure semantic diff and merge behavior.
- `src/git/`, `src/sops/`, and `src/runtime/` contain external adapters.
- Top-level feature modules such as `configure`, `seal`, `open`, `status`, `verify`, and `resolve` orchestrate pure decisions and explicit effects.
- `src/git/`, `src/sops/`, `src/age.rs`, and `src/runtime/` contain external adapters.
- Top-level feature modules such as `identity`, `configure`, `seal`, `open`, `status`, `verify`, and `resolve` orchestrate pure decisions and explicit effects.
- `src/main.rs` remains a thin CLI entry point; reusable behavior belongs in the library.

Choose the correct abstraction rather than the smallest diff. Do not duplicate policy in an orchestrator when it belongs in a pure model or plan.
Expand All @@ -45,10 +45,10 @@ Treat these as product contracts:
- Secret values, private identities, data keys, and decrypted comments must not enter argv, Git metadata, logs, errors, reports, baselines, or tracked fixtures.
- Secret-bearing values must not gain `Debug` or `Display` implementations that can reveal their contents.
- Unknown Git or SOPS stderr is redacted by default. Preserve typed error categories instead of forwarding arbitrary subprocess output.
- SOPS subprocess arguments never contain secret values.
- SOPS and age-keygen subprocess arguments never contain secret values or private identity material.
- Git subprocesses use read-only plumbing. Gitveil never modifies the index, Git configuration, hooks, attributes, or Git state outside `.git/gitveil/`.
- Workspace paths are validated before descriptor-relative filesystem access. Symlink and submodule escapes must fail closed.
- Owner-only permissions are required for plaintext, runtime directories, temporary files, local IPC, and installed private sidecars where applicable.
- Owner-only permissions are required for plaintext, generated identities, identity staging, runtime directories, temporary files, local IPC, and installed private sidecars where applicable.
- Production code must not use `unwrap` or `expect` for external input, filesystem, Git, SOPS, process, or protocol results.
- Do not add `unsafe` code. If a platform security API has no safe wrapper, obtain maintainer approval, document the local invariant, and add platform coverage first.
- Do not weaken a security assertion to accommodate nondeterministic fixture behavior. Isolate the external source of nondeterminism instead.
Expand All @@ -72,7 +72,7 @@ Match the test level to the contract:

- Keep pure decision tests colocated with their modules.
- Use contract tests for public domain behavior.
- Use real ephemeral repositories and real pinned SOPS executables for Git, SOPS, filesystem, process, and runtime integration behavior.
- Use real ephemeral repositories and real pinned SOPS and age-keygen executables for Git, SOPS, identity, filesystem, process, and runtime integration behavior.
- Inject failures only at external boundaries that cannot be triggered reliably; do not mock the semantic core.
- Generate unique age identities and plaintext canaries at test time. Never add a private identity or plaintext secret fixture.
- Assert unchanged ciphertext and encrypted leaves byte-for-byte where stability is part of the contract.
Expand Down
5 changes: 3 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ Preserve these invariants:
- Unknown subprocess output is redacted by default.
- Workspace paths are validated before descriptor-relative filesystem access; symlink and submodule escapes are rejected.
- Git subprocesses are read-only. Gitveil does not modify the index, configuration, hooks, attributes, or Git state outside `.git/gitveil/`.
- SOPS subprocess arguments never contain secret values.
- SOPS and age-keygen subprocess arguments never contain secret values or private identity material.
- Production code does not use `unwrap` or `expect` for external input, filesystem, Git, SOPS, process, or protocol results.
- Do not use `unsafe` unless a platform security API has no safe wrapper; document the local invariant and add a platform test.
- Identity generation uses the pinned official age-keygen sidecar, stages private material in owner-only storage, and never replaces an existing identity path.
- Tests generate private identities and plaintext canaries at runtime instead of storing them in tracked fixtures.
- Unchanged ciphertext and encrypted leaves are asserted byte-for-byte where stability is part of the contract.

Expand All @@ -61,7 +62,7 @@ On macOS, run both through the unified gate before requesting review:
./scripts/check-all.sh
```

The gates cover formatting, architecture boundaries, public documentation links, Clippy, unit and integration tests, dependency policy, release builds, and native archive validation. Integration tests use real Git and checksum-pinned SOPS executables. Initial cold runs download dependencies and build artifacts and may take substantially longer than warm runs.
The gates cover formatting, architecture boundaries, public documentation links, Clippy, unit and integration tests, dependency policy, release builds, and native archive validation. Integration tests use real Git and checksum-pinned SOPS and age-keygen executables. Initial cold runs download dependencies and build artifacts and may take substantially longer than warm runs.

GitHub Actions runs `scripts/check-host.sh` natively on Linux and macOS for every pull request and push to `main`. CI uses no repository secrets and does not publish artifacts or releases.

Expand Down
41 changes: 24 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ Gitveil provides file-level encryption for dotenv, JSON, YAML, and TOML secret f
- macOS or Linux
- Git 2.20+ for `verify` and `resolve`
- A matching age identity on machines that need plaintext access
- An external `age-keygen` executable for initial identity generation; existing identities and keyless commands do not require it

Gitveil itself does not require a system SOPS or age executable. Release archives include a checksum-verified official SOPS 3.13.3 sidecar at `libexec/gitveil/sops`; Gitveil does not search for SOPS on the system `PATH`.
Gitveil does not require a system SOPS or age executable. Release archives include checksum-verified official SOPS 3.13.3 and age-keygen 1.3.1 sidecars under `libexec/gitveil/`; Gitveil does not search for either tool on the system `PATH`.

## Installation

Expand All @@ -27,7 +26,7 @@ The default prefix is `${CARGO_HOME:-$HOME/.cargo}`. To select another prefix:
./scripts/install-local.py --prefix ~/.local
```

The installer performs a locked release build, fetches and verifies official SOPS 3.13.3, validates the complete release archive, and transactionally installs the binary, private sidecar, and license files. Do not use `cargo install --path .`: Cargo installs only the binary and omits the private sidecar.
The installer performs a locked release build, fetches and verifies official SOPS 3.13.3 and age-keygen 1.3.1 artifacts, validates the complete release archive, and transactionally installs the binary, private sidecars, and license files. Do not use `cargo install --path .`: Cargo installs only the binary and omits the private sidecars.

The user-facing installation unit is a native release archive. Maintainers build one on each target platform with:

Expand All @@ -40,37 +39,44 @@ The archive is written to `dist/gitveil-v<version>-<os>-<arch>.tar.gz` with this
```text
bin/gitveil
libexec/gitveil/sops
libexec/gitveil/age-keygen
share/licenses/gitveil/LICENSE
share/licenses/gitveil/SOPS-MPL-2.0.txt
share/licenses/gitveil/SOPS-NOTICE.txt
share/licenses/gitveil/AGE-BSD-3-Clause.txt
share/licenses/gitveil/AGE-NOTICE.txt
```

Install all three top-level directories under the same prefix and place `<prefix>/bin` on `PATH`. Gitveil never downloads executables at runtime.

Source development and tests may explicitly override the private sidecar with `SOPS_BIN`; the override must still be SOPS 3.13.3. This variable is not part of the user installation contract.
Source development and tests may explicitly override the private sidecars with `SOPS_BIN` and `AGE_KEYGEN_BIN`; each override must still have its pinned version. These variables are not part of the user installation contract.

## Initial setup

Use an external `age-keygen` once to generate a private identity and derive its public recipient. Run these commands only when the identity file does not already exist:
Generate a private identity explicitly after installation. The command selects `SOPS_AGE_KEY_FILE` when set, then the standard SOPS identity file for the current platform, creates every missing identity directory at mode `0700`, writes the identity at mode `0600`, and never replaces an existing path:

```bash
umask 077
identity_file="$HOME/.config/sops/age/keys.txt"
mkdir -p "$(dirname "$identity_file")"
age-keygen -o "$identity_file"
chmod 600 "$identity_file"

export SOPS_AGE_KEY_FILE="$identity_file"
recipient="$(age-keygen -y "$SOPS_AGE_KEY_FILE")"
gitveil identity generate
recipient="$(gitveil identity recipients)"
```

If a native age identity file already exists at the selected location, skip `generate` and use `gitveil identity recipients` to derive its public value. Both commands select one file using an explicit command option, then `SOPS_AGE_KEY_FILE`, then the platform default key file. `recipients` does not inspect `SOPS_AGE_KEY`, execute `SOPS_AGE_KEY_CMD`, or enumerate every identity source that SOPS may load. To use a custom location, select it consistently for generation and subsequent commands:

```bash
export SOPS_AGE_KEY_FILE="$HOME/.config/sops/age/keys.txt"
gitveil identity generate
recipient="$(gitveil identity recipients)"
```

After the identity has been atomically published, a directory-sync failure is reported as a warning rather than a generation failure: the command still prints the published path and recipient and exits successfully, but durability across an immediate system crash was not confirmed. Preserve the identity file; `gitveil identity recipients --identity <path>` can print its recipient again.

`init` and `add` must run at a Git repository root containing a `.git` file or directory. `init` accepts only public `age1...` recipients, never private identities:

```bash
gitveil init --policy team --recipient "$recipient"
```

After installing `age-keygen` and Gitveil in a source checkout, run the isolated end-to-end example to exercise identity generation, `init`, `add`, `seal`, commit, clone, `open`, `status`, and `verify` with real command output:
From a source checkout, run the isolated end-to-end example to exercise Gitveil-managed identity generation, `init`, `add`, `seal`, commit, clone, `open`, `status`, and `verify` with real command output:

```bash
./examples/quickstart.sh
Expand Down Expand Up @@ -117,7 +123,7 @@ Gitveil does not currently provide a managed-entry removal command. Manually rem

## Identity management

Gitveil does not generate, store, or synchronize private identities. Keep identity files at mode `0600`, back them up securely outside the repository, and have each team member generate an independent identity. Exchange only public recipients, never private keys. The repository stores only public `age1...` recipients. `status` and `verify` remain available without an identity. To add a member, add the member's public recipient to the manifest policy and run `gitveil seal` to rewrap access.
Gitveil generates native age identities only when `gitveil identity generate` is explicitly invoked. It does not overwrite, synchronize, distribute, back up, or automatically rotate them. Keep identity files at mode `0600`, back them up securely outside the repository, and have each team member generate an independent identity. Exchange only public recipients, never private keys. The repository stores only public `age1...` recipients. `status` and `verify` remain available without an identity. To add a member, add the member's public recipient to the manifest policy and run `gitveil seal` to rewrap access.

To remove a member, first rotate the actual secret values in plaintext, remove the member's recipient from the manifest policy, and run `gitveil seal`. When Gitveil detects a recipient removal, it encrypts the entire file under a fresh data key and reports `sealed; data key rotated`; neither the new values nor the new key are decryptable by the removed identity. Addition-only changes keep the existing data key and preserve encrypted leaf bytes.

Expand Down Expand Up @@ -175,7 +181,8 @@ Gitveil reads index stages but never runs `git add`.
- Gitveil enforces SOPS `encrypted_regex: .*`; every source scalar and layout/comment value is encrypted.
- Each ciphertext file uses one data key. Removing a recipient causes `seal` and `resolve` to rotate that key, preventing the removed identity from decrypting later versions. Access to historical versions cannot be revoked; forward exclusion also requires rotating the actual secret values.
- Paths undergo workspace-relative validation and descriptor-relative confinement; symlink and submodule escapes are rejected.
- SOPS subprocesses have deadlines, and temporary files and IPC endpoints live in owner-only runtime directories.
- SOPS and age-keygen subprocesses have deadlines. Unknown sidecar stderr is redacted and converted into fixed, typed diagnostics rather than being forwarded verbatim.
- Temporary files and IPC endpoints live in owner-only runtime directories.
- Gitveil writes no Git-internal state outside `.git/gitveil/` and never modifies the index, configuration, hooks, or attributes. Only root-level `init` and `add` write repository configuration; `add` owns and verifies the managed `.gitignore` block.

## Development
Expand All @@ -190,4 +197,4 @@ The unified local quality gate covers the macOS host, Docker Linux, real SOPS co

## License

Gitveil source code is available under the [MIT License](LICENSE). The official SOPS executable bundled in native release archives is distributed under the [Mozilla Public License 2.0](licenses/SOPS-MPL-2.0.txt); its version and corresponding source location are recorded in the [SOPS notice](licenses/SOPS-NOTICE.txt).
Gitveil source code is available under the [MIT License](LICENSE). The official SOPS executable bundled in native release archives is distributed under the [Mozilla Public License 2.0](licenses/SOPS-MPL-2.0.txt); its version and source location are recorded in the [SOPS notice](licenses/SOPS-NOTICE.txt). The bundled official age-keygen executable is distributed under the [BSD 3-Clause License](licenses/AGE-BSD-3-Clause.txt); its version and source location are recorded in the [age notice](licenses/AGE-NOTICE.txt).
3 changes: 2 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ Reports are especially valuable when they involve:
- Unauthorized decryption after recipient removal or incorrect recipient-policy enforcement.
- Failure to rotate the data key when a recipient is removed.
- Modification of the Git index, configuration, hooks, attributes, or state outside `.git/gitveil/`.
- SOPS sidecar substitution, checksum bypass, version confusion, or repository configuration injection.
- SOPS or age-keygen sidecar substitution, checksum bypass, version confusion, or repository configuration injection.
- Identity generation that overwrites an existing path, leaves partial private material, uses unsafe permissions, or reveals a private identity.
- Local IPC authentication, permissions, process cleanup, or lock isolation failures.
- Ciphertext integrity, MAC, merge, or rollback behavior that can expose or silently replace secret data.

Expand Down
7 changes: 5 additions & 2 deletions docker/test.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,16 @@ ARG SOPS_SHA256
ARG SOPS_BASELINE_VERSION=3.13.2
ARG SOPS_BASELINE_ARTIFACT
ARG SOPS_BASELINE_SHA256
# age-keygen is a test-only provisioning dependency for the public quickstart.
# age-keygen is the pinned identity sidecar used by integration and packaging tests.
ARG AGE_VERSION=1.3.1
ARG AGE_ARTIFACT
ARG AGE_SHA256
ARG NEXTEST_VERSION=0.9.140
ENV SOPS_BIN=/usr/local/bin/sops \
SOPS_BASELINE_BIN=/usr/local/bin/sops-baseline \
SOPS_DISABLE_VERSION_CHECK=1 \
AGE_KEYGEN_BIN=/usr/local/bin/age-keygen
AGE_KEYGEN_BIN=/usr/local/bin/age-keygen \
AGE_KEYGEN_ARCHIVE=/usr/local/share/gitveil-test/age.tar.gz

RUN apt-get update \
&& apt-get install --yes --no-install-recommends ca-certificates curl git python3 \
Expand Down Expand Up @@ -61,6 +62,8 @@ RUN set -eu; \
echo "${AGE_SHA256} /tmp/age.tar.gz" | sha256sum --check --strict; \
tar -xzf /tmp/age.tar.gz -C /tmp age/age-keygen; \
install -m 0755 /tmp/age/age-keygen /usr/local/bin/age-keygen; \
mkdir -p /usr/local/share/gitveil-test; \
install -m 0644 /tmp/age.tar.gz /usr/local/share/gitveil-test/age.tar.gz; \
rm -rf /tmp/age /tmp/age.tar.gz; \
age-keygen --version

Expand Down
Loading