380 lines
18 KiB
Plaintext
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.
|