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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ edition = "2021"
# the ROOT manifest (`[workspace.package].version`), so it MUST be set here for a
# release to fire (§3.6). The library crates (dig-node-core/dig-runtime/dig-wallet)
# keep their own independent versions — only the released binary tracks the workspace version.
version = "0.38.0"
version = "0.38.1"

# Release hardening, matching digstore: keep integer-overflow checks ON in release.
# The node parses untrusted serialized input and does offset/length arithmetic over
Expand Down
10 changes: 10 additions & 0 deletions DEVELOPMENT_LOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,13 @@ CLI path (not via a native package) needs the `sc config`/`sc qc` dance.
("link.exe could not be run" / `DirectoryNotFoundException` on a `.tlog`) because the deep
`modules/.worktrees/dig-node-624/target/...` path trips MSBuild/cmake MAX_PATH — set a short
`CARGO_TARGET_DIR` (e.g. `C:\dnt624`) to build.
- **Privileged-owner check walks the WHOLE path (#712):** `crate::security::dir_is_privileged` (the
shared #565/#661/#46 gate) verifies EVERY ancestor component, not just the leaf, and rejects any
symlink/junction/reparse component — a privileged leaf under a user-writable or symlinked ancestor
is still swappable (intermediate rename/replace obeys the PARENT's perms; a reparse redirects the
whole path). Windows gotcha: `C:\Program Files` is owned by `NT SERVICE\TrustedInstaller`
(fixed SID `S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464`, byte-identical on
every host), NOT SYSTEM/Administrators — so the ancestor walk MUST accept that SID or it
false-rejects the canonical `%ProgramFiles%\DIG\bin` install root. Reparse detection uses the
no-follow `symlink_metadata` + `FILE_ATTRIBUTE_REPARSE_POINT` (catches junctions, not just
symlinks). Mirrors dig-dns's `ensure_prefix_root_owned_not_writable` (#701).
37 changes: 25 additions & 12 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,13 +311,26 @@ node runs ONE catch-up pass (which fully reconciles the leaf) rather than bursti
per missed tick.

**TLS-root owner gate (#661, defence-in-depth).** Before reading ANY TLS material — the leaf to
serve, and `ca.key` on the renewal path — the node verifies the TLS root directory is owned by a
privileged principal (Windows: the well-known `SYSTEM`/`Administrators` SID, by exact SID equality;
unix: `root` and not group/world-writable) and **fails CLOSED to plaintext** otherwise. A
user-writable TLS root could hold an attacker-swapped `ca.key` that this privileged service would
otherwise read and sign with; refusing it removes that vector. The owner SID is read directly through
the Win32 security API (launching no process — the same spawn-free check the self-heal LPE gate uses,
§ the shared owner check), so the guard never itself executes an attacker-planted binary.
serve, and `ca.key` on the renewal path — the node verifies the TLS root directory is privileged-owned
(§ the shared whole-path owner check) and **fails CLOSED to plaintext** otherwise. A user-writable TLS
root could hold an attacker-swapped `ca.key` that this privileged service would otherwise read and sign
with; refusing it removes that vector. The owner SID is read directly through the Win32 security API
(launching no process — the same spawn-free check the self-heal LPE gate uses), so the guard never
itself executes an attacker-planted binary.

**Shared whole-path owner check (#565/#661/#46, #712).** The three gates above defer to ONE shared
helper that classifies a directory as privileged-owned. It verifies the ENTIRE path, not just the leaf:
EVERY existing ancestor component (the directory, its parent, … up to the filesystem root) MUST be
privileged-owned AND MUST NOT be a symlink/junction/reparse point. A privileged-owned leaf under a
user-writable or symlinked ANCESTOR is still tamperable — an intermediate rename/replace is governed by
the parent's permissions, and a reparse point anywhere redirects the whole path — so a single weak
component fails the check. Per component: **unix** — owned by `root`/uid 0, no group/other write bit,
judged via `symlink_metadata` (lstat) so a symlink is rejected on its own identity; **Windows** — owner
SID equal (exact equality) to the well-known LocalSystem `S-1-5-18`, BUILTIN\Administrators
`S-1-5-32-544`, or `NT SERVICE\TrustedInstaller` (the fixed service SID that owns `C:\Program Files`
and its protected subtree — required so the canonical `%ProgramFiles%` install root is not
false-rejected), and the component carries no `FILE_ATTRIBUTE_REPARSE_POINT`. Fails CLOSED on any
indeterminate or missing component.

**The CA trust anchor is NEVER auto-rotated by the node.** An approaching CA expiry is only REPORTED
(`ca_renewal_due`); anchor rotation is an explicit, installer-coordinated `dig-cert rotate_ca` (it
Expand Down Expand Up @@ -1464,11 +1477,11 @@ SCM, always LocalSystem; a root systemd/launchd daemon) records the currently-ru
user-writable directory, a non-privileged local user could replace it and gain persistent
SYSTEM/root code execution on the next service start — a privilege-escalation vector. So before
registering a system-level service, `install` MUST verify the program's directory is
**privileged-owned** (Unix: `root`/uid 0 and no group/other write bit; Windows: an owner SID equal
to the well-known LocalSystem `S-1-5-18` or BUILTIN\Administrators `S-1-5-32-544`) and **refuse with
`PERMISSION_DENIED`** otherwise, before any side effect (no state-dir harden, no service create).
This is the SAME spawn-free owner gate the self-heal spawn root (§7 #565) and the TLS material root
(§4.1a #661) use — one shared check, fail-closed on an indeterminate owner. A **user-level** install
**privileged-owned across its whole path** (§ the shared whole-path owner check — every ancestor
component privileged-owned, non-reparse) and **refuse with `PERMISSION_DENIED`** otherwise, before any
side effect (no state-dir harden, no service create). This is the SAME spawn-free owner gate the
self-heal spawn root (§7 #565) and the TLS material root (§4.1a #661) use — one shared check,
fail-closed on an indeterminate owner. A **user-level** install
(the Linux/macOS default) runs as the very user who owns the binary, crosses no privilege boundary,
and is always allowed. The canonical install path (native OS package, §9.7) places the binary in a
protected admin-owned location (`%ProgramFiles%\DIG Network\dig-node\`, `/usr/…`), so it satisfies
Expand Down
1 change: 1 addition & 0 deletions crates/dig-node-service/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ windows-sys = { version = "0.61", features = [
"Win32_Foundation",
"Win32_Security",
"Win32_Security_Authorization",
"Win32_Storage_FileSystem",
] }

# Test-only WS client for the `GET /ws/status` integration tests (#239): connects
Expand Down
Loading
Loading