Files
libglacier-ng/SECURITY_DESIGN.txt
2026-07-19 17:49:23 -04:00

380 lines
18 KiB
Plaintext

+--------------------------------------------+
| Package integrity & scope security |
+--------------------------------------------+
This document records design decisions made in one planning session,
covering package integrity verification and the hard separation
between per-user and system-scope package tools. NONE OF THIS IS
IMPLEMENTED YET. It exists so implementation can start from a settled
plan instead of re-deriving it.
Companion document: PACKAGE_SCOPES.txt (repo/scope symlink routing —
referenced but not repeated here).
+--------------------------------------------+
| 1. Scope of this phase |
+--------------------------------------------+
Two different guarantees are in play, and they are NOT the same thing:
integrity - has this file been corrupted or altered
authenticity - did this actually come from who it claims to be
A hash alone only ever proves integrity. If the hash travels with the
file from the same untrusted source, tampering both together defeats
it. This phase is INTEGRITY ONLY. Authenticity (signing, a trusted
keyring) is a deliberate future phase, tied to remote-repo support —
not being designed in detail now, but a few choices below are made to
stay cheap to extend into later.
Everest is a security-focused distro (this is also *why* per-user
package isolation exists in the first place). Two sides of the CIA
triad — confidentiality is out of scope here, but integrity and
(eventually) authenticity are both treated as first-class, not
best-effort.
+--------------------------------------------+
| 2. Hash algorithm: SHA-256 |
+--------------------------------------------+
Decision: SHA-256, not SHA-512.
Rationale (SHA-512 was seriously considered, not dismissed by default):
- SHA-512's usual advantage (faster on 64-bit CPUs, due to 64-bit
word operations) does not hold across this project's actual
target set. Everest explicitly supports multiple architectures
via SYS_PROFILE, including 32-bit ones — on a 32-bit target,
SHA-512 is SLOWER than SHA-256 (64-bit math has to be emulated).
- SHA-256's 128-bit collision resistance is not a meaningful
weakness for this use case. There is no realistic attack path
where 128 bits is the bottleneck for "did this archive get
corrupted or tampered with."
- SHA-256 is what every comparable tool already defaults to (apk,
pacman, dpkg), and is the simpler implementation to vendor and
audit (smaller constant tables, no 64-bit rotates).
Escape hatch, explicitly kept open: hash values are namespaced as
"sha256:<hex>", never a bare hex string, specifically so a future
algorithm change is additive, not a format migration. "sha256 can
always be added to base if needed" — i.e. if the vendored
implementation is ever found lacking, the fallback is depending on a
real sha256sum binary once one exists in the base repo, not a redesign
of the sidecar format.
+--------------------------------------------+
| 3. What gets hashed, and how |
+--------------------------------------------+
WHAT: whole-archive only, for now. Per-file hashing (and the
eventual `gpkg --verify`-style installed-file audit it would enable)
is explicitly deferred, not rejected.
WHERE: a sidecar file next to the .gpkg —
musl-1.2.5.gpkg
musl-1.2.5.gpkg.sha256 <- contains exactly: sha256:<hex>
Sidecar was chosen over embedding in the manifest specifically because
a whole-archive hash cannot cleanly be embedded inside the file it is
hashing without excluding the hash field itself from the computation.
This also generalizes naturally to a future remote repo index that
just lists hashes for everything it serves.
IMPLEMENTATION SHAPE: one shared vendored implementation, compiled
two different ways, so there is exactly one place the algorithm
itself is ever implemented:
- src/sha256.c / src/sha256.h
Streaming API (init/update/final), not just "hash this file" —
chosen now specifically so per-file hashing later reuses the
same primitive without a rewrite.
- gpkg / syspkg side:
Compiled into a PRIVATE .so (no public header, not part of the
split-library public API surface). Loaded directly via FFI from
gpkg_common.lua (see section 6). Private for now because this is
new, unaudited code; promoting it to a real public
libglacier_crypto.so is the natural path once per-file hashing
is built and the API has proven itself.
- gbuild side:
Compiled into a tiny standalone CLI helper, `gsha256sum FILE`,
printing `sha256:<hex>` to stdout. gbuild shells out to it the
same way it already shells out to git/tar/make. This keeps
gbuild from depending on the host having sha256sum or openssl
installed, while still not needing gbuild to link against the
private .so directly (gbuild is a bash script, not something
that can drive FFI).
gbuild change: after producing the .gpkg archive, hash it with
gsha256sum and write the sidecar alongside it. This is not optional
groundwork — the moment verification is turned on, EVERYTHING
becomes a "missing sidecar" failure until this exists, so the two
changes (gbuild writes sidecars / gpkg+syspkg check them) ship as
ONE atomic change, not sequential phases.
+--------------------------------------------+
| 4. Refusal policy |
+--------------------------------------------+
Missing sidecar and hash mismatch are treated IDENTICALLY. There is no
soft "unverified, proceed anyway by default" tier — absence of proof
is treated the same as proof of tampering.
repo == "base" -> ALWAYS refuse. No flag, no
scope, no exception. Ever.
scope == SYS (via syspkg) -> ALWAYS refuse, for ANY repo,
including extra/community.
No flag overrides this.
scope == USR && repo != base -> refuse by default. The ONLY
place a bypass flag could ever
apply, once one exists.
No bypass flag exists yet. One may be added later, but ONLY for the
USR-scope/non-base case above — never for base, never for system
scope, regardless of how the flag is invoked.
Net effect, stated as a real security property: there is no flag
combination, present or future, that can install a corrupted base
package or a corrupted system-scope package.
HOOK POINT: gpkg's read_manifest_fields already resolves a package's
repo before any transaction machinery runs (it's needed for the
existing confirmation summary). Verification belongs right there,
before gl_init_stage_context is ever called. A failure aborts that
package the same way any other install error already does today — no
new failure-handling pattern needed.
+--------------------------------------------+
| 5. Tool separation: gpkg / gstore / syspkg |
+--------------------------------------------+
gpkg and gstore are BOTH strictly per-user tools. Neither has, or will
ever have, any code path into GL_SCOPE_SYS. This is a reversal of
earlier work in this project (gpkg briefly gained a -s/--system flag
and gstore briefly gained -N/--new-system) — both need to come back
out. Recorded here explicitly so it isn't missed during
implementation: removing capability that already exists in the
current tree, not just "don't add it."
syspkg is a new, separate binary — the SOLE tool with any system-scope
capability at all: install, remove, AND store creation/init (what
gstore -N used to do). One tool to secure, one tool to audit, one
place the privilege boundary has to be enforced.
Why store creation belongs in syspkg and not "the installer calling
gl_init_sys() directly": that would create a second, independent,
unaudited entry point into system-scope mutation — the exact problem
already solved for install/remove. syspkg owning ALL system-scope
mutation, with nothing else able to touch it, is the actual point.
gstore's existing UID bounds-check behavior (hard-refuses < 1000, even
under doas, even as UID 0) is confirmed already working correctly and
is being preserved exactly as-is — gstore stays permanently
user-scope-only; the fix is removing -N, not touching the bounds
check.
+--------------------------------------------+
| 6. syspkg privilege model |
+--------------------------------------------+
syspkg is always run as root. It is invoked via doas (or sudo) —
NEVER made setuid. This was an explicit rejected alternative, not an
unconsidered default:
A setuid LuaJIT interpreter is a real anti-pattern. It would inherit
environment variables, library search paths, and require() resolution
from whatever untrusted shell invoked it — all of which become
privilege-escalation surface the instant the process is running as
root. This is the same class of problem that has made setuid
scripts (shell, Perl, anything with a runtime) a long-standing
security no-go.
syspkg's own responsibility is limited to checking geteuid() == 0 and
refusing otherwise. The actual privilege boundary is enforced by doas
— a hardened, audited, purpose-built tool for exactly this — not
reimplemented inside syspkg itself.
+--------------------------------------------+
| 7. Shared code: gpkg_common.lua |
+--------------------------------------------+
gpkg and syspkg need nearly identical machinery: manifest reading, the
summary/confirmation prompt, staged-transaction handling
(gl_init_stage_context -> install/remove loop -> commit/abort), and
the hash-verification check from section 4. All of that lives in
lib/gpkg_common.lua, required by both.
What stays DISTINCT per-binary:
- Which scope each one is even capable of requesting. gpkg's code
simply never constructs a GL_SCOPE_SYS context — not "doesn't
expose a flag for it," does not have the capability in its code
path at all.
- The privilege model (only syspkg is ever invoked with elevation).
This split means the refusal matrix in section 4 lives ONCE in the
shared module — it does not need to be written twice or kept in sync
across two files. syspkg is simply the only caller that can ever
reach the scope == SYS branch of it.
This decision exists partly because of history in this project: a
previously-duplicated ffi.cdef block across three files caused a real,
hard-to-diagnose bug. Not repeating that shape here on purpose.
+--------------------------------------------+
| 8. Base repo cannot install to USR |
+--------------------------------------------+
A package in the base repo can NEVER be installed under GL_SCOPE_USR
(per-user scope). This is very unlikely to be attempted in practice,
but is being hardcoded as a real invariant anyway, not left as an
assumption.
ENFORCEMENT LEVEL: the library itself (gl_install_pkg), not any CLI.
scope == GL_SCOPE_USR && repo == GL_BASE_REPO refuses unconditionally,
regardless of which caller asked for it.
Rationale: the whole point of the split-.so architecture is that a
third-party tool can be built directly against libglacier-ng without
going through gpkg/syspkg at all. A rule that only one CLI happens to
respect is not a real guarantee. This must be a property of the
package system itself.
+--------------------------------------------+
| 9. syspkg self-update |
+--------------------------------------------+
syspkg will itself ship as a base-repo package (it has to, to be part
of the minimal system) and therefore has to be able to update itself
while running. Resolved the same way the earlier libc-live-update
question was resolved (see PACKAGE_SCOPES.txt section 5): POSIX
unlink() semantics already make this safe — a running process keeps
its old inode open even after the symlink swap, only newly-spawned
processes see the new version. No special self-replace mechanism
needed; this falls directly out of the existing atomic-rename commit
design.
+--------------------------------------------+
| 10. Open: the installer / grootstrap |
+--------------------------------------------+
STATUS: genuinely undecided, not just unimplemented. The actual
process of installing Everest end-to-end is, in the author's own
words, "kind of elusive" right now. grootstrap's rework is explicitly
BLOCKED on this getting its own dedicated design pass — not something
to force an answer on tonight.
What IS clear:
- grootstrap (or whatever the installer turns out to be) cannot be
correctly rebuilt until syspkg, gpkg_common.lua, and hash
verification all exist — every package a bootstrap installs is
base-repo, the strictest tier, so the installer is the FIRST real
consumer of all three at once.
- Whatever places syspkg itself onto a target filesystem, before
syspkg exists there to enforce anything, is definitionally outside
the "corrupted base package can never install" guarantee. This is
an honest edge, not a flaw — it only closes once the installer's
actual shape is designed. Live/install media is presumably the
root of trust at that point (if you trust what you booted from,
you trust what it lays down) — but this has not been decided, only
named as the shape of the problem.
REFERENCE MATERIAL (from an old installation guide at
everestlinux.org/install, read only through its "Syncing the Build
Environment" section — earlier material only, deliberately not
consulting anything past that point. Describes a previous,
possibly-outdated vision of the install process; treat as INPUT to the
future design pass, not as decisions already made. No system image
tarball concept appears anywhere in this range — that idea is
discarded entirely, not merely unmentioned):
- "system mountpoint" — a target directory (example used:
/mnt/everest, with a SYS_MNT env var pointing at it) that the new
system's root gets built at, kept separate from the host doing the
building. Conceptually the same thing grootstrap's TARGET_DIR
already is.
- Partition layout described around that mountpoint: an EFI system
partition at .../boot, an OPTIONAL dedicated partition for
/glacier itself (suggested minimum 15 GB), an optional /home
partition, and the remainder as root. The optional standalone
glacier partition is worth noting specifically — it implies
/glacier was originally conceived as something that could be its
own filesystem, separate from root, not necessarily just a
directory tree living on the root filesystem. Not a decision,
just a detail worth not losing.
- "template index" — a named, downloadable list of packages
defining what a given install should contain (the guide's own
words: "provides a list of packages which can be merged into a
build environment, thereby creating a semi-functional system").
The guide frames CHOOSING a template index around a specific set
of questions: does the install need 32-bit libraries, will it run
proprietary software that can't be recompiled, does it need a
specific C library for the hardware, is SELinux wanted, is a
non-default init system wanted. Those are essentially the exact
axes SYS_PROFILE already encodes (ARCH-LIBC[-FEATURE...]) — this
is a real correspondence, not a coincidence to ignore. Worth
deciding whether a template index simply IS a SYS_PROFILE plus a
recipes/ set under current design, or a separate downstream
concept (e.g. a pre-resolved list of already-built package
references to fetch, rather than a list of things to build from
source). The guide also warns that switching template
indexes/profiles after install requires rebuilding most of the
system — consistent with SYS_PROFILE being a foundational,
not-meant-to-change-casually choice in the current design too.
- "glacier-bootstrap PATH_TO_TEMPLATE_INDEX /mnt/everest" — a
referenced (possibly not-yet-existing-in-current-form) tool
described as taking "the specified template index, and
bootstrap[ping] a system at the specified directory, using the
packages listed within the template index as a guide." This is
package-list-driven bootstrapping, not image-based — it lines up
with what grootstrap is already trying to be (extract/install a
defined set of packages into a target directory), not with
anything tarball-shaped.
None of the above is a decision. It's what exists to react to when the
installer gets its own design session — scoped deliberately to only
the early, environment-setup half of the old guide for now.
+--------------------------------------------+
| 11. Build order for tomorrow |
+--------------------------------------------+
Dependency order, not necessarily literal implementation order within
a day, but grootstrap specifically cannot start until the first three
exist:
1. src/sha256.c / src/sha256.h (no dependencies)
2. gsha256sum CLI (needs 1)
3. private hash .so for gpkg/syspkg (needs 1)
4. gbuild: write sidecar on build (needs 2)
5. lib/gpkg_common.lua (shared logic extraction)
6. gl_install_pkg: base->USR refusal (library-level, needs nothing
above, can happen any time)
7. gpkg: remove -s/--system entirely (needs 5, to not duplicate
logic while trimming it)
8. gstore: remove -N/--new-system entirely
9. syspkg: new binary — install/remove/init under GL_SCOPE_SYS,
doas-only, no setuid (needs 1, 3, 5)
10. grootstrap rework BLOCKED — needs 4, 9, AND a
separate installer design
pass (section 10). Do not
start until that exists.
Verify-downloads-from-remote-repos (authenticity, signing, keyring)
is the explicitly agreed NEXT major item after everything above is
implemented — not part of this phase.