diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..578baa0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+# Build outputs (make; make libglacier-ng)
+/build/
diff --git a/CLAUDE.md b/CLAUDE.md
index 4064097..6cceaa7 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -3,7 +3,8 @@
Context for Claude Code sessions on libglacier-ng: the package manager
for Everest Linux. Kept as a handoff, so a fresh session starts with full
continuity instead of re-deriving everything. Last full rewrite:
-2026-10-04, at the end of a long session. **Update it as you go.**
+2026-10-04; brought fully up to date at the end of the 2026-10-07
+session. **Update it as you go.**
## Start here
@@ -15,15 +16,231 @@ continuity instead of re-deriving everything. Last full rewrite:
- `SECURITY_DESIGN.txt`: integrity verification, scope separation,
refusal matrix, syspkg's privilege model.
- `PACKAGE_SCOPES.txt`: layout, link routing, `/etc` config handling.
- - `IMAGE_DESIGN.txt`: PROPOSAL for system images (bootstrap OUTPUT,
- the sysroot library change). Open decisions are in section 9.
- - `GLACIER_BOOTSTRAP.txt`: PLAN for `glacier-bootstrap`. Prerequisites
- are in section 7, decisions in section 9 (9c and 9d are decided).
+ - `IMAGE_DESIGN.txt`: system images (bootstrap OUTPUT). Sections 3
+ (sysroot) and 4 (`syspkg --root`) are IMPLEMENTED (milestones 1-2);
+ the rest is still a proposal. Open decisions are in section 9.
+ - `GLACIER_BOOTSTRAP.txt`: `glacier-bootstrap`. Local mode is
+ IMPLEMENTED (ROADMAP 1.3; section 10 says how it differs from the
+ plan); remote mode and `--pin` are still a plan. Open: 9a, 9f.
+ - `REPO_DESIGN.txt`: remote repos + signing (ROADMAP 3.1). Sections
+ 1 and 10 = the user's decisions (TLS library still open); steps
+ 1-4 of section 9 DONE (see "3.1 status"); sections 7, 5 and 6
+ describe steps 2, 3 and 4 as implemented.
+ - `SECURITY_REVIEW_2026-10-06.md`: the full audit (findings, fixes,
+ what was removed, what's still open).
- `SECURITY_REVIEW_2026-07-16.md`: older review findings.
-Nothing from the 2026-10-04 session is committed. libglacier-ng is on
-branch `dev` with a large uncommitted diff (`src/`, the docs). Commit only
-when the user asks.
+Nothing from the 2026-10-04 through 2026-10-07 sessions is
+committed. The user said "do not commit yet" (2026-10-05), and on
+2026-10-06 that their git server is down: they will commit themselves
+once it's back. libglacier-ng is on branch
+`dev` with a large uncommitted diff (`src/`, the docs, `build/`). Commit
+only when the user asks; when they do, propose a few logical commits.
+
+2026-10-05 session: system-scope permissions fix (bug 24), ROADMAP 2.3
+dependencies (`src/deps.c`), index corruption on abort (bug 25), ROADMAP
+1.1 sysroot, live removal deleting foreign files (bug 26), ROADMAP 1.2
+(`syspkg -R`, `--strict`) + 2.6 (file collisions), gpkg ignoring unknown
+flags (bug 27).
+
+2026-10-06 session: ROADMAP 1.3 `glacier-bootstrap` local mode, and
+Everest's initramfs switched to it (boots in QEMU). Tidy-up (gbuild's
+hint removed). ROADMAP 1.4 DONE (packages for glacier itself; details in
+ROADMAP 1.4 and section 7): full regression green, new
+`tests/installed_alpine.sh` for the installed layout, which found bug 29
+(fixed, mutation-checked, Everest rebuilt and booted with the fix).
+
+The installer (1.5) is DEFERRED by the user: manual installation is
+one of Everest's main features. Then ROADMAP 3.1 was designed with the
+user (REPO_DESIGN.txt, every decision taken; only the TLS library for
+curl in base is open) and steps 1, 2 and (that evening, after the
+laptop died mid-rebuild) 3 of its section 9 were done.
+
+2026-10-07 session: ROADMAP 3.1 step 4 (`-f`, `-u`, dependency
+closure, the cache; see "3.1 status"). Help text for -f/-u/-O
+shortened at the user's request. Refresh output (user's wording): `[i] Syncing
+all repositories...`, then `[✓] All repositories synced successfully.`
+if every repo succeeded; the per-repo success/up-to-date lines only
+with `-V`; errors always (repo_alpine.sh checks both ways).
+Then (same day) glacier-bootstrap configures repositories (user: it must
+leave a semi-functional system, pacstrap-like): see "Bootstrap
+repositories" below.
+
+2026-10-09: help text. `gsign`, `grepo`, `gsha256sum` (C, `getopt_long`,
+static `help()` in each tool's own file), `glacier-bootstrap` and `gworld`
+now have `-h/--help` and `-v/--version` (`v4.0.0`) in gpkg's format
+(`%-25s` `{-x --long} ARG` lines, GPL footer), with long forms for every
+flag. `-h` goes to stdout, exit 0; usage errors stay one usage block on
+stderr, exit 1. repo_alpine.sh checks the help of grepo/gsign/gsha256sum.
+Backups: `glacier-bootstrap.old2`, `gworld.old`, `tests/repo_alpine.sh.old5`.
+
+### Bootstrap repositories (2026-10-07)
+
+The user chose: the config comes from a NEW base package
+(`everest-repos` 1.0.0: `etc/glacier/repos.conf` + `keys/everest-2026.pub`,
+source `~/Projects/everest/pkgsrc/everest-repos`; everest-bootscripts
+1.3.0 no longer ships `etc/glacier`, its old copy is
+`pkgsrc/everest-bootscripts/glacier.old`); bootstrap runs `syspkg -R T
+-y` after installing and a failed fetch is an error (exit 7, no
+"Bootstrapped"); under `-R`, `-y`/`-f`/`-u` use T's OWN keyring
+(`keyring(sysroot)` in gpkg_common.lua; `require_trusted` also refuses a
+file whose realpath leaves T). `-R -l` signatures stay the host's
+(`HOST_KEYRING`). No url in T: `[!] No remote repositories are
+configured in T.`, exit 0 (Everest today: base isn't published).
+Copying the host's repos.conf (pacstrap's mirrorlist) left out for now.
+The user said everything a template lists for base also goes in the
+base repo (the published repo = the template's base packages); no url
+yet. Everest rebuilt (bootscripts 1.3.0, everest-repos, glacier-ng 4.0.0
+in place), initramfs remade, booted: `/etc/glacier` from everest-repos,
+12 packages.
+
+### Audit and cleanup (2026-10-06, late)
+
+At the user's request, before 3.1 step 4: a full security audit
+(`SECURITY_REVIEW_2026-10-06.md`, bugs 30-35 below), unused code removed,
+and junk cleaned up. The user's choices: delete only the `.old` backups
+and stray binaries (every prototype/note file stays: libglacier-ng's
+`src/{deb,gpkg,templ,report,test_*}.c`, `src/html`, `pkg/`, glacier-ng's
+`todo`, `tree/`, `test-*`, `pkgs/`...); libglacier-ng's build outputs are
+untracked (`.gitignore`: `/build/`; the deletions show in the next
+commit); the `.old` backup rule STAYS (see "Backups" below for current ones). New tests:
+`tests/audit_alpine.sh` (26 checks), `tests/test_abort_unlocked.lua`.
+Full regression green; Everest rebuilt and booted. Everest's own `.old`
+files were not touched (not one of the two repos).
+
+### 3.1 status
+
+Step 4 DONE (2026-10-07; REPO_DESIGN section 6 "As implemented"). The
+user chose: the FIRST repo (repos.conf order) serving a name wins, at
+its highest version; a dependency installed too old is upgraded in the
+same transaction (keeps its auto/explicit mark); cached downloads are
+deleted after the commit that installs them.
+- libglacier-ng: `gl_scope_cache_dir(ctx, repo, out)` (transaction.c;
+ `/cache/REPO`, checked like `repos` via the shared static
+ `checked_subdir`); `log.c` help lists `-f`/`-u`.
+- glacier-ng `gpkg_common.lua`: `M.fetch(names, opts)`,
+ `M.upgrade(names, opts)`; `load_indexes` (refresh, then read the
+ accepted copies), `pick`, `close_deps`, `download` (SIZE-capped,
+ hashed), `matches_index` (manifest vs index entry), `install_plan`.
+ `M.install` is now `verify_all` + `install_copies(copies, original,
+ opts, meta)`, shared with -f/-u (`meta`: auto, may_replace, dep,
+ cache). `verify_and_copy(path, vouched)` takes the index's hash in
+ place of a sidecar. syspkg `-f`/`-u` modes; gpkg `-f`/`-u` (no
+ longer "not implemented").
+- New suite `tests/fetch_alpine.sh` (84 checks; 16 Lua mutants + 1 C,
+ all caught except the download-level hash check, equivalent: kept as
+ defence in depth). The `gpkg -u "%s%n"` format check moved there from
+ audit_alpine.sh. Full regression green.
+- Everest's libglacier-ng/glacier-ng 4.0.0 were REBUILT in place
+ 2026-10-07 with -f/-u/-y (user: keep 4.0.0, no bump) and the
+ initramfs remade; in QEMU `gpkg -f hi` as a wheel user now says
+ `[x] No remote repositories are configured.` (shipped repos.conf has
+ no url; no curl in base). `container.sh initramfs` now `mkdir -p`s
+ `$G/lib/glacier` (gone from the tree). Not done: no published repo;
+ TLS library still open. Next: step 5 (`glacier-bootstrap -r`),
+ then 6 (user repos).
+
+Step 3 DONE (2026-10-06, evening; REPO_DESIGN section 5 "As
+implemented"). The user chose: a refresh-only flag `-y`, the date-form
+SERIAL (YYYYMMDDNN), grepo in C.
+- libglacier-ng: `src/grepo.c` (`grepo -s KEY.sec -r REPO [-n SERIAL]
+ [-e DAYS] DIR`, Makefile target in `all`, installed to bin, built from
+ source like gsign); `pkg.c`: `gl_index_parse(text, len, repo, out)` /
+ `gl_index_free` (strict; takes the signed BYTES; refuses NUL and '@'),
+ `gl_conf_repos(path, out, len)` ("NAME\tURL\n" lines), repos.conf
+ validation shared in `read_repos_conf`; `transaction.c`:
+ `gl_scope_state_dir(ctx, "repos", out)` (creates/checks
+ `/repos`). `log.c` help lists `-y`.
+- glacier-ng: `M.refresh(opts)` in `gpkg_common.lua` (fetch file:// or
+ curl https, verify with a key listed for THAT repo, parse, rollback /
+ same-serial / expiry checks, rename into `/repos`, under the
+ scope lock); `syspkg -y` (needs root and `-n`; `-R` works), `gpkg -y`
+ (system repos.conf + keyring until step 6). Dockerfiles copy grepo to
+ /usr/local/bin. New suite `tests/repo_alpine.sh` (103 checks; 18
+ mutants, all caught). Full regression green. Everest's libglacier-ng
+ and glacier-ng packages rebuilt with it.
+- Next: step 4 (`-f` with dependency closure, then `-u`; the cache in
+ `/cache`, section 5's Fetch).
+
+Step 2 DONE (2026-10-06, second part of the session; REPO_DESIGN
+section 7 "As implemented" has the details):
+- libglacier-ng `src/pkg.c`: `gl_conf_repo_keys(path, repo, out, len)`,
+ a strict repos.conf reader; new statuses `GL_CONF_ERR_INVALID = 4`,
+ `GL_CONF_ERR_SPACE = 5`.
+- glacier-ng `lib/gpkg_common.lua`: `verify_all(paths, opts)` refuses a
+ base package in system scope without a trusted signature
+ (`signed_for`, `trusted_keys`, `untrusted`); `verify_and_copy` returns
+ the sidecar's bytes. glacier-bootstrap copies the sidecar's BYTES and
+ the `.sig`. `gbuild -k`/`--key`, `tests/mkpkg -k`.
+- Every `Dockerfile.*` installs gsign + gsha256sum in /usr/local/bin and
+ a TEST keyring (`glacier-test`, secret `/root/glacier-test.sec`).
+ `syspkg_alpine.sh` / `overwrite_alpine.sh` make their own key.
+- New suite `tests/keyring_alpine.sh` (68 checks; 17 mutants: 10 Lua/
+ shell, 7 C, all caught). Full regression green.
+- Everest: `./everest.sh keygen` made `everest-2026`; the SECRET key is
+ `~/.config/everest/keys/everest-2026.sec` (user's choice: outside the
+ project), mounted read-only for `build`, which refuses without it.
+ Every package is built `gbuild -k`. everest-bootscripts 1.2.0 ships
+ `etc/glacier/repos.conf` (base: everest-2026) and
+ `etc/glacier/keys/everest-2026.pub`; `container.sh initramfs`
+ installs the same into the build container's `/etc/glacier`.
+ The signed `build all` was cut off after busybox (laptop died); the
+ other 9 were rebuilt `-k` the evening of 2026-10-06, `initramfs` ran
+ through glacier-bootstrap (signature checks passed for all 11), and
+ Everest booted in QEMU: repos.conf + key in `/etc/glacier`, index
+ lists all 11, syspkg/gpkg run as root and as a fresh user.
+- Base KEEP_OLD backups can't be restored with `-lO` (no signature):
+ the user accepted that until the restore interface (REPO_DESIGN 10f).
+
+Step 1 DONE (2026-10-06):
+- `src/monocypher/`: Monocypher 4.0.3, vendored UNMODIFIED (provenance
+ and sha512 in its README; GitHub and monocypher.org copies matched).
+ Compiles clean under the project's `-Werror` flags.
+- `src/sign.{c,h}`: signify formats (strict parse, canonical base64),
+ `gl_sign_read_pubkey`/`_read_sig`/`_check_seckey`/`_verify`/
+ `_verify_file`/`_file`/`_generate`, statuses `GL_SIGN_OK`..
+ `GL_SIGN_ERR_WRITE = 9`. Built into `libglacier_verify.so` (NOT into
+ the combined `libglacier-ng.so`).
+- `src/gsign.c` (`-G`/`-S`/`-V`, lg_printf messages naming the file at
+ fault), Makefile targets `gsign` (in `all`, installed to bin like
+ gsha256sum).
+- `glacier-ng/tests/sign_alpine.sh` (alpine:latest, mounts /libsrc):
+ round trips, signify interop both ways (Alpine `signify`), encrypted
+ and corrupted secret keys, malformed key/sig files, 64 MiB limit,
+ O_NOFOLLOW, key modes, a direct FFI call of `gl_sign_generate`.
+ Passes in full. (`luajit -e CODE ARG` runs ARG as a script, so the
+ FFI snippet is written to `/gen.lua` first.)
+- Mutation checks: 12 mutants, all caught except m9 (padding position,
+ `j < 2`), which is EQUIVALENT: kept, it prevents `3 - pad`
+ underflowing. m5 (fchmod), m6 (final newline) and m10 (O_EXCL) needed
+ the umask-077, final-char and FFI checks respectively.
+- `glacier-ng/lib/glacier_cdef.lua`: `gl_sign_*` declarations and
+ `M.sign_name()`; checked against the real `.so` (struct sizes 40/72,
+ every call on signify-made files). Full regression green after it.
+
+Also open, for the user (raised 2026-10-06, not answered yet):
+- libgcc: luajit is built `-static-libgcc` (Everest has no libgcc_s).
+ Claude's call; the alternative is a GCC runtime package in base
+ (ROADMAP section 6).
+- Leftover `~/Projects/everest/pkgs/everest-bootscripts-1.0.0.gpkg`
+ (+ sidecar), superseded by 1.1.0. Not deleted: ask first.
+
+### Everest's glacier packages (1.4)
+
+- `~/Projects/everest/scripts/container.sh` builds zlib, libarchive
+ (gzip/tar only), libconfig, luajit (pinned commit,
+ `LDFLAGS=-static-libgcc`), libglacier-ng, glacier-ng (all base) and
+ doas (extra, OpenDoas, no PAM).
+- glacier-ng layout: `usr/lib/glacier-ng/{syspkg,gpkg,gstore,lib/*.lua}`,
+ `lib/glacier -> /usr/lib/glacier` (absolute: relative links resolve
+ inside the store), `usr/bin/X -> ../lib/glacier-ng/X`,
+ `etc/glacier/glacier.conf`.
+- everest-bootscripts 1.2.0 (1.1.0 before signing) ships `/etc/doas.conf` (0400): `permit
+ :wheel` plus nopass `gstore args -n` for BOTH `cmd gstore` and `cmd
+ /usr/bin/gstore` (OpenDoas matches `cmd` against what was typed).
+- `templates/minimal.template` and `PACKAGES` in everest.sh list all 12
+ packages (everest-repos added 2026-10-07). `Dockerfile.build` has autoconf, automake, libtool, cmake,
+ pkgconf.
## The three projects
@@ -31,11 +248,12 @@ when the user asks.
|---|---|---|
| `~/Projects/libglacier-ng` | C library, design docs, this file | yes (`dev`) |
| `~/Projects/glacier-ng` | Frontend: `gpkg`, `gstore`, `syspkg`, `gbuild`, `gworld`, `lib/*.lua`, `tests/`, Dockerfiles, `pkgs/` | **no**: back files up before editing (`file.old`; if that exists, `.old2`, `.old3`, ...) |
-| `~/Projects/everest` | The distro build: `everest.sh build\|initramfs\|boot`, packages, bootable QEMU image | no |
+| `~/Projects/everest` | The distro build: `everest.sh build\|initramfs\|boot`, packages, bootable QEMU image | **no**: same backup rule (but never inside `pkgsrc/*/etc/`, see Boot test) |
-`glacier-ng/lib/glacier/` holds `.so`s built on the host (glibc). They
-won't load on musl. Every Alpine image and test builds libglacier-ng from
-source instead.
+Nothing prebuilt is kept in either repo (the host-built `.so`s in
+`glacier-ng/lib/glacier/` were removed 2026-10-06). Every image and test
+builds libglacier-ng from source; the Dockerfiles create
+`/app/lib/glacier/` with the musl `.so`s.
## What this project is
@@ -56,10 +274,20 @@ These are preferences stated in sessions. Follow them.
- The pre-trust-check `die()` in syspkg/gstore reproduces the `[x]`
format byte for byte, because it can't load `libglacier_log.so`
before that file has been verified.
+- **Never say "Completed" after an error.** If the post-commit link pass
+ refused any path (`ctx->link_refusals`, counted only for packages
+ installed by that transaction), gpkg/syspkg end with
+ `[x] N paths were not linked.` (`1 path was not linked.`) and exit 1.
+ The transaction itself stays committed (2026-10-05).
- **No hint messages.** Don't print lines that point users to other flags
or commands (for example "use -lO to ..."). The user called these
clutter. A refusal states the fact, in one line. The user also removed a
`Run doas gstore -n ...` follow-up hint from gstore themselves.
+- **What goes where.** ALL design docs (and ROADMAP, this file) live in
+ libglacier-ng, frontend ones included. Other than docs, libglacier-ng
+ holds only backend material (C sources, the build). Frontend code,
+ tests, Dockerfiles and everything the bootstrap runs on live in
+ glacier-ng. Clarified 2026-10-06.
- **Ask before settling a design choice that is the user's to make**, and
give a recommendation. They have answered every such question promptly.
Don't silently pick a layout or semantics.
@@ -68,6 +296,10 @@ These are preferences stated in sessions. Follow them.
defence in a throwaway copy and confirm the test fails ("mutation
check"). Several defences here are verified that way.
- Use doas, not sudo. syspkg is never setuid.
+- **Help text is terse** (2026-10-07): one short phrase per flag, no
+ mechanism details (`-f` "Install packages from a remote repository",
+ `-u` "Update system or individual packages", `-O` "Allow Glacier to
+ overwrite existing files").
## Decisions the user has made (don't relitigate)
@@ -90,18 +322,105 @@ These are preferences stated in sessions. Follow them.
- "Backed up" means the old package's files are compressed into a
`.gpkg`. A restore interface comes later.
- **`gpkg -u` / `syspkg -u` upgrade from a REMOTE repository**, and
- `gpkg -f` is `-l` but fetched by name from a remote repository. Both wait
- for remote repos + signing (ROADMAP 3.1). Don't implement them against
- local files.
+ `gpkg -f` is `-l` but fetched by name from a remote repository. Done
+ 2026-10-07 (3.1 step 4). Never against local files.
+- **-f/-u details (2026-10-07):** first repo by priority wins (then its
+ highest version); a too-old installed dependency is upgraded in the
+ same transaction; cached downloads are deleted after commit.
+- **Dependencies (ROADMAP 2.3, decided 2026-10-05):** see the spec in
+ ROADMAP 2.3. In short: minimum versions, conflicts both ways by name,
+ no override flag, report every problem, missing deps refuse until 3.1
+ brings automatic closure, auto/explicit recorded in the index now.
+- **sysroot (IMAGE_DESIGN section 3) accepted as proposed**, plus the
+ library refuses a non-"/" sysroot for `GL_SCOPE_USR`. Order of work:
+ 2.3 first, then 1.1.
+- **System scope is world-readable** (dirs 0755, index files 0644): every
+ user must be able to run what syspkg installs (2026-10-05). User scope
+ stays 0700, and other users' programs stay unreachable to non-root
+ users. Root CAN run them by full path, but other users' links are
+ deliberately NOT on root's `PATH` (a user could get code run as root).
+- **Link refusals after a commit** end with `[x] N paths were not linked.`
+ and exit 1, never "Completed" (the user's wording rule: don't say
+ "completed" if there was an error).
+- **`syspkg -R DIR`** (not `-r`), `--root DIR`, `--root=DIR`; `-R`
+ IMPLIES `--strict`; `--strict` alone is opt-in on the host.
+- **File collisions (2.6) are always refused**, every scope, strict or
+ not (folded into 1.2 at the user's go-ahead).
+- **PATH:** per-user links, then `/glacier/sys/links/bin`, then the system
+ (`/usr/bin` on Everest), in the Docker images AND Everest's
+ `/etc/profile`. Non-base system packages must be on `PATH`; base
+ programs must be runnable by every user.
- `everest-bootscripts` is the name of the base `/etc` package. Its source
lives in `~/Projects/everest/pkgsrc/`.
+- **glacier-bootstrap (2026-10-06):** template = `NAME=` + `PKG=name ver
+ [sha256:hex]` lines, no `SYS_PROFILE` (unknown key until ROADMAP 3.3);
+ it runs the `syspkg` on PATH (doas unless root); a template dependency
+ must be listed at that version OR LATER (the 2.3 rule); Everest's
+ initramfs is built through it.
+- **Installing Everest (2026-10-07):** possible from a live Everest
+ system OR any Linux host; the LIVE SYSTEM is the preferred, officially
+ supported way (so base/the live medium must eventually carry what a
+ manual install needs). The user does the first QEMU disk install
+ themselves: don't start it.
+- **Everest supports glibc as well as musl** (2026-10-07), Gentoo/Void
+ style: the user picks the libc at install time, one per system.
+ Today everything is built for musl (Alpine); glibc needs ROADMAP
+ 4.1's profiles. How packages are kept apart is still open (ROADMAP 6).
+- **Signing (2026-10-06, REPO_DESIGN 10e/10f):** Everest's SECRET key
+ stays outside the project (`~/.config/everest/keys`, mounted
+ read-only); a base KEEP_OLD backup refusing under `-lO` (unsigned) is
+ accepted until the restore interface.
## Core architecture
-**`gl_context_t`** (transaction.h) is the central abstraction. Fields:
-`uid`, `root_path`, `index_path`, `store_path`, `links_path`, `mode`
-(`GL_CTX_LIVE`/`GL_CTX_STAGE`), `scope` (`GL_SCOPE_USR`/`GL_SCOPE_SYS`),
-`lock_fd`, and `overwrite` (last field, zeroed by the init functions).
+**`gl_context_t`** (transaction.h) is the central abstraction. Fields, in
+order: `uid`, `root_path`, `index_path`, `store_path`, `links_path`,
+`mode` (`GL_CTX_LIVE`/`GL_CTX_STAGE`), `scope`
+(`GL_SCOPE_USR`/`GL_SCOPE_SYS`), `lock_fd`, then:
+
+| Field | Set by | Meaning |
+|---|---|---|
+| `overwrite` | caller (after init, which zeroes it) | `-O`: the next link pass may displace foreign files |
+| `sysroot` | `*_at` init | install root, `""` = the running system |
+| `link_refusals` | link pass; commit copies the relink's count back | paths refused for this transaction's packages |
+| `strict` | caller (after init, which zeroes it) | refuse those before committing instead |
+
+New fields go at the END, and `lib/glacier_cdef.lua` must mirror the
+struct exactly (FFI has no compiler to catch a mismatch).
+
+**Status codes worth knowing:** `GL_TXN_ERR_DEPS = 9` covers every
+pre-commit refusal (dependency, conflict, file collision, strict link
+refusal); the library has already printed why, so the CLIs print nothing
+more. Live-context equivalents: `GL_INSTALL_ERR_DEPS = 8`,
+`GL_REMOVE_ERR_DEPS = 4`. `GL_INSTALL_ERR_SCOPE = 7` is base-in-user-
+scope.
+
+**Install root / sysroot** (ROADMAP 1.1, done 2026-10-05):
+- `gl_init_live_context_at` / `gl_init_stage_context_at` / `gl_init_sys_at`
+ take a sysroot; the old functions wrap them with `"/"`.
+ `gl_normalize_sysroot` (common.h) refuses relative paths and `.`/`..`
+ components. **User scope refuses any sysroot** other than `/`.
+- The context's paths are **INSTALL paths** (`T/glacier/sys/store`), so
+ every filesystem operation uses them unchanged: a forgotten translation
+ writes into T, never the host. That's deliberately the reverse of
+ IMAGE_DESIGN's wording (runtime paths stored, `gl_at` on every
+ operation), with the same effect and a safer failure mode.
+- Translate ONLY where a path's runtime meaning matters:
+ `gl_runtime_path(ctx, p)` for link targets, `is_glacier_symlink`
+ prefixes and `.displaced` keys; `gl_at(ctx, runtime, out)` to check a
+ link's target (prune). `/usr` and `/etc` destinations are
+ `ctx->sysroot` + path.
+- **Trust check** (`gl_check_sysroot` in transaction.c, also inside
+ every `*_at` init and `gl_init_sys_at`; helper `gl_sysroot_trusted` in
+ common.h): the root and every ancestor must exist and be root-owned,
+ non-symlink directories, not group/world-writable. So `/tmp/x` is
+ refused. Messages: `Refusing install root R: P is a symlink.` / `is
+ missing.` / `is not root-owned, or is group/world-writable.`
+- Test: `tests/sysroot_alpine.sh` (host `/usr`, `/glacier`, `/etc`
+ byte-identical; `chroot T` runs a linked binary; 8 mutations caught).
+- Not translated, on purpose: `KEEP_OLD` is read from the HOST's
+ `/etc/glacier/glacier.conf` even under `-R` (the build host's policy);
+ user scope's dependency view of "the system" is always the host's.
**Layout.** Each scope owns ONE base directory holding
`{index,store,links,stage,old,lock,backup}`. Every create, rename and lock
@@ -110,7 +429,7 @@ a transaction does stays inside it.
| Scope | Base dir | Ownership |
|---|---|---|
| `GL_SCOPE_USR` | `/glacier/usr//` | uid, mode 0700, inside a root-owned 0755 `/glacier/usr` |
-| `GL_SCOPE_SYS` | `/glacier/sys/` | root |
+| `GL_SCOPE_SYS` | `/glacier/sys/` | root, world-readable: dirs 0755, index files 0644 (`gl_scope_dir_mode`/`gl_scope_file_mode` in transaction.h), whatever the umask |
- The helpers `gl_usr_base`, `gl_usr_base_check` and the layout comment
are in `common.h`.
@@ -126,11 +445,22 @@ a transaction does stays inside it.
live package inside a transaction needs no special casing. Seeding
copies dotfiles: only `.` and `..` are skipped.
- A stage on another filesystem fails at seeding with `GL_TXN_ERR_XDEV`.
-- **Commit:** rename-based promotion (stage → live, previous live → `old/`),
- then fsync the base. Then `gl_relink_store` + `gl_rebuild_index`
- regenerate links and index from a *fresh live context* and rescan the
- WHOLE store (every commit relinks every package, so link-time logic must
- be idempotent). `old/` is then deleted.
+- **Commit:** first `gl_check_txn` (deps, conflicts, collisions, and
+ strict link refusals if `ctx->strict`); any problem → nothing changes,
+ `GL_TXN_ERR_DEPS`. Then rename-based promotion (stage → live, previous
+ live → `old/`), then fsync the base. Then `gl_relink_store` regenerates
+ links from a *fresh live context* and rescans the WHOLE store (every
+ commit relinks every package, so link-time logic must be idempotent);
+ its `link_refusals` is copied back into the caller's context. `old/` is
+ then deleted. (The index is rebuilt inside the stage by each
+ install/remove, and promoted with it.)
+- **Live contexts** (no transaction; tests and third-party callers):
+ `gl_install_pkg` runs `gl_check_plan` itself, and `gl_remove_pkg`
+ removes the package's links by pruning (`gl_relink_store`), like a
+ commit.
+- **Index files** are named to the second and seeding hardlinks them:
+ never open a seeded file for writing, always write elsewhere and
+ `rename()` (bug 25).
**Link routing** (per package, in `gl_link_pkg`):
@@ -157,7 +487,8 @@ transactions):
| `/.glacier-manifest` | the package's manifest, saved at install, used by `gl_backup_pkg` |
| `/.glacier-fresh` | one-shot "installed in this transaction"; link conflicts are only reported when present |
| `/.glacier-overwrite` | one-shot `-O` permission for this package's next link pass |
-| `/.displaced/` | originals displaced by `-O`, restored when the link that replaced them is pruned |
+| `/.glacier-auto` | installed as a dependency, not by name (index field 4 = `auto`); set by `gl_set_pkg_auto`, nothing sets it until 3.1 |
+| `/.displaced/` | originals displaced by `-O`, restored when the link that replaced them is pruned; keyed by RUNTIME path (no sysroot prefix) |
| `/.etc-state/` | the last version of each `/etc` file a package offered |
**Configuration.** `/etc/glacier/glacier.conf` (libconfig syntax).
@@ -189,8 +520,20 @@ possibly shared:
plus `O_NOFOLLOW`).
- Any entry with `..` or an absolute path refuses the whole package.
- A failed extraction removes its half-made version directory.
-- **Recursive deletes.** `gl_rmdir_recursive` skips only `.`/`..`. Every
- caller passes a glacier-only path; keep it that way. `gl_delete_user`
+- **Recursive deletes.** `gl_rmdir_recursive` skips only `.`/`..`, and
+ walks by descriptor (`openat(O_NOFOLLOW)`, `unlinkat`): an entry
+ swapped for a symlink is unlinked, never followed (bug 31). Every
+ caller passes a glacier-only path; keep it that way.
+- **Root reading a user's tree** (`add_entry_at`, `gstore -b`) is
+ fd-relative with `O_NOFOLLOW` too (bug 32).
+- **Reserved names.** A package's top-level `files/.glacier-*` entries
+ are refused (`GL_MARKER_PREFIX`, common.h): those names are glacier's
+ own version-dir files, and every marker write uses `O_NOFOLLOW`
+ (bug 30). The marker names are defined ONCE, in common.h.
+- **Manifests are untrusted**: `gl_gpm2gpkg` refuses `@include` and
+ dependency/conflict entries without a valid name (bug 33). The CLIs
+ read manifests ONLY through the library (`manifest_of` in
+ gpkg_common.lua), never by pattern matching (bug 34). `gl_delete_user`
lstat-checks its target first, because a symlinked base would otherwise
have its *target* emptied.
- **Wide wrappers.** Don't add convenience wrappers that silently touch a
@@ -225,7 +568,8 @@ possibly shared:
**Version replacement** (`-lO`, `M.install` in `gpkg_common.lua`):
1. Installed entries are read from the live index's `current` file
- (`repo::name::version` lines).
+ (`repo::name::version::auto|explicit` lines; files before 2026-10-05
+ stop at the version, and every parser accepts both).
2. A same name+repo with a different version is a replacement. Without
`-O` it's refused; with `-O`, `keep_old()` reads `KEEP_OLD`, and the
summary shows `old -> new`.
@@ -260,7 +604,70 @@ the merged-/usr tree: `/bin` and `/sbin` → `usr/bin`, `/lib` → `usr/lib`,
- `gl_init_sys` runs it before writing the index. The index marks `-n` as
complete, so a second `syspkg -n` is refused: `Cannot create
/glacier/sys: already exists`.
-- It takes a root prefix, ready for `syspkg --root`.
+- It takes a root prefix; `gl_init_sys_at` / `syspkg -R DIR -n` use it.
+
+**Dependencies and conflicts** (ROADMAP 2.3; `src/deps.c`, built into
+`libglacier_istoreutils.so`; rules in `deps.h`):
+- `gl_check_plan(live, adds, n, "repo::name" removes, n)` runs before the
+ prompt (`check_plan` in `gpkg_common.lua`). `gl_check_txn(stage)`
+ compares stage with live, and `gl_commit_transaction` calls it itself
+ (`GL_TXN_ERR_DEPS = 9`). A live context runs the plan check inside
+ `gl_install_pkg`/`gl_remove_pkg` (`GL_INSTALL_ERR_DEPS = 8`,
+ `GL_REMOVE_ERR_DEPS = 4`). So no library caller can skip it.
+- It reads each version dir's `.glacier-manifest`; a version without one
+ has no deps. Only what the transaction changes is checked.
+- `-x` of a name installed in several repos is only checked at commit
+ (its repo is chosen after the prompt).
+- Claude's own calls, not reviewed by the user: conflicts only apply
+ within one scope; only what a transaction changes is checked.
+
+**Strict link check** (`check_links` in deps.c; `ctx->strict`): system
+scope only, for this transaction's packages, mirrors `gl_link_pkg`: a
+foreign file/symlink at a file's destination (unless `-O` and no earlier
+`.displaced` copy), a non-directory where a directory goes, or a
+directory over another package's symlink → `[x] Refusing to overwrite
+non-glacier path: P` / `Refusing to merge directory ...`. A foreign
+symlink where a directory goes (`usr/sbin -> bin`) is followed, as the
+link pass does.
+
+**File collisions** (ROADMAP 2.6, `check_collisions` in deps.c): two
+packages whose entries land on the same path (by `dest_of`, which mirrors
+`gl_link_pkg`) are refused in every scope, before the prompt and at
+commit, when either is in the transaction: `[x] extra::a and extra::b
+both ship /glacier/sys/links/bin/x.` Sharing a directory is fine; a file
+or symlink where the other has a directory is a collision.
+
+**Signatures** (REPO_DESIGN section 7; `gpkg_common.lua`): in system
+scope, a package whose LIBRARY-read manifest (`gl_read_pkg_manifest`,
+never `read_manifest_fields`) says base needs `FILE.sha256.sig` over the
+exact sidecar bytes verified, by the key with that keynum among those
+`/etc/glacier/repos.conf` lists for base (`gl_conf_repo_keys`). Else
+`[x] base::NAME is not signed by a key trusted for base.` before the
+prompt. repos.conf and keys (and all ancestors, real paths too) must be
+root-owned, not group/world-writable. Under `-R`, the HOST's keyring
+for this check; `-y`/`-f`/`-u` under `-R` use T's (inside T only).
+No repos.conf = nothing trusted. gpkg / other repos: sidecar only.
+
+**Repository indexes** (REPO_DESIGN sections 4-5; `M.refresh` in
+`gpkg_common.lua`): `syspkg -y` / `gpkg -y` fetch `URL/repo.idx` + `.sig`
+for every repos.conf entry with a url, check the signature over the
+fetched bytes with a key listed for that repo, `gl_index_parse` the
+same bytes, refuse SERIAL below the accepted one (or equal with other
+bytes) and an expired VALID_UNTIL, then rename into
+`/repos/REPO.idx` (+ `.sig`). Any failure keeps the accepted
+copy; every repo is tried; exit 1 if any failed.
+
+**`-f` / `-u`** (REPO_DESIGN section 6; `M.fetch`/`M.upgrade`):
+refresh first (any failure refuses), pick each name from the first
+repo serving it, add missing deps (auto) and upgrade too-old ones,
+download into `/cache/REPO` (hash and SIZE from the index), copy
+privately with the index's hash, require the manifest to equal the
+index entry (`[x] extra::x 1.0.0 does not match its index.`), then the
+`-l` path (`install_copies`): summary with `(dependency)`, 2.3 checks,
+prompt, staged transaction, `gl_set_pkg_auto`, backups, cache files
+deleted after commit. A base package fetched this way needs no `.sig`
+(the base index vouches). `-f` of an installed other version needs
+`-O`; `-u` and dependency upgrades don't. gpkg never fetches from base.
**Package symlinks** are extracted verbatim and linked like files.
Hardlink, device and fifo entries are skipped. `add_to_archive` (used by
@@ -270,14 +677,28 @@ backups and `gstore -b`) archives symlinks.
| Tool | Flags | Runs as |
|---|---|---|
-| `gpkg` | `-l FILE...` `-x NAME...` `-O` `-S` `-V` `-h` `-v`; `-f`/`-u` are stubs | the user; per-user scope only, NEVER constructs `GL_SCOPE_SYS` |
-| `syspkg` | `-n` `-l FILE...` `-x NAME...` `-O` `-S` `-V` `-h` `-v` | root via doas; `-h`/`-v` without root |
-| `gstore` | `-n [UID]` `-b UID OUTFILE` `-x UID` `-s` (stub) `-V` `-h` `-v` | `-n` needs root (direct, or `doas gstore -n` for yourself) |
+| `gpkg` | `-l FILE...` `-f NAME...` `-u [NAME...]` `-x NAME...` `-y` `-O` `-S` `-V` `-h` `-v` | the user; per-user scope only, NEVER constructs `GL_SCOPE_SYS` |
+| `syspkg` | `-n` `-l FILE...` `-f NAME...` `-u [NAME...]` `-x NAME...` `-y` `-O` `-S` `-V` `-R DIR` `--strict` `-h` `-v` | root via doas; `-h`/`-v` without root |
+| `gstore` | `-n [UID]` `-b UID OUTFILE` `-x UID` `-s` (refuses: not implemented) `-V` `-h` `-v` | `-n` needs root (direct, or `doas gstore -n` for yourself) |
+| `glacier-bootstrap` | `-d PKGDIR TEMPLATE TARGET` `-n` `-t` `-V` `-h` `-v` | the user; borrows root only for `doas syspkg` / `doas chroot` |
- **gpkg:** modifiers `-O`/`-S`/`-V` are applied in a pre-pass, so their
position doesn't matter (`-lO` == `-Ol`). It finds its user via
- `/proc/self/loginuid`, falling back to `getuid()`.
+ `/proc/self/loginuid`, falling back to `getuid()`. Unknown flags are
+ refused (`[x] Unknown option: -R`); gpkg has no `-R`/`--strict`.
+- **Endings (both CLIs):** `[✓] Completed with no errors.` (exit 0), or
+ after link refusals `[x] N paths were not linked.` (exit 1;
+ `report_done` in gpkg_common.lua). Refusals before the prompt exit 1
+ with only the `[x]` lines.
- **syspkg, `-n`:** creates the tree plus `/glacier/sys`.
+- **syspkg, `-R DIR` / `--root DIR` / `--root=DIR`** (last in a bundle:
+ `-lR DIR`): operate on the system under DIR, through the `*_at`
+ contexts. Refused first by `gl_check_sysroot`: DIR and every ancestor
+ must be root-owned, non-symlink directories, not group/world-writable
+ (`/tmp/...` is refused). Implies `--strict`.
+- **syspkg, `--strict`:** anything the link pass would refuse fails the
+ whole transaction before the prompt (and at commit, `ctx->strict`);
+ `-O` still displaces.
- **syspkg as non-root:** `-n`/`-l`/`-x` are refused after argument
parsing with `Cannot open system index: Permission denied`.
- **gstore, `-n` without a UID, under doas:** acts for the invoking user
@@ -290,11 +711,25 @@ backups and `gstore -b`) archives symlinks.
installed fails before the prompt.
- **Hardened loading** (syspkg and gstore): `#!/usr/bin/luajit -E`,
`package.path` and every `ffi.load` pinned to their own `realpath`, and
- when euid is 0, every ancestor directory and every loaded `.lua`/`.so`
- must be root-owned and not group/world-writable. The preamble is
+ when euid is 0, every loaded `.lua`/`.so`, and every ancestor of the
+ script's realpath AND of each loaded file's realpath, must be
+ root-owned and not group/world-writable (bug 29). The preamble is
DUPLICATED in both files on purpose: change one, change both.
-- **gpkg** uses the older CWD-relative convention (`require("lib/...")`,
- `ffi.load("./lib/glacier/...")`): run it from its own directory.
+- **gpkg** (2026-10-06) loads `lib/` from its own realpath, like
+ glacier-bootstrap (no ownership checks; it's unprivileged), so an
+ installed `/usr/bin/gpkg` works from any directory.
+- **glacier-bootstrap** (GLACIER_BOOTSTRAP section 10): parse →
+ verify (copies each `PKGDIR/NAME-VER.gpkg` into a private `mktemp -d`
+ while hashing; sidecar + pin; writes the copy's sidecar from that hash)
+ → resolve (reads manifests of the COPIES via `gl_read_pkg_manifest`:
+ name/version match, deps listed at >= version, conflicts) → `syspkg -R
+ T -n` (skipped if T is initialised with nothing installed) → `syspkg -R
+ T -l COPIES` (one strict transaction; its prompt passes through) →
+ checks T's index lists every package (syspkg exits 0 when declined) →
+ `syspkg -R T -y` if T's repos.conf has a url → `-t` smoke test.
+ Never writes into T itself. Exit: 1 usage/target, 2 template,
+ 3 verify, 4 resolve, 5 install, 6 smoke, 7 repositories. Loads its `lib/`
+ from its realpath, no root checks. No `-r` / `--pin` yet.
- **Declarations:** `lib/glacier_cdef.lua` is the single source of truth
for every `ffi.cdef`. `tests/glacier_ffi.lua` is only a wrapper around
it. Declaration ORDER matters: a declaration using `gl_context_t` must
@@ -303,7 +738,9 @@ backups and `gstore -b`) archives symlinks.
## Build tooling
- **gbuild** (bash, the only copy is `glacier-ng/gbuild`):
- - `-n -V -g -c -d -m -D -C -b -i -o -p -j -F -t -x -u -r -s`;
+ - `-n -V -g -c -d -m -D -C -b -i -o -p -j -F -t -x -u -r -k -s`
+ (`-k KEY.sec`: sign the sidecar with gsign, `$GSIGN_BIN` / build/bin
+ / PATH, key tried before building);
`-v`/`--version` prints the version, `--verbose` turns on verbose
output.
- Autodetects the build system, with autotools checked BEFORE a bare
@@ -320,15 +757,29 @@ backups and `gstore -b`) archives symlinks.
(`Dockerfile.build`).
- **Kernel version:** linux `6.12.112`; plain `v6.12` doesn't build with
GCC 15.
+- **Everest's image IS built through glacier** (2026-10-06): `container.sh
+ initramfs` copies glacier-ng to a root-owned `/opt/glacier-ng` (with
+ the musl-built `.so`s; syspkg's trust checks need that) and runs
+ `glacier-bootstrap -t -d /everest/pkgs templates/minimal.template
+ /target` (root-owned: not under `/tmp`). T's links name RUNTIME paths,
+ so anything reading through them from the build host must resolve
+ inside T (vmlinuz is read with `chroot T cat`). Only `root` exists in
+ the shipped `/etc/passwd`.
+- gbuild ends with the package's contents list. Its "To install with
+ gpkg: gpkg -l ..." hint was removed 2026-10-06 at the user's request.
## libglacier-ng build
- **Separate `.so` per module** (`libglacier_log`, `_pkg`, `_istoreutils`,
- `_transaction`, `_dag`, `_verify`). This split is deliberate: a
+ `_transaction`, `_verify`; `_dag` was removed 2026-10-06, unused). This split is deliberate: a
third-party program links only what it needs.
- **The combined `libglacier-ng.so`** (`make libglacier-ng`) exists only
for the FFI tests.
- **`libglacier_verify.so`** is private: no public header.
+- **Tools** in `build/bin`: `gsha256sum`, `gsign`, `grepo` (gsign and
+ grepo compile the sources they need in, no rpath). Maintainer tools,
+ later "glacier-tools": Everest's libglacier-ng package drops
+ `usr/bin`.
- **Warnings:** `config.mk` builds with `-Werror`. `-Wno-format-truncation`
is set because that warning is noise; `-Wstringop-truncation` stays
fatal.
@@ -340,7 +791,7 @@ backups and `gstore -b`) archives symlinks.
Run everything in throwaway containers. Several suites write to `/usr`,
`/etc` and `/glacier`.
-**FFI suite** (`glacier-ng/tests/*.lua`; 8 files, all passing):
+**FFI suite** (`glacier-ng/tests/*.lua`; 11 files, all passing):
```sh
docker run --rm --cap-add SYS_ADMIN -v ~/Projects/glacier-ng:/src:ro \
-v ~/Projects/libglacier-ng:/libsrc:ro alpine:latest sh -c '
@@ -355,8 +806,9 @@ GNU `tar` is required: the hostile-archive fixtures use `tar --append`.
| Suite | Image |
|---|---|
+| `sign_alpine.sh` | `alpine:latest` with `/libsrc` and `/t.sh` mounted |
| `syspkg_alpine.sh`, `overwrite_alpine.sh` | `alpine:latest` with `/src`, `/libsrc` and `/t.sh` mounted (see each header); `overwrite_alpine.sh` runs both as-is and with `--tmpfs /glacier:exec` |
-| `verify_alpine.sh`, `user_layout_alpine.sh`, `upgrade_alpine.sh` | `glacier-alpine_root` |
+| `verify_alpine.sh`, `user_layout_alpine.sh`, `upgrade_alpine.sh`, `sys_perms_alpine.sh`, `deps_alpine.sh`, `sysroot_alpine.sh`, `root_alpine.sh`, `bootstrap_alpine.sh`, `installed_alpine.sh`, `keyring_alpine.sh`, `repo_alpine.sh`, `audit_alpine.sh`, `fetch_alpine.sh` | `glacier-alpine_root` |
Build the `glacier-alpine_root` image with:
```sh
@@ -365,6 +817,46 @@ cd ~/Projects/glacier-ng && docker build -f Dockerfile.alpine_root \
docker run --rm -v "$PWD/tests/NAME.sh:/t.sh:ro" glacier-alpine_root sh /t.sh
```
+`glacier-ng/lib/glacier/` does not exist in the tree (the `.so`s are
+built per image); syspkg_alpine/overwrite_alpine `mkdir -p` it
+(2026-10-07). If Docker fails with `veth ... operation not supported`,
+the host kernel was upgraded without a reboot (modules missing): use
+`docker build --network host` and `docker run --network=host` (or
+`--network none` where nothing is fetched) until the user reboots.
+Mutating a Lua file syspkg loads: mount it elsewhere and `cp` it over
+`/app/lib/...` inside the container (a bind mount is user-owned, so
+syspkg's trust check refuses it).
+
+**Demo packages:** `glacier-ng/tests/mkpkg [-o DIR] [-k KEY.sec]
+[-d NAME:VER]... [-c NAME:VER]... NAME VERSION REPO` writes a one-script
+`.gpkg` plus its sidecar (`/app/tests/mkpkg` in the images); a base one
+needs `-k /root/glacier-test.sec` (the images' test key) for syspkg. The user plays with changes
+in `./run_docker.sh Dockerfile.alpine_root` using it; for an image,
+`mkdir -p /mnt/img && ./syspkg -R /mnt/img -n`.
+
+**Mutation checks, how:** `cp -a` libglacier-ng (and glacier-ng, for Lua
+changes) into the scratchpad, break the defence there, then
+`docker build -f Dockerfile.alpine_root --build-context libglacier=COPY
+-t glacier-mut .` from the (copied) glacier-ng and run the suite against
+`glacier-mut`; delete the image after. Docker tags must be lowercase. A
+mutation that doesn't compile under `-Werror` proves nothing: rewrite it
+so it compiles. Watch for checks that pass for the wrong reason (e.g. a
+refusal caused by a missing directory instead of the defence). In shell
+suites, `sh -c '... $OUT ...'` sees an EMPTY `$OUT` (not exported), so a
+negative check written that way passes vacuously; use a function
+(`lacks` in bootstrap_alpine.sh). Found 2026-10-06 by a surviving
+mutation. The host shell is zsh: an unquoted `$VAR` holding `-v a:b`
+is NOT word-split, so a docker command built that way silently runs
+without the mount (a mutant "survived" that way, 2026-10-06). To
+mutate a Lua CLI without rebuilding, bind-mount the mutated file over
+`/app/NAME` (`-v $PWD/mut:/app/glacier-bootstrap:ro`).
+
+**Full regression** = the FFI suite + `syspkg_alpine.sh` +
+`overwrite_alpine.sh` (both ways) + `sign_alpine.sh` + the thirteen `glacier-alpine_root`
+suites, all in parallel; the two output-based suites (`syspkg_alpine`,
+`user_layout_alpine`) have no PASS lines, so grep their output for
+`Unknown|traceback|failed|GL_` instead (only `--bogus` is expected).
+
**Interactive images:** `./run_docker.sh Dockerfile.{alpine,alpine_root,arch,debian}`.
`run_docker.sh` passes the `libglacier` build context. `alpine`, `arch`
and `debian` default to user `glacier` (uid 1000, in wheel); `alpine_root`
@@ -372,10 +864,24 @@ defaults to root. All of them have:
- root-owned `/app`;
- no `/glacier` until `gstore -n` / `syspkg -n`;
- the doas rule plus `permit nopass glacier`;
+- `/etc/profile.d/glacier.sh` putting `/glacier/usr//links/bin` then
+ `/glacier/sys/links/bin` on `PATH` (and `lib` on `LD_LIBRARY_PATH`)
+ unconditionally, since `/glacier` only appears after login (fixed
+ 2026-10-05: it had no system links and checked `-d` at login);
- `glacier.conf` installed.
**Boot test:** `cd ~/Projects/everest && ./everest.sh boot` boots QEMU
-(KVM). Press Enter for a root shell; `poweroff -f` to leave.
+(KVM). Press Enter for a root shell; `poweroff -f` to leave. To test as
+a non-root user, `adduser -D -H -h /tmp tester` at runtime, then
+`su - tester -c ...` (nothing shipped has a user besides root).
+Everest's `/etc/profile` sets `PATH` to per-user links, then
+`/glacier/sys/links/bin`, then `/usr/bin` (2026-10-05). Backups of
+everest-bootscripts files go NEXT TO `etc/`, never inside it: the package
+copies `etc/` wholesale. After changing a package:
+`./everest.sh build PKG && ./everest.sh initramfs`, then boot.
+To drive the boot non-interactively, fork QEMU on a pty from Python
+(`pty.fork()`), wait for `Please press Enter`, send lines and match the
+`# `/`$ ` prompt (no `expect` on the host).
## Bug history: so these don't get reintroduced
@@ -430,6 +936,99 @@ All fixed in the current tree.
context every retry reported `ALREADY_INSTALLED`.
23. Backups written in the same second were pruned in the wrong order
(whole-second mtimes).
+24. System scope was root 0700 (index, store, links, every version dir),
+ so no non-root user could run anything syspkg installed, through
+ `/usr` or `/glacier/sys/links`. Found 2026-10-05. Also fixed: every
+ mode glacier creates was filtered by the caller's umask (`gl_mkdirp`,
+ seeding, extracted files), so syspkg under umask 077 made even a
+ 0755 store private. `gl_mkdirp`/`gl_mkdir_exact` now chmod what they
+ create, and extracted entries get `archive mode & 07755` explicitly
+ (never group/world-writable). Test: `tests/sys_perms_alpine.sh`
+ (mutation-checked). No migration: a store made before the fix keeps
+ its 0700 dirs until reinstalled.
+25. `gl_rebuild_index` `fopen(name, "w")`'d the new index file. Index
+ names have one-second resolution and seeding HARDLINKS live's index
+ files into the stage, so a staged install in the same second as the
+ last commit rewrote the LIVE index in place; an aborted transaction
+ then left live listing the stage's packages (and history files were
+ overwritten). It now writes to `tmpfile()` and only `rename()`s onto
+ the name. Found 2026-10-05 by `deps_alpine.sh`; regression test
+ `tests/test_index_abort.lua` (mutation-checked). Rule: never open a
+ seeded file for writing; replace it by rename.
+26. Live-context removal (`gl_unlink_pkg`) `unlink()`ed the destination
+ of every regular file in the package WITHOUT `is_glacier_symlink`:
+ a foreign file the package had been refused to replace was deleted,
+ and the package's own symlink entries were never unlinked. The CLIs
+ always stage, so only library callers were exposed. Removed;
+ live removal now prunes like a commit (`gl_relink_store`). Found
+ 2026-10-05; `tests/test_live_remove_foreign.lua` (fails on the old
+ code).
+27. gpkg's `handle_flag` had no `else`: an unknown flag (`-R`) was
+ silently ignored and the NEXT word reported as the unknown option.
+ Now `[x] Unknown option: -R` (capitalized, as syspkg). 2026-10-05.
+28. gbuild's `clone_source` printed its "Shallow clone failed" warning
+ (and the full clone's/checkout's output) to stdout, which
+ `SOURCE_DIR="$(clone_source)"` captured, so building from a commit
+ hash (`-r `) always failed. Those now go to stderr. Found
+ 2026-10-06 building luajit.
+29. syspkg's and gstore's trust checks walked only the ancestors of the
+ script's own realpath. Installed, `lib/glacier -> /usr/lib/glacier`
+ and the `.so`s link into libglacier-ng's store dir, so a non-root-
+ owned directory there was never checked: root loaded `.so`s from it.
+ Both now also check every ancestor of each loaded file's realpath.
+ Found 2026-10-06 by `tests/installed_alpine.sh` (both mutations
+ caught).
+
+30. A package could ship top-level `files/.glacier-*` entries. As a
+ symlink, `.glacier-fresh` made syspkg truncate its target as root
+ (an unsigned extra package emptied `/root/victim`, reproduced);
+ `.glacier-overwrite` self-granted `-O`. Reserved names refused,
+ marker writes `O_NOFOLLOW`. 2026-10-06 audit.
+31. `gl_rmdir_recursive` (lstat, then opendir by path) could be steered
+ into a symlink's target when root deleted a user's tree (`gstore
+ -x`). Now fd-relative. Race test in audit_alpine.sh.
+32. `add_to_archive` (same class, `gstore -b`). Now fd-relative; no test
+ fails on the old code (window too narrow; review only).
+33. Manifests could `@include` any file (read as root); a dependency
+ entry without a name cut the list short. Both refused.
+34. gpkg_common's `read_manifest_fields` (tar + pattern match) could
+ disagree with the library: a decoy comment hid a replacement. Now
+ `manifest_of` (`gl_read_pkg_manifest`).
+35. Format strings: gstore's `argv[0]`/UID and gpkg `-u`'s operand
+ reached `lg_printf` as the format; `%d` was given Lua numbers;
+ gstore UIDs wrapped past uid_t; `gl_abort_transaction` wiped a stage
+ without holding the lock. All fixed (`lg_printf` now has a printf
+ format attribute).
+
+## Known gaps (not bugs, not yet scheduled)
+
+- No migration for stores made before bug 24's fix (0700 dirs stay).
+- Extracted entries keep setuid/setgid bits (`& 07755`), as before.
+- No size limit on extraction (a decompression bomb fills the disk).
+- Bug 32's fix has no test that fails on the old code.
+- Hash verification is still CLI-only (ROADMAP 3.2); everything else
+ added 2026-10-05 is enforced in the library.
+- A dependency refusal for `-x` of a name installed in several repos
+ comes at commit, after the prompt (its repo is chosen after it).
+
+## Backups of glacier-ng / everest files (no git there)
+
+The rule stands (back up before editing: `file.old`, then `.old2`...).
+All of glacier-ng's backups were deleted 2026-10-06 at the user's
+request. The 2026-10-07 session made new ones: `lib/glacier_cdef.lua.old`,
+`lib/gpkg_common.lua.old`, `gpkg.old`, `syspkg.old`,
+`tests/{audit,overwrite,syspkg,repo}_alpine.sh.old`,
+`lib/gpkg_common.lua.old2`/`.old3`, `tests/repo_alpine.sh.old2`/`.old3`,
+`tests/fetch_alpine.sh.old`, `lib/gpkg_common.lua.old4`,
+`glacier-bootstrap.old`, `tests/bootstrap_alpine.sh.old`,
+`tests/repo_alpine.sh.old4`, `tests/fetch_alpine.sh.old2`; Everest:
+`scripts/container.sh.old4`, `everest.sh.old3`, `README.md.old3`,
+`templates/minimal.template.old2`, `pkgsrc/everest-bootscripts/glacier.old`.
+Everest still has its earlier ones (`everest.sh.old`/`.old2`,
+`scripts/container.sh.old`/`.old2`/`.old3`, `README.md.old`/`.old2`,
+`Dockerfile.build.old`, `templates/minimal.template.old`,
+`pkgsrc/everest-bootscripts/profile.old`). Leave them unless the user
+says to clean up.
## Conventions
diff --git a/GLACIER_BOOTSTRAP.txt b/GLACIER_BOOTSTRAP.txt
index e179c62..428fd76 100644
--- a/GLACIER_BOOTSTRAP.txt
+++ b/GLACIER_BOOTSTRAP.txt
@@ -2,9 +2,10 @@
| glacier-bootstrap |
+--------------------------------------------+
-STATUS: PLAN. Nothing here is implemented. Section 9 lists the
-decisions still open; everything else is a recommendation to argue
-with, not a settled design.
+STATUS: LOCAL MODE IMPLEMENTED 2026-10-06 (glacier-ng/glacier-bootstrap,
+tests/bootstrap_alpine.sh; see section 10 for how it differs from the
+plan below). Remote mode (-r) and the pinning helper are still a plan.
+Section 9 lists the decisions still open.
glacier-bootstrap takes a template index and builds a chrootable
Everest system in a target directory, from either a local directory of
@@ -37,7 +38,8 @@ Sequence:
3. fetch into a private cache; verify every hash (section 5)
4. doas syspkg --root TARGET -n create the tree
5. doas syspkg --root TARGET --strict -l PKG... ONE transaction
- 6. optional smoke test (-t)
+ 6. doas syspkg --root TARGET -y TARGET's indexes
+ 7. optional smoke test (-t)
Step 5 is one transaction on purpose: TARGET ends up either the whole
system or an empty tree, never half a system.
@@ -100,7 +102,6 @@ per package, so packages can carry pins:
# templates/minimal.template
NAME=minimal
- SYS_PROFILE=x86_64-musl
PKG=filesystem 1.0.0
PKG=musl 1.2.5 sha256:3a5f...
PKG=busybox 1.37.0 sha256:91c0...
@@ -110,7 +111,9 @@ per package, so packages can carry pins:
- A pin is optional for -d (if present, it must ALSO match the
sidecar) and required for -r until signed repos exist.
- Strict parsing: unknown keys, a duplicate PKG name, a malformed pin
- and an empty package list are all hard errors. Remember gworld bug
+ and an empty package list are all hard errors. SYS_PROFILE is an
+ unknown key (9b): nothing could check it until manifests record a
+ profile (ROADMAP 3.3). Remember gworld bug
10: a field silently never parsing, with a default that looks right.
- A `glacier-bootstrap --pin PKGDIR TEMPLATE` helper that fills pins in
from local sidecars is the obvious way to produce a pinned template
@@ -131,8 +134,10 @@ doesn't.)
Recommendation: CHECK, don't auto-resolve.
- Every PKG_DEPS entry of every listed package must itself be listed,
- with a matching version. Otherwise error: "busybox needs musl 1.2.5,
- which the template does not list."
+ at that version or later (decided 2026-10-06: the ROADMAP 2.3 rule, so
+ the template check and syspkg's commit check agree). Otherwise error:
+ "base::busybox requires musl >= 1.2.5, which the template does not
+ list." / "..., but the template lists 1.2.4."
- No two listed packages may conflict (PKG_CONFS).
- Each manifest's PKG_NAME/PKG_VER must match its template line. This
catches a mis-named file in -d mode, where there may be no pin.
@@ -242,14 +247,18 @@ independent of glacier-bootstrap itself:
section 4). Pins mean nothing until syspkg actually checks sidecars.
2. [DONE] Symlink extraction + linking, with the write-through-symlink
defense (6a). Needed for anything to run.
- 3. sysroot in the library (IMAGE_DESIGN section 3), plus the "host
+ 3. [DONE 2026-10-05] sysroot in the library (IMAGE_DESIGN section 3), plus the "host
/usr and /glacier untouched" test.
- 4. syspkg --root DIR (IMAGE_DESIGN section 4), --strict (any link
+ 4. [DONE 2026-10-05, as -R/--root] syspkg --root DIR (IMAGE_DESIGN section 4), --strict (any link
refusal fails the transaction), and the skeleton on -n (6b) [the
skeleton is DONE; --root and --strict are not].
- 5. Packages: busybox recipe (base), sbase rebuilt as base, every
+ 5. [DONE: ~/Projects/everest builds musl, busybox, linux and
+ everest-bootscripts, all base, with sidecars.] Packages: busybox
+ recipe (base), sbase rebuilt as base, every
package rebuilt with sidecars (6d-f).
- 6. glacier-bootstrap, local mode.
+ 6. [DONE 2026-10-06: Everest's `everest.sh initramfs` now builds its
+ tree this way, and the result boots in QEMU.] glacier-bootstrap,
+ local mode.
MILESTONE: `glacier-bootstrap -t -d pkgs templates/minimal.template
/mnt/everest` succeeds, `chroot /mnt/everest /bin/sh` gives a
working shell, and the host is byte-identical before and after.
@@ -281,8 +290,8 @@ orchestration over things that have to exist anyway.
a. IMAGE_DESIGN.txt section 9a still stands: images are bootstrap
OUTPUT, never install INPUT. Everything here assumes yes.
- b. Template format (section 3): one PKG= line per package with an
- optional pin, or something else.
+ b. DECIDED 2026-10-06: NAME= plus one `PKG=name version [sha256:hex]`
+ per package; no SYS_PROFILE until manifests record one (section 3).
c. DECIDED 2026-10-04: option 1, copy on install, .new on conflict.
Implemented in istoreutils.c (install_config / prune_config): a
system-scope package's top-level etc/ is copied into /etc, never
@@ -293,7 +302,58 @@ orchestration over things that have to exist anyway.
d. DECIDED 2026-10-04: syspkg -n creates the top-level tree itself
(6b). Implemented as gl_create_fs_tree(root, verbose), called by
gl_init_sys before the index is written.
- e. Dependency handling (section 4): check-only, as recommended, or
- automatic closure.
+ e. DECIDED 2026-10-05: automatic closure, once there is a source to
+ resolve from (remote repos, ROADMAP 3.1). Until then a missing
+ dependency refuses; the resolver is written so 3.1 plugs in. The
+ index already records auto vs explicit installs (ROADMAP 2.3).
+ Section 4's template check still applies to bootstrap: a template
+ lists what it installs.
f. Pinning helper: part of glacier-bootstrap (--pin) or of gworld
(which already knows what it just built).
+
+
++--------------------------------------------+
+| 10. As implemented (local mode, 2026-10-06)|
++--------------------------------------------+
+
+ glacier-bootstrap [-hvntV] -d PKGDIR TEMPLATE TARGET
+
+Where it differs from, or adds to, sections 1-8:
+
+ - syspkg is the one on the caller's PATH (decided 2026-10-06),
+ resolved to an absolute path first and run through doas, or
+ directly when already root. -t runs `doas chroot`.
+ - Order: parse, then verify, then resolve. Manifests are read only
+ from VERIFIED copies: each PKGDIR/NAME-VER.gpkg is copied into a
+ private mktemp -d while being hashed, checked against its sidecar and
+ the pin, and given a sidecar written from that hash. syspkg is handed
+ the copies, so a pin binds syspkg too, and a file swapped in PKGDIR
+ later is never read. The copies are removed on every exit.
+ - TARGET must be absolute, not /, and pass gl_check_sysroot (checked
+ read-only up front). It must be fresh, or initialised by syspkg -n
+ with nothing installed: then -n is skipped, so a run declined at
+ syspkg's prompt can simply be repeated. TARGET with packages is
+ refused.
+ - syspkg's prompt is passed through (it's the review step). It exits 0
+ when declined, so afterwards TARGET's index is read (it's world-
+ readable) and every template package must be listed.
+ - Every problem in a phase is reported before failing. Exit status:
+ 1 usage or target, 2 template, 3 verification, 4 resolution,
+ 5 installation, 6 smoke test, 7 repositories.
+ - Repositories (2026-10-07; the user: bootstrap must leave a
+ semi-functional system, so it configures repositories, pacstrap
+ style). The configuration comes from a PACKAGE in the template
+ (Everest: everest-repos, shipping /etc/glacier/repos.conf and
+ keys/*.pub), so bootstrap still never writes into TARGET. After the
+ install check, if TARGET's repos.conf gives any repository a url,
+ it runs `syspkg -R TARGET -y`: syspkg reads TARGET's OWN keyring
+ under -R for repository operations (REPO_DESIGN section 6, "Keyrings
+ under -R"), fetches into TARGET/glacier/sys/repos, and so sets
+ TARGET's rollback floor at build time. A failed fetch: exit 7,
+ TARGET stays installed, no "Bootstrapped" line. No repos.conf, or
+ no url: `[!] No remote repositories are configured in TARGET.` and
+ exit 0. Copying the host's repos.conf (pacstrap's mirrorlist copy)
+ is left out for now (user, 2026-10-07).
+ - Its own lib/ is found from its realpath (package.path and ffi.load),
+ not the current directory; no root-ownership checks (unprivileged).
+ - Not implemented: -r (remote) and --pin (9f still open).
diff --git a/IMAGE_DESIGN.txt b/IMAGE_DESIGN.txt
index d7ca5b4..0648e88 100644
--- a/IMAGE_DESIGN.txt
+++ b/IMAGE_DESIGN.txt
@@ -116,8 +116,11 @@ string against the runtime store prefix. But every "does the target
exist" check has to become "does T + target exist". The same goes for
restore_displaced and for anything else that follows a link.
-Proposed shape:
+Shape (ACCEPTED 2026-10-05):
+ - The library refuses a sysroot other than "/" for GL_SCOPE_USR
+ contexts. Only system scope may target another root, for every
+ caller, not just the CLIs (section 4).
- gl_context_t gains `char sysroot[PATH_MAX]` ("" == "/"; appended
at the end, matched in lib/glacier_cdef.lua and
tests/glacier_ffi.lua).
@@ -126,6 +129,12 @@ Proposed shape:
wrappers passing "/", so every current caller is unchanged.
- One helper, gl_at(ctx, runtime_path, out), used for EVERY
filesystem operation. Runtime paths stay what's stored and compared.
+ AS BUILT (2026-10-05): the reverse, same effect. The context stores
+ INSTALL paths, used for every filesystem operation unchanged, and
+ gl_runtime_path() translates only where runtime meaning matters
+ (link targets, is_glacier_symlink, .displaced keys); gl_at() checks
+ a link's target during prune. A missed translation then lands in T,
+ never on the host.
- GL_BASE_LINKS_DEST resolves to sysroot + "/usr". Never the host's
/usr when a sysroot is set.
@@ -239,9 +248,13 @@ Consequences of /usr starting empty that the design has to own:
| 8. Milestones |
+--------------------------------------------+
- 1. sysroot in the library (section 3), plus the host-untouched test.
+ 1. [DONE 2026-10-05] sysroot in the library (section 3), plus the
+ host-untouched test (glacier-ng/tests/sysroot_alpine.sh). Implemented
+ with INSTALL paths in the context and runtime translation where a
+ path's meaning matters; see CLAUDE.md "Install root / sysroot".
No CLI changes, testable through FFI alone.
- 2. syspkg --root (section 4), with the root-owned-target check.
+ 2. [DONE 2026-10-05] syspkg --root (section 4), with the root-owned-target
+ check, as -R/--root (implies --strict). glacier-ng/tests/root_alpine.sh.
3. A `filesystem` base package (section 7).
4. images/minimal.image + a driver script (gworld -> syspkg --root
-> finalize). Milestone: `chroot T /bin/sh` works, and
diff --git a/Makefile b/Makefile
index 3b627c5..1bb07b6 100644
--- a/Makefile
+++ b/Makefile
@@ -1,8 +1,8 @@
include config.mk
-.PHONY: all prepare log log_test rt pkg istoreutils make_conf dag install clean static libglacier.a install_static libglacier-ng sha256 gsha256sum libglacier_verify
+.PHONY: all prepare log log_test pkg transaction istoreutils install clean static libglacier.a install_static libglacier-ng sha256 gsha256sum libglacier_verify gsign grepo
-all: prepare log log_test rt pkg transaction istoreutils make_conf dag sha256 libglacier_verify gsha256sum
+all: prepare log log_test pkg transaction istoreutils sha256 libglacier_verify gsha256sum gsign grepo
@echo "libglacier-ng has finished building"
static: prepare log_static pkg_static istoreutils_static libglacier.a
@@ -15,12 +15,14 @@ pkg_static:
istoreutils_static: pkg_static
$(CC) $(CFLAGS) -c src/istoreutils.c -o build/lib/tmp/istoreutils.o
+ $(CC) $(CFLAGS) -c src/deps.c -o build/lib/tmp/deps.o
libglacier.a:
ar rcs build/lib/libglacier.a \
build/lib/tmp/log.o \
build/lib/tmp/pkg.o \
- build/lib/tmp/istoreutils.o
+ build/lib/tmp/istoreutils.o \
+ build/lib/tmp/deps.o
prepare:
mkdir -p build
@@ -38,19 +40,30 @@ log:
log_test:
$(CC) $(CFLAGS) -shared -fPIC src/log.c -o build/lib/shared/libglacier_log.so
-rt:
- $(CC) $(CFLAGS) src/runtime.c -c -o build/lib/tmp/runtime.o
-
sha256:
$(CC) $(CFLAGS) src/sha256.c -c -o build/lib/tmp/sha256.o
+# Monocypher, vendored unmodified (src/monocypher/README): Ed25519 for
+# sign.c.
+MONOCYPHER = src/monocypher/monocypher.c src/monocypher/monocypher-ed25519.c
+
libglacier_verify:
- $(CC) $(CFLAGS) -shared -fPIC src/sha256.c \
+ $(CC) $(CFLAGS) -shared -fPIC src/sha256.c src/sign.c $(MONOCYPHER) \
-o build/lib/shared/libglacier_verify.so
gsha256sum: prepare
$(CC) $(CFLAGS) src/gsha256sum.c src/sha256.c -o build/bin/gsha256sum
+gsign: prepare
+ $(CC) $(CFLAGS) src/gsign.c src/sign.c src/log.c $(MONOCYPHER) -o build/bin/gsign
+
+# grepo reads packages (manifest, hash) and signs the index: built
+# from source like gsign, so it needs no rpath to the shared objects.
+grepo: prepare
+ $(CC) $(CFLAGS) src/grepo.c src/sign.c src/sha256.c src/log.c src/pkg.c \
+ src/istoreutils.c src/deps.c src/transaction.c $(MONOCYPHER) \
+ -larchive -lconfig -o build/bin/grepo
+
transaction: log istoreutils
$(CC) $(CFLAGS) -shared -fPIC src/transaction.c \
-Lbuild/lib/shared -lglacier_istoreutils -lglacier_log \
@@ -58,7 +71,7 @@ transaction: log istoreutils
-o build/lib/shared/libglacier_transaction.so
istoreutils: pkg
- $(CC) $(CFLAGS) -shared -fPIC src/istoreutils.c \
+ $(CC) $(CFLAGS) -shared -fPIC src/istoreutils.c src/deps.c \
-Lbuild/lib/shared -lglacier_log -lglacier_pkg -larchive \
-Wl,-rpath,'$$ORIGIN' \
-o build/lib/shared/libglacier_istoreutils.so
@@ -69,21 +82,12 @@ pkg:
-Wl,-rpath,'$$ORIGIN' \
-o build/lib/shared/libglacier_pkg.so
-make_conf:
- $(CC) $(CFLAGS) src/make_conf.c -c -o build/lib/tmp/make_conf.o
-
libglacier-ng: prepare
$(CC) $(CFLAGS) -shared -fPIC \
- src/log.c src/pkg.c src/transaction.c src/istoreutils.c \
+ src/log.c src/pkg.c src/transaction.c src/istoreutils.c src/deps.c \
-larchive -lconfig \
-o build/lib/shared/libglacier-ng.so
-dag:
- $(CC) $(CFLAGS) -shared -fPIC src/dag.c \
- -Lbuild/lib/shared -lglacier_log \
- -Wl,-rpath,'$$ORIGIN' \
- -o build/lib/shared/libglacier_dag.so
-
install:
mkdir -p $(PREFIX)/lib/glacier/
install build/lib/shared/libglacier_log.so $(PREFIX)/lib/glacier/ -m 755
@@ -92,14 +96,12 @@ install:
install build/lib/shared/libglacier_pkg.so $(PREFIX)/lib/glacier -m 755
install build/lib/shared/libglacier_verify.so $(PREFIX)/lib/glacier -m 755
install build/bin/gsha256sum $(PREFIX)/bin/ -m 755
+ install build/bin/gsign $(PREFIX)/bin/ -m 755
+ install build/bin/grepo $(PREFIX)/bin/ -m 755
install_static:
mkdir -p $(PREFIX)/lib/glacier
install build/lib/libglacier.a $(PREFIX)/lib/glacier/ -m 644
-install_test_dir:
- install build/lib/shared/lg_log.so $(TEST_DIR)
- install build/lib/shared/
-
clean:
rm -rf build
diff --git a/PACKAGE_SCOPES.txt b/PACKAGE_SCOPES.txt
index df46661..03daac9 100644
--- a/PACKAGE_SCOPES.txt
+++ b/PACKAGE_SCOPES.txt
@@ -35,6 +35,13 @@ SYS (GL_SCOPE_SYS)
System-wide packages, not owned by any particular uid:
/glacier/sys/{index,store,links,stage,old,lock}
Symlink destination depends on the package's repo (see section 3).
+ Root-owned and world-READABLE, like /usr: index/store/links, the
+ store's repo/name/version dirs and implicit parent dirs are 0755,
+ index files 0644 (gl_scope_dir_mode / gl_scope_file_mode). /usr's
+ links point into the store, so every user must be able to traverse
+ it, and user-scope dependency checks read the system index. The
+ modes are applied explicitly, not left to syspkg's umask. The lock
+ stays 0600; .displaced, .etc-state and backup/ stay 0700.
Requires root. gpkg enforces this via geteuid() before it will
even attempt a system-scope operation.
diff --git a/REPO_DESIGN.txt b/REPO_DESIGN.txt
new file mode 100644
index 0000000..eea4b82
--- /dev/null
+++ b/REPO_DESIGN.txt
@@ -0,0 +1,446 @@
++--------------------------------------------+
+| Remote repositories and signing |
++--------------------------------------------+
+
+STATUS: DESIGN, 2026-10-06. ROADMAP 3.1. Steps 1 (Ed25519, signify
+formats, gsign), 2 (keyring, the local base rule, gbuild -k,
+bootstrap carrying .sig, Everest's key) and 3 (grepo, the index,
+refresh with -y) of section 9 DONE 2026-10-06, step 4 (-f, -u,
+dependency closure, the cache) DONE 2026-10-07; sections 7, 5 and 6
+say how they came out. Step 5 next. See CLAUDE.md.
+Sections 1 and 10 are decided; the only open item is 10a's TLS library.
+
+Adds AUTHENTICITY to glacier: SECURITY_DESIGN.txt section 1 deferred it
+to "a future phase, tied to remote-repo support". This is that phase.
+It also unblocks gpkg/syspkg -f (ROADMAP 2.2), -u (2.1), automatic
+dependency closure (2.3) and glacier-bootstrap -r (GLACIER_BOOTSTRAP
+section 5), and closes Finding 1 of SECURITY_REVIEW_2026-07-16.md (a
+manifest that says PKG_REPO = "base" gets linked into /usr).
+
+Companion documents: SECURITY_DESIGN.txt (integrity, refusal matrix),
+PACKAGE_SCOPES.txt (repo routing), GLACIER_BOOTSTRAP.txt (pins).
+
+
++--------------------------------------------+
+| 1. Decisions (the user, 2026-10-06) |
++--------------------------------------------+
+
+ a. Signatures: Ed25519, vendored (Monocypher's Ed25519 variant, which
+ brings its own SHA-512), compiled into the private
+ libglacier_verify.so next to sha256. Key and signature files are
+ signify-compatible, so OpenBSD's signify can make and check them
+ too. No new base dependency.
+ b. What is signed: one INDEX per repository (repo.idx + repo.idx.sig),
+ listing every package it serves with its sha256. Packages are
+ verified through the hash in the signed index, i.e. through the
+ existing sha256 path. The index has a serial; a client refuses an
+ index older than the last one it accepted (rollback).
+ c. Local base packages: once signing exists, `syspkg -l FILE` of a
+ package whose manifest says base requires a signature by a key
+ trusted for base. extra/community local installs stay
+ sidecar-only (integrity). Manual installation keeps working: a
+ builder makes a key and root adds it to the keyring.
+ d. Users may add their own repositories and keys, for their own scope
+ only (gpkg). Never read by syspkg.
+
+
++--------------------------------------------+
+| 2. Keys and signatures (proposal) |
++--------------------------------------------+
+
+signify's formats, byte for byte (two lines: "untrusted comment: ..."
+then base64):
+
+ public key "Ed" | keynum[8] | pubkey[32]
+ signature "Ed" | keynum[8] | sig[64]
+ secret key "Ed" | "BK" | rounds[4] | salt[16] | checksum[8] |
+ keynum[8] | seckey[64]
+
+ - keynum is a random 8-byte key id; a signature names the key that
+ made it, so verification picks the key by keynum and never tries
+ every key in the keyring.
+ - Signing is pure Ed25519 over the WHOLE message, as signify does. So
+ we never sign a .gpkg directly (it would have to be read into memory
+ whole). What gets signed is small:
+ - a repository's repo.idx;
+ - a local package's SIDECAR (NAME-VER.gpkg.sha256, the one line
+ "sha256:"), as NAME-VER.gpkg.sha256.sig. Checking the
+ sidecar against the file is the existing path; the signature
+ makes the sidecar authentic. `signify -V -p KEY -m X.sha256`
+ checks the same file.
+ - Secret keys: glacier's tools read and write only UNENCRYPTED secret
+ keys (rounds = 0, what `signify -G -n` makes): passphrase support
+ needs bcrypt_pbkdf, which is more vendored crypto for a build-host
+ convenience. A passphrase-protected key works by signing with
+ signify itself (10b).
+
+New tool `gsign` (C, built with gsha256sum; a build/maintainer tool,
+later in "glacier-tools", never in base):
+
+ gsign -G -p KEY.pub -s KEY.sec [-c COMMENT] make a keypair
+ gsign -S -s KEY.sec -m FILE [-x SIGFILE] sign (default FILE.sig)
+ gsign -V -p KEY.pub -m FILE [-x SIGFILE] verify
+
+gbuild gains `-k KEY.sec`: sign the sidecar it writes.
+
+
++--------------------------------------------+
+| 3. Keyring and repository configuration |
++--------------------------------------------+
+
+System (root-owned; syspkg applies its trust checks to these files and
+every ancestor, like its own lib/):
+
+ /etc/glacier/keys/NAME.pub
+ /etc/glacier/repos.conf
+
+repos.conf (libconfig, like glacier.conf):
+
+ repos = (
+ { name = "base"; url = "https://pkg.example.org/base";
+ keys = [ "everest-2026" ]; },
+ { name = "extra"; url = "https://pkg.example.org/extra";
+ keys = [ "everest-2026" ]; }
+ );
+
+ - A key is trusted ONLY for the repos that list it. That binding is
+ what makes "base" mean something: the base index must be signed by a
+ key listed for base, and a local base package's sidecar likewise.
+ Several keys per repo allow rotation (old and new side by side).
+ - Order = priority, when one name is served by several repos.
+ - name follows the gl_valid_pkg_ident rules. url is https:// or
+ file:// (local mirrors, tests); http:// is refused.
+ - url is optional: an entry without one only says which keys are
+ trusted for that repo, for a system installed and kept up by hand
+ from signed local files (section 7).
+ - Missing file = no remote repositories; -f/-u then refuse in one
+ line. No defaults, as with KEEP_OLD.
+
+User (gpkg only):
+
+ ~/.config/glacier/repos.conf ($XDG_CONFIG_HOME)
+ ~/.config/glacier/keys/NAME.pub
+
+ - Merged AFTER the system list (lower priority).
+ - A user repo named "base", or named like a system repo, is refused.
+ - System keys are trusted for system repos only; a user's keys for the
+ user's repos only.
+
+
++--------------------------------------------+
+| 4. The index |
++--------------------------------------------+
+
+URL/repo.idx and URL/repo.idx.sig. libconfig, parsed only AFTER its
+signature checks:
+
+ REPO = "base";
+ SERIAL = 2026100601L; # strictly increases with each publish
+ VALID_UNTIL = 1761955200L; # unix time; required (10c)
+ PKGS = (
+ { NAME = "musl"; VER = "1.2.5"; FILE = "musl-1.2.5.gpkg";
+ SHA256 = "sha256:3a5f..."; SIZE = 812345L;
+ DEPS = (); CONFS = (); },
+ ...
+ );
+
+ - REPO must equal the configured name the index was fetched for.
+ - Every version a repo serves is listed (so a pinned or older version
+ can still be fetched by -f NAME=VER later).
+ - DEPS/CONFS repeat the manifest's, so dependency closure can be
+ planned before downloading anything. After download, the manifest
+ must match the entry (NAME, VER, PKG_REPO = REPO, DEPS, CONFS):
+ a mismatch refuses the package. That is the Finding 1 rule for
+ remote packages: a package is "base" because the base index lists
+ it AND it says so, never because it says so.
+ - FILE is a bare file name (gl_valid_pkg_ident rules plus ".gpkg"):
+ no paths, no URLs.
+
+New tool `grepo` (maintainer side, glacier-tools): `grepo -s KEY.sec
+-r REPO DIR` reads every NAME-VER.gpkg in DIR (manifests, hashes),
+writes DIR/repo.idx with SERIAL = the previous one + 1 (or the date
+form), and signs it. The server is then any static file host.
+
+
++--------------------------------------------+
+| 5. Client state and fetching |
++--------------------------------------------+
+
+Inside each scope's base directory (one more entry beside index, store,
+...; same ownership and modes as the rest):
+
+ /repos/REPO.idx, REPO.idx.sig the last ACCEPTED index
+ /cache/REPO/FILE downloaded packages
+
+Refresh (every -f / -u; indexes are small; 10d):
+ 1. curl --proto =https --proto-redir =https --fail (or a file:// copy)
+ URL/repo.idx and .sig into a temporary file in /repos/.
+ 2. Verify the signature with a key listed for that repo (by keynum).
+ 3. Parse. REPO must match. SERIAL must be > the stored one, or equal
+ with identical bytes. VALID_UNTIL must be in the future.
+ 4. rename() over the stored copy (never written in place: bug 25).
+Any failure keeps the old index and refuses the operation in one line:
+ [x] Index for extra is older than the one already accepted.
+
+Fetch: download FILE into the cache under a temporary name, hash it
+while downloading, rename into place only if the hash matches the
+index, then write the sidecar FROM THE INDEX and hand the cached file
+to the existing install path (which re-hashes it, as today). The cache
+is a download optimization, not a trusted store: a cache hit is hashed
+again.
+
+ As implemented (2026-10-06, step 3; the user chose a refresh flag,
+ the date-form SERIAL and grepo in C):
+ - grepo is C (src/grepo.c, libglacier-ng; built from source like
+ gsign, installed to bin; not in base): `grepo -s KEY.sec -r REPO
+ [-n SERIAL] [-e DAYS] DIR`. SERIAL = max(previous + 1, today (UTC)
+ * 100 + 1), e.g. 2026100601; -n must still be above the previous
+ one. VALID_UNTIL = now + DAYS (default 30, 1..3650). Packages are
+ sorted by file name; each must be a regular file, its manifest must
+ say REPO and its name must be NAME-VER.gpkg (VER as the store names
+ versions, "%d.%d.%d"). An existing DIR/repo.idx must be a
+ well-formed index of REPO (its SERIAL is the previous one). It is
+ written to a mkstemp file, signed, and both renamed into place, the
+ .sig first.
+ - The index format is section 4's, with DEPS and CONFS as lists of
+ { NAME = "x"; VER = "1.0.0"; } (the manifest's PKG_DEPS shape).
+ gl_index_parse (pkg.c) takes the BYTES the signature was checked
+ over, never a path, and is strict: exactly REPO, SERIAL > 0,
+ VALID_UNTIL > 0, PKGS; each package exactly NAME, VER, FILE, SHA256,
+ SIZE >= 0, DEPS, CONFS; FILE must be exactly "NAME-VER.gpkg"; SHA256
+ "sha256:" + 64 lowercase hex; no NAME + VER twice. A NUL or an '@'
+ anywhere is refused before libconfig sees it ('@include').
+ GL_INDEX_ERR_REPO when REPO is not the repo it was fetched for.
+ - `syspkg -y` / `gpkg -y` (--refresh) refresh every repos.conf entry
+ with a url and stop (decision 10d still holds for -f/-u). gpkg
+ reads the SYSTEM repos.conf and keyring until step 6 (user repos),
+ and stores into the user's base. No repos.conf, or no entry with a
+ url: `[x] No remote repositories are configured.` (exit 1).
+ - /repos is made by gl_scope_state_dir (transaction.c): the
+ base must be the scope's; repos is created with the scope's mode
+ (0755 root / 0700 uid) and refused unless it is a real directory
+ with exactly that owner and mode. Files are 0644 / 0600. -y holds
+ the scope's lock (gl_begin_transaction on a live context); under
+ -R the indexes go under the install root, checked with the install
+ root's OWN keyring (DIR/etc/glacier; changed 2026-10-07, see
+ "Keyrings under -R" below).
+ - file:// is read directly (absolute paths only); https:// goes
+ through `curl -sf --proto =https --proto-redir =https`. 64 MiB at
+ most (GL_SIGN_MAX_MSG). Every repo is tried; exit 1 if any failed.
+ - One line per repo:
+ [✓] Index for base: serial 2026100601, 11 packages.
+ [i] Index for base is up to date: serial 2026100601.
+ [x] Cannot fetch the index for base. (a missing .sig too)
+ [x] Index for base is not signed by a key trusted for base.
+ [x] Malformed index for base.
+ [x] Index fetched for base is the index of another repository.
+ [x] Index for base is older than the one already accepted.
+ [x] Index for base has the serial of the one already accepted, but other contents.
+ [x] Index for base has expired.
+ [x] Malformed /glacier/sys/repos/base.idx. (left for the admin)
+ - The accepted copy is trusted by where it lives (root's or the
+ user's private repos dir): it is parsed for its SERIAL, not
+ re-verified.
+ - Test: glacier-ng/tests/repo_alpine.sh (103 checks; 18 mutants, 12 C
+ and 6 Lua, all caught).
+
+
++--------------------------------------------+
+| 6. -f, -u and dependency closure |
++--------------------------------------------+
+
+ -f NAME... (ROADMAP 2.2): like -l, with the file found by name in the
+ accepted indexes (highest version, first repo by priority). Missing
+ dependencies are added automatically (ROADMAP 2.3's closure): the
+ highest version satisfying the minimum, recorded `auto`. The
+ summary lists them, marked as dependencies. Conflicts and every
+ 2.3 check run as today, before the prompt.
+ -u (ROADMAP 2.1): every installed package whose repo is configured and
+ whose index has a higher version is replaced, as -lO does (backups,
+ KEEP_OLD). Packages from repos not configured are left alone.
+ `-u NAME...` limits it to those names.
+ syspkg -f/-u fetch only from system repos; gpkg -f/-u from system and
+ the user's repos. Base is still refused in user scope (library rule).
+
+ As implemented (2026-10-07, step 4; M.fetch / M.upgrade in
+ glacier-ng/lib/gpkg_common.lua). The user decided three details:
+ - Which repo: the FIRST repo by repos.conf order that serves NAME, at
+ its highest version. A lower-priority repo never wins by publishing
+ a bigger version. -u stays within the installed package's repo.
+ - A dependency installed at too old a version is UPGRADED in the same
+ transaction from its own repo (backed up per KEEP_OLD, like -lO),
+ keeping its auto/explicit mark; the summary shows
+ "old -> new (dependency)".
+ - Cache: downloads stay in /cache/REPO while a run is declined
+ or refused; the files a commit installed are deleted after it.
+ The rest:
+ - Every -f/-u runs -y's refresh first; if any repo fails, the
+ operation is refused (decision 10d). The accepted indexes are then
+ read from /repos (gl_index_parse again; trusted by location).
+ - Closure: for each planned package, each DEPS entry of its INDEX
+ entry is met by a planned package, an installed one at >= VER
+ (this scope; for a user, the system's index too), else upgraded (an
+ installed one too old) or added (pick(): first repo serving it,
+ recorded auto). What can't be met is left to gl_check_plan, which
+ prints the 2.3 refusal. The summary marks additions
+ "(dependency)".
+ - Download: gl_scope_cache_dir (transaction.c) makes and checks
+ /cache and /cache/REPO as gl_scope_state_dir checks
+ repos (owner, exact mode, not a symlink). A cached file whose hash
+ matches the index is reused; otherwise file:// is read, https://
+ goes through `curl -sf --proto =https --proto-redir =https` on a
+ pipe; at most SIZE + 1 bytes are ever read, hashed on the way, into
+ a dot-named temporary, renamed into place only if size and hash
+ match. 0644 / 0600.
+ - No sidecar is written: the index's sha256 is handed to
+ verify_and_copy directly (its `vouched` argument), which re-hashes
+ while copying into the private dir, as for -l. A base package needs
+ no .sig of its own: the base index vouches for it.
+ - The copy's manifest (library-read) must say exactly what the index
+ entry says: NAME, VER, PKG_REPO = the repo, DEPS and CONFS (same
+ order, "NAME MAJ.MIN.PAT"). Else, and for a bad hash or size:
+ [x] extra::evil 1.0.0 does not match its index.
+ and the cached file is deleted. Nothing to download:
+ [x] Cannot fetch extra::foo 1.0.0.
+ No repo serves a name:
+ [x] foo is not in any repository.
+ - -f of a name installed at another version needs -O (as -l);
+ dependency upgrades and -u don't (meta.may_replace). -O still also
+ means displacing foreign files.
+ - -u [NAME...]: a named package not installed, or whose repo has no
+ url, is refused (`[x] foo is not installed.` / `[x] community::foo
+ is not in any configured repository.`); plain -u skips the latter.
+ Nothing newer: `[✓] Nothing to do.`, exit 0.
+ - gpkg: user scope never fetches from base (base indexes are not even
+ considered, so `[x] core is not in any repository.`); the system's
+ repos.conf until step 6; indexes and cache in the user's base.
+ - syspkg -R DIR -f/-u: indexes and cache under DIR, DIR's keyring.
+
+ Keyrings under -R (decided 2026-10-07, for glacier-bootstrap
+ configuring the system's repositories): -y, -f and -u read
+ DIR/etc/glacier/repos.conf and DIR/etc/glacier/keys, with the same
+ trust checks, plus: each file's real path must lie inside DIR
+ (`[x] Refusing P: it resolves outside DIR.`), so a symlink in DIR
+ can't make the host's file stand in for DIR's. The indexes are DIR's
+ state and DIR checks them with its own keys from then on, and a host
+ with no glacier configuration can still bootstrap. A local `-R -l`
+ base package's .sig is still checked with the HOST's keyring (section
+ 7). Tests: repo_alpine.sh / fetch_alpine.sh give the image its own
+ keyring; bootstrap_alpine.sh sections 14-16.
+ - Test: glacier-ng/tests/fetch_alpine.sh (84 checks; 16 Lua mutants
+ and 1 C mutant, all caught except the download-level hash check,
+ which is EQUIVALENT: the private copy's re-hash refuses the same
+ package and the mismatch path deletes the cached file. Kept as
+ defence in depth: a bad download never sits in the cache under its
+ final name).
+ - Known gap: a run killed mid-download leaves its dot-named temporary
+ in the cache (never read; the next fetch writes its own).
+
+
++--------------------------------------------+
+| 7. Local installs (decision 1c) |
++--------------------------------------------+
+
+ syspkg -l FILE, manifest says base:
+ FILE.sha256 must match (as today) AND FILE.sha256.sig must verify
+ with a key listed for base in repos.conf. Otherwise:
+ [x] base::musl is not signed by a key trusted for base.
+ syspkg -l, other repos; gpkg -l: unchanged (sidecar only).
+ glacier-bootstrap -d: carries each package's .sig into its private
+ copies, so syspkg -R checks it. The keyring checked is the HOST's
+ /etc/glacier (the build host's trust, as KEEP_OLD is today).
+ Consequence for ~/Projects/everest: it needs a key; container.sh signs
+ every base package (gbuild -k); the build host's /etc/glacier/keys and
+ repos.conf list it. The image ships the PUBLIC key and a repos.conf so
+ the installed system trusts its own base (since 2026-10-07 in the
+ everest-repos package, split out of everest-bootscripts).
+
+ As implemented (2026-10-06, step 2):
+ - The repo is the one the LIBRARY reads from the verified copy
+ (gl_read_pkg_manifest, as gl_install_pkg), never gpkg_common's
+ pattern-matching read_manifest_fields: a manifest comment
+ `# PKG_REPO = "extra"` would make that one say extra.
+ - The sidecar is read ONCE; those bytes are both what the copy's hash
+ is checked against and what the signature is checked over. So
+ glacier-bootstrap copies the sidecar's bytes as read (it used to
+ write a new one from the hash) next to the .sig.
+ - The key is the one, among those listed for base, whose keynum the
+ signature names. Any other problem with the signature (missing,
+ malformed, other bytes, other key) is the one-line refusal above,
+ before the prompt; one refused package refuses the batch.
+ - repos.conf is read by gl_conf_repo_keys (pkg.c), strictly: `repos`
+ is the only setting; each entry has name, keys and optionally url,
+ nothing else; names and key names follow gl_valid_pkg_ident; a
+ duplicate name, or a url other than https://X or file://X, is
+ refused. Any of that: `[x] Malformed /etc/glacier/repos.conf.`
+ - repos.conf and each listed key, every ancestor of them and of their
+ real paths, must be root-owned and not group/world-writable:
+ `[x] Refusing F: P is not root-owned, or is group/world-writable.`
+ A listed key that is missing (`Cannot read F.`) or not a public key
+ (`Malformed F.`) stops the run: the admin's configuration is at
+ fault, not the package. Read only if some package is base.
+ - gbuild -k KEY.sec signs the sidecar (gsign from $GSIGN_BIN,
+ build/bin, or PATH), trying the key before building; a build
+ without -k removes a stale .sig. tests/mkpkg -k likewise.
+ - Test images (Dockerfile.*) carry a TEST key, glacier-test, listed for
+ base, its secret at /root/glacier-test.sec. tests/keyring_alpine.sh
+ covers this section (17 mutants, all caught).
+ - KEEP_OLD backups are re-packed from the store, so a base backup has
+ no signature: `syspkg -lO` refuses it until the restore interface
+ exists (decision 10f).
+
+
++--------------------------------------------+
+| 8. Where the checks live |
++--------------------------------------------+
+
+Signature verification is C (libglacier_verify.so). The POLICY (which
+key for which repo, the base rule, rollback state) lives in the CLIs'
+shared module, like hash verification today, so a third-party caller of
+gl_install_pkg still isn't checked. ROADMAP 3.2 (move verification into
+the library) now covers signatures too.
+
+
++--------------------------------------------+
+| 9. Order of work |
++--------------------------------------------+
+
+ 1. Vendored Ed25519 + signify formats in libglacier_verify.so; gsign;
+ tests including interop with signify (Alpine's `signify` package)
+ and hostile inputs (bad base64, wrong keynum, truncated files).
+ 2. Keyring + repos.conf + the local base rule (section 7); gbuild -k;
+ glacier-bootstrap carries .sig; Everest gets a key.
+ 3. Index: grepo; refresh, verify, rollback state. DONE 2026-10-06
+ (section 5, "As implemented"; syspkg/gpkg -y).
+ 4. -f with closure; then -u. DONE 2026-10-07 (section 6, "As
+ implemented").
+ 5. glacier-bootstrap -r against an index (pins stay optional extra
+ assurance).
+ 6. User repos.
+Each step is tested in Docker (file:// repos need no network) and
+mutation-checked, as usual.
+
+
++--------------------------------------------+
+| 10. Decisions on the details (2026-10-06) |
++--------------------------------------------+
+
+ a. Fetching on Everest: curl plus one TLS library in base. Signatures
+ make packages authentic regardless; TLS also hides WHAT is fetched
+ and stops a network attacker serving a stale (still valid) index.
+ STILL OPEN: which TLS library (BearSSL or LibreSSL); decide when
+ packaging it. Until then, everything is built and tested with
+ file:// repos and Alpine's curl.
+ b. No encrypted secret keys in gsign (no bcrypt_pbkdf). A
+ passphrase-protected key signs with signify itself.
+ c. VALID_UNTIL is REQUIRED; an expired index is refused. The
+ maintainer sets the lifetime; grepo defaults to 30 days.
+ d. Indexes are refreshed on every -f / -u. Offline, they refuse.
+ e. Everest's SECRET key lives outside the project tree:
+ ~/.config/everest/keys/everest-2026.sec (EVEREST_KEYS), made by
+ `everest.sh keygen`, mounted read-only into the build container.
+ The public key ships in everest-bootscripts (/etc/glacier/keys).
+ f. Base backups (KEEP_OLD) are refused by `syspkg -lO` for want of a
+ signature; accepted for now. The restore interface (KEEP_OLD's
+ "later") can trust backups glacier made in its root-owned backup
+ dir.
diff --git a/ROADMAP.txt b/ROADMAP.txt
index 7ea18e0..b05a16a 100644
--- a/ROADMAP.txt
+++ b/ROADMAP.txt
@@ -2,13 +2,14 @@
| Glacier roadmap |
+--------------------------------------------+
-What's left to build, in priority order. Status as of 2026-10-04.
+What's left to build, in priority order. Status as of 2026-10-07.
Design detail lives in the companion docs; this file says WHAT and in
what order, and links to WHERE:
SECURITY_DESIGN.txt integrity, scope separation, refusal matrix
PACKAGE_SCOPES.txt layout, link routing, /etc config handling
IMAGE_DESIGN.txt system images as bootstrap output, sysroot
+ REPO_DESIGN.txt remote repositories, signing, keyring
GLACIER_BOOTSTRAP.txt glacier-bootstrap plan, prerequisites, decisions
Mark an item [DONE yyyy-mm-dd] rather than deleting it, so the history
@@ -23,7 +24,9 @@ file tracks work.
The critical path: from "QEMU boots an initramfs assembled by a script"
(~/Projects/everest) to "glacier installs a system".
- 1.1 sysroot in the library (IMAGE_DESIGN section 3). Every path is
+ 1.1 [DONE 2026-10-05: see CLAUDE.md "Install root / sysroot" and
+ tests/sysroot_alpine.sh.]
+ sysroot in the library (IMAGE_DESIGN section 3). Every path is
hardcoded to /glacier and /usr. Needs separate install-root
(where bytes are written) and runtime-root (what symlink targets
say). The trap: prune_stale_links follows absolute links, so a
@@ -31,20 +34,58 @@ The critical path: from "QEMU boots an initramfs assembled by a script"
filesystem. Required test: an image build leaves the host's /usr
and /glacier byte-identical.
- 1.2 syspkg --root DIR (refuses unless DIR and its ancestors are
+ 1.2 [DONE 2026-10-05: -R/--root, strict by default under -R, --strict.]
+ syspkg --root DIR (refuses unless DIR and its ancestors are
root-owned and not group/world-writable) and --strict (any link
refusal fails the transaction; an image's /usr starts empty).
gl_create_fs_tree already takes a root prefix.
- 1.3 glacier-bootstrap, local mode (GLACIER_BOOTSTRAP.txt sections 1-6).
+ 1.3 [DONE 2026-10-06: glacier-ng/glacier-bootstrap, GLACIER_BOOTSTRAP
+ section 10; Everest's initramfs is built with it and boots.]
+ glacier-bootstrap, local mode (GLACIER_BOOTSTRAP.txt sections 1-6).
Template index -> resolve -> verify -> syspkg --root, one
transaction. Never writes into TARGET itself.
+ [DONE 2026-10-07: repositories. TARGET's repos.conf + keys come
+ from a package (Everest: everest-repos); bootstrap then runs
+ syspkg -R TARGET -y, which reads TARGET's own keyring; exit 7 on
+ a failed fetch. GLACIER_BOOTSTRAP section 10. Later, maybe: an
+ opt-in copy of the host's repos.conf (pacstrap's mirrorlist).]
- 1.4 Packages for glacier itself, so an installed system can manage
+ 1.4 [DONE 2026-10-06: built by ~/Projects/everest/scripts/container.sh,
+ in templates/minimal.template, boot-verified in QEMU; full
+ regression passes.]
+ Packages for glacier itself, so an installed system can manage
itself: luajit, libarchive (+ zlib), libconfig, libglacier-ng,
glacier-ng (syspkg, gpkg, gstore), doas.
+ Decided 2026-10-06:
+ - base: luajit, zlib, libarchive, libconfig, libglacier-ng,
+ glacier-ng. extra: doas (replaceable, e.g. by sudo).
+ - glacier-ng ships syspkg, gpkg and gstore only. glacier-bootstrap,
+ gbuild, gworld and gsha256sum become a later "glacier-tools"
+ package in extra (after the release, which brings a real
+ install Makefile).
+ - everest-bootscripts ships /etc/doas.conf: `permit :wheel`, plus
+ `permit nopass :wheel as root cmd /usr/bin/gstore args -n`.
+ - Layout: scripts + lib/ in usr/lib/glacier-ng/, usr/bin/{syspkg,
+ gpkg,gstore} symlink into it (realpath lands in the store, lib/
+ beside it); gpkg moves to the same real-path loading.
+ As built: zlib 1.3.2, libarchive 3.8.9 (gzip + tar only; every
+ other codec, crypto library and front-end disabled), libconfig
+ 1.8.2 (no C++), luajit 2.1.1788856981 (commit c6ffc141),
+ libglacier-ng 4.0.0, glacier-ng 4.0.0, doas 6.8.2 (OpenDoas, no
+ PAM, no timestamp). The doas.conf rule is given for both `cmd
+ gstore` and `cmd /usr/bin/gstore`: OpenDoas matches what was typed.
+ luajit is linked -static-libgcc, since Everest ships no libgcc_s
+ (see section 6).
- 1.5 The installer. Undesigned; needs its own design session
+ 1.5 [DEFERRED 2026-10-06 by the user: manual installation is one of
+ Everest's main features, so the installer comes later. 3.1 is
+ next.]
+ [2026-10-07, the user: a manual install works from a live Everest
+ system or any Linux host; the live system is the preferred,
+ officially supported way. The user is trying the first disk
+ install (QEMU) themselves.]
+ The installer. Undesigned; needs its own design session
(SECURITY_DESIGN section 10). Partitioning, a bootloader or an
EFI-stub kernel on the ESP, copying the kernel there (a symlink
won't do on FAT), a disk image.
@@ -72,16 +113,35 @@ The critical path: from "QEMU boots an initramfs assembled by a script"
- A restore interface for backups: later. (A backup can already
be reinstalled with -lO.)
- `gpkg -u` / `syspkg -u` = upgrade from a REMOTE repository;
- waits for 3.1, like -f.
+ waits for 3.1, like -f. [DONE 2026-10-07: REPO_DESIGN
+ section 6, "As implemented".]
- /etc follows the existing config rules regardless.
- 2.2 gpkg -f / syspkg -f: MERGE. Spec (2026-10-04): exactly like -l,
+ 2.2 [DONE 2026-10-07: REPO_DESIGN section 6, "As implemented".]
+ gpkg -f / syspkg -f: MERGE. Spec (2026-10-04): exactly like -l,
except the package comes from a remote repository by name
instead of from a local file. Same summary, verification, scope
rules and transaction. Depends on 3.1 (remote repositories +
signing): until then there is nothing safe to fetch from.
- 2.3 Dependencies. PKG_DEPS / PKG_CONFS are parsed and ignored.
+ 2.3 Dependencies. [DONE 2026-10-05; automatic closure DONE
+ 2026-10-07 with -f/-u (REPO_DESIGN section 6).] Spec (2026-10-05), all three checks in one pass, no override flag,
+ every problem reported (one line each) before failing:
+ - A dependency's PKG_VER is a MINIMUM (installed >= PKG_VER).
+ Matched by name, any repo.
+ - A user package's dependency may be met by system scope or the
+ user's own scope. syspkg -x checks system-scope dependents
+ only.
+ - Conflicts: both directions (new lists installed, or installed
+ lists new), by name; PKG_VER is ignored.
+ - Missing dependencies REFUSE until 3.1; then automatic closure
+ (GLACIER_BOOTSTRAP 9e).
+ - Index records auto vs explicit now: /.glacier-auto in the
+ store, index line repo::name::version::auto|explicit.
+ - Messages:
+ extra::foo requires bar >= 1.2.0, which is not installed.
+ Removing base::musl breaks extra::foo.
+ extra::foo conflicts with extra::bar.
- install: refuse if a dependency isn't installed (or in the
same transaction);
- REMOVE: refuse to remove a package others depend on. Today
@@ -89,7 +149,7 @@ The critical path: from "QEMU boots an initramfs assembled by a script"
the system. Highest-value item in this section;
- conflicts: refuse to install a package that conflicts with
an installed one.
- Check-only vs automatic resolution: GLACIER_BOOTSTRAP decision 9e.
+ 9e DECIDED 2026-10-05: automatic closure, after 3.1 (see above).
2.4 Queries: list installed packages, show a package's details, find
which package owns a file. The index has the data; no CLI shows it.
@@ -98,7 +158,9 @@ The critical path: from "QEMU boots an initramfs assembled by a script"
deletes it. Keep it (or the last N), plus a command to return to
it. Interacts with 2.1's KEEP_OLD.
- 2.6 File conflicts between packages: two packages shipping the same
+ 2.6 [DONE 2026-10-05: refused in every scope, before the prompt and at
+ commit (deps.c check_collisions).]
+ File conflicts between packages: two packages shipping the same
path — whichever links last silently wins. Detect and refuse.
2.7 glacier.conf parsing. [DONE 2026-10-04 for integers:
@@ -121,7 +183,14 @@ The critical path: from "QEMU boots an initramfs assembled by a script"
+--------------------------------------------+
3.1 Remote repositories + signing (authenticity, keyring). The agreed
- next major item; separate design pass. Unblocks 2.2 and remote
+ next major item. Design: REPO_DESIGN.txt (2026-10-06; decisions in
+ sections 1 and 10, order of work section 9). [IN PROGRESS
+ 2026-10-06: steps 1 and 2 DONE (gsign; the local base rule,
+ repos.conf + keyring, gbuild -k, bootstrap carries .sig, Everest
+ signs with everest-2026) and 3 (grepo; the signed index;
+ syspkg/gpkg -y refresh with rollback state) DONE; step 4 (-f with
+ closure, -u, the cache) DONE 2026-10-07; step 5 (glacier-
+ bootstrap -r) next, then 6 (user repos).] Unblocks remote
glacier-bootstrap. Until then, GLACIER_BOOTSTRAP's pinned
templates are the safe bridge for remote fetches.
@@ -145,6 +214,10 @@ The critical path: from "QEMU boots an initramfs assembled by a script"
exists in the tree (only one gbuild, in glacier-ng). Today the
target libc is decided by where gbuild runs (~/Projects/everest
builds in Alpine for musl).
+ [2026-10-07, the user: Everest will run under glibc as well as
+ musl, so profiles are a real requirement, not a nicety. The
+ libc is picked at install time (Gentoo/Void style), one per
+ system. Open: how packages are kept apart (section 6).]
4.2 Move ~/Projects/everest's per-package gbuild commands into gworld
recipes once 4.1 lands.
@@ -164,18 +237,88 @@ The critical path: from "QEMU boots an initramfs assembled by a script"
- Images are only ever bootstrap OUTPUT (IMAGE_DESIGN 9a /
GLACIER_BOOTSTRAP 9a).
- - Template index format (GLACIER_BOOTSTRAP 9b).
- - Dependencies: check-only or automatic (GLACIER_BOOTSTRAP 9e).
- Where the pinning helper lives (GLACIER_BOOTSTRAP 9f).
- 2.1's open questions a-d (upgrade backups).
- extra vs community: should they ever behave differently?
- - The installer (1.5).
+ - The installer (1.5), deferred.
+ - libgcc (1.4): luajit is built with -static-libgcc because Everest
+ has no libgcc_s. Claude's call, not yet reviewed; the alternative is
+ a GCC runtime package in base, which later C++/unwinding packages
+ would need anyway.
+ - glibc and musl (4.1): DECIDED 2026-10-07 that the user picks the
+ libc at install time, like Gentoo's profiles / Void's glibc and musl
+ ISOs; one libc per system. Still open: how packages are kept apart
+ (Claude recommends both per-libc repos, as Void, AND a libc field in
+ manifest + index that the library refuses on a mismatch with the
+ system's recorded libc).
+--------------------------------------------+
| 7. Done |
+--------------------------------------------+
+ [DONE 2026-10-07] -f and -u from repositories (3.1 step 4; 2.1's -u,
+ 2.2, 2.3's closure): first repo serving a name wins; missing deps
+ added auto, too-old ones upgraded; downloads in /cache/REPO,
+ hashed against the signed index, manifest must match the entry,
+ deleted after commit. tests/fetch_alpine.sh (84 checks, 17 mutants).
+
+ [DONE 2026-10-06] Packages for glacier itself (1.4): zlib, libarchive,
+ libconfig, luajit, libglacier-ng, glacier-ng in base, doas in extra;
+ everest-bootscripts 1.1.0 ships /etc/doas.conf. Everest's initramfs
+ installs all 11 packages; in QEMU, syspkg/gpkg run from the store,
+ syspkg -x musl is refused (dependents), a wheel user runs doas
+ gstore -n without a password, then gpkg -l. gpkg loads lib/ from its
+ realpath. tests/installed_alpine.sh (30 checks) installs glacier in
+ that layout and runs it from the store. Found on the way: gbuild's
+ clone warning broke builds from a commit hash (CLAUDE.md bug 28);
+ syspkg/gstore never checked the ancestors of libglacier-ng's store
+ dir, which the installed .so's live in (bug 29; both mutations
+ caught).
+ [DONE 2026-10-07] glacier-bootstrap configures repositories (1.3):
+ syspkg -R's -y/-f/-u use TARGET's own repos.conf and keys (each
+ file's real path must stay inside TARGET); bootstrap runs
+ syspkg -R T -y when T's repos.conf gives a url (exit 7 on failure,
+ a warning when none does). Everest: new base package everest-repos
+ 1.0.0 (/etc/glacier/repos.conf + everest-2026.pub), moved out of
+ everest-bootscripts (now 1.3.0). bootstrap_alpine.sh 100 checks
+ (6 mutants, all caught); repo/fetch suites updated; full regression
+ green; Everest rebuilt and booted.
+ [DONE 2026-10-06] glacier-bootstrap, local mode (1.3): template
+ (NAME=, PKG=name ver [sha256:pin]; strict), verified private copies
+ handed to syspkg -R in one transaction, template-level dependency /
+ conflict / name checks, -n, -t. tests/bootstrap_alpine.sh (77
+ checks, 11 mutations caught). ~/Projects/everest's initramfs is now
+ a glacier-installed system (templates/minimal.template), booted in
+ QEMU: /glacier/sys, merged-/usr links into the store, /etc copied,
+ a non-root user runs busybox.
+
+ [DONE 2026-10-05] syspkg -R/--root DIR + --strict (1.2) and file
+ collisions (2.6): gl_check_sysroot (root-owned, non-symlink, not
+ group/world-writable, every ancestor), ctx->strict (a would-be link
+ refusal fails the transaction before the prompt and at commit; -O
+ still displaces), two packages shipping one path refused in every
+ scope. tests/root_alpine.sh (50 checks, 7 mutations caught). gpkg
+ silently ignored unknown flags (CLAUDE.md bug 27).
+ [DONE 2026-10-05] sysroot in the library (1.1): *_at contexts and
+ gl_init_sys_at, install paths in the context with runtime translation
+ at link/prune/displace, /usr and /etc under the sysroot, user scope
+ refuses one. tests/sysroot_alpine.sh (33 checks, 8 mutations caught).
+ Found on the way: live-context removal deleted foreign files
+ (CLAUDE.md bug 26).
+ [DONE 2026-10-05] Dependencies and conflicts (2.3): src/deps.c
+ (gl_check_plan before the prompt; gl_check_txn enforced by
+ gl_commit_transaction; live-context installs/removals checked inside
+ gl_install_pkg/gl_remove_pkg), index field 4 auto|explicit +
+ .glacier-auto + gl_set_pkg_auto, gl_read_pkg_manifest split out of
+ gl_install_pkg. tests/deps_alpine.sh (41 checks, 8 mutations caught).
+ Found on the way: the live index was rewritten through a seeded
+ hardlink (CLAUDE.md bug 25), tests/test_index_abort.lua.
+ [DONE 2026-10-05] System scope world-readable (found while specifying
+ 2.3): /glacier/sys was root 0700 throughout, so non-root users could
+ run nothing syspkg installed. Dirs 0755 / index 0644, and created
+ modes no longer depend on the umask. tests/sys_perms_alpine.sh.
+
[DONE 2026-10-04] Version replacement + backups (2.1): -lO replaces,
KEEP_OLD backups, gl_backup_pkg / gl_prune_backups / gl_conf_get_int,
the manifest saved in the store (.glacier-manifest), add_to_archive
diff --git a/SECURITY_DESIGN.txt b/SECURITY_DESIGN.txt
index 7909ba0..a56ef01 100644
--- a/SECURITY_DESIGN.txt
+++ b/SECURITY_DESIGN.txt
@@ -27,6 +27,9 @@ 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.
+[2026-10-06: that phase is REPO_DESIGN.txt. Its first part is in: a
+base package installed with syspkg -l needs a signed sidecar
+(REPO_DESIGN section 7).]
Everest is a security-focused distro (this is also *why* per-user
package isolation exists in the first place). Two sides of the CIA
@@ -209,6 +212,17 @@ refusing otherwise. The actual privilege boundary is enforced by doas
— a hardened, audited, purpose-built tool for exactly this — not
reimplemented inside syspkg itself.
+LATER AMENDMENT (loading, as built): syspkg and gstore run under
+`luajit -E`, and pin package.path and every ffi.load to their own
+realpath's lib/. Before loading anything as root, each file they load
+(every .lua, every .so, including those pulled in through $ORIGIN) and
+lib/ itself must be root-owned and not group/world-writable, and so
+must every ancestor of the script's realpath AND of each loaded file's
+realpath. The second set matters once installed: lib/glacier is a
+symlink to /usr/lib/glacier, whose entries link into libglacier-ng's
+store directory, a tree the script's own ancestors never reach
+(2026-10-06, CLAUDE.md bug 29).
+
+--------------------------------------------+
| 7. Shared code: gpkg_common.lua |
diff --git a/SECURITY_REVIEW_2026-07-16.md b/SECURITY_REVIEW_2026-07-16.md
index dec0589..d21ba06 100644
--- a/SECURITY_REVIEW_2026-07-16.md
+++ b/SECURITY_REVIEW_2026-07-16.md
@@ -27,6 +27,12 @@ root, or shadows real `/usr` binaries."
* **Severity:** High
* **Category:** `authorization-bypass`
* **Confidence:** High
+* **Status (2026-10-06):** closed for local installs by recommendation
+ (b): `syspkg -l` of a package whose manifest says base requires its
+ sidecar signed by a key `/etc/glacier/repos.conf` lists for base
+ (REPO_DESIGN.txt section 7); user scope refuses base outright. Remote
+ packages get (a) with the signed index (REPO_DESIGN section 4). Still
+ open for a third-party caller of `gl_install_pkg` (ROADMAP 3.2).
**Description:** System-scope symlink placement into `/usr` is gated only
by a package's manifest self-declaring `PKG_REPO="base"`. There is no
diff --git a/SECURITY_REVIEW_2026-10-06.md b/SECURITY_REVIEW_2026-10-06.md
new file mode 100644
index 0000000..2911d07
--- /dev/null
+++ b/SECURITY_REVIEW_2026-10-06.md
@@ -0,0 +1,62 @@
+# Security review, 2026-10-06
+
+Scope: every active source file in libglacier-ng (`src/*.c`, `src/*.h`,
+not the vendored Monocypher, not the unbuilt early prototypes) and
+glacier-ng (`gpkg`, `syspkg`, `gstore`, `glacier-bootstrap`, `gbuild`,
+`gworld`, `lib/*.lua`, `tests/mkpkg`, the Dockerfiles). Read line by line;
+findings confirmed by running them in Docker where possible; each fix
+has a regression test, mutation-checked unless noted.
+
+Tests: `glacier-ng/tests/audit_alpine.sh` (shell, `glacier-alpine_root`)
+and `glacier-ng/tests/test_abort_unlocked.lua` (FFI suite).
+
+## Findings and fixes
+
+| # | Severity | Where | Finding | Fix | Test |
+|---|---|---|---|---|---|
+| 1 | **High** | `gl_install_pkg` | A package could ship top-level `files/.glacier-*` entries, glacier's own store files. As a symlink, `.glacier-fresh` made syspkg `open(O_CREAT\|O_TRUNC)` its target as root: an unsigned extra package truncated `/root/victim` to 0 bytes (reproduced). `.glacier-overwrite` granted the package `-O` without the user asking; `.glacier-manifest` rewrote its saved manifest. | Top-level names starting `.glacier-` are refused (`GL_MARKER_PREFIX`, common.h); the marker writes use `O_NOFOLLOW`. | audit_alpine: reserved names (mutants c3, c4 caught) |
+| 2 | Medium | `gl_rmdir_recursive` (common.h) | `lstat` then `opendir` by path: root deleting a tree a user can write to (`gstore -x UID`) could be steered, by swapping a directory for a symlink between the two calls, into emptying the symlink's target. | fd-relative walk: `openat(O_DIRECTORY\|O_NOFOLLOW)`, `fdopendir`, `unlinkat`; the top path must not be a symlink. | audit_alpine: `gstore -x` race, 40 runs (mutant c1 caught 34-38/40) |
+| 3 | Medium | `add_to_archive` (istoreutils.c) | Same class: `lstat` then `open`/`opendir` by path, no `O_NOFOLLOW`. Root backing up a user's store (`gstore -b`) could be steered into archiving root-only files. | fd-relative (`fstatat`/`openat(O_NOFOLLOW\|O_NONBLOCK)`/`readlinkat`); the opened descriptor's type is what gets archived. | **No failing-on-old test**: the window is too narrow for a shell race to hit (mutant c2 survived). Verified by review only. |
+| 4 | Medium | `gl_gpm2gpkg` (pkg.c) | Manifests come from untrusted archives; libconfig honours `@include "FILE"`, so a manifest could make syspkg read any file as root. | A manifest with a line starting `@include` is refused (read into memory first, 1 MiB cap, no NUL). | audit_alpine (mutant c5 caught) |
+| 5 | Low | `gl_gpm2gpkg` | A dependency/conflict entry without `PKG_NAME` ended the list early (later entries silently ignored, and leaked); names were not checked. | Every entry needs a `gl_valid_pkg_ident` name, else the manifest is refused. | audit_alpine (mutants c6, c7 caught) |
+| 6 | Low | `gpkg_common.lua` | `read_manifest_fields` pattern-matched the manifest (via `tar` as root) and could disagree with the library: a decoy `// PKG_REPO = "community";` line hid that a package replaced an installed one, skipping the `-O` rule and the KEEP_OLD backup (two versions in the store). The tar extraction itself was not exploitable (GNU and busybox tar strip `..`; checked). | Replaced by `manifest_of`, a wrapper around `gl_read_pkg_manifest`; an unreadable manifest is refused before the prompt. | audit_alpine (old file as mutant l1, caught) |
+| 7 | Low | `gstore`, `gpkg` | Text concatenated into `lg_printf` format strings: gstore's `argv[0]` and UID (as root), gpkg `-u`'s operand. | Passed as `%s` arguments; the `-u`/`-f`/`-s` stubs now refuse in one line. | audit_alpine (mutants l2, l3 caught) |
+| 8 | Low | `gstore` | A UID above uid_t's range wrapped around in C to another UID. | Refused (`0..4294967294`). | audit_alpine (mutant l4 caught) |
+| 9 | Low | `gl_abort_transaction` | Wiped the stage even for a context that never got the lock, i.e. another process's running transaction. | Only the lock holder's abort removes the stage (`GL_TXN_ERR_NO_TXN` otherwise). | test_abort_unlocked.lua (mutant f8 caught) |
+| 10 | Low | `gpkg_common.lua` | Lua numbers passed to `lg_printf("%d")` (a double into varargs: undefined behaviour). | `%s` + `tostring`. | none (not observable) |
+| 11 | Low | `gl_parse_semver` | `sscanf("%d")` on an untrusted, oversized number is undefined behaviour. | `strtol`, clamped to `0..INT_MAX`. | none (not observable) |
+| 12 | Hardening | `is_glacier_symlink` | A plain prefix match: `/glacier/sys/store` also matched `/glacier/sys/store2/...` (only root can create such paths). | The prefix must be followed by `/` or end the target. | none |
+| 13 | Bug | `join_with_cwd` (pkg.c) | `strlen(cwd + strlen(path) + 2)`: an out-of-bounds read. Unused. | Removed (see below). | n/a |
+| 14 | Hardening | `lg_printf` | No format checking for C callers. | `__attribute__((format(printf, 2, 3)))`; the build is clean under `-Wformat=2 -Werror`. | the build |
+
+Reviewed and found sound: signature code (`sign.c`: strict signify
+parsing, secrets wiped, `O_NOFOLLOW|O_EXCL` writes), the index parser,
+extraction's tar-slip defences (`is_safe_rel_path`, deferred symlinks,
+`crosses_symlink`, `O_NOFOLLOW`), the sysroot trust check, syspkg's and
+gstore's hardened loading, the private-copy verification, shell quoting
+in every `os.execute`/`io.popen`, `/etc` config handling, `-O` displace
+and restore.
+
+## Removed as unused or redundant
+
+- libglacier-ng: the `dag`, `runtime` and `make_conf` modules (nothing
+ linked them); `gl_pkgver`, `gl_mkdir`, `gl_spawn_pkg`, `gl_cat_dir`,
+ `gl_cat_dir_and_ver`, `gl_return_dir`, `gl_return_dir_and_ver`,
+ `gl_free_path`, `gl_free_dir`, `gl_show_pkg_props`, `join_with_cwd`,
+ `empty`/`null_dep`/`null_conf`, `dir_t`; `gl_parse_index`,
+ `gl_free_index` (and `struct gindex`), `gl_find_pkg_repo` (duplicate of
+ `gl_find_pkg_repos`), `gl_sys_istore_exists`, `gl_restore_istore`
+ (also unsafe: followed symlinks when restoring); `lg_printf_no_newline`,
+ `lg_syslog`, `lg_log` (its declaration and definition disagreed),
+ `p2s`; `GL_CONF_PATH`; the broken `install_test_dir` Makefile target.
+ The marker names, defined twice (deps.c, istoreutils.c), now live once
+ in common.h; log.c includes log.h instead of redefining its enum.
+- glacier-ng: `M.read_manifest_fields`, `M.extract_pkg_info`,
+ `M.summary`; `install_with_reinstall` is module-local; the matching
+ `glacier_cdef.lua` declarations.
+
+## Open
+
+- Finding 3 has no test that fails on the old code (see the table).
+- Extracted entries keep setuid/setgid bits (known gap, unchanged).
+- No size limits on extraction (a decompression bomb fills the disk).
diff --git a/build/bin/gsha256sum b/build/bin/gsha256sum
deleted file mode 100755
index 8c74a0f..0000000
Binary files a/build/bin/gsha256sum and /dev/null differ
diff --git a/build/include/glacier/log.h b/build/include/glacier/log.h
deleted file mode 100644
index 0521ae1..0000000
--- a/build/include/glacier/log.h
+++ /dev/null
@@ -1,41 +0,0 @@
-#ifndef LOG_H_
-#define LOG_H_
-
-#define _POSIX_C_SOURCE 200809L
-#define _DEFAULT_SOURCE
-#define _XOPEN_SOURCE 700
-
-#include
-#include
-#include
-#include
-#include
-
-#include "color.h"
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-typedef enum
-{
- LG_INFO,
- LG_WARN,
- LG_ERR,
- LG_SUCCESS
-}
-loglevel_t;
-
-int supports_utf8(void);
-const char *p2s(int priority);
-
-void lg_printf(loglevel_t loglevel, const char *fmt, ...);
-void lg_printf_no_newline(loglevel_t loglevel, const char *fmt, ...);
-void lg_syslog(int priority, const char *fmt, ...);
-void lg_log(bool use_syslog, FILE *logfile, int priority, const char *fmt, ...);
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif /* LOG_H_ */
diff --git a/build/lib/shared/libglacier_dag.so b/build/lib/shared/libglacier_dag.so
deleted file mode 100755
index 0f61c7d..0000000
Binary files a/build/lib/shared/libglacier_dag.so and /dev/null differ
diff --git a/build/lib/shared/libglacier_istoreutils.so b/build/lib/shared/libglacier_istoreutils.so
deleted file mode 100755
index 480cade..0000000
Binary files a/build/lib/shared/libglacier_istoreutils.so and /dev/null differ
diff --git a/build/lib/shared/libglacier_log.so b/build/lib/shared/libglacier_log.so
deleted file mode 100755
index 1f3806e..0000000
Binary files a/build/lib/shared/libglacier_log.so and /dev/null differ
diff --git a/build/lib/shared/libglacier_pkg.so b/build/lib/shared/libglacier_pkg.so
deleted file mode 100755
index 09c019f..0000000
Binary files a/build/lib/shared/libglacier_pkg.so and /dev/null differ
diff --git a/build/lib/shared/libglacier_transaction.so b/build/lib/shared/libglacier_transaction.so
deleted file mode 100755
index f367968..0000000
Binary files a/build/lib/shared/libglacier_transaction.so and /dev/null differ
diff --git a/build/lib/shared/libglacier_verify.so b/build/lib/shared/libglacier_verify.so
deleted file mode 100755
index 9ba87ef..0000000
Binary files a/build/lib/shared/libglacier_verify.so and /dev/null differ
diff --git a/build/lib/tmp/log.o b/build/lib/tmp/log.o
deleted file mode 100644
index 12351dd..0000000
Binary files a/build/lib/tmp/log.o and /dev/null differ
diff --git a/build/lib/tmp/make_conf.o b/build/lib/tmp/make_conf.o
deleted file mode 100644
index 03ea142..0000000
Binary files a/build/lib/tmp/make_conf.o and /dev/null differ
diff --git a/build/lib/tmp/runtime.o b/build/lib/tmp/runtime.o
deleted file mode 100644
index 3c35911..0000000
Binary files a/build/lib/tmp/runtime.o and /dev/null differ
diff --git a/build/lib/tmp/sha256.o b/build/lib/tmp/sha256.o
deleted file mode 100644
index e2beb7a..0000000
Binary files a/build/lib/tmp/sha256.o and /dev/null differ
diff --git a/config.mk b/config.mk
index d57e28c..66759d1 100644
--- a/config.mk
+++ b/config.mk
@@ -6,4 +6,3 @@ PREFIX ?= /usr
CFLAGS = -std=c11 -pedantic -O2 -Wall -Wextra -Wshadow -Wformat=2 -Wconversion -Wpedantic -Werror -Wno-format-truncation
-TEST_DIR = /home/lw/Projects/glacier-ng/lib
diff --git a/src/common.h b/src/common.h
index e165967..5250188 100644
--- a/src/common.h
+++ b/src/common.h
@@ -6,6 +6,7 @@
#include
#include
+#include
#include
#include
#include
@@ -13,13 +14,6 @@
#include
#include
-typedef struct
-{
- const char *path;
- mode_t mode;
-}
-dir_t;
-
/*
* On-disk layout. Each scope keeps everything it ever creates, renames
* or locks under ONE base directory it owns:
@@ -35,6 +29,21 @@ dir_t;
* an unprivileged user run their own transactions at all.
*/
#define GL_USR_ROOT "/glacier/usr"
+
+/* In a package's version dir: "installed as a dependency, not by name".
+ * Dot-prefixed like the other store markers, so seeding carries it and
+ * linking skips it; the index's 4th field is derived from it. */
+#define GL_AUTO_MARKER ".glacier-auto"
+/* The other files glacier keeps in a version dir (see gl_install_pkg):
+ * the package's saved manifest, and the one-shot markers for the next
+ * link pass (this transaction installed it; it may displace, -O). A
+ * package may not ship any top-level name starting with GL_MARKER_PREFIX:
+ * these are glacier's, and one shipped as a symlink would redirect
+ * glacier's own writes. */
+#define GL_MARKER_PREFIX ".glacier-"
+#define GL_MANIFEST_COPY ".glacier-manifest"
+#define GL_FRESH_MARKER ".glacier-fresh"
+#define GL_OVERWRITE_MARKER ".glacier-overwrite"
#define GL_SYS_ROOT "/glacier/sys"
static inline void
@@ -78,6 +87,98 @@ gl_usr_base_check(uid_t uid)
return GL_USR_BASE_OK;
}
+/* An install root (gl_context_t.sysroot) in canonical form: NULL, "" and
+ * "/" mean the running system and give ""; anything else must be an
+ * absolute path with no "." or ".." components, and loses its trailing
+ * slashes. Room is left for the paths built under it. Returns -1 if
+ * it's refused. */
+static inline int
+gl_normalize_sysroot(const char *in, char *out, size_t outsz)
+{
+ out[0] = '\0';
+ if (!in || in[0] == '\0') { return 0; }
+ if (in[0] != '/') { return -1; }
+
+ size_t len = strlen(in);
+ while (len > 0 && in[len - 1] == '/') { len--; }
+ if (len == 0) { return 0; } /* "/" or "//" */
+ if (len + 256 > outsz || len + 256 > PATH_MAX) { return -1; }
+
+ for (size_t i = 0; i < len; ) {
+ size_t j = i + 1; /* in[i] == '/' */
+ while (j < len && in[j] != '/') { j++; }
+ size_t clen = j - i - 1;
+ const char *comp = in + i + 1;
+ if (clen == 0 || (clen == 1 && comp[0] == '.') ||
+ (clen == 2 && comp[0] == '.' && comp[1] == '.')) {
+ return -1;
+ }
+ i = j;
+ }
+
+ memcpy(out, in, len);
+ out[len] = '\0';
+ return 0;
+}
+
+/* Is a (normalized, non-empty) install root safe for root to write into?
+ * It and every directory above it must be a real directory (not a
+ * symlink), owned by root, and not group/world-writable: otherwise a
+ * user could swap part of the tree for a symlink to the host's /usr
+ * between two syscalls (IMAGE_DESIGN section 4). Returns 0 if trusted;
+ * otherwise -1, with the offending path in `bad` and `why` set to
+ * "a symlink", "not a directory", or "not root-owned, or is
+ * group/world-writable". */
+static inline int
+gl_sysroot_check_one(const char *path, char *bad, size_t badsz, const char **why)
+{
+ struct stat st;
+ if (lstat(path, &st) != 0) {
+ *why = "missing";
+ } else if (S_ISLNK(st.st_mode)) {
+ *why = "a symlink";
+ } else if (!S_ISDIR(st.st_mode)) {
+ *why = "not a directory";
+ } else if (st.st_uid != 0 || (st.st_mode & 022) != 0) {
+ *why = "not root-owned, or is group/world-writable";
+ } else {
+ return 0;
+ }
+ snprintf(bad, badsz, "%s", path);
+ return -1;
+}
+
+static inline int
+gl_sysroot_trusted(const char *root, char *bad, size_t badsz, const char **why)
+{
+ if (gl_sysroot_check_one("/", bad, badsz, why) != 0) { return -1; }
+
+ char prefix[PATH_MAX];
+ snprintf(prefix, sizeof(prefix), "%s", root);
+ /* each ancestor, shortest first, then root itself */
+ for (char *p = prefix + 1; *p; p++) {
+ if (*p != '/') { continue; }
+ *p = '\0';
+ int rc = gl_sysroot_check_one(prefix, bad, badsz, why);
+ *p = '/';
+ if (rc != 0) { return -1; }
+ }
+ return gl_sysroot_check_one(prefix, bad, badsz, why);
+}
+
+/* mkdir -p. Every directory this creates gets exactly `mode`, whatever
+ * the caller's umask: a system store made under umask 077 would otherwise
+ * be unreachable for every user but root. Directories that already exist
+ * are left alone. */
+static inline int
+gl_mkdir_exact(const char *path, mode_t mode)
+{
+ if (mkdir(path, mode) == 0) {
+ return chmod(path, mode);
+ }
+ return errno == EEXIST ? 0 : -1;
+}
+
static inline int
gl_mkdirp(const char *path, mode_t mode)
{
@@ -88,52 +189,58 @@ gl_mkdirp(const char *path, mode_t mode)
for (char *p = tmp + 1; *p; p++) {
if (*p == '/') {
*p = '\0';
- if (mkdir(tmp, mode) == -1 && errno != EEXIST) {
+ if (gl_mkdir_exact(tmp, mode) != 0) {
return -1;
}
*p = '/';
}
}
- if (mkdir(tmp, mode) == -1 && errno != EEXIST) {
- return -1;
- }
-
- return 0;
+ return gl_mkdir_exact(tmp, mode);
}
+/* Empties the directory open at `fd` (and closes it). Every step is
+ * relative to a descriptor opened with O_NOFOLLOW, so nothing is ever
+ * resolved through a symlink: an entry swapped for a symlink mid-walk
+ * (root deleting a tree a user can write to, gstore -x) is unlinked
+ * itself, never followed into its target. */
+static inline void
+gl_empty_dir_fd(int fd)
+{
+ DIR *dir = fdopendir(fd);
+ if (!dir) {
+ close(fd);
+ return;
+ }
+
+ struct dirent *ent;
+ while ((ent = readdir(dir))) {
+ /* Only . and ..: dotfiles are content too (a package's own,
+ * the store's .displaced tree). */
+ if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) {
+ continue;
+ }
+ int sub = openat(dirfd(dir), ent->d_name,
+ O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
+ if (sub >= 0) {
+ gl_empty_dir_fd(sub);
+ unlinkat(dirfd(dir), ent->d_name, AT_REMOVEDIR);
+ } else {
+ unlinkat(dirfd(dir), ent->d_name, 0);
+ }
+ }
+ closedir(dir);
+}
+
+/* rm -r of a directory. `path` itself must be a directory, not a
+ * symlink to one (refused, -1). */
static inline int
gl_rmdir_recursive(const char *path)
{
- DIR *dir = opendir(path);
- if (!dir) { return -1; }
-
- struct dirent *ent;
- while ((ent = readdir(dir))) {
- /* Only . and .. — skipping every dotfile left any directory
- * containing one (a package's own dotfiles, the store's
- * .displaced tree) impossible to rmdir, so the old/ and stage
- * wipes silently failed and the next commit hit ENOTEMPTY. */
- if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) {
- continue;
- }
-
- char full[PATH_MAX];
- snprintf(full, sizeof(full), "%s/%s", path, ent->d_name);
-
- struct stat st;
- if (lstat(full, &st) != 0) { continue; }
-
- if (S_ISDIR(st.st_mode)) {
- gl_rmdir_recursive(full);
- }
- else {
- unlink(full);
- }
- }
-
- closedir(dir);
- return rmdir(path);
+ int fd = open(path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
+ if (fd < 0) { return -1; }
+ gl_empty_dir_fd(fd);
+ return rmdir(path);
}
diff --git a/src/dag.c b/src/dag.c
deleted file mode 100644
index 7957cb1..0000000
--- a/src/dag.c
+++ /dev/null
@@ -1,132 +0,0 @@
-#define _POSIX_C_SOURCE 200809L
-
-#include
-#include
-
-#include "dag.h"
-
-graph_t
-*gl_graph_create(void)
-{
- return calloc(1, sizeof(graph_t));
-}
-
-void
-gl_graph_free(graph_t *g)
-{
- if (!g) { return; }
-
- for (size_t i = 0; i < g->node_count; i++) {
- free(g->nodes[i]->edges);
- free(g->nodes[i]);
- }
-
- free(g->nodes);
- free(g);
-}
-
-graph_node_t
-*gl_graph_add_node(graph_t *g, void *data)
-{
- if (!g) { return NULL; }
-
- graph_node_t *n = calloc(1, sizeof(graph_node_t));
- if (!n) { return NULL; }
- n->data = data;
-
- graph_node_t **tmp = realloc(g->nodes, sizeof(*g->nodes) * (g->node_count + 1));
- if (!tmp) {
- free(n);
- return NULL;
- }
-
- g->nodes = tmp;
- g->nodes[g->node_count++] = n;
- return n;
-}
-
-void
-gl_graph_add_edge(graph_t *g, graph_node_t *from, graph_node_t *to)
-{
- if (!g || !from || !to) { return; }
- graph_node_t **tmp = realloc(from->edges, sizeof(*from->edges) * (from->edge_count + 1));
- if (!tmp) { return; }
- from->edges = tmp;
- from->edges[from->edge_count++] = to;
-}
-
-static bool
-dfs_cycle(graph_node_t *n)
-{
- n->visited = true;
- n->in_stack = true;
-
- for (size_t i = 0; i < n->edge_count; i++) {
- graph_node_t *child = n->edges[i];
- if (!child->visited && dfs_cycle(child)) {
- return true;
- }
- else if (child->in_stack) {
- return true;
- }
- }
-
- n->in_stack = false;
- return false;
-}
-
-bool
-gl_graph_has_cycle(graph_t *g)
-{
- if (!g) { return false; }
- for (size_t i = 0; i < g->node_count; i++) {
- g->nodes[i]->visited = false;
- g->nodes[i]->in_stack = false;
- }
- for (size_t i = 0; i < g->node_count; i++) {
- if (!g->nodes[i]->visited && dfs_cycle(g->nodes[i])) {
- return true;
- }
- }
- return false;
-}
-
-static void
-dfs_toposort(graph_node_t *n, void **result, size_t *index)
-{
- n->visited = true;
- for (size_t i = 0; i < n->edge_count; i++) {
- if (!n->edges[i]->visited) {
- dfs_toposort(n->edges[i], result, index);
- }
- }
-
- result[(*index)++] = n->data;
-}
-
-void
-**gl_graph_toposort(graph_t *g, size_t *out_count)
-{
- if (!g || !out_count) { return NULL; }
- if (gl_graph_has_cycle(g)) {
- fprintf(stderr, "Error: dependency cycle detected\n");
- return NULL;
- }
-
- *out_count = g->node_count;
- void **order = calloc(g->node_count, sizeof(void *));
- if (!order) { return NULL; }
-
- for (size_t i = 0; i < g->node_count; i++) {
- g->nodes[i]->visited = false;
- }
-
- size_t index = 0;
- for (size_t i = 0; i < g->node_count; i++) {
- if (!g->nodes[i]->visited) {
- dfs_toposort(g->nodes[i], order, (size_t *)&index);
- }
- }
-
- return order;
-}
diff --git a/src/dag.h b/src/dag.h
deleted file mode 100644
index 79f79e3..0000000
--- a/src/dag.h
+++ /dev/null
@@ -1,34 +0,0 @@
-#ifndef DAG_H_
-#define DAG_H_
-
-#define _POSIX_C_SOURCE 200809L
-
-#include
-#include
-
-typedef struct graph_node graph_node_t;
-typedef struct graph graph_t;
-
-struct graph_node {
- void *data;
- graph_node_t **edges;
- size_t edge_count;
- bool visited;
- bool in_stack;
-};
-
-struct graph {
- graph_node_t **nodes;
- size_t node_count;
-};
-
-graph_t *graph_create(void);
-void graph_free(graph_t *g);
-
-graph_node_t *graph_add_node(graph_t *g, void *data);
-void graph_add_edge(graph_t *g, graph_node_t *from, graph_node_t *to);
-
-bool graph_has_cycle(graph_t *g);
-void **graph_toposort(graph_t *g, size_t *out_count);
-
-#endif
diff --git a/src/deps.c b/src/deps.c
new file mode 100644
index 0000000..e04814e
--- /dev/null
+++ b/src/deps.c
@@ -0,0 +1,727 @@
+#define _POSIX_C_SOURCE 200809L
+#define _XOPEN_SOURCE 700
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "common.h"
+#include "deps.h"
+#include "istoreutils.h"
+#include "log.h"
+#include "pkg.h"
+
+/* Where one entry of a package lands once linked: its RUNTIME path (what
+ * a message names and what two packages are compared by). */
+struct dest {
+ char *path;
+ bool dir;
+};
+
+/* One installed (or about to be installed) package. repo/name/dirname
+ * come from the store layout (store///-), or from
+ * the manifest for a package that isn't installed yet. deps/confs come
+ * from the manifest; a version dir without one (installed before
+ * manifests were saved) has none. */
+struct rec {
+ char repo[NAME_MAX + 1];
+ char name[NAME_MAX + 1];
+ char dirname[NAME_MAX + 1];
+ semver_t ver;
+ gl_pkg_t pkg;
+ bool touched; /* added or replaced by this transaction */
+ bool gone; /* removed or replaced: not in the result */
+
+ /* For the file checks: where its files come from (a version dir in
+ * a store, or a .gpkg not installed yet), whether its link pass is
+ * this transaction's (fresh: it reports refusals), whether that
+ * pass may displace foreign files (-O), and, once computed, where
+ * every entry lands. */
+ char src[PATH_MAX];
+ bool from_archive;
+ bool fresh;
+ bool overwrite;
+ struct dest *dests;
+ size_t ndests;
+ bool dests_loaded;
+};
+
+/* What the file checks need to know about the transaction's context. */
+struct env {
+ gl_ctx_scope_t scope;
+ uid_t uid;
+ const char *sysroot;
+ int strict;
+ char store_rt[PATH_MAX]; /* live store, runtime path: link targets */
+ char store_at[PATH_MAX]; /* the store being checked, install path */
+};
+
+struct set {
+ struct rec *v;
+ size_t n;
+};
+
+/* A package the transaction removes outright (not replaced). */
+struct removed {
+ char repo[NAME_MAX + 1];
+ char name[NAME_MAX + 1];
+};
+
+static void
+set_free(struct set *s)
+{
+ for (size_t i = 0; i < s->n; i++) {
+ gl_free_pkg(&s->v[i].pkg);
+ gl_free_semver(&s->v[i].ver);
+ for (size_t k = 0; k < s->v[i].ndests; k++) { free(s->v[i].dests[k].path); }
+ free(s->v[i].dests);
+ }
+ free(s->v);
+ s->v = NULL;
+ s->n = 0;
+}
+
+static struct rec *
+set_add(struct set *s)
+{
+ struct rec *grown = realloc(s->v, (s->n + 1) * sizeof(*s->v));
+ if (!grown) { return NULL; }
+ s->v = grown;
+ memset(&s->v[s->n], 0, sizeof(*s->v));
+ return &s->v[s->n++];
+}
+
+static int
+semver_cmp(const semver_t *a, const semver_t *b)
+{
+ if (a->maj != b->maj) { return a->maj < b->maj ? -1 : 1; }
+ if (a->min != b->min) { return a->min < b->min ? -1 : 1; }
+ if (a->pat != b->pat) { return a->pat < b->pat ? -1 : 1; }
+ return 0;
+}
+
+/* Every installed package in `store`. A missing store is empty (nothing
+ * installed); any other failure to read it is an error, unless `lenient`
+ * (the system store as seen from user scope: unreadable means "nothing
+ * there to depend on", and the check then says what's missing). */
+static int
+load_store(const char *store, struct set *s, bool lenient)
+{
+ DIR *sd = opendir(store);
+ if (!sd) {
+ return (errno == ENOENT || lenient) ? 0 : -1;
+ }
+
+ int rc = 0;
+ struct dirent *re;
+ while (rc == 0 && (re = readdir(sd))) {
+ if (re->d_name[0] == '.') { continue; }
+
+ char repo_path[PATH_MAX];
+ snprintf(repo_path, sizeof(repo_path), "%s/%s", store, re->d_name);
+ DIR *rd = opendir(repo_path);
+ if (!rd) { continue; } /* not a directory */
+
+ struct dirent *ne;
+ while (rc == 0 && (ne = readdir(rd))) {
+ if (ne->d_name[0] == '.') { continue; }
+
+ char name_path[PATH_MAX];
+ snprintf(name_path, sizeof(name_path), "%s/%s", repo_path, ne->d_name);
+ DIR *nd = opendir(name_path);
+ if (!nd) { continue; }
+
+ struct dirent *ve;
+ while ((ve = readdir(nd))) {
+ if (ve->d_name[0] == '.') { continue; }
+
+ struct rec *r = set_add(s);
+ if (!r) { rc = -1; break; }
+ snprintf(r->repo, sizeof(r->repo), "%s", re->d_name);
+ snprintf(r->name, sizeof(r->name), "%s", ne->d_name);
+ snprintf(r->dirname, sizeof(r->dirname), "%s", ve->d_name);
+ snprintf(r->src, sizeof(r->src), "%s/%s", name_path, ve->d_name);
+
+ char manifest[PATH_MAX];
+ snprintf(manifest, sizeof(manifest), "%s/%s/" GL_MANIFEST_COPY,
+ name_path, ve->d_name);
+ if (gl_gpm2gpkg(manifest, &r->pkg) != 0) {
+ gl_free_pkg(&r->pkg);
+ memset(&r->pkg, 0, sizeof(r->pkg));
+ }
+
+ /* The version is the dir name minus "-". */
+ size_t prefix = strlen(ne->d_name) + 1;
+ r->ver = gl_parse_semver(strlen(ve->d_name) > prefix
+ ? ve->d_name + prefix : NULL);
+ break; /* one version per package */
+ }
+ closedir(nd);
+ }
+ closedir(rd);
+ }
+ closedir(sd);
+ return rc;
+}
+
+static const struct rec *
+find_live(const struct set *s, const char *repo, const char *name)
+{
+ for (size_t i = 0; i < s->n; i++) {
+ if (!s->v[i].gone && strcmp(s->v[i].repo, repo) == 0 &&
+ strcmp(s->v[i].name, name) == 0) {
+ return &s->v[i];
+ }
+ }
+ return NULL;
+}
+
+/* Is there a package named `name` (any repo) in the result or, from user
+ * scope, the system? With `min`, it must also be at least that version. */
+static bool
+provided(const struct set *res, const struct set *sys, const char *name,
+ const semver_t *min)
+{
+ const struct set *sets[] = { res, sys };
+ for (int k = 0; k < 2; k++) {
+ for (size_t i = 0; i < sets[k]->n; i++) {
+ const struct rec *r = &sets[k]->v[i];
+ if (r->gone || strcmp(r->name, name) != 0) { continue; }
+ if (!min || semver_cmp(&r->ver, min) >= 0) { return true; }
+ }
+ }
+ return false;
+}
+
+static bool
+lists_conflict(const struct rec *r, const char *name)
+{
+ if (!r->pkg.pkg_conflicts) { return false; }
+ for (const conf_t *c = r->pkg.pkg_conflicts; c->conf_name; c++) {
+ if (strcmp(c->conf_name, name) == 0) { return true; }
+ }
+ return false;
+}
+
+/* Does the transaction touch packages named `name`: add or replace one,
+ * or remove one outright? */
+static bool
+name_touched(const struct set *res, const struct removed *rm, size_t nrm,
+ const char *name)
+{
+ for (size_t i = 0; i < res->n; i++) {
+ if (res->v[i].touched && strcmp(res->v[i].name, name) == 0) { return true; }
+ }
+ for (size_t i = 0; i < nrm; i++) {
+ if (strcmp(rm[i].name, name) == 0) { return true; }
+ }
+ return false;
+}
+
+static void
+fmt_ver(const semver_t *v, char *out, size_t outsz)
+{
+ snprintf(out, outsz, "%d.%d.%d", v->maj, v->min, v->pat);
+}
+
+/*
+ * File checks (ROADMAP 2.6 and 1.2's --strict).
+ *
+ * Every entry of a package lands somewhere once linked; dest_of mirrors
+ * gl_link_pkg exactly: entries under a dot-prefixed name are never
+ * linked, a leading usr/ is stripped, base-repo packages in system scope
+ * land in /usr and everything else in the scope's links tree, and in
+ * system scope the top-level etc/ is copied into /etc.
+ */
+static bool
+has_dot_component(const char *rel)
+{
+ for (const char *c = rel; c; c = strchr(c, '/')) {
+ if (*c == '/') { c++; }
+ if (*c == '.') { return true; }
+ if (!*c) { break; }
+ }
+ return false;
+}
+
+static int
+dest_of(const struct env *e, const char *repo, const char *rel, char *out, size_t outsz)
+{
+ if (e->scope == GL_SCOPE_SYS &&
+ (strcmp(rel, "etc") == 0 || strncmp(rel, "etc/", 4) == 0)) {
+ return snprintf(out, outsz, "/%s", rel) >= (int)outsz ? -1 : 0;
+ }
+ if (strncmp(rel, "usr/", 4) == 0) { rel += 4; }
+
+ char base[PATH_MAX];
+ if (e->scope == GL_SCOPE_SYS) {
+ snprintf(base, sizeof(base), "%s", strcmp(repo, "base") == 0
+ ? "/usr" : GL_SYS_ROOT "/links");
+ } else {
+ char ub[PATH_MAX];
+ gl_usr_base(e->uid, ub, sizeof(ub));
+ snprintf(base, sizeof(base), "%s/links", ub);
+ }
+ return snprintf(out, outsz, "%s/%s", base, rel) >= (int)outsz ? -1 : 0;
+}
+
+static int
+add_dest(struct rec *r, const char *path, bool dir)
+{
+ for (size_t k = 0; k < r->ndests; k++) {
+ if (strcmp(r->dests[k].path, path) == 0) {
+ if (!dir) { r->dests[k].dir = false; }
+ return 0;
+ }
+ }
+ struct dest *grown = realloc(r->dests, (r->ndests + 1) * sizeof(*grown));
+ if (!grown) { return -1; }
+ r->dests = grown;
+ r->dests[r->ndests].path = strdup(path);
+ if (!r->dests[r->ndests].path) { return -1; }
+ r->dests[r->ndests].dir = dir;
+ r->ndests++;
+ return 0;
+}
+
+/* `rel` (and, for an archive entry, its parents, which tar needn't list) */
+static int
+add_rel(const struct env *e, struct rec *r, const char *rel, bool dir, bool parents)
+{
+ if (rel[0] == '\0' || has_dot_component(rel)) { return 0; }
+
+ char d[PATH_MAX];
+ if (parents) {
+ char buf[PATH_MAX];
+ snprintf(buf, sizeof(buf), "%s", rel);
+ for (char *p = buf; (p = strchr(p, '/')); p++) {
+ *p = '\0';
+ if (dest_of(e, r->repo, buf, d, sizeof(d)) != 0 || add_dest(r, d, true) != 0) {
+ return -1;
+ }
+ *p = '/';
+ }
+ }
+ if (dest_of(e, r->repo, rel, d, sizeof(d)) != 0) { return -1; }
+ return add_dest(r, d, dir);
+}
+
+static int
+walk_store_dir(const struct env *e, struct rec *r, const char *dir, size_t strip)
+{
+ DIR *d = opendir(dir);
+ if (!d) { return 0; }
+ int rc = 0;
+ struct dirent *ent;
+ while (rc == 0 && (ent = readdir(d))) {
+ if (ent->d_name[0] == '.') { continue; }
+ char full[PATH_MAX];
+ snprintf(full, sizeof(full), "%s/%s", dir, ent->d_name);
+ struct stat st;
+ if (lstat(full, &st) != 0) { continue; }
+ const char *rel = full + strip;
+ if (S_ISDIR(st.st_mode)) {
+ rc = add_rel(e, r, rel, true, false);
+ if (rc == 0) { rc = walk_store_dir(e, r, full, strip); }
+ } else if (S_ISREG(st.st_mode) || S_ISLNK(st.st_mode)) {
+ rc = add_rel(e, r, rel, false, false);
+ }
+ }
+ closedir(d);
+ return rc;
+}
+
+static int
+walk_archive(const struct env *e, struct rec *r)
+{
+ char prefix[PATH_MAX];
+ snprintf(prefix, sizeof(prefix), "%s-%d.%d.%d/files/", r->name,
+ r->pkg.pkg_ver.maj, r->pkg.pkg_ver.min, r->pkg.pkg_ver.pat);
+ size_t plen = strlen(prefix);
+
+ struct archive *a = archive_read_new();
+ archive_read_support_filter_gzip(a);
+ archive_read_support_format_tar(a);
+ if (archive_read_open_filename(a, r->src, 10240) != ARCHIVE_OK) {
+ archive_read_free(a);
+ return -1;
+ }
+
+ int rc = 0;
+ struct archive_entry *entry;
+ while (rc == 0 && archive_read_next_header(a, &entry) == ARCHIVE_OK) {
+ const char *path = archive_entry_pathname(entry);
+ if (strncmp(path, prefix, plen) == 0) {
+ char rel[PATH_MAX];
+ snprintf(rel, sizeof(rel), "%s", path + plen);
+ size_t len = strlen(rel);
+ while (len > 0 && rel[len - 1] == '/') { rel[--len] = '\0'; }
+ mode_t type = archive_entry_filetype(entry);
+ if (type == AE_IFDIR) {
+ rc = add_rel(e, r, rel, true, true);
+ } else if (type == AE_IFREG || type == AE_IFLNK) {
+ rc = add_rel(e, r, rel, false, true);
+ }
+ }
+ archive_read_data_skip(a);
+ }
+ archive_read_free(a);
+ return rc;
+}
+
+static int
+load_dests(const struct env *e, struct rec *r)
+{
+ if (r->dests_loaded) { return 0; }
+ r->dests_loaded = true;
+ if (r->from_archive) { return walk_archive(e, r); }
+ return walk_store_dir(e, r, r->src, strlen(r->src) + 1);
+}
+
+struct item {
+ const char *path;
+ bool dir;
+ size_t rec;
+};
+
+static int
+item_cmp(const void *a, const void *b)
+{
+ const struct item *x = a, *y = b;
+ int c = strcmp(x->path, y->path);
+ if (c != 0) { return c; }
+ return x->rec < y->rec ? -1 : x->rec > y->rec;
+}
+
+/* Two packages shipping the same path (ROADMAP 2.6): whichever linked
+ * last used to win silently. Refused when either package is part of
+ * this transaction; at least one of the two entries must be a file or
+ * symlink (sharing a directory is normal). */
+static int
+check_collisions(const struct env *e, struct set *res)
+{
+ size_t n = 0;
+ for (size_t i = 0; i < res->n; i++) {
+ if (res->v[i].gone) { continue; }
+ if (load_dests(e, &res->v[i]) != 0) { return -1; }
+ n += res->v[i].ndests;
+ }
+ if (n == 0) { return 0; }
+
+ struct item *items = malloc(n * sizeof(*items));
+ if (!items) { return -1; }
+ size_t k = 0;
+ for (size_t i = 0; i < res->n; i++) {
+ if (res->v[i].gone) { continue; }
+ for (size_t j = 0; j < res->v[i].ndests; j++) {
+ items[k++] = (struct item){ res->v[i].dests[j].path, res->v[i].dests[j].dir, i };
+ }
+ }
+ qsort(items, n, sizeof(*items), item_cmp);
+
+ int problems = 0;
+ for (size_t start = 0; start < n; ) {
+ size_t end = start + 1;
+ while (end < n && strcmp(items[end].path, items[start].path) == 0) { end++; }
+ for (size_t a = start; a < end; a++) {
+ const struct rec *ra = &res->v[items[a].rec];
+ if (!ra->touched && !ra->fresh) { continue; }
+ for (size_t b = start; b < end; b++) {
+ if (b == a || (items[a].dir && items[b].dir)) { continue; }
+ const struct rec *rb = &res->v[items[b].rec];
+ /* two of this transaction's packages: report once */
+ if ((rb->touched || rb->fresh) && b < a) { continue; }
+ lg_printf(2, "%s::%s and %s::%s both ship %s.",
+ ra->repo, ra->name, rb->repo, rb->name, items[a].path);
+ problems++;
+ }
+ }
+ start = end;
+ }
+ free(items);
+ return problems;
+}
+
+/* --strict: what the post-commit link pass would refuse, refused now
+ * instead, before anything is committed. Same rules as gl_link_pkg, for
+ * this transaction's packages only (the ones whose refusals it reports).
+ * User scope never has a foreign file in its links tree (rebuilt on
+ * every commit), and config in /etc is offered as .new, never refused. */
+static int
+check_links(const struct env *e, struct set *res)
+{
+ if (!e->strict || e->scope != GL_SCOPE_SYS) { return 0; }
+
+ char store_prefix[PATH_MAX];
+ snprintf(store_prefix, sizeof(store_prefix), "%s/", e->store_rt);
+ size_t splen = strlen(store_prefix);
+
+ int problems = 0;
+ for (size_t i = 0; i < res->n; i++) {
+ struct rec *r = &res->v[i];
+ if (r->gone || !(r->touched || r->fresh)) { continue; }
+ if (load_dests(e, r) != 0) { return -1; }
+
+ for (size_t k = 0; k < r->ndests; k++) {
+ const struct dest *d = &r->dests[k];
+ if (strcmp(d->path, "/etc") == 0 || strncmp(d->path, "/etc/", 5) == 0) { continue; }
+
+ char at[PATH_MAX];
+ snprintf(at, sizeof(at), "%s%s", e->sysroot, d->path);
+ struct stat st;
+ if (lstat(at, &st) != 0) {
+ if (errno == ENOTDIR) {
+ lg_printf(2, "Refusing to overwrite non-glacier path: %s", at);
+ problems++;
+ }
+ continue;
+ }
+
+ bool ours = false;
+ if (S_ISLNK(st.st_mode)) {
+ char target[PATH_MAX];
+ ssize_t tn = readlink(at, target, sizeof(target) - 1);
+ ours = tn >= 0 && (size_t)tn >= splen &&
+ strncmp(target, store_prefix, splen) == 0;
+ }
+
+ if (d->dir) {
+ /* a foreign symlink here (usr/sbin -> bin) is followed,
+ * like gl_link_pkg does; one of glacier's isn't */
+ if (S_ISDIR(st.st_mode) || (S_ISLNK(st.st_mode) && !ours)) { continue; }
+ if (ours) {
+ lg_printf(2, "Refusing to merge directory %s into another package's symlink", at);
+ } else {
+ lg_printf(2, "Refusing to overwrite non-glacier path: %s", at);
+ }
+ problems++;
+ continue;
+ }
+
+ if (ours) { continue; }
+ if (r->overwrite && (S_ISREG(st.st_mode) || S_ISLNK(st.st_mode))) {
+ char kept[PATH_MAX];
+ snprintf(kept, sizeof(kept), "%s/.displaced%s", e->store_at, d->path);
+ struct stat kst;
+ if (lstat(kept, &kst) != 0) { continue; } /* -O moves it aside */
+ }
+ lg_printf(2, "Refusing to overwrite non-glacier path: %s", at);
+ problems++;
+ }
+ }
+ return problems;
+}
+
+/* The checks themselves, on the result of a transaction. */
+static int
+check(const struct env *e, struct set *res, const struct set *sys,
+ const struct removed *rm, size_t nrm)
+{
+ int problems = 0;
+ char ver[64];
+
+ for (size_t i = 0; i < res->n; i++) {
+ const struct rec *p = &res->v[i];
+ if (p->gone) { continue; }
+
+ const dep_t *deps = p->pkg.pkg_dependencies;
+ for (const dep_t *d = deps; d && d->dep_name; d++) {
+ /* An added package: all its deps. An untouched one:
+ * only deps on something this transaction changes. */
+ if (!p->touched && !name_touched(res, rm, nrm, d->dep_name)) { continue; }
+ if (provided(res, sys, d->dep_name, &d->dep_ver)) { continue; }
+
+ const struct removed *gone = NULL;
+ for (size_t k = 0; !p->touched && k < nrm; k++) {
+ if (strcmp(rm[k].name, d->dep_name) == 0) { gone = &rm[k]; break; }
+ }
+ if (gone && !provided(res, sys, d->dep_name, NULL)) {
+ lg_printf(2, "Removing %s::%s breaks %s::%s.",
+ gone->repo, gone->name, p->repo, p->name);
+ } else {
+ fmt_ver(&d->dep_ver, ver, sizeof(ver));
+ lg_printf(2, "%s::%s requires %s >= %s, which is not installed.",
+ p->repo, p->name, d->dep_name, ver);
+ }
+ problems++;
+ }
+
+ if (!p->touched) { continue; }
+ for (size_t j = 0; j < res->n; j++) {
+ const struct rec *q = &res->v[j];
+ if (j == i || q->gone) { continue; }
+ /* Two added packages: report the pair once. */
+ if (q->touched && j < i) { continue; }
+ if (lists_conflict(p, q->name) || lists_conflict(q, p->name)) {
+ lg_printf(2, "%s::%s conflicts with %s::%s.",
+ p->repo, p->name, q->repo, q->name);
+ problems++;
+ }
+ }
+ }
+
+ int files = check_collisions(e, res);
+ if (files < 0) { return -1; }
+ problems += files;
+
+ files = check_links(e, res);
+ if (files < 0) { return -1; }
+ return problems + files;
+}
+
+/* The runtime path of the live store for a context's scope and root. */
+static void
+env_init(struct env *e, const gl_context_t *ctx, const char *store_at)
+{
+ e->scope = ctx->scope;
+ e->uid = ctx->uid;
+ e->sysroot = ctx->sysroot;
+ e->strict = ctx->strict;
+ if (ctx->scope == GL_SCOPE_USR) {
+ char ub[PATH_MAX];
+ gl_usr_base(ctx->uid, ub, sizeof(ub));
+ snprintf(e->store_rt, sizeof(e->store_rt), "%s/store", ub);
+ } else {
+ snprintf(e->store_rt, sizeof(e->store_rt), "%s/store", GL_SYS_ROOT);
+ }
+ snprintf(e->store_at, sizeof(e->store_at), "%s", store_at);
+}
+
+/* The system store, which a user-scope transaction's deps may rely on.
+ * Always the running system's: user scope never has an install root. */
+static int
+load_sys(gl_ctx_scope_t scope, struct set *sys)
+{
+ if (scope != GL_SCOPE_USR) { return 0; }
+ return load_store(GL_SYS_ROOT "/store", sys, true);
+}
+
+int
+gl_check_plan(const gl_context_t *live,
+ const char *const *add_paths, size_t n_add,
+ const char *const *remove_ids, size_t n_remove)
+{
+ if (!live) { return -1; }
+
+ struct set res = { NULL, 0 }, sys = { NULL, 0 };
+ struct removed *rm = calloc(n_remove + 1, sizeof(*rm));
+ size_t nrm = 0;
+ int rc = -1;
+
+ if (!rm || load_store(live->store_path, &res, false) != 0 ||
+ load_sys(live->scope, &sys) != 0) {
+ goto out;
+ }
+
+ for (size_t i = 0; i < n_remove; i++) {
+ const char *sep = strstr(remove_ids[i], "::");
+ if (!sep) { goto out; }
+ char repo[NAME_MAX + 1];
+ snprintf(repo, sizeof(repo), "%.*s", (int)(sep - remove_ids[i]), remove_ids[i]);
+ struct rec *r = (struct rec *)find_live(&res, repo, sep + 2);
+ if (!r) { continue; } /* not installed: nothing to break */
+ r->gone = true;
+ snprintf(rm[nrm].repo, sizeof(rm[nrm].repo), "%s", r->repo);
+ snprintf(rm[nrm].name, sizeof(rm[nrm].name), "%s", r->name);
+ nrm++;
+ }
+
+ for (size_t i = 0; i < n_add; i++) {
+ gl_pkg_t pkg = {0};
+ if (gl_read_pkg_manifest(add_paths[i], &pkg) != GL_INSTALL_OK) { goto out; }
+
+ /* Same repo::name installed: this replaces it. */
+ struct rec *old = (struct rec *)find_live(&res, pkg.pkg_repo, pkg.pkg_name);
+ if (old) { old->gone = true; }
+
+ struct rec *r = set_add(&res);
+ if (!r) { gl_free_pkg(&pkg); goto out; }
+ snprintf(r->repo, sizeof(r->repo), "%s", pkg.pkg_repo);
+ snprintf(r->name, sizeof(r->name), "%s", pkg.pkg_name);
+ r->ver = (semver_t){ pkg.pkg_ver.maj, pkg.pkg_ver.min, pkg.pkg_ver.pat, NULL };
+ r->pkg = pkg;
+ r->touched = true;
+ r->fresh = true;
+ r->overwrite = live->overwrite != 0;
+ r->from_archive = true;
+ snprintf(r->src, sizeof(r->src), "%s", add_paths[i]);
+ }
+
+ struct env e;
+ env_init(&e, live, live->store_path);
+ rc = check(&e, &res, &sys, rm, nrm);
+out:
+ set_free(&res);
+ set_free(&sys);
+ free(rm);
+ return rc;
+}
+
+int
+gl_check_txn(const gl_context_t *stage)
+{
+ if (!stage || stage->mode != GL_CTX_STAGE) { return -1; }
+
+ char base[PATH_MAX], live_store[PATH_MAX];
+ if (stage->scope == GL_SCOPE_USR) {
+ gl_usr_base(stage->uid, base, sizeof(base));
+ } else {
+ snprintf(base, sizeof(base), "%s%s", stage->sysroot, GL_SYS_ROOT);
+ }
+ snprintf(live_store, sizeof(live_store), "%s/store", base);
+
+ struct set res = { NULL, 0 }, live = { NULL, 0 }, sys = { NULL, 0 };
+ struct removed *rm = NULL;
+ size_t nrm = 0;
+ int rc = -1;
+
+ if (load_store(stage->store_path, &res, false) != 0 ||
+ load_store(live_store, &live, false) != 0 ||
+ load_sys(stage->scope, &sys) != 0) {
+ goto out;
+ }
+
+ rm = calloc(live.n + 1, sizeof(*rm));
+ if (!rm) { goto out; }
+
+ /* Touched: in the stage, but not as this exact version in live.
+ * Fresh: installed by this transaction (a reinstall of the same
+ * version is fresh but not touched); its link pass is this one's. */
+ for (size_t i = 0; i < res.n; i++) {
+ struct rec *r = &res.v[i];
+ const struct rec *l = find_live(&live, r->repo, r->name);
+ r->touched = !l || strcmp(l->dirname, r->dirname) != 0;
+
+ char marker[PATH_MAX];
+ struct stat mst;
+ snprintf(marker, sizeof(marker), "%s/" GL_FRESH_MARKER, r->src);
+ r->fresh = lstat(marker, &mst) == 0;
+ snprintf(marker, sizeof(marker), "%s/" GL_OVERWRITE_MARKER, r->src);
+ r->overwrite = lstat(marker, &mst) == 0;
+ }
+ /* Removed: in live, and no longer in the stage under that repo::name. */
+ for (size_t i = 0; i < live.n; i++) {
+ if (find_live(&res, live.v[i].repo, live.v[i].name)) { continue; }
+ snprintf(rm[nrm].repo, sizeof(rm[nrm].repo), "%s", live.v[i].repo);
+ snprintf(rm[nrm].name, sizeof(rm[nrm].name), "%s", live.v[i].name);
+ nrm++;
+ }
+
+ struct env e;
+ env_init(&e, stage, stage->store_path);
+ rc = check(&e, &res, &sys, rm, nrm);
+out:
+ set_free(&res);
+ set_free(&live);
+ set_free(&sys);
+ free(rm);
+ return rc;
+}
diff --git a/src/deps.h b/src/deps.h
new file mode 100644
index 0000000..aca34cb
--- /dev/null
+++ b/src/deps.h
@@ -0,0 +1,55 @@
+#ifndef DEPS_H_
+#define DEPS_H_
+
+#include
+
+#include "transaction.h"
+
+/*
+ * Dependency and conflict checks (ROADMAP 2.3). Built into
+ * libglacier_istoreutils.so.
+ *
+ * Rules:
+ * - A PKG_DEPS entry {name, ver} is met by any installed package of
+ * that name (any repo) at version >= ver.
+ * - In user scope, a dependency may also be met by a system package.
+ * System scope never looks at user scopes.
+ * - PKG_CONFS is matched by name, both ways (either package listing
+ * the other is a conflict); its version is ignored. Only packages in
+ * the same scope conflict.
+ * - Only what a transaction CHANGES is checked: the packages it adds
+ * (deps and conflicts), and the installed packages that depend on
+ * something it removes or replaces. A store that was already
+ * inconsistent doesn't block unrelated work.
+ * - Two packages may not ship the same path (ROADMAP 2.6), compared by
+ * where each entry lands once linked; sharing a directory is fine.
+ * Checked whenever either package is part of the transaction.
+ * - With ctx->strict (syspkg --strict, implied by -R), anything the
+ * post-commit link pass would refuse (a foreign file in the way, a
+ * directory over another package's symlink) is refused up front,
+ * unless -O will move it aside. System scope only: a user's links
+ * tree is rebuilt on every commit.
+ * - Every problem is reported with lg_printf, one line each:
+ * extra::foo requires bar >= 1.2.0, which is not installed.
+ * Removing base::musl breaks extra::foo.
+ * extra::foo conflicts with extra::bar.
+ * extra::foo and extra::bar both ship /glacier/sys/links/bin/x.
+ * Refusing to overwrite non-glacier path: /usr/bin/x
+ *
+ * Both return the number of problems (0 = fine), or -1 if a store or a
+ * manifest couldn't be read.
+ */
+
+/* Before a transaction: `live` is a live context; add_paths are .gpkg
+ * files to install (a package already installed under the same
+ * repo::name is replaced); remove_ids are "repo::name" strings. */
+int gl_check_plan(const gl_context_t *live,
+ const char *const *add_paths, size_t n_add,
+ const char *const *remove_ids, size_t n_remove);
+
+/* At commit: compares the stage against live. gl_commit_transaction
+ * calls this itself and refuses with GL_TXN_ERR_DEPS, so a caller that
+ * skips gl_check_plan is still held to the rules. */
+int gl_check_txn(const gl_context_t *stage);
+
+#endif
diff --git a/src/grepo.c b/src/grepo.c
new file mode 100644
index 0000000..15edf8c
--- /dev/null
+++ b/src/grepo.c
@@ -0,0 +1,389 @@
+/*
+ * grepo: write and sign a repository index (REPO_DESIGN.txt section 4).
+ * A maintainer tool, like gsign.
+ *
+ * grepo -s KEY.sec -r REPO [-n SERIAL] [-e DAYS] DIR
+ *
+ * Reads every NAME-VER.gpkg in DIR (its manifest, sha256 and size) and
+ * writes DIR/repo.idx plus DIR/repo.idx.sig, signed with KEY.sec. Each
+ * package's manifest must say REPO, and its file name must be
+ * NAME-VER.gpkg as the manifest names it. The server is then any static
+ * file host serving DIR.
+ *
+ * SERIAL (decided 2026-10-06) is the date form YYYYMMDDNN: the larger
+ * of the previous DIR/repo.idx's SERIAL + 1 and today (UTC) * 100 + 1.
+ * -n sets it explicitly; it must still be above the previous one, or
+ * clients would refuse the index. VALID_UNTIL is now + DAYS days
+ * (default 30; decision 10c).
+ *
+ * Exit 0 on success, 1 otherwise.
+ */
+
+#define _POSIX_C_SOURCE 200809L
+#define _XOPEN_SOURCE 700
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "istoreutils.h"
+#include "log.h"
+#include "pkg.h"
+#include "sha256.h"
+#include "sign.h"
+
+#define DEFAULT_DAYS 30
+#define MAX_DAYS 3650
+
+#define USAGE "usage: grepo [-hv] -s KEY.sec -r REPO [-n SERIAL] [-e DAYS] DIR\n"
+
+static const struct option longopts[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'v' },
+ { "seckey", required_argument, NULL, 's' },
+ { "repo", required_argument, NULL, 'r' },
+ { "serial", required_argument, NULL, 'n' },
+ { "expire", required_argument, NULL, 'e' },
+ { NULL, 0, NULL, 0 }
+};
+
+static void
+usage(void)
+{
+ fprintf(stderr, USAGE);
+}
+
+static void
+help(void)
+{
+ printf("grepo: Write and sign a repository index\n");
+ printf(USAGE);
+ printf("\n");
+ printf("%-25s %s\n", "{-h --help}", "Show this message");
+ printf("%-25s %s\n", "{-v --version}", "Show the current version");
+ printf("%-25s %s\n", "{-s --seckey} KEY.sec", "Sign the index with this secret key");
+ printf("%-25s %s\n", "{-r --repo} REPO", "Repository the packages in DIR belong to");
+ printf("%-25s %s\n", "{-n --serial} SERIAL", "Index serial (default: YYYYMMDDNN)");
+ printf("%-25s Days until the index expires (default: %d)\n", "{-e --expire} DAYS", DEFAULT_DAYS);
+ printf("\n");
+ printf("This program is free software.\n");
+ printf("See the GNU GPL version 3 for details.\n");
+}
+
+static int
+by_name(const void *a, const void *b)
+{
+ return strcmp(*(char *const *)a, *(char *const *)b);
+}
+
+/* sha256 ("sha256:") and size of a regular file, read once. */
+static int
+hash_file(const char *path, char sha[72], int64_t *size)
+{
+ int fd = open(path, O_RDONLY | O_NOFOLLOW);
+ if (fd < 0)
+ return -1;
+ gl_sha256_ctx_t h;
+ gl_sha256_init(&h);
+ uint8_t buf[65536];
+ ssize_t n;
+ int64_t total = 0;
+ while ((n = read(fd, buf, sizeof buf)) > 0) {
+ gl_sha256_update(&h, buf, (size_t)n);
+ total += n;
+ }
+ close(fd);
+ if (n < 0)
+ return -1;
+ uint8_t digest[32];
+ char hex[65];
+ gl_sha256_final(&h, digest);
+ gl_sha256_to_hex(digest, hex);
+ snprintf(sha, 72, "sha256:%s", hex);
+ *size = total;
+ return 0;
+}
+
+/* Reads a whole file (an index: at most GL_SIGN_MAX_MSG). NULL if it
+ * doesn't exist (errno ENOENT) or can't be read. */
+static char *
+slurp(const char *path, size_t *len)
+{
+ FILE *f = fopen(path, "rb");
+ if (!f)
+ return NULL;
+ char *buf = malloc(GL_SIGN_MAX_MSG + 1);
+ size_t n = buf ? fread(buf, 1, GL_SIGN_MAX_MSG + 1, f) : 0;
+ int bad = !buf || ferror(f) || n > GL_SIGN_MAX_MSG;
+ fclose(f);
+ if (bad) {
+ free(buf);
+ errno = EIO;
+ return NULL;
+ }
+ *len = n;
+ return buf;
+}
+
+/* One DEPS/CONFS element. 0 having said why if the name is unsafe. */
+static int
+relation(FILE *f, const char *path, const char *what, const char *name,
+ const semver_t *v, int first)
+{
+ if (!gl_valid_pkg_ident(name)) {
+ lg_printf(LG_ERR, "%s has a %s that is not a safe name.", path, what);
+ return 0;
+ }
+ fprintf(f, "%s { NAME = \"%s\"; VER = \"%d.%d.%d\"; }", first ? "" : ",",
+ name, v->maj, v->min, v->pat);
+ return 1;
+}
+
+/* One package's PKGS entry, appended to f. 0, or -1 having said why. */
+static int
+write_entry(FILE *f, const char *dir, const char *file, const char *repo)
+{
+ char path[PATH_MAX];
+ snprintf(path, sizeof path, "%s/%s", dir, file);
+
+ struct stat st;
+ if (lstat(path, &st) != 0 || !S_ISREG(st.st_mode)) {
+ lg_printf(LG_ERR, "Refusing %s: not a regular file.", path);
+ return -1;
+ }
+ gl_pkg_t pkg = {0};
+ if (gl_read_pkg_manifest(path, &pkg) != 0) {
+ lg_printf(LG_ERR, "Cannot read the manifest of %s.", path);
+ return -1;
+ }
+
+ int rc = -1;
+ char ver[64], want[PATH_MAX];
+ snprintf(ver, sizeof ver, "%d.%d.%d", pkg.pkg_ver.maj, pkg.pkg_ver.min,
+ pkg.pkg_ver.pat);
+ snprintf(want, sizeof want, "%s-%s.gpkg", pkg.pkg_name, ver);
+ if (strcmp(pkg.pkg_repo, repo) != 0) {
+ lg_printf(LG_ERR, "%s is a %s package, not %s.", path, pkg.pkg_repo, repo);
+ goto out;
+ }
+ if (strcmp(file, want) != 0) {
+ lg_printf(LG_ERR, "%s must be named %s, as its manifest says.", path, want);
+ goto out;
+ }
+
+ char sha[72];
+ int64_t size;
+ if (hash_file(path, sha, &size) != 0) {
+ lg_printf(LG_ERR, "Cannot read %s.", path);
+ goto out;
+ }
+
+ fprintf(f, "\t{ NAME = \"%s\"; VER = \"%s\"; FILE = \"%s\";\n"
+ "\t SHA256 = \"%s\"; SIZE = %" PRId64 "L;\n\t DEPS = (",
+ pkg.pkg_name, ver, file, sha, size);
+ int first = 1;
+ for (dep_t *d = pkg.pkg_dependencies; d && d->dep_name; d++) {
+ if (!relation(f, path, "dependency", d->dep_name, &d->dep_ver, first))
+ goto out;
+ first = 0;
+ }
+ fprintf(f, "%s); CONFS = (", first ? "" : " ");
+ first = 1;
+ for (conf_t *k = pkg.pkg_conflicts; k && k->conf_name; k++) {
+ if (!relation(f, path, "conflict", k->conf_name, &k->conf_ver, first))
+ goto out;
+ first = 0;
+ }
+ fprintf(f, "%s); }", first ? "" : " ");
+ rc = 0;
+out:
+ gl_free_pkg(&pkg);
+ return rc;
+}
+
+int
+main(int argc, char *argv[])
+{
+ const char *sec = NULL, *repo = NULL;
+ int64_t serial = 0;
+ long days = DEFAULT_DAYS;
+ int c;
+
+ while ((c = getopt_long(argc, argv, "hvs:r:n:e:", longopts, NULL)) != -1) {
+ char *end;
+ switch (c) {
+ case 'h':
+ help();
+ return 0;
+ case 'v':
+ printf("v4.0.0\n");
+ return 0;
+ case 's': sec = optarg; break;
+ case 'r': repo = optarg; break;
+ case 'n':
+ errno = 0;
+ serial = strtoll(optarg, &end, 10);
+ if (errno || *end || end == optarg || serial <= 0) {
+ lg_printf(LG_ERR, "SERIAL must be a whole number above 0.");
+ return 1;
+ }
+ break;
+ case 'e':
+ errno = 0;
+ days = strtol(optarg, &end, 10);
+ if (errno || *end || end == optarg || days < 1 || days > MAX_DAYS) {
+ lg_printf(LG_ERR, "DAYS must be a whole number from 1 to %d.", MAX_DAYS);
+ return 1;
+ }
+ break;
+ default:
+ usage();
+ return 1;
+ }
+ }
+ if (!sec || !repo || optind != argc - 1) {
+ usage();
+ return 1;
+ }
+ const char *dir = argv[optind];
+ if (!gl_valid_pkg_ident(repo)) {
+ lg_printf(LG_ERR, "Repository name %s is not a safe name ([A-Za-z0-9._-]).", repo);
+ return 1;
+ }
+
+ int st = gl_sign_check_seckey(sec);
+ if (st != GL_SIGN_OK) {
+ lg_printf(LG_ERR, "Secret key %s %s.", sec,
+ st == GL_SIGN_ERR_ENCRYPTED ? "is encrypted: sign with signify" :
+ st == GL_SIGN_ERR_FORMAT ? "is malformed" : "cannot be read");
+ return 1;
+ }
+
+ char idx_path[PATH_MAX], sig_path[PATH_MAX];
+ if ((size_t)snprintf(idx_path, sizeof idx_path, "%s/repo.idx", dir) >= sizeof idx_path ||
+ (size_t)snprintf(sig_path, sizeof sig_path, "%s/repo.idx.sig", dir) >= sizeof sig_path) {
+ lg_printf(LG_ERR, "Path too long: %s", dir);
+ return 1;
+ }
+
+ /* The previous index, for its SERIAL. The maintainer's own file:
+ * parsed, not verified, but it must be a well-formed index for
+ * this repo (a mixed-up DIR is better caught here). */
+ int64_t prev = 0;
+ size_t plen;
+ char *ptext = slurp(idx_path, &plen);
+ if (!ptext && errno != ENOENT) {
+ lg_printf(LG_ERR, "Cannot read %s.", idx_path);
+ return 1;
+ }
+ if (ptext) {
+ gl_index_t old;
+ gl_index_status_t ist = gl_index_parse(ptext, plen, repo, &old);
+ free(ptext);
+ if (ist == GL_INDEX_ERR_REPO) {
+ lg_printf(LG_ERR, "%s is the index of another repository.", idx_path);
+ return 1;
+ }
+ if (ist != GL_INDEX_OK) {
+ lg_printf(LG_ERR, "Malformed %s.", idx_path);
+ return 1;
+ }
+ prev = old.serial;
+ gl_index_free(&old);
+ }
+
+ time_t now = time(NULL);
+ if (serial == 0) {
+ struct tm tm;
+ gmtime_r(&now, &tm);
+ int64_t today = ((int64_t)(tm.tm_year + 1900) * 10000 +
+ (tm.tm_mon + 1) * 100 + tm.tm_mday) * 100 + 1;
+ serial = prev + 1 > today ? prev + 1 : today;
+ } else if (serial <= prev) {
+ lg_printf(LG_ERR, "SERIAL must be above %" PRId64 ", the previous index's.", prev);
+ return 1;
+ }
+
+ DIR *d = opendir(dir);
+ if (!d) {
+ lg_printf(LG_ERR, "Cannot open %s: %s", dir, strerror(errno));
+ return 1;
+ }
+ char **names = NULL;
+ size_t n = 0;
+ struct dirent *de;
+ while ((de = readdir(d))) {
+ size_t l = strlen(de->d_name);
+ if (de->d_name[0] == '.' || l < 6 || strcmp(de->d_name + l - 5, ".gpkg") != 0)
+ continue;
+ char **grown = realloc(names, (n + 1) * sizeof *names);
+ if (!grown || !(grown[n] = strdup(de->d_name))) {
+ lg_printf(LG_ERR, "Out of memory.");
+ return 1;
+ }
+ names = grown;
+ n++;
+ }
+ closedir(d);
+ qsort(names, n, sizeof *names, by_name);
+
+ /* Written beside the result, then renamed over it: never in place. */
+ char tmp_idx[PATH_MAX], tmp_sig[PATH_MAX];
+ snprintf(tmp_idx, sizeof tmp_idx, "%s/.repo.idx.XXXXXX", dir);
+ int fd = mkstemp(tmp_idx);
+ FILE *f = fd < 0 ? NULL : fdopen(fd, "w");
+ if (!f) {
+ lg_printf(LG_ERR, "Cannot write in %s: %s", dir, strerror(errno));
+ return 1;
+ }
+ snprintf(tmp_sig, sizeof tmp_sig, "%s.sig", tmp_idx);
+
+ fprintf(f, "# Repository index written by grepo. Signed: repo.idx.sig.\n"
+ "REPO = \"%s\";\nSERIAL = %" PRId64 "L;\nVALID_UNTIL = %" PRId64 "L;\n"
+ "PKGS = (\n", repo, serial, (int64_t)now + (int64_t)days * 86400);
+ int rc = 0;
+ for (size_t i = 0; i < n && rc == 0; i++) {
+ if (i)
+ fprintf(f, ",\n");
+ rc = write_entry(f, dir, names[i], repo);
+ }
+ fprintf(f, "%s);\n", n ? "\n" : "");
+ if (fchmod(fd, 0644) != 0 || fflush(f) != 0 || fsync(fd) != 0)
+ rc = rc ? rc : (lg_printf(LG_ERR, "Cannot write %s.", tmp_idx), -1);
+ if (fclose(f) != 0 && rc == 0) {
+ lg_printf(LG_ERR, "Cannot write %s.", tmp_idx);
+ rc = -1;
+ }
+ for (size_t i = 0; i < n; i++)
+ free(names[i]);
+ free(names);
+
+ if (rc == 0 && (st = gl_sign_file(sec, tmp_idx, tmp_sig)) != GL_SIGN_OK) {
+ lg_printf(LG_ERR, "Cannot sign %s.", tmp_idx);
+ rc = -1;
+ }
+ /* The signature first: a client that fetches between the renames
+ * gets a new .sig with the old index, which fails to verify and is
+ * refused, never an index accepted under the wrong signature. */
+ if (rc == 0 && (rename(tmp_sig, sig_path) != 0 || rename(tmp_idx, idx_path) != 0)) {
+ lg_printf(LG_ERR, "Cannot replace %s: %s", idx_path, strerror(errno));
+ rc = -1;
+ }
+ if (rc != 0) {
+ unlink(tmp_idx);
+ unlink(tmp_sig);
+ return 1;
+ }
+ lg_printf(LG_SUCCESS, "Wrote %s: %zu %s, serial %" PRId64 ".", idx_path, n,
+ n == 1 ? "package" : "packages", serial);
+ return 0;
+}
diff --git a/src/gsha256sum.c b/src/gsha256sum.c
index 1f26b5b..b54ecdf 100644
--- a/src/gsha256sum.c
+++ b/src/gsha256sum.c
@@ -1,18 +1,54 @@
+#include
#include
#include "sha256.h"
+#define USAGE "usage: gsha256sum [-hv] FILE\n"
+
+static const struct option longopts[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'v' },
+ { NULL, 0, NULL, 0 }
+};
+
+static void
+help(void)
+{
+ printf("gsha256sum: Print a file's sha256 as a Glacier sidecar line\n");
+ printf(USAGE);
+ printf("\n");
+ printf("%-25s %s\n", "{-h --help}", "Show this message");
+ printf("%-25s %s\n", "{-v --version}", "Show the current version");
+ printf("\n");
+ printf("This program is free software.\n");
+ printf("See the GNU GPL version 3 for details.\n");
+}
+
int
main(int argc, char *argv[])
{
- if (argc != 2) {
- fprintf(stderr, "usage: %s FILE\n", argv[0]);
+ int c;
+ while ((c = getopt_long(argc, argv, "hv", longopts, NULL)) != -1) {
+ switch (c) {
+ case 'h':
+ help();
+ return 0;
+ case 'v':
+ printf("v4.0.0\n");
+ return 0;
+ default:
+ fprintf(stderr, USAGE);
+ return 1;
+ }
+ }
+ if (optind != argc - 1) {
+ fprintf(stderr, USAGE);
return 1;
}
char hex[GL_SHA256_HEX_SIZE];
- if (gl_sha256_file(argv[1], hex) != 0) {
- perror(argv[1]);
+ if (gl_sha256_file(argv[optind], hex) != 0) {
+ perror(argv[optind]);
return 1;
}
diff --git a/src/gsign.c b/src/gsign.c
new file mode 100644
index 0000000..8137f20
--- /dev/null
+++ b/src/gsign.c
@@ -0,0 +1,212 @@
+/*
+ * gsign: make signify-compatible Ed25519 keys and signatures
+ * (REPO_DESIGN.txt section 2). A build/maintainer tool, like
+ * gsha256sum.
+ *
+ * gsign -G -p KEY.pub -s KEY.sec [-c COMMENT]
+ * gsign -S -s KEY.sec -m FILE [-x SIGFILE]
+ * gsign -V -p KEY.pub -m FILE [-x SIGFILE]
+ *
+ * SIGFILE defaults to FILE.sig. Exit 0 on success, 1 otherwise.
+ */
+
+#define _POSIX_C_SOURCE 200809L
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "log.h"
+#include "sign.h"
+
+#define USAGE \
+ "usage: gsign [-hv]\n" \
+ " gsign -G -p KEY.pub -s KEY.sec [-c COMMENT]\n" \
+ " gsign -S -s KEY.sec -m FILE [-x SIGFILE]\n" \
+ " gsign -V -p KEY.pub -m FILE [-x SIGFILE]\n"
+
+static const struct option longopts[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'v' },
+ { "generate", no_argument, NULL, 'G' },
+ { "sign", no_argument, NULL, 'S' },
+ { "verify", no_argument, NULL, 'V' },
+ { "pubkey", required_argument, NULL, 'p' },
+ { "seckey", required_argument, NULL, 's' },
+ { "message", required_argument, NULL, 'm' },
+ { "sig", required_argument, NULL, 'x' },
+ { "comment", required_argument, NULL, 'c' },
+ { NULL, 0, NULL, 0 }
+};
+
+static void
+usage(void)
+{
+ fprintf(stderr, USAGE);
+}
+
+static void
+help(void)
+{
+ printf("gsign: Make and check signify-compatible signatures\n");
+ printf(USAGE);
+ printf("\n");
+ printf("%-25s %s\n", "{-h --help}", "Show this message");
+ printf("%-25s %s\n", "{-v --version}", "Show the current version");
+ printf("%-25s %s\n", "{-G --generate}", "Generate a new key pair");
+ printf("%-25s %s\n", "{-S --sign}", "Sign a file");
+ printf("%-25s %s\n", "{-V --verify}", "Verify a file's signature");
+ printf("%-25s %s\n", "{-p --pubkey} KEY.pub", "Public key file");
+ printf("%-25s %s\n", "{-s --seckey} KEY.sec", "Secret key file");
+ printf("%-25s %s\n", "{-m --message} FILE", "File to sign or verify");
+ printf("%-25s %s\n", "{-x --sig} SIGFILE", "Signature file (default: FILE.sig)");
+ printf("%-25s %s\n", "{-c --comment} COMMENT", "Comment for a new key pair (default: gsign)");
+ printf("\n");
+ printf("This program is free software.\n");
+ printf("See the GNU GPL version 3 for details.\n");
+}
+
+/* " ." about one file. */
+static int
+fail(const char *what, const char *path, int st)
+{
+ const char *problem;
+ switch (st) {
+ case GL_SIGN_ERR_FORMAT: problem = "is malformed"; break;
+ case GL_SIGN_ERR_ENCRYPTED: problem = "is encrypted: sign with signify"; break;
+ case GL_SIGN_ERR_TOOBIG: problem = "is too large"; break;
+ case GL_SIGN_ERR_EXISTS: problem = "already exists"; break;
+ case GL_SIGN_ERR_WRITE: problem = "cannot be written"; break;
+ default: problem = "cannot be read"; break;
+ }
+ lg_printf(LG_ERR, "%s %s %s.", what, path, problem);
+ return 1;
+}
+
+int
+main(int argc, char *argv[])
+{
+ char mode = 0;
+ const char *pub = NULL, *sec = NULL, *msg = NULL, *sig = NULL;
+ const char *comment = "gsign";
+ int c;
+
+ while ((c = getopt_long(argc, argv, "hvGSVp:s:m:x:c:", longopts, NULL)) != -1) {
+ switch (c) {
+ case 'h':
+ help();
+ return 0;
+ case 'v':
+ printf("v4.0.0\n");
+ return 0;
+ case 'G': case 'S': case 'V':
+ if (mode && mode != c) {
+ usage();
+ return 1;
+ }
+ mode = (char)c;
+ break;
+ case 'p': pub = optarg; break;
+ case 's': sec = optarg; break;
+ case 'm': msg = optarg; break;
+ case 'x': sig = optarg; break;
+ case 'c': comment = optarg; break;
+ default:
+ usage();
+ return 1;
+ }
+ }
+ if (optind != argc) {
+ usage();
+ return 1;
+ }
+
+ char sigbuf[PATH_MAX];
+ if (msg && !sig) {
+ if ((size_t)snprintf(sigbuf, sizeof sigbuf, "%s.sig", msg) >= sizeof sigbuf) {
+ lg_printf(LG_ERR, "Path too long: %s", msg);
+ return 1;
+ }
+ sig = sigbuf;
+ }
+
+ int st;
+ switch (mode) {
+ case 'G':
+ if (!pub || !sec || msg) {
+ usage();
+ return 1;
+ }
+ /* The library refuses these too (O_EXCL); this names which. */
+ struct stat sb;
+ if (lstat(sec, &sb) == 0)
+ return fail("Secret key", sec, GL_SIGN_ERR_EXISTS);
+ if (lstat(pub, &sb) == 0)
+ return fail("Public key", pub, GL_SIGN_ERR_EXISTS);
+ st = gl_sign_generate(pub, sec, comment);
+ if (st == GL_SIGN_ERR_FORMAT) {
+ lg_printf(LG_ERR, "Comment must be one line, at most %d characters.",
+ GL_SIGN_MAX_COMMENT - 11);
+ return 1;
+ }
+ if (st == GL_SIGN_ERR_RANDOM) {
+ lg_printf(LG_ERR, "Cannot get random bytes for a new key.");
+ return 1;
+ }
+ if (st != GL_SIGN_OK) {
+ lg_printf(LG_ERR, "Cannot write %s and %s.", pub, sec);
+ return 1;
+ }
+ lg_printf(LG_SUCCESS, "Wrote %s and %s.", pub, sec);
+ return 0;
+
+ case 'S':
+ if (!sec || !msg || pub) {
+ usage();
+ return 1;
+ }
+ if ((st = gl_sign_check_seckey(sec)) != GL_SIGN_OK)
+ return fail("Secret key", sec, st);
+ st = gl_sign_file(sec, msg, sig);
+ if (st == GL_SIGN_ERR_WRITE)
+ return fail("Signature", sig, st);
+ if (st != GL_SIGN_OK)
+ return fail("File", msg, st);
+ lg_printf(LG_SUCCESS, "Signed %s: %s.", msg, sig);
+ return 0;
+
+ case 'V': {
+ if (!pub || !msg || sec) {
+ usage();
+ return 1;
+ }
+ gl_sign_pubkey_t pk;
+ gl_sign_sig_t sg;
+ if ((st = gl_sign_read_pubkey(pub, &pk)) != GL_SIGN_OK)
+ return fail("Public key", pub, st);
+ if ((st = gl_sign_read_sig(sig, &sg)) != GL_SIGN_OK)
+ return fail("Signature", sig, st);
+ st = gl_sign_verify_file(pub, msg, sig);
+ switch (st) {
+ case GL_SIGN_OK:
+ lg_printf(LG_SUCCESS, "Signature verified: %s.", msg);
+ return 0;
+ case GL_SIGN_ERR_KEYNUM:
+ lg_printf(LG_ERR, "%s was signed by a different key than %s.", msg, pub);
+ return 1;
+ case GL_SIGN_ERR_BAD:
+ lg_printf(LG_ERR, "Signature does not verify: %s.", msg);
+ return 1;
+ default:
+ return fail("File", msg, st);
+ }
+ }
+
+ default:
+ usage();
+ return 1;
+ }
+}
diff --git a/src/istoreutils.c b/src/istoreutils.c
index 821afa0..6ea20bc 100644
--- a/src/istoreutils.c
+++ b/src/istoreutils.c
@@ -18,15 +18,13 @@
#include
#include "common.h"
+#include "deps.h"
#include "istoreutils.h"
#include "log.h"
#include "pkg.h"
#include "transaction.h"
/* Layout: see the comment above GL_USR_ROOT in common.h. */
-static const dir_t sys_index = { GL_SYS_ROOT "/index", 0700 };
-static const dir_t sys_store = { GL_SYS_ROOT "/store", 0700 };
-static const dir_t sys_links = { GL_SYS_ROOT "/links", 0700 };
/* Base-system exception: packages in the "base" repo under system
* scope get their symlinks placed directly in /usr rather than the
@@ -35,13 +33,12 @@ static const dir_t sys_links = { GL_SYS_ROOT "/links", 0700 };
* setup. Everything else installed under system scope (an operator
* running `gpkg -s` on something that isn't part of the base system)
* stays under /glacier/sys/links. See gl_link_pkg / gl_relink_store. */
-/* The package's manifest, kept beside its files in the store so
- * gl_backup_pkg can rebuild a faithful .gpkg from an installed version.
- * Dot-prefixed, so linking, index rebuilds and the store scanners all
- * skip it. */
-#define GL_MANIFEST_COPY ".glacier-manifest"
-
#define GL_BASE_REPO "base"
+
+/* An extracted entry's mode: the archive's, never group/world-writable,
+ * and applied explicitly so the caller's umask doesn't decide whether
+ * other users can run a system package. */
+#define GL_ENTRY_MODE(e) ((mode_t)(archive_entry_perm(e) & 07755))
#define GL_BASE_LINKS_DEST "/usr"
bool
@@ -78,31 +75,6 @@ gl_usr_base_status(uid_t uid)
return (int)gl_usr_base_check(uid);
}
-bool
-gl_sys_istore_exists(index_store_t index_or_store)
-{
- char path[PATH_MAX];
- struct stat st;
-
- switch (index_or_store) {
- case INDEX:
- snprintf(path, sizeof(path), "%s", sys_index.path);
- break;
- case STORE:
- snprintf(path, sizeof(path), "%s", sys_store.path);
- break;
- default:
- printf("libglacier: argument index_or_store of function gl_sys_istore_exists must be INDEX or STORE\n");
- return false;
- }
-
- if (stat(path, &st) != 0) {
- return false;
- }
-
- return S_ISDIR(st.st_mode);
-}
-
static void
make_index_name(char *buffer, size_t buffersz)
{
@@ -112,7 +84,7 @@ make_index_name(char *buffer, size_t buffersz)
}
static int
-create_index(const char *ind_path, uid_t uid)
+create_index(const char *ind_path, uid_t uid, mode_t mode)
{
char fname[64];
make_index_name(fname, sizeof(fname));
@@ -128,11 +100,10 @@ create_index(const char *ind_path, uid_t uid)
/* fopen()'s mode is subject to the caller's umask; every later
* index file goes through mkstemp() in gl_rebuild_index(), which
- * POSIX guarantees creates at 0600 regardless of umask. Match that
- * here explicitly so the first index isn't the one file whose
- * permissions depend on whatever umask gstore happened to run
- * under. */
- if (chmod(full_path, 0600) != 0) { return -1; }
+ * then sets the scope's mode explicitly. Do the same here so the
+ * first index isn't the one file whose permissions depend on
+ * whatever umask gstore happened to run under. */
+ if (chmod(full_path, mode) != 0) { return -1; }
if (chown(full_path, uid, (gid_t)-1) != 0) { return -1; }
@@ -172,7 +143,11 @@ is_glacier_symlink(const char *path, const char *store_prefix)
ssize_t n = readlink(path, target, sizeof(target) - 1);
if (n < 0) { return false; } /* not a symlink, or doesn't exist */
target[n] = '\0';
- return strncmp(target, store_prefix, strlen(store_prefix)) == 0;
+ /* The store itself, or something under it: "/glacier/sys/store"
+ * must not also match "/glacier/sys/store2/...". */
+ size_t plen = strlen(store_prefix);
+ return strncmp(target, store_prefix, plen) == 0 &&
+ (target[plen] == '/' || target[plen] == '\0');
}
@@ -254,7 +229,7 @@ gl_init_user(uid_t uid, bool isVerbose)
}
}
- if (create_index(ind_path, uid) != 0) {
+ if (create_index(ind_path, uid, gl_scope_file_mode(GL_SCOPE_USR)) != 0) {
lg_printf(2, "Cannot create initial index file");
goto fail;
}
@@ -359,47 +334,81 @@ gl_create_fs_tree(const char *root, bool isVerbose)
int
gl_init_sys(bool isVerbose)
{
+ return gl_init_sys_at("/", isVerbose);
+}
+
+/* gl_init_sys under an install root (IMAGE_DESIGN section 3): the tree,
+ * /glacier/sys and its index are all created under `sysroot`, which
+ * must already exist. "/" is the running system. */
+int
+gl_init_sys_at(const char *sysroot, bool isVerbose)
+{
+ char root[PATH_MAX];
+ if (gl_normalize_sysroot(sysroot, root, sizeof(root)) != 0) {
+ lg_printf(2, "Refusing install root %s: not an absolute path without . or .. components.",
+ sysroot ? sysroot : "(null)");
+ return 1;
+ }
+
+ if (root[0] != '\0') {
+ char bad[PATH_MAX];
+ const char *why = "";
+ if (gl_sysroot_trusted(root, bad, sizeof(bad), &why) != 0) {
+ lg_printf(2, "Refusing install root %s: %s is %s.", root, bad, why);
+ return 1;
+ }
+ }
+
+ char glacier[PATH_MAX], sys[PATH_MAX], index[PATH_MAX], store[PATH_MAX], links[PATH_MAX];
+ snprintf(glacier, sizeof(glacier), "%s/glacier", root);
+ snprintf(sys, sizeof(sys), "%s%s", root, GL_SYS_ROOT);
+ snprintf(index, sizeof(index), "%s%s/index", root, GL_SYS_ROOT);
+ snprintf(store, sizeof(store), "%s%s/store", root, GL_SYS_ROOT);
+ snprintf(links, sizeof(links), "%s%s/links", root, GL_SYS_ROOT);
+
/* Already initialized == a system index exists. Refuse, same as
* gl_init_user does for an existing user base, before touching
* anything. A partial tree (dirs but no index yet, e.g. pre-created
* by an image build) is NOT this case: it gets completed below. */
char current[PATH_MAX];
- snprintf(current, sizeof(current), "%s/current", sys_index.path);
+ snprintf(current, sizeof(current), "%s/current", index);
struct stat st;
if (lstat(current, &st) == 0) {
- lg_printf(2, "Cannot create %s: already exists", GL_SYS_ROOT);
+ lg_printf(2, "Cannot create %s: already exists", sys);
return 1;
}
/* The top-level tree goes BEFORE the index: the index is what marks
* -n as done, so if the tree fails part-way a re-run can finish it
* instead of being refused as "already exists". */
- if (gl_create_fs_tree("/", isVerbose) != 0) {
+ if (gl_create_fs_tree(root[0] ? root : "/", isVerbose) != 0) {
return 1;
}
/* Parents first: unlike /glacier/usr, nothing else creates these,
* and mkdir() of sys/index would otherwise fail ENOENT. */
const char *paths[] = {
- "/glacier",
- "/glacier/sys",
- sys_index.path,
- sys_store.path,
- sys_links.path,
- };
- mode_t modes[] = {
- 0755,
- 0755,
- sys_index.mode,
- sys_store.mode,
- sys_links.mode,
+ glacier,
+ sys,
+ index,
+ store,
+ links,
};
+ mode_t mode = gl_scope_dir_mode(GL_SCOPE_SYS);
for (int i = 0; i < 5; i++) {
if (isVerbose) { lg_printf(0, "Creating %s", paths[i]); }
- if (mkdir(paths[i], modes[i]) == -1) {
+ /* chmod after a successful mkdir: the store must be 0755
+ * whatever umask syspkg runs under (see gl_scope_dir_mode).
+ * A directory that already existed is left as it is. */
+ if (mkdir(paths[i], mode) == 0) {
+ if (chmod(paths[i], mode) != 0) {
+ lg_printf(2, "Cannot set the mode of %s: %s", paths[i], strerror(errno));
+ return 1;
+ }
+ } else {
switch (errno) {
case EEXIST:
break;
@@ -424,7 +433,7 @@ gl_init_sys(bool isVerbose)
* fresh index. lstat() rather than stat() so a dangling `current`
* still counts as present and is left for a human to look at. */
if (lstat(current, &st) == 0) {
- lg_printf(2, "Cannot create %s: already exists", GL_SYS_ROOT);
+ lg_printf(2, "Cannot create %s: already exists", sys);
return 1;
}
if (errno != ENOENT) {
@@ -433,7 +442,7 @@ gl_init_sys(bool isVerbose)
}
if (isVerbose) { lg_printf(0, "Creating initial system index"); }
- if (create_index(sys_index.path, 0) != 0) {
+ if (create_index(index, 0, gl_scope_file_mode(GL_SCOPE_SYS)) != 0) {
lg_printf(2, "Cannot create initial system index file");
return 1;
}
@@ -441,118 +450,6 @@ gl_init_sys(bool isVerbose)
return 0;
}
-static void
-trim(char *s)
-{
- char *p = s;
- while (*p && isspace((unsigned char)*p)) {
- p++;
- }
-
- if (p != s) {
- memmove(s, p, strlen(p) + 1);
- }
-
- size_t len = strlen(s);
- while (len && isspace((unsigned char)s[len - 1])) {
- s[--len] = 0;
- }
-}
-
-static char
-*dupstr(const char *s)
-{
- size_t n = strlen(s);
- char *p = malloc(n + 1);
- if (!p) {
- return NULL;
- }
-
- memcpy(p, s, n + 1);
- return p;
-}
-
-int
-gl_parse_index(struct gindex *idx, FILE *f)
-{
- idx->uid = -1;
- idx->listed = -1;
- idx->entries = NULL;
- idx->count = 0;
-
- char line[512];
-
- while (fgets(line, sizeof(line), f)) {
- trim(line);
- if (!line[0] || line[0] == '#') { continue; }
-
- char *eq = strchr(line, '=');
- if (eq) {
- *eq = 0;
- char *key = line;
- char *val = eq + 1;
- trim(key);
- trim(val);
-
- if (strcmp(key, "uid") == 0) { idx->uid = atoi(val); }
- else if (strcmp(key, "listed") == 0) { idx->listed=atoi(val); }
- continue;
- }
-
- char *first = strstr(line, "::");
- if (!first) { continue; }
- *first = 0;
-
- char *repo = line;
- char *rest = first + 2;
-
- char *second = strstr(rest, "::");
- char *pkg;
- char *ver;
- if (second) {
- *second = 0;
- pkg = rest;
- ver = second + 2;
- } else {
- /* Older index files may only have repo::pkg with no
- * version field — tolerate that rather than reject it. */
- pkg = rest;
- ver = "unknown";
- }
-
- trim(repo);
- trim(pkg);
- trim(ver);
-
- struct gpkg_entry ent;
- ent.repo = dupstr(repo);
- ent.pkg = dupstr(pkg);
- ent.ver = dupstr(ver);
- if (!ent.repo || !ent.pkg || !ent.ver) { return -1; }
-
- struct gpkg_entry *tmp =
- realloc(idx->entries, sizeof(*idx->entries) * (idx->count + 1));
- if (!tmp) { return -1; }
- idx->entries = tmp;
- idx->entries[idx->count++] = ent;
- }
-
- return 0;
-}
-
-void
-gl_free_index(struct gindex *idx)
-{
- for (size_t i = 0; i < idx->count; i++) {
- free(idx->entries[i].repo);
- free(idx->entries[i].pkg);
- free(idx->entries[i].ver);
- }
-
- free(idx->entries);
- idx->entries = NULL;
- idx->count = 0;
-}
int
gl_rebuild_index(gl_context_t *ctx)
{
@@ -569,7 +466,15 @@ gl_rebuild_index(gl_context_t *ctx)
snprintf(new_index_path, sizeof(new_index_path), "%s/%s",
ctx->index_path, fname);
- FILE *out = fopen(new_index_path, "w");
+ /* Entries go to an anonymous temp file first (the header needs
+ * the count). Never fopen(new_index_path, "w"): in a stage, an
+ * index file written in the same second as live's has the same
+ * name and is a HARDLINK to live's (seeding), so truncating it
+ * rewrote the live index in place, and an aborted transaction
+ * left live listing the stage's packages. The new index only
+ * reaches its name by rename(), which replaces the entry, not
+ * the inode. */
+ FILE *out = tmpfile();
if (!out) {
closedir(store_dir);
return -1;
@@ -614,6 +519,7 @@ gl_rebuild_index(gl_context_t *ctx)
* contains dashes, since we already know its
* exact length from pkg_ent->d_name. */
char pkg_ver[64] = "unknown";
+ bool automatic = false;
DIR *ver_dir = opendir(pkg_path);
if (ver_dir) {
struct dirent *ver_ent;
@@ -624,31 +530,42 @@ gl_rebuild_index(gl_context_t *ctx)
snprintf(pkg_ver, sizeof(pkg_ver), "%s",
ver_ent->d_name + prefix_len);
}
+ char marker[PATH_MAX];
+ struct stat mst;
+ snprintf(marker, sizeof(marker), "%s/%s/" GL_AUTO_MARKER,
+ pkg_path, ver_ent->d_name);
+ automatic = lstat(marker, &mst) == 0;
break;
}
closedir(ver_dir);
}
- fprintf(out, "%s::%s::%s\n",
+ /* repo::name::version::auto|explicit. "auto" =
+ * pulled in as a dependency (GL_AUTO_MARKER),
+ * not asked for by name. */
+ fprintf(out, "%s::%s::%s::%s\n",
repo_ent->d_name,
pkg_ent->d_name,
- pkg_ver);
+ pkg_ver,
+ automatic ? "auto" : "explicit");
listed++;
}
closedir(repo_dir);
}
closedir(store_dir);
- fclose(out);
/* Prepend header by copying through a temp file then renaming over */
- FILE *tmp_in = fopen(new_index_path, "r");
- if (!tmp_in) return -1;
+ FILE *tmp_in = out;
+ rewind(tmp_in);
char tmp_path[PATH_MAX];
snprintf(tmp_path, sizeof(tmp_path), "%s/.index_tmp_XXXXXX",
ctx->index_path);
int tmp_fd = mkstemp(tmp_path);
if (tmp_fd < 0) { fclose(tmp_in); return -1; }
+ if (fchmod(tmp_fd, gl_scope_file_mode(ctx->scope)) != 0) {
+ close(tmp_fd); unlink(tmp_path); fclose(tmp_in); return -1;
+ }
FILE *tmp_out = fdopen(tmp_fd, "w");
if (!tmp_out) { close(tmp_fd); fclose(tmp_in); return -1; }
@@ -677,107 +594,26 @@ gl_rebuild_index(gl_context_t *ctx)
return 0;
}
+/* Archive the entry `name` of the directory open at `parent` (AT_FDCWD:
+ * a path) as `archive_path`, recursively. Everything is opened relative
+ * to its parent's descriptor with O_NOFOLLOW, and what is archived is
+ * what the descriptor turned out to be: root backing up a user's store
+ * (gstore -b) can't be steered into a symlink to /etc/shadow, or into
+ * /root, by swapping an entry between a check and an open. */
static int
-add_to_archive(struct archive *a, const char *real_path, const char *archive_path)
+add_entry_at(struct archive *a, int parent, const char *name, const char *archive_path)
{
struct stat st;
-
- if (lstat(real_path, &st) != 0) {
+ if (fstatat(parent, name, &st, AT_SYMLINK_NOFOLLOW) != 0) {
return -1;
}
- if (S_ISDIR(st.st_mode)) {
- struct archive_entry *entry = archive_entry_new();
- archive_entry_set_pathname(entry, archive_path);
- archive_entry_set_filetype(entry, AE_IFDIR);
- archive_entry_set_perm(entry, st.st_mode & 0777);
-
- if (archive_write_header(a, entry) != ARCHIVE_OK) {
- archive_entry_free(entry);
- return -1;
- }
-
- archive_entry_free(entry);
-
- DIR *dir = opendir(real_path); /* NEW */
- if (!dir) {
- return -1;
- }
-
- struct dirent *ent;
- while ((ent = readdir(dir))) { /* NEW */
- if (strcmp(ent->d_name, ".") == 0 ||
- strcmp(ent->d_name, "..") == 0) {
- continue;
- }
-
- char child_real[PATH_MAX]; /* NEW */
- char child_arch[PATH_MAX]; /* NEW */
-
- snprintf(child_real, sizeof(child_real),
- "%s/%s", real_path, ent->d_name);
-
- snprintf(child_arch, sizeof(child_arch),
- "%s/%s", archive_path, ent->d_name);
-
- if (add_to_archive(a, child_real, child_arch) != 0) {
- closedir(dir);
- return -1;
- }
- }
-
- closedir(dir);
- return 0;
- }
-
- /* existing file handling logic continues below */
-
- if (S_ISREG(st.st_mode)) {
-
- int fd = open(real_path, O_RDONLY);
- if (fd < 0) {
- return -1;
- }
-
- struct archive_entry *entry = archive_entry_new();
- archive_entry_set_pathname(entry, archive_path);
- archive_entry_set_size(entry, st.st_size);
- archive_entry_set_filetype(entry, AE_IFREG);
- archive_entry_set_perm(entry, st.st_mode & 0777);
-
- if (archive_write_header(a, entry) != ARCHIVE_OK) {
- archive_entry_free(entry);
- close(fd);
- return -1;
- }
-
- char buf[8192];
- ssize_t n;
-
- while ((n = read(fd, buf, sizeof(buf))) > 0) {
- if (archive_write_data(a, buf, (size_t)n) < 0) {
- archive_entry_free(entry);
- close(fd);
- return -1;
- }
- }
-
- archive_entry_free(entry);
- close(fd);
-
- if (n < 0) {
- return -1;
- }
-
- return 0;
- }
-
/* Symlinks too: a package's soname links and its loader
* (musl's lib/ld-musl-*.so.1) are symlinks, and a backup that
* dropped them wouldn't restore a working package. */
if (S_ISLNK(st.st_mode)) {
char target[PATH_MAX];
- ssize_t n = readlink(real_path, target, sizeof(target) - 1);
+ ssize_t n = readlinkat(parent, name, target, sizeof(target) - 1);
if (n < 0) { return -1; }
target[n] = '\0';
@@ -790,8 +626,71 @@ add_to_archive(struct archive *a, const char *real_path, const char *archive_pat
archive_entry_free(entry);
return rc;
}
+ if (!S_ISDIR(st.st_mode) && !S_ISREG(st.st_mode)) {
+ return 0; /* devices, fifos, sockets: never in a package */
+ }
- return 0; /* devices, fifos, sockets: never in a package */
+ /* O_NONBLOCK: an entry swapped for a fifo must not hang us. */
+ int fd = openat(parent, name, O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC |
+ (S_ISDIR(st.st_mode) ? O_DIRECTORY : 0));
+ if (fd < 0 || fstat(fd, &st) != 0) {
+ if (fd >= 0) { close(fd); }
+ return -1;
+ }
+
+ struct archive_entry *entry = archive_entry_new();
+ archive_entry_set_pathname(entry, archive_path);
+ archive_entry_set_perm(entry, st.st_mode & 0777);
+
+ if (S_ISDIR(st.st_mode)) {
+ archive_entry_set_filetype(entry, AE_IFDIR);
+ int rc = archive_write_header(a, entry) == ARCHIVE_OK ? 0 : -1;
+ archive_entry_free(entry);
+ DIR *dir = rc == 0 ? fdopendir(fd) : NULL;
+ if (!dir) {
+ close(fd);
+ return -1;
+ }
+
+ struct dirent *ent;
+ while (rc == 0 && (ent = readdir(dir))) {
+ if (strcmp(ent->d_name, ".") == 0 ||
+ strcmp(ent->d_name, "..") == 0) {
+ continue;
+ }
+ char child_arch[PATH_MAX];
+ snprintf(child_arch, sizeof(child_arch),
+ "%s/%s", archive_path, ent->d_name);
+ rc = add_entry_at(a, dirfd(dir), ent->d_name, child_arch);
+ }
+ closedir(dir);
+ return rc;
+ }
+
+ if (!S_ISREG(st.st_mode)) { /* swapped for something else */
+ archive_entry_free(entry);
+ close(fd);
+ return 0;
+ }
+
+ archive_entry_set_filetype(entry, AE_IFREG);
+ archive_entry_set_size(entry, st.st_size);
+ int rc = archive_write_header(a, entry) == ARCHIVE_OK ? 0 : -1;
+ archive_entry_free(entry);
+
+ char buf[8192];
+ ssize_t n = 0;
+ while (rc == 0 && (n = read(fd, buf, sizeof(buf))) > 0) {
+ if (archive_write_data(a, buf, (size_t)n) < 0) { rc = -1; }
+ }
+ close(fd);
+ return (rc == 0 && n == 0) ? 0 : -1;
+}
+
+static int
+add_to_archive(struct archive *a, const char *real_path, const char *archive_path)
+{
+ return add_entry_at(a, AT_FDCWD, real_path, archive_path);
}
/* Write `content` as a regular file entry. */
@@ -834,6 +733,40 @@ find_version_dir(const char *store, const char *repo, const char *name,
return rc;
}
+/*
+ * gl_set_pkg_auto
+ * DESCRIPTION: marks the installed repo/name as installed automatically
+ * (as a dependency) or explicitly, and rebuilds ctx's index so
+ * its 4th field says so. Nothing calls this with true until
+ * dependencies are resolved from remote repositories (ROADMAP
+ * 2.3 / 3.1); reinstalling a package by name drops the marker.
+ * RETURNS: 0 on success, -1 if the package isn't installed or the marker
+ * or index can't be written
+ */
+int
+gl_set_pkg_auto(gl_context_t *ctx, const char *repo, const char *name, bool automatic)
+{
+ if (!ctx || !gl_valid_pkg_ident(repo) || !gl_valid_pkg_ident(name)) { return -1; }
+
+ char vdir[PATH_MAX], dirname[NAME_MAX + 1];
+ if (find_version_dir(ctx->store_path, repo, name, vdir, sizeof(vdir),
+ dirname, sizeof(dirname)) != 0) {
+ return -1;
+ }
+
+ char marker[PATH_MAX];
+ snprintf(marker, sizeof(marker), "%s/" GL_AUTO_MARKER, vdir);
+ if (automatic) {
+ int fd = open(marker, O_WRONLY | O_CREAT | O_NOFOLLOW, 0600);
+ if (fd < 0) { return -1; }
+ close(fd);
+ } else if (unlink(marker) != 0 && errno != ENOENT) {
+ return -1;
+ }
+
+ return gl_rebuild_index(ctx) == 0 ? 0 : -1;
+}
+
/*
* Archive the installed version of repo/name back into a .gpkg at
* out_path (same layout gl_install_pkg reads: NAME-VER/manifest.gpm.cfg
@@ -1026,67 +959,6 @@ gl_backup_istore(const char *tar_path,
return GL_BACKUP_OK;
}
-static int
-gl_unlink_pkg(gl_context_t *ctx, const char *pkg_store_final, const char *pkg_repo)
-{
- char links_base[PATH_MAX];
- if (ctx->scope == GL_SCOPE_SYS && strcmp(pkg_repo, GL_BASE_REPO) == 0) {
- snprintf(links_base, sizeof(links_base), "%s", GL_BASE_LINKS_DEST);
- } else {
- snprintf(links_base, sizeof(links_base), "%s", ctx->links_path);
- }
-
- size_t strip_len = strlen(pkg_store_final);
-
- char *stack[4096];
- int top = 0;
- stack[top++] = strdup(pkg_store_final);
-
- while (top > 0) {
- char * current = stack[--top];
-
- DIR *dir = opendir(current);
- if (!dir) {
- free(current);
- continue;
- }
-
- struct dirent *ent;
- while ((ent = readdir(dir))) {
- if (ent->d_name[0] == '.') { continue; }
-
- char full_path[PATH_MAX];
- snprintf(full_path, sizeof(full_path), "%s/%s",
- current, ent->d_name);
-
- struct stat st;
- if (lstat(full_path, &st) != 0) { continue; }
- const char *rel = full_path + strip_len;
- if (rel[0] == '/') { rel++; }
-
- if (strncmp(rel, "usr/", 4) == 0) { rel += 4; }
-
- char link_path[PATH_MAX];
- snprintf(link_path, sizeof(link_path), "%s/%s",
- links_base, rel);
-
- if (S_ISDIR(st.st_mode)) {
- if (top < 4096) {
- stack[top++] = strdup(full_path);
- }
- }
- else if (S_ISREG(st.st_mode)) {
- unlink(link_path);
- }
- }
-
- closedir(dir);
- free(current);
- }
-
- return 0;
-}
-
gl_remove_status_t
gl_remove_pkg(gl_context_t *ctx, const char *pkg_name, const char *pkg_repo)
{
@@ -1109,6 +981,16 @@ gl_remove_pkg(gl_context_t *ctx, const char *pkg_name, const char *pkg_repo)
return GL_REMOVE_ERR_NOT_FOUND;
}
+ /* Live context: no commit will check this (see gl_install_pkg). */
+ if (ctx->mode == GL_CTX_LIVE) {
+ char id[2 * NAME_MAX + 3];
+ snprintf(id, sizeof(id), "%s::%s", pkg_repo, pkg_name);
+ const char *ids = id;
+ if (gl_check_plan(ctx, NULL, 0, &ids, 1) != 0) {
+ return GL_REMOVE_ERR_DEPS;
+ }
+ }
+
/* find the versioned subdirectory */
DIR *dir = opendir(pkg_store_base);
if (!dir) { return GL_REMOVE_ERR_NOT_FOUND; }
@@ -1127,16 +1009,6 @@ gl_remove_pkg(gl_context_t *ctx, const char *pkg_name, const char *pkg_repo)
return GL_REMOVE_ERR_NOT_FOUND;
}
- /* remove symlinks from links tree. GL_CTX_STAGE: skip — the
- * stage links tree isn't seeded (links are fully derived from
- * the store post-commit by relink_store), so there's nothing
- * there to unlink. */
- if (ctx->mode == GL_CTX_LIVE) {
- if (gl_unlink_pkg(ctx, pkg_store_final, pkg_repo) != 0) {
- return GL_REMOVE_ERR_UNLINK;
- }
- }
-
/* remove package from store */
if (gl_rmdir_recursive(pkg_store_base) != 0) {
return GL_REMOVE_ERR_UNLINK;
@@ -1147,6 +1019,16 @@ gl_remove_pkg(gl_context_t *ctx, const char *pkg_name, const char *pkg_repo)
return GL_REMOVE_ERR_INDEX;
}
+ /* Live context: drop the package's links the way a commit does,
+ * by pruning glacier's now-stale links (which also restores what
+ * -O displaced, and handles /etc). This used to unlink() each of
+ * the package's destinations unchecked, deleting foreign files
+ * the package had been refused to replace (CLAUDE.md bug 26).
+ * GL_CTX_STAGE: nothing to do, links are derived after commit. */
+ if (ctx->mode == GL_CTX_LIVE && gl_relink_store(ctx) != 0) {
+ return GL_REMOVE_ERR_UNLINK;
+ }
+
return GL_REMOVE_OK;
}
@@ -1170,11 +1052,10 @@ gl_remove_pkg(gl_context_t *ctx, const char *pkg_name, const char *pkg_repo)
* anything on its own.
*/
#define GL_DISPLACED_DIR ".displaced"
-#define GL_OVERWRITE_MARKER ".glacier-overwrite"
-/* Same one-shot mechanism, for every install: marks a package as new in
- * this transaction, so its link conflicts are reported once, when it's
- * installed — not again on every later commit's full relink. */
-#define GL_FRESH_MARKER ".glacier-fresh"
+/* GL_FRESH_MARKER (common.h) is the same one-shot mechanism, for every
+ * install: marks a package as new in this transaction, so its link
+ * conflicts are reported once, when it's installed — not again on every
+ * later commit's full relink. */
static int
displaced_path(char *buf, size_t bufsz, const char *store_path, const char *path)
@@ -1266,8 +1147,10 @@ move_path(const char *src, const char *dst)
* untouched) for anything but a regular file or symlink, and if a
* displaced original for this path is already being kept — never
* overwrite the one copy of something we promised to put back. */
+/* `path` is an install path; its backup is keyed by its RUNTIME path, so
+ * an image's .displaced means the same thing once the image is booted. */
static int
-displace_path(const char *store_path, const char *path, const struct stat *st)
+displace_path(const gl_context_t *ctx, const char *path, const struct stat *st)
{
if (!S_ISREG(st->st_mode) && !S_ISLNK(st->st_mode)) {
lg_printf(1, "Refusing to overwrite %s: not a regular file or symlink", path);
@@ -1275,7 +1158,8 @@ displace_path(const char *store_path, const char *path, const struct stat *st)
}
char backup[PATH_MAX];
- if (displaced_path(backup, sizeof(backup), store_path, path) != 0) {
+ if (displaced_path(backup, sizeof(backup), ctx->store_path,
+ gl_runtime_path(ctx, path)) != 0) {
lg_printf(1, "Refusing to overwrite %s: path too long", path);
return -1;
}
@@ -1310,10 +1194,12 @@ displace_path(const char *store_path, const char *path, const struct stat *st)
* Returns 1 if restored, 0 if there was nothing to restore, -1 on
* failure (backup left where it is). */
static int
-restore_displaced(const char *store_path, const char *path)
+restore_displaced(const gl_context_t *ctx, const char *path)
{
+ const char *store_path = ctx->store_path;
char backup[PATH_MAX];
- if (displaced_path(backup, sizeof(backup), store_path, path) != 0) { return 0; }
+ if (displaced_path(backup, sizeof(backup), store_path,
+ gl_runtime_path(ctx, path)) != 0) { return 0; }
struct stat bst;
if (lstat(backup, &bst) != 0) { return 0; }
@@ -1429,7 +1315,7 @@ static int
install_config(gl_context_t *ctx, const char *src, const char *rel)
{
char target[PATH_MAX], record[PATH_MAX], offer[PATH_MAX];
- snprintf(target, sizeof(target), GL_SYS_ETC "/%s", rel);
+ snprintf(target, sizeof(target), "%s" GL_SYS_ETC "/%s", ctx->sysroot, rel);
snprintf(record, sizeof(record), "%s/" GL_ETC_STATE_DIR "/%s", ctx->store_path, rel);
snprintf(offer, sizeof(offer), "%s.new", target);
@@ -1517,7 +1403,8 @@ store_provides_config(const char *store, const char *rel)
/* Walk the records; for any file no package ships any more, remove it
* from /etc if the admin never changed it, keep it if they did. */
static void
-prune_config_dir(const char *store, const char *state_root, const char *sub)
+prune_config_dir(const char *store, const char *etc, const char *state_root,
+ const char *sub)
{
char dir[PATH_MAX];
snprintf(dir, sizeof(dir), "%s%s%s", state_root, sub[0] ? "/" : "", sub);
@@ -1536,7 +1423,7 @@ prune_config_dir(const char *store, const char *state_root, const char *sub)
struct stat st;
if (lstat(record, &st) != 0) { continue; }
if (S_ISDIR(st.st_mode)) {
- prune_config_dir(store, state_root, rel);
+ prune_config_dir(store, etc, state_root, rel);
rmdir(record); /* only succeeds once empty */
continue;
}
@@ -1544,7 +1431,7 @@ prune_config_dir(const char *store, const char *state_root, const char *sub)
if (store_provides_config(store, rel)) { continue; }
char target[PATH_MAX];
- snprintf(target, sizeof(target), GL_SYS_ETC "/%s", rel);
+ snprintf(target, sizeof(target), "%s/%s", etc, rel);
if (config_equal(target, record)) {
unlink(target);
} else if (lstat(target, &st) == 0) {
@@ -1560,15 +1447,22 @@ prune_config(gl_context_t *ctx)
{
char state_root[PATH_MAX];
snprintf(state_root, sizeof(state_root), "%s/" GL_ETC_STATE_DIR, ctx->store_path);
- prune_config_dir(ctx->store_path, state_root, "");
+ char etc[PATH_MAX];
+ snprintf(etc, sizeof(etc), "%s" GL_SYS_ETC, ctx->sysroot);
+ prune_config_dir(ctx->store_path, etc, state_root, "");
}
static int
gl_link_pkg(gl_context_t *ctx, const char *pkg_store_final, const char *pkg_repo)
{
+ /* Install paths for everything touched on disk; the RUNTIME store
+ * path for what a link says and for recognising glacier's links
+ * (see gl_context_t.sysroot). */
+ const char *store_rt = gl_runtime_path(ctx, ctx->store_path);
char links_base[PATH_MAX];
if (ctx->scope == GL_SCOPE_SYS && strcmp(pkg_repo, GL_BASE_REPO) == 0) {
- snprintf(links_base, sizeof(links_base), "%s", GL_BASE_LINKS_DEST);
+ snprintf(links_base, sizeof(links_base), "%s%s", ctx->sysroot,
+ GL_BASE_LINKS_DEST);
} else {
snprintf(links_base, sizeof(links_base), "%s", ctx->links_path);
}
@@ -1646,15 +1540,22 @@ gl_link_pkg(gl_context_t *ctx, const char *pkg_store_final, const char *pkg_repo
* INSIDE A's store directory. */
struct stat lst;
if (lstat(link_path, &lst) == 0 && S_ISLNK(lst.st_mode) &&
- is_glacier_symlink(link_path, ctx->store_path)) {
- lg_printf(1,
- "Refusing to merge directory %s into another package's symlink",
- link_path);
+ is_glacier_symlink(link_path, store_rt)) {
+ /* Reported (and counted) once, by
+ * the transaction that installs
+ * this package, like the refusal
+ * below (bug 21). */
+ if (fresh) {
+ lg_printf(1,
+ "Refusing to merge directory %s into another package's symlink",
+ link_path);
+ ctx->link_refusals++;
+ }
continue;
}
/* create the directory in the links tree */
- gl_mkdirp(link_path, 0700);
+ gl_mkdirp(link_path, gl_scope_dir_mode(ctx->scope));
/* push onto stack to recurse into */
if (top < 4096) {
@@ -1675,7 +1576,7 @@ gl_link_pkg(gl_context_t *ctx, const char *pkg_store_final, const char *pkg_repo
char *slash = strrchr(parent, '/');
if (slash) {
*slash = '\0';
- gl_mkdirp(parent, 0700);
+ gl_mkdirp(parent, gl_scope_dir_mode(ctx->scope));
}
/* Only ever replace an existing entry if it's a
@@ -1688,16 +1589,20 @@ gl_link_pkg(gl_context_t *ctx, const char *pkg_store_final, const char *pkg_repo
bool was_displaced = false;
struct stat existing_st;
if (lstat(link_path, &existing_st) == 0 &&
- !is_glacier_symlink(link_path, ctx->store_path)) {
+ !is_glacier_symlink(link_path, store_rt)) {
if (!overwrite) {
if (fresh) {
lg_printf(1,
"Refusing to overwrite non-glacier path: %s",
link_path);
+ ctx->link_refusals++;
}
may_link = false;
- } else if (displace_path(ctx->store_path, link_path,
+ } else if (displace_path(ctx, link_path,
&existing_st) != 0) {
+ /* only with the -O marker,
+ * i.e. a fresh package */
+ ctx->link_refusals++;
may_link = false;
} else {
was_displaced = true;
@@ -1708,10 +1613,11 @@ gl_link_pkg(gl_context_t *ctx, const char *pkg_store_final, const char *pkg_repo
if (may_link) {
unlink(link_path);
- /* symlink store path -> links tree */
- if (symlink(full_path, link_path) != 0) {
+ /* symlink store path -> links tree; the
+ * target is the runtime path */
+ if (symlink(gl_runtime_path(ctx, full_path), link_path) != 0) {
if (was_displaced) {
- restore_displaced(ctx->store_path, link_path);
+ restore_displaced(ctx, link_path);
}
closedir(dir);
free(current);
@@ -1730,8 +1636,8 @@ gl_link_pkg(gl_context_t *ctx, const char *pkg_store_final, const char *pkg_repo
if (displaced > 0) {
const char *pkg_label = strrchr(pkg_store_final, '/');
- lg_printf(0, "%s: overwrote %d existing path%s; originals kept under %s/"
- GL_DISPLACED_DIR " and restored on removal",
+ lg_printf(0, "%s: displaced %d existing path%s to %s"
+ GL_DISPLACED_DIR,
pkg_label ? pkg_label + 1 : pkg_store_final,
displaced, displaced == 1 ? "" : "s", ctx->store_path);
}
@@ -1771,38 +1677,11 @@ gl_delete_user(uid_t uid, bool isVerbose)
return 0;
}
-char
-*gl_find_pkg_repo(gl_context_t *ctx, const char *pkg_name)
-{
- char store_path[PATH_MAX];
- snprintf(store_path, sizeof(store_path), "%s", ctx->store_path);
-
- DIR *uid_dir = opendir(store_path);
- if (!uid_dir) { return NULL; }
-
- struct dirent *repo_entry;
- while ((repo_entry = readdir(uid_dir)) != NULL) {
- if (repo_entry->d_name[0] == '.') { continue; }
-
- char pkg_path[PATH_MAX];
- snprintf(pkg_path, sizeof(pkg_path), "%s/%s/%s", store_path, repo_entry->d_name, pkg_name);
-
- struct stat st;
- if (stat(pkg_path, &st) == 0 && S_ISDIR(st.st_mode)) {
- char *repo = strdup(repo_entry->d_name);
- closedir(uid_dir);
- return repo;
- }
- }
-
- closedir(uid_dir);
- return NULL;
-}
-
gl_repo_list_t
gl_find_pkg_repos(gl_context_t *ctx, const char *pkg_name)
{
gl_repo_list_t result = { NULL, 0 };
+ if (!ctx || !gl_valid_pkg_ident(pkg_name)) { return result; }
char store_path[PATH_MAX];
snprintf(store_path, sizeof(store_path), "%s", ctx->store_path);
@@ -1843,80 +1722,6 @@ gl_free_repo_list(gl_repo_list_t *list)
list->count = 0;
}
-static bool is_safe_rel_path(const char *rel);
-
-gl_restore_status_t
-gl_restore_istore(const char *tar_path, const char *glacier_root, const char *uid)
-{
- struct archive *a = archive_read_new();
- archive_read_support_filter_gzip(a);
- archive_read_support_format_tar(a);
-
- if (archive_read_open_filename(a, tar_path, 10240) != ARCHIVE_OK) {
- archive_read_free(a);
- return GL_RESTORE_ERR_OPEN;
- }
-
- struct archive_entry *entry;
- while (archive_read_next_header(a, &entry) == ARCHIVE_OK) {
- const char *rel = archive_entry_pathname(entry);
-
- /* Entries are written as "/{index,store,links}/..."
- * by gl_backup_istore, so they join onto glacier_root
- * directly (joining "//" doubled the uid).
- * Only accept this uid's own subtree, with no traversal:
- * the archive is just a file someone hands us. */
- size_t uid_len = strlen(uid);
- if (strncmp(rel, uid, uid_len) != 0 || rel[uid_len] != '/' ||
- !is_safe_rel_path(rel)) {
- archive_read_free(a);
- return GL_RESTORE_ERR_EXTRACT;
- }
-
- char dest[PATH_MAX];
- snprintf(dest, sizeof(dest), "%s/%s", glacier_root, rel);
-
- archive_entry_set_pathname(entry, dest);
-
- if (archive_entry_filetype(entry) == AE_IFDIR) {
- gl_mkdirp(dest, archive_entry_perm(entry));
- archive_read_data_skip(a);
- continue;
- }
-
- /* create parent dirs */
- char parent[PATH_MAX];
- strncpy(parent, dest, sizeof(parent));
- char *slash = strrchr(parent, '/');
- if (slash) {
- *slash = '\0';
- gl_mkdirp(parent, 0700);
- }
-
- int fd = open(dest, O_WRONLY | O_CREAT | O_TRUNC,
- archive_entry_perm(entry));
- if (fd < 0) {
- archive_read_free(a);
- return GL_RESTORE_ERR_EXTRACT;
- }
-
- char buf[8192];
- ssize_t n;
- while ((n = archive_read_data(a, buf, sizeof(buf))) > 0) {
- ssize_t written = write(fd, buf, (size_t)n);
- if (written < 0 || written != n) {
- close(fd);
- archive_read_free(a);
- return GL_RESTORE_ERR_EXTRACT;
- }
- }
- close(fd);
- }
-
- archive_read_free(a);
- return GL_RESTORE_OK;
-}
-
/*
* A tar entry path (after the "-/files/" prefix is stripped) is
* safe to join onto the package's store directory only if it is relative
@@ -1965,16 +1770,16 @@ crosses_symlink(const char *base, const char *path)
return false;
}
-gl_install_status_t
-gl_install_pkg(gl_context_t *ctx, const char *gpkg_path)
+/*
+ * gl_read_pkg_manifest
+ * DESCRIPTION: parses the manifest.gpm.cfg inside a .gpkg into *pkg
+ * (which must be zeroed; free it with gl_free_pkg on success).
+ * RETURNS: GL_INSTALL_OK, GL_INSTALL_ERR_OPEN (archive unreadable) or
+ * GL_INSTALL_ERR_MANIFEST (missing, unparseable, or unsafe name/repo)
+ */
+int
+gl_read_pkg_manifest(const char *gpkg_path, gl_pkg_t *pkg)
{
- if (!ctx) { return GL_INSTALL_ERR_OPEN; }
- char original_cwd[PATH_MAX];
- if (getcwd(original_cwd, sizeof(original_cwd)) == NULL) {
- return GL_INSTALL_ERR_OPEN;
- }
-
- /* The archive is opened and the manifest is parsed here */
struct archive *a = archive_read_new();
archive_read_support_filter_gzip(a);
archive_read_support_format_tar(a);
@@ -1985,9 +1790,7 @@ gl_install_pkg(gl_context_t *ctx, const char *gpkg_path)
}
struct archive_entry *entry;
- gl_pkg_t pkg = {0};
bool manifest_found = false;
-
while (archive_read_next_header(a, &entry) == ARCHIVE_OK) {
const char *path = archive_entry_pathname(entry);
@@ -2017,11 +1820,11 @@ gl_install_pkg(gl_context_t *ctx, const char *gpkg_path)
close(fd);
- int rc = gl_gpm2gpkg(tmp_manifest, &pkg);
+ int rc = gl_gpm2gpkg(tmp_manifest, pkg);
if (rc != 0) {
if (rc == EINVAL) {
lg_printf(2,
- "Manifest PKG_NAME/PKG_REPO missing or not a safe identifier ([A-Za-z0-9._-]).");
+ "Manifest has a missing or unsafe name ([A-Za-z0-9._-] only).");
}
unlink(tmp_manifest);
archive_read_free(a);
@@ -2042,6 +1845,20 @@ gl_install_pkg(gl_context_t *ctx, const char *gpkg_path)
return GL_INSTALL_ERR_MANIFEST;
}
+ return GL_INSTALL_OK;
+}
+
+gl_install_status_t
+gl_install_pkg(gl_context_t *ctx, const char *gpkg_path)
+{
+ if (!ctx) { return GL_INSTALL_ERR_OPEN; }
+
+ gl_pkg_t pkg = {0};
+ int mrc = gl_read_pkg_manifest(gpkg_path, &pkg);
+ if (mrc != GL_INSTALL_OK) {
+ return (gl_install_status_t)mrc;
+ }
+
/*
* Base-repo packages can NEVER be installed under per-user scope.
* Enforced here, at the library level, rather than in any CLI: the
@@ -2062,7 +1879,18 @@ gl_install_pkg(gl_context_t *ctx, const char *gpkg_path)
gl_free_pkg(&pkg);
return GL_INSTALL_ERR_SCOPE;
}
-
+
+ /* A live context has no commit to check it (gl_check_txn), so the
+ * dependency rules are applied here instead. Staged installs are
+ * checked as a whole at commit: a batch may satisfy itself. */
+ if (ctx->mode == GL_CTX_LIVE) {
+ const char *self = gpkg_path;
+ if (gl_check_plan(ctx, &self, 1, NULL, 0) != 0) {
+ gl_free_pkg(&pkg);
+ return GL_INSTALL_ERR_DEPS;
+ }
+ }
+
char pkg_store_base[PATH_MAX];
char pkg_store_final[PATH_MAX];
@@ -2074,12 +1902,20 @@ gl_install_pkg(gl_context_t *ctx, const char *gpkg_path)
"%s/%s-%d.%d.%d", pkg_store_base, pkg.pkg_name,
pkg.pkg_ver.maj, pkg.pkg_ver.min, pkg.pkg_ver.pat);
- if (gl_mkdirp(pkg_store_base, 0700) == -1) {
+ /* store/, store// and the version dir get the
+ * scope's mode explicitly, whatever the umask: in system scope
+ * every user must be able to reach what /usr links to. */
+ mode_t dir_mode = gl_scope_dir_mode(ctx->scope);
+ char pkg_repo_dir[PATH_MAX];
+ snprintf(pkg_repo_dir, sizeof(pkg_repo_dir), "%s/%s", ctx->store_path, pkg.pkg_repo);
+
+ if (gl_mkdirp(pkg_store_base, dir_mode) == -1 ||
+ chmod(pkg_repo_dir, dir_mode) != 0 || chmod(pkg_store_base, dir_mode) != 0) {
gl_free_pkg(&pkg);
return GL_INSTALL_ERR_EXTRACT;
}
- if (mkdir(pkg_store_final, 0700) == -1) {
+ if (mkdir(pkg_store_final, dir_mode) == -1) {
if (errno == EEXIST) {
gl_free_pkg(&pkg);
return GL_INSTALL_ERR_ALREADY_INSTALLED;
@@ -2088,8 +1924,14 @@ gl_install_pkg(gl_context_t *ctx, const char *gpkg_path)
gl_free_pkg(&pkg);
return GL_INSTALL_ERR_EXTRACT;
}
+ if (chmod(pkg_store_final, dir_mode) != 0) {
+ gl_rmdir_recursive(pkg_store_final);
+ gl_free_pkg(&pkg);
+ return GL_INSTALL_ERR_EXTRACT;
+ }
/* now the archive must be reopened in order to extract package files */
+ struct archive_entry *entry;
struct archive *a2 = archive_read_new();
archive_read_support_filter_gzip(a2);
archive_read_support_format_tar(a2);
@@ -2164,6 +2006,17 @@ gl_install_pkg(gl_context_t *ctx, const char *gpkg_path)
goto extract_fail;
}
+ /* Top-level .glacier-* names are glacier's own files in the
+ * version dir (GL_MARKER_PREFIX). A package shipping one could
+ * grant itself -O, rewrite its saved manifest, or, as a
+ * symlink, redirect glacier's marker write (as root, under
+ * syspkg) to any file on the system. */
+ if (strncmp(rel_path, GL_MARKER_PREFIX, strlen(GL_MARKER_PREFIX)) == 0) {
+ lg_printf(2, "Refusing archive entry '%s': %s* names are reserved.",
+ entry_path, GL_MARKER_PREFIX);
+ goto extract_fail;
+ }
+
char dest_path[PATH_MAX];
snprintf(dest_path, sizeof(dest_path), "%s/%s",
pkg_store_final, rel_path);
@@ -2176,8 +2029,7 @@ gl_install_pkg(gl_context_t *ctx, const char *gpkg_path)
switch (archive_entry_filetype(entry)) {
case AE_IFDIR:
- if (mkdir(dest_path, archive_entry_perm(entry)) == -1
- && errno != EEXIST) {
+ if (gl_mkdir_exact(dest_path, GL_ENTRY_MODE(entry)) != 0) {
goto extract_fail;
}
break;
@@ -2188,12 +2040,16 @@ gl_install_pkg(gl_context_t *ctx, const char *gpkg_path)
char *slash = strrchr(parent, '/');
if (slash) {
*slash = '\0';
- gl_mkdirp(parent, 0700);
+ gl_mkdirp(parent, dir_mode);
}
int fd = open(dest_path, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW,
- archive_entry_perm(entry));
+ GL_ENTRY_MODE(entry));
if (fd < 0) { goto extract_fail; }
+ if (fchmod(fd, GL_ENTRY_MODE(entry)) != 0) {
+ close(fd);
+ goto extract_fail;
+ }
char buffer[8192];
ssize_t n;
@@ -2256,7 +2112,7 @@ gl_install_pkg(gl_context_t *ctx, const char *gpkg_path)
char *slash = strrchr(parent, '/');
if (slash) {
*slash = '\0';
- gl_mkdirp(parent, 0700);
+ gl_mkdirp(parent, dir_mode);
}
/* symlink() fails EEXIST rather than replacing: an archive that
@@ -2318,7 +2174,7 @@ extract_fail:
if (!markers[m]) { continue; }
char marker[PATH_MAX];
snprintf(marker, sizeof(marker), "%s/%s", pkg_store_final, markers[m]);
- int mfd = open(marker, O_WRONLY | O_CREAT | O_TRUNC, 0600);
+ int mfd = open(marker, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, 0600);
if (mfd < 0) {
gl_free_pkg(&pkg);
return GL_INSTALL_ERR_EXTRACT;
@@ -2340,7 +2196,6 @@ extract_fail:
* incrementally here (see relink_store in transaction.c). */
gl_free_pkg(&pkg);
- chdir(original_cwd);
return GL_INSTALL_OK;
}
@@ -2356,8 +2211,14 @@ extract_fail:
* holds plenty of content glacier has no business deleting.
*/
static void
-prune_stale_links(const char *dir_path, const char *store_prefix, int *restored)
+prune_stale_links(const gl_context_t *ctx, const char *dir_path, int *restored)
{
+ /* Links say runtime paths; their targets are checked under the
+ * install root. Without that, an image's links would be judged
+ * against the HOST's store: pruned because the host lacks the
+ * package, or kept because it has it (IMAGE_DESIGN section 3). */
+ const char *store_prefix = gl_runtime_path(ctx, ctx->store_path);
+
DIR *dir = opendir(dir_path);
if (!dir) { return; }
@@ -2380,20 +2241,21 @@ prune_stale_links(const char *dir_path, const char *store_prefix, int *restored)
* stat() through it: a package's own symlink may
* legitimately dangle, and following the chain would
* prune (and un-displace) it on every commit. */
- char store_entry[PATH_MAX];
+ char store_entry[PATH_MAX], entry_at[PATH_MAX];
ssize_t tn = readlink(full, store_entry, sizeof(store_entry) - 1);
struct stat target_st;
if (tn >= 0) { store_entry[tn] = '\0'; }
- if (tn >= 0 && lstat(store_entry, &target_st) != 0 && errno == ENOENT) {
+ if (tn >= 0 && gl_at(ctx, store_entry, entry_at, sizeof(entry_at)) == 0 &&
+ lstat(entry_at, &target_st) != 0 && errno == ENOENT) {
/* our symlink, but its target is gone — and if
* installing it displaced an original, put that back */
if (unlink(full) == 0 &&
- restore_displaced(store_prefix, full) == 1) {
+ restore_displaced(ctx, full) == 1) {
(*restored)++;
}
}
} else if (S_ISDIR(lst.st_mode)) {
- prune_stale_links(full, store_prefix, restored);
+ prune_stale_links(ctx, full, restored);
}
/* regular files: never ours, never touched */
}
@@ -2413,11 +2275,11 @@ gl_relink_store(gl_context_t *ctx)
* glacier's own stale symlinks get removed. gl_mkdirp is
* non-destructive (mkdir tolerates EEXIST) so this is still safe
* to call even if links_path already exists. */
- if (gl_mkdirp(ctx->links_path, 0700) != 0) {
+ if (gl_mkdirp(ctx->links_path, gl_scope_dir_mode(ctx->scope)) != 0) {
return -1;
}
int restored = 0;
- prune_stale_links(ctx->links_path, ctx->store_path, &restored);
+ prune_stale_links(ctx, ctx->links_path, &restored);
/* Base-repo packages under system scope route to /usr instead of
* ctx->links_path (see GL_BASE_REPO) — prune there too, since a
@@ -2426,7 +2288,9 @@ gl_relink_store(gl_context_t *ctx)
* for system scope; /usr is never a valid destination for
* per-uid packages. */
if (ctx->scope == GL_SCOPE_SYS) {
- prune_stale_links(GL_BASE_LINKS_DEST, ctx->store_path, &restored);
+ char usr[PATH_MAX];
+ snprintf(usr, sizeof(usr), "%s" GL_BASE_LINKS_DEST, ctx->sysroot);
+ prune_stale_links(ctx, usr, &restored);
prune_config(ctx);
}
@@ -2484,4 +2348,4 @@ gl_relink_store(gl_context_t *ctx)
}
closedir(repo_dir);
return 0;
-}
\ No newline at end of file
+}
diff --git a/src/istoreutils.h b/src/istoreutils.h
index 87acd6c..dc878dc 100644
--- a/src/istoreutils.h
+++ b/src/istoreutils.h
@@ -5,6 +5,7 @@
#include
#include
+#include "pkg.h"
#include "transaction.h"
typedef enum {
@@ -24,12 +25,6 @@ typedef enum {
}
gl_backup_status_t;
-typedef enum {
- GL_RESTORE_OK = 0,
- GL_RESTORE_ERR_OPEN = 1,
- GL_RESTORE_ERR_EXTRACT = 2
-} gl_restore_status_t;
-
/*
* GL_INSTALL_OK: installation successful
* GL_INSTALL_ERR_OPEN: unable to open .gpkg file
@@ -37,6 +32,9 @@ typedef enum {
* GL_INSTALL_ERR_EXTRACT: unable to extract files
* GL_INSTALL_ERR_INDEX: unable to update index
* GL_INSTALL_ERR_ALREADY_INSTALLED: package already installed
+ * GL_INSTALL_ERR_SCOPE: base-repo package under user scope
+ * GL_INSTALL_ERR_DEPS: unmet dependency or conflict (live context only;
+ * a staged install is checked at commit, GL_TXN_ERR_DEPS)
*/
typedef enum {
@@ -48,6 +46,7 @@ typedef enum {
GL_INSTALL_ERR_ALREADY_INSTALLED = 5,
GL_INSTALL_SKIPPED = 6,
GL_INSTALL_ERR_SCOPE = 7,
+ GL_INSTALL_ERR_DEPS = 8, /* live context only; see deps.h */
}
gl_install_status_t;
@@ -56,39 +55,22 @@ typedef enum {
GL_REMOVE_ERR_NOT_FOUND = 1,
GL_REMOVE_ERR_UNLINK = 2,
GL_REMOVE_ERR_INDEX = 3,
+ GL_REMOVE_ERR_DEPS = 4, /* live context only; see deps.h */
} gl_remove_status_t;
-struct gpkg_entry {
- char *repo;
- char *pkg;
- char *ver;
-};
-
-struct gindex {
- int uid;
- int listed;
- struct gpkg_entry *entries;
- size_t count;
-};
-
bool gl_usr_istore_exists(index_store_t index_or_store, int uid);
-bool gl_sys_istore_exists(index_store_t index_or_store);
int gl_usr_base_status(uid_t uid);
int gl_backup_pkg(gl_context_t *ctx, const char *repo, const char *name, const char *out_path);
int gl_prune_backups(const char *dir, int keep);
int gl_init_user(uid_t uid, bool isVerbose);
int gl_init_sys(bool isVerbose);
+int gl_init_sys_at(const char *sysroot, bool isVerbose);
int gl_create_fs_tree(const char *root, bool isVerbose);
int gl_delete_user(uid_t uid, bool isVerbose);
-int gl_parse_index(struct gindex *idx, FILE *f);
-void gl_free_index(struct gindex *idx);
-
int gl_rebuild_index(gl_context_t *ctx);
-char *gl_find_pkg_repo(gl_context_t *ctx, const char *pkg_name);
-
typedef struct {
char **repos;
size_t count;
@@ -100,9 +82,10 @@ void gl_free_repo_list(gl_repo_list_t *list);
gl_backup_status_t gl_backup_istore(const char *tar_path, const char *glacier_root,
const char *uid);
-gl_restore_status_t gl_restore_istore(const char *tar_path, const char *glacier_root, const char *uid);
+int gl_read_pkg_manifest(const char *gpkg_path, gl_pkg_t *pkg);
gl_install_status_t gl_install_pkg(gl_context_t *ctx, const char *gpkg_path);
+int gl_set_pkg_auto(gl_context_t *ctx, const char *repo, const char *name, bool automatic);
gl_remove_status_t gl_remove_pkg(gl_context_t *ctx, const char *pkg_name, const char *pkg_repo);
int gl_relink_store(gl_context_t *ctx);
diff --git a/src/log.c b/src/log.c
index 3cef043..9498327 100644
--- a/src/log.c
+++ b/src/log.c
@@ -8,19 +8,8 @@
#include
#include
#include
-#include
-#include
-#include "color.h"
-
-typedef enum
-{
- LG_INFO,
- LG_WARN,
- LG_ERR,
- LG_SUCCESS
-}
-loglevel_t;
+#include "log.h"
static int
supports_utf8(void)
@@ -34,26 +23,6 @@ supports_utf8(void)
#endif
}
-
-/* Converts priority integer to string */
-const char
-*p2s(int priority)
-{
- switch (priority)
- {
- case LOG_EMERG: return "EMERG"; break;
- case LOG_ALERT: return "ALERT"; break;
- case LOG_CRIT: return "CRIT"; break;
- case LOG_ERR: return "ERR"; break;
- case LOG_WARNING: return "WARN"; break;
- case LOG_NOTICE: return "NOTICE"; break;
- case LOG_INFO: return "INFO"; break;
- case LOG_DEBUG: return "DEBUG"; break;
- default: return "UNKNOWN"; break;
- }
-}
-
-
void
lg_printf(loglevel_t loglevel, const char *fmt, ...)
{
@@ -92,104 +61,22 @@ lg_printf(loglevel_t loglevel, const char *fmt, ...)
printf("\n");
}
-void
-lg_printf_no_newline(loglevel_t loglevel, const char *fmt, ...)
-{
- const char *prefix;
-
- int utf8_ok = supports_utf8();
-
- switch (loglevel) {
- case LG_INFO:
- prefix = COL_BLUE "[i]" COL_RESET " ";
- break;
- case LG_WARN:
- prefix = COL_YELLOW "[!]" COL_RESET " ";
- break;
- case LG_ERR:
- prefix = COL_RED "[x]" COL_RESET " ";
- break;
- case LG_SUCCESS:
- if (utf8_ok) {
- prefix = COL_GREEN "[" "\xE2\x9C\x93" "]" COL_RESET " ";
- } else {
- prefix = COL_GREEN "[*]" COL_RESET " ";
- }
- break;
- default:
- prefix = "[?] ";
- }
-
- fputs(prefix, stdout);
-
- va_list args;
- va_start(args, fmt);
- vprintf(fmt, args);
- va_end(args);
-}
-
-void
-lg_syslog(int priority, const char *fmt, ...)
-{
- va_list args;
- va_start(args, fmt);
- vsyslog(priority, fmt, args);
- va_end(args);
-}
-
-void
-lg_log(bool use_syslog, const char *logfile, int priority, const char *fmt, ...)
-{
- FILE *log = fopen(logfile, "a");
- if (!log) {
- perror("fopen");
- return;
- }
-
- time_t now = time(NULL);
- struct tm *t = localtime(&now);
-
- char timestamp[20];
- strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", t);
-
- fprintf(log, "[%s] [%s] ", timestamp, p2s(priority));
-
- va_list args;
- va_start(args, fmt);
- vfprintf(log, fmt, args);
- va_end(args);
-
- fputc('\n', log);
- fflush(log);
-
- if (use_syslog) {
- va_list args2;
- va_start(args2, fmt);
- vsyslog(priority, fmt, args2);
- va_end(args2);
- fclose(log);
- }
- else {
- fclose(log);
- return;
- }
-}
-
void
lg_gpkg_help(int argc, char *argv[])
{
(void)argc;
printf("%s: Install, update, and remove packages\n", argv[0]);
- printf("usage: %s [-hv] [-flux PKG] [-OSV]\n", argv[0]);
+ printf("usage: %s [-hv] [-y] [-flux PKG] [-OSV]\n", argv[0]);
printf("\n");
printf("%-25s %s\n", "{-h --help}", "Show this message");
printf("%-25s %s\n", "{-v --version}", "Show the current version");
printf("%-25s %s\n", "{-l --local} FILE...", "Install local package files");
- printf("%-25s %s\n", "{-f --merge} PKG...", "Install packages from a remote repository (not yet implemented)");
- printf("%-25s %s\n", "{-u --update} PKG...", "Upgrade installed packages (not yet implemented)");
+ printf("%-25s %s\n", "{-f --merge} PKG...", "Install packages from a remote repository");
+ printf("%-25s %s\n", "{-u --update} [PKG...]", "Update system or individual packages");
printf("%-25s %s\n", "{-x --remove} PKG...", "Remove installed packages");
- printf("%-25s %s\n", "{-O --overwrite}", "With -l: replace an installed version (backed up per KEEP_OLD) and existing non-glacier files");
+ printf("%-25s %s\n", "{-y --refresh}", "Fetch and verify the remote repositories' indexes");
+ printf("%-25s %s\n", "{-O --overwrite}", "Allow Glacier to overwrite existing files");
printf("%-25s %s\n", "{-S --skip-installed}", "Skip packages that are already up to date");
printf("%-25s %s\n", "{-V --verbose}", "Log events more verbosely");
printf("\n");
@@ -203,16 +90,21 @@ lg_syspkg_help(int argc, char *argv[])
(void)argc;
printf("%s: Manage the system package index\n", argv[0]);
- printf("usage: %s [-hv] [-n] [-lx PKG] [-OSV]\n", argv[0]);
+ printf("usage: %s [-hv] [-n] [-y] [-flux PKG] [-OSV] [-R DIR] [--strict]\n", argv[0]);
printf("\n");
printf("%-25s %s\n", "{-h --help}", "Show this message");
printf("%-25s %s\n", "{-v --version}", "Show the current version");
printf("%-25s %s\n", "{-n --new}", "Create the system index and store");
printf("%-25s %s\n", "{-l --local} PKG...", "Install a local package into the system index and store");
+ printf("%-25s %s\n", "{-f --merge} PKG...", "Install packages from a remote repository");
+ printf("%-25s %s\n", "{-u --update} [PKG...]", "Update system or individual packages");
printf("%-25s %s\n", "{-x --remove} PKG...", "Remove a package from the system index and store");
- printf("%-25s %s\n", "{-O --overwrite}", "With -l: replace an installed version (backed up per KEEP_OLD) and existing non-glacier files");
+ printf("%-25s %s\n", "{-y --refresh}", "Fetch and verify the remote repositories' indexes");
+ printf("%-25s %s\n", "{-O --overwrite}", "Allow Glacier to overwrite existing files");
printf("%-25s %s\n", "{-S --skip-installed}", "Skip packages that are already up to date");
printf("%-25s %s\n", "{-V --verbose}", "Log events more verbosely");
+ printf("%-25s %s\n", "{-R --root} DIR", "Operate on the system installed under DIR (implies --strict)");
+ printf("%-25s %s\n", "{--strict}", "Refuse the whole transaction if any path can't be linked");
printf("\n");
printf("This program is free software.\n");
printf("See the GNU GPL version 3 for details.\n");
diff --git a/src/log.h b/src/log.h
index 0521ae1..f66eb7c 100644
--- a/src/log.h
+++ b/src/log.h
@@ -7,9 +7,6 @@
#include
#include
-#include
-#include
-#include
#include "color.h"
@@ -26,13 +23,13 @@ typedef enum
}
loglevel_t;
-int supports_utf8(void);
-const char *p2s(int priority);
+void lg_printf(loglevel_t loglevel, const char *fmt, ...)
+ __attribute__((format(printf, 2, 3)));
-void lg_printf(loglevel_t loglevel, const char *fmt, ...);
-void lg_printf_no_newline(loglevel_t loglevel, const char *fmt, ...);
-void lg_syslog(int priority, const char *fmt, ...);
-void lg_log(bool use_syslog, FILE *logfile, int priority, const char *fmt, ...);
+/* The CLIs' -h output. */
+void lg_gpkg_help(int argc, char *argv[]);
+void lg_syspkg_help(int argc, char *argv[]);
+void lg_gstore_help(int argc, char *argv[]);
#ifdef __cplusplus
}
diff --git a/src/make_conf.c b/src/make_conf.c
deleted file mode 100644
index 985622b..0000000
--- a/src/make_conf.c
+++ /dev/null
@@ -1,95 +0,0 @@
-#define _POSIX_C_SOURcE 200809L
-#define _DEFAULT_SOURCE
-#define _XOPEN_SOURCE 700
-
-#include
-#include
-#include
-#include
-
-#define MAX_LINE 1024
-
-static char
-*trim(char *s) {
- char *end;
- while (isspace((unsigned char)*s)) { s++; }
- if (*s == 0) { return s; }
- end = s + strlen(s) - 1;
- while (end > s && isspace((unsigned char)*end)) { *end-- = 0; }
- return s;
-}
-
-static char
-*expand(char *value)
-{
- size_t cap = strlen(value) + 1;
- char *buffer = malloc(cap);
- if (!buffer) { return NULL; }
- char *out = buffer;
-
- char *p = value;
-
- while (*p) {
- if (*p == '$') {
- p++;
- char var[128], *vp = var;
- while (*p && (isalnum((unsigned char)*p) || *p == '_')) {
- *vp++ = *p++;
- }
-
- *vp = '\0';
- char *val = getenv(var);
- if (val) {
- size_t len = strlen(val);
- size_t used = (size_t)(out - buffer);
- if (used + len + 1 > cap) {
- cap = used + len + 1;
- buffer = realloc(buffer, cap);
- if (!buffer) { return NULL; }
- out = buffer + used;
- }
-
- memcpy(out, val, len);
- out += len;
- }
- }
- else {
- *out++ = *p++;
- }
- }
-
- *out = '\0';
- return buffer;
-}
-
-int
-parse_makeconf(const char *filename)
-{
- FILE *make_conf = fopen(filename, "r");
- if (!make_conf) {
- perror("fopen");
- return -1;
- }
-
- char line[MAX_LINE];
- while (fgets(line, sizeof(line), make_conf)) {
- char *p = trim(line);
- if (*p == '\0' || *p == '#') { continue; }
-
- char *eq = strchr(p, '=');
- if (!eq) { continue; }
-
- *eq = '\0';
- char *key = trim(p);
- char *val = trim(eq + 1);
-
- char *expanded = expand(val);
- if (expanded) {
- setenv(key, expanded, 1);
- free(expanded);
- }
- }
-
- fclose(make_conf);
- return 0;
-}
diff --git a/src/monocypher/LICENCE.md b/src/monocypher/LICENCE.md
new file mode 100644
index 0000000..9c3fae9
--- /dev/null
+++ b/src/monocypher/LICENCE.md
@@ -0,0 +1,167 @@
+Monocypher as a whole is dual-licensed. Choose whichever licence you
+want from the two licences listed below.
+
+The first licence is a regular 2-clause BSD licence. The second licence
+is the CC-0 from Creative Commons. It is intended to release Monocypher
+to the public domain. The BSD licence serves as a fallback option.
+
+See the individual files for specific information about who contributed
+to what file during which years. See below for special notes.
+
+Licence 1 (2-clause BSD)
+------------------------
+
+Copyright (c) 2017-2023, Loup Vaillant
+Copyright (c) 2017-2019, Michael Savage
+Copyright (c) 2017-2023, Fabio Scotoni
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the
+ distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+Licence 2 (CC-0)
+----------------
+
+> CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+> LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
+> ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+> INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+> REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
+> PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+> THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
+> HEREUNDER.
+
+### Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator
+and subsequent owner(s) (each and all, an "owner") of an original work
+of authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for
+the purpose of contributing to a commons of creative, cultural and
+scientific works ("Commons") that the public can reliably and without
+fear of later claims of infringement build upon, modify, incorporate in
+other works, reuse and redistribute as freely as possible in any form
+whatsoever and for any purposes, including without limitation commercial
+purposes. These owners may contribute to the Commons to promote the
+ideal of a free culture and the further production of creative, cultural
+and scientific works, or to gain reputation or greater distribution for
+their Work in part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any
+expectation of additional consideration or compensation, the person
+associating CC0 with a Work (the "Affirmer"), to the extent that he or
+she is an owner of Copyright and Related Rights in the Work, voluntarily
+elects to apply CC0 to the Work and publicly distribute the Work under
+its terms, with knowledge of his or her Copyright and Related Rights in
+the Work and the meaning and intended legal effect of CC0 on those
+rights.
+
+1. **Copyright and Related Rights.** A Work made available under CC0 may
+ be protected by copyright and related or neighboring rights
+ ("Copyright and Related Rights"). Copyright and Related Rights
+ include, but are not limited to, the following:
+
+ - the right to reproduce, adapt, distribute, perform, display,
+ communicate, and translate a Work;
+ - moral rights retained by the original author(s) and/or
+ performer(s); publicity and privacy rights pertaining to a person's
+ image or likeness depicted in a Work;
+ - rights protecting against unfair competition in regards to a Work,
+ subject to the limitations in paragraph 4(a), below;
+ - rights protecting the extraction, dissemination, use and reuse of
+ data in a Work;
+ - database rights (such as those arising under Directive 96/9/EC of
+ the European Parliament and of the Council of 11 March 1996 on the
+ legal protection of databases, and under any national
+ implementation thereof, including any amended or successor version
+ of such directive); and
+ - other similar, equivalent or corresponding rights throughout the
+ world based on applicable law or treaty, and any national
+ implementations thereof.
+
+2. **Waiver.** To the greatest extent permitted by, but not in
+ contravention of, applicable law, Affirmer hereby overtly, fully,
+ permanently, irrevocably and unconditionally waives, abandons, and
+ surrenders all of Affirmer's Copyright and Related Rights and
+ associated claims and causes of action, whether now known or unknown
+ (including existing as well as future claims and causes of action),
+ in the Work (i) in all territories worldwide, (ii) for the maximum
+ duration provided by applicable law or treaty (including future time
+ extensions), (iii) in any current or future medium and for any number
+ of copies, and (iv) for any purpose whatsoever, including without
+ limitation commercial, advertising or promotional purposes (the
+ "Waiver"). Affirmer makes the Waiver for the benefit of each member
+ of the public at large and to the detriment of Affirmer's heirs and
+ successors, fully intending that such Waiver shall not be subject to
+ revocation, rescission, cancellation, termination, or any other legal
+ or equitable action to disrupt the quiet enjoyment of the Work by the
+ public as contemplated by Affirmer's express Statement of Purpose.
+
+3. **Public License Fallback.** Should any part of the Waiver for any
+ reason be judged legally invalid or ineffective under applicable law,
+ then the Waiver shall be preserved to the maximum extent permitted
+ taking into account Affirmer's express Statement of Purpose. In
+ addition, to the extent the Waiver is so judged Affirmer hereby
+ grants to each affected person a royalty-free, non transferable, non
+ sublicensable, non exclusive, irrevocable and unconditional license
+ to exercise Affirmer's Copyright and Related Rights in the Work (i)
+ in all territories worldwide, (ii) for the maximum duration provided
+ by applicable law or treaty (including future time extensions), (iii)
+ in any current or future medium and for any number of copies, and
+ (iv) for any purpose whatsoever, including without limitation
+ commercial, advertising or promotional purposes (the "License"). The
+ License shall be deemed effective as of the date CC0 was applied by
+ Affirmer to the Work. Should any part of the License for any reason
+ be judged legally invalid or ineffective under applicable law, such
+ partial invalidity or ineffectiveness shall not invalidate the
+ remainder of the License, and in such case Affirmer hereby affirms
+ that he or she will not (i) exercise any of his or her remaining
+ Copyright and Related Rights in the Work or (ii) assert any
+ associated claims and causes of action with respect to the Work, in
+ either case contrary to Affirmer's express Statement of Purpose.
+
+4. **Limitations and Disclaimers.**
+
+ - No trademark or patent rights held by Affirmer are waived,
+ abandoned, surrendered, licensed or otherwise affected by this
+ document.
+ - Affirmer offers the Work as-is and makes no representations or
+ warranties of any kind concerning the Work, express, implied,
+ statutory or otherwise, including without limitation warranties of
+ title, merchantability, fitness for a particular purpose, non
+ infringement, or the absence of latent or other defects, accuracy,
+ or the present or absence of errors, whether or not discoverable,
+ all to the greatest extent permissible under applicable law.
+ - Affirmer disclaims responsibility for clearing rights of other
+ persons that may apply to the Work or any use thereof, including
+ without limitation any person's Copyright and Related Rights in the
+ Work. Further, Affirmer disclaims responsibility for obtaining any
+ necessary consents, permissions or other rights required for any
+ use of the Work.
+ - Affirmer understands and acknowledges that Creative Commons is not
+ a party to this document and has no duty or obligation with respect
+ to this CC0 or use of the Work.
diff --git a/src/monocypher/README b/src/monocypher/README
new file mode 100644
index 0000000..dd96e6c
--- /dev/null
+++ b/src/monocypher/README
@@ -0,0 +1,14 @@
+Monocypher 4.0.3, vendored UNMODIFIED (REPO_DESIGN.txt section 1a).
+
+ monocypher.c, monocypher.h from src/
+ monocypher-ed25519.c, monocypher-ed25519.h from src/optional/
+ LICENCE.md BSD-2-Clause or CC0
+
+Source: monocypher-4.0.3.tar.gz, fetched 2026-10-06 from both
+https://github.com/LoupVaillant/Monocypher/releases/download/4.0.3/
+and https://monocypher.org/download/ ; identical, sha512
+40904ada5c7ee4f7741733e38b69a30a4b0561cbffba5ffe7c2dce16136d540251ec0d9056ff606510d3b5b708fb8a40db7e0870d4a0b2dc17ba2bfb880f8965
+
+Only Ed25519 (with its SHA-512) is used, through src/sign.c. Never
+edit these files: replace them with a newer release, and update this
+note.
diff --git a/src/monocypher/monocypher-ed25519.c b/src/monocypher/monocypher-ed25519.c
new file mode 100644
index 0000000..8619117
--- /dev/null
+++ b/src/monocypher/monocypher-ed25519.c
@@ -0,0 +1,503 @@
+// Monocypher version 4.0.3
+//
+// This file is dual-licensed. Choose whichever licence you want from
+// the two licences listed below.
+//
+// The first licence is a regular 2-clause BSD licence. The second licence
+// is the CC-0 from Creative Commons. It is intended to release Monocypher
+// to the public domain. The BSD licence serves as a fallback option.
+//
+// SPDX-License-Identifier: BSD-2-Clause OR CC0-1.0
+//
+// ------------------------------------------------------------------------
+//
+// Copyright (c) 2017-2019, Loup Vaillant
+// All rights reserved.
+//
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// 1. Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the
+// distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+//
+// ------------------------------------------------------------------------
+//
+// Written in 2017-2019 by Loup Vaillant
+//
+// To the extent possible under law, the author(s) have dedicated all copyright
+// and related neighboring rights to this software to the public domain
+// worldwide. This software is distributed without any warranty.
+//
+// You should have received a copy of the CC0 Public Domain Dedication along
+// with this software. If not, see
+//
+
+#include "monocypher-ed25519.h"
+
+#ifdef MONOCYPHER_CPP_NAMESPACE
+namespace MONOCYPHER_CPP_NAMESPACE {
+#endif
+
+/////////////////
+/// Utilities ///
+/////////////////
+#define FOR(i, min, max) for (size_t i = min; i < max; i++)
+#define COPY(dst, src, size) FOR(_i_, 0, size) (dst)[_i_] = (src)[_i_]
+#define ZERO(buf, size) FOR(_i_, 0, size) (buf)[_i_] = 0
+#define WIPE_CTX(ctx) crypto_wipe(ctx , sizeof(*(ctx)))
+#define WIPE_BUFFER(buffer) crypto_wipe(buffer, sizeof(buffer))
+#define MIN(a, b) ((a) <= (b) ? (a) : (b))
+typedef uint8_t u8;
+typedef uint64_t u64;
+
+// Returns the smallest positive integer y such that
+// (x + y) % pow_2 == 0
+// Basically, it's how many bytes we need to add to "align" x.
+// Only works when pow_2 is a power of 2.
+// Note: we use ~x+1 instead of -x to avoid compiler warnings
+static size_t align(size_t x, size_t pow_2)
+{
+ return (~x + 1) & (pow_2 - 1);
+}
+
+static u64 load64_be(const u8 s[8])
+{
+ return((u64)s[0] << 56)
+ | ((u64)s[1] << 48)
+ | ((u64)s[2] << 40)
+ | ((u64)s[3] << 32)
+ | ((u64)s[4] << 24)
+ | ((u64)s[5] << 16)
+ | ((u64)s[6] << 8)
+ | (u64)s[7];
+}
+
+static void store64_be(u8 out[8], u64 in)
+{
+ out[0] = (u8)(in >> 56);
+ out[1] = (u8)(in >> 48);
+ out[2] = (u8)(in >> 40);
+ out[3] = (u8)(in >> 32);
+ out[4] = (u8)(in >> 24);
+ out[5] = (u8)(in >> 16);
+ out[6] = (u8)(in >> 8);
+ out[7] = (u8) in ;
+}
+
+static void load64_be_buf (u64 *dst, const u8 *src, size_t size) {
+ FOR(i, 0, size) { dst[i] = load64_be(src + i*8); }
+}
+
+///////////////
+/// SHA 512 ///
+///////////////
+static u64 rot(u64 x, int c ) { return (x >> c) | (x << (64 - c)); }
+static u64 ch (u64 x, u64 y, u64 z) { return (x & y) ^ (~x & z); }
+static u64 maj(u64 x, u64 y, u64 z) { return (x & y) ^ ( x & z) ^ (y & z); }
+static u64 big_sigma0(u64 x) { return rot(x, 28) ^ rot(x, 34) ^ rot(x, 39); }
+static u64 big_sigma1(u64 x) { return rot(x, 14) ^ rot(x, 18) ^ rot(x, 41); }
+static u64 lit_sigma0(u64 x) { return rot(x, 1) ^ rot(x, 8) ^ (x >> 7); }
+static u64 lit_sigma1(u64 x) { return rot(x, 19) ^ rot(x, 61) ^ (x >> 6); }
+
+static const u64 K[80] = {
+ 0x428a2f98d728ae22,0x7137449123ef65cd,0xb5c0fbcfec4d3b2f,0xe9b5dba58189dbbc,
+ 0x3956c25bf348b538,0x59f111f1b605d019,0x923f82a4af194f9b,0xab1c5ed5da6d8118,
+ 0xd807aa98a3030242,0x12835b0145706fbe,0x243185be4ee4b28c,0x550c7dc3d5ffb4e2,
+ 0x72be5d74f27b896f,0x80deb1fe3b1696b1,0x9bdc06a725c71235,0xc19bf174cf692694,
+ 0xe49b69c19ef14ad2,0xefbe4786384f25e3,0x0fc19dc68b8cd5b5,0x240ca1cc77ac9c65,
+ 0x2de92c6f592b0275,0x4a7484aa6ea6e483,0x5cb0a9dcbd41fbd4,0x76f988da831153b5,
+ 0x983e5152ee66dfab,0xa831c66d2db43210,0xb00327c898fb213f,0xbf597fc7beef0ee4,
+ 0xc6e00bf33da88fc2,0xd5a79147930aa725,0x06ca6351e003826f,0x142929670a0e6e70,
+ 0x27b70a8546d22ffc,0x2e1b21385c26c926,0x4d2c6dfc5ac42aed,0x53380d139d95b3df,
+ 0x650a73548baf63de,0x766a0abb3c77b2a8,0x81c2c92e47edaee6,0x92722c851482353b,
+ 0xa2bfe8a14cf10364,0xa81a664bbc423001,0xc24b8b70d0f89791,0xc76c51a30654be30,
+ 0xd192e819d6ef5218,0xd69906245565a910,0xf40e35855771202a,0x106aa07032bbd1b8,
+ 0x19a4c116b8d2d0c8,0x1e376c085141ab53,0x2748774cdf8eeb99,0x34b0bcb5e19b48a8,
+ 0x391c0cb3c5c95a63,0x4ed8aa4ae3418acb,0x5b9cca4f7763e373,0x682e6ff3d6b2b8a3,
+ 0x748f82ee5defb2fc,0x78a5636f43172f60,0x84c87814a1f0ab72,0x8cc702081a6439ec,
+ 0x90befffa23631e28,0xa4506cebde82bde9,0xbef9a3f7b2c67915,0xc67178f2e372532b,
+ 0xca273eceea26619c,0xd186b8c721c0c207,0xeada7dd6cde0eb1e,0xf57d4f7fee6ed178,
+ 0x06f067aa72176fba,0x0a637dc5a2c898a6,0x113f9804bef90dae,0x1b710b35131c471b,
+ 0x28db77f523047d84,0x32caab7b40c72493,0x3c9ebe0a15c9bebc,0x431d67c49c100d4c,
+ 0x4cc5d4becb3e42b6,0x597f299cfc657e2a,0x5fcb6fab3ad6faec,0x6c44198c4a475817
+};
+
+static void sha512_compress(crypto_sha512_ctx *ctx)
+{
+ u64 a = ctx->hash[0]; u64 b = ctx->hash[1];
+ u64 c = ctx->hash[2]; u64 d = ctx->hash[3];
+ u64 e = ctx->hash[4]; u64 f = ctx->hash[5];
+ u64 g = ctx->hash[6]; u64 h = ctx->hash[7];
+
+ FOR (j, 0, 16) {
+ u64 in = K[j] + ctx->input[j];
+ u64 t1 = big_sigma1(e) + ch (e, f, g) + h + in;
+ u64 t2 = big_sigma0(a) + maj(a, b, c);
+ h = g; g = f; f = e; e = d + t1;
+ d = c; c = b; b = a; a = t1 + t2;
+ }
+ size_t i16 = 0;
+ FOR(i, 1, 5) {
+ i16 += 16;
+ FOR (j, 0, 16) {
+ ctx->input[j] += lit_sigma1(ctx->input[(j- 2) & 15]);
+ ctx->input[j] += lit_sigma0(ctx->input[(j-15) & 15]);
+ ctx->input[j] += ctx->input[(j- 7) & 15];
+ u64 in = K[i16 + j] + ctx->input[j];
+ u64 t1 = big_sigma1(e) + ch (e, f, g) + h + in;
+ u64 t2 = big_sigma0(a) + maj(a, b, c);
+ h = g; g = f; f = e; e = d + t1;
+ d = c; c = b; b = a; a = t1 + t2;
+ }
+ }
+
+ ctx->hash[0] += a; ctx->hash[1] += b;
+ ctx->hash[2] += c; ctx->hash[3] += d;
+ ctx->hash[4] += e; ctx->hash[5] += f;
+ ctx->hash[6] += g; ctx->hash[7] += h;
+}
+
+// Write 1 input byte
+static void sha512_set_input(crypto_sha512_ctx *ctx, u8 input)
+{
+ size_t word = ctx->input_idx >> 3;
+ size_t byte = ctx->input_idx & 7;
+ ctx->input[word] |= (u64)input << (8 * (7 - byte));
+}
+
+// Increment a 128-bit "word".
+static void sha512_incr(u64 x[2], u64 y)
+{
+ x[1] += y;
+ if (x[1] < y) {
+ x[0]++;
+ }
+}
+
+void crypto_sha512_init(crypto_sha512_ctx *ctx)
+{
+ ctx->hash[0] = 0x6a09e667f3bcc908;
+ ctx->hash[1] = 0xbb67ae8584caa73b;
+ ctx->hash[2] = 0x3c6ef372fe94f82b;
+ ctx->hash[3] = 0xa54ff53a5f1d36f1;
+ ctx->hash[4] = 0x510e527fade682d1;
+ ctx->hash[5] = 0x9b05688c2b3e6c1f;
+ ctx->hash[6] = 0x1f83d9abfb41bd6b;
+ ctx->hash[7] = 0x5be0cd19137e2179;
+ ctx->input_size[0] = 0;
+ ctx->input_size[1] = 0;
+ ctx->input_idx = 0;
+ ZERO(ctx->input, 16);
+}
+
+void crypto_sha512_update(crypto_sha512_ctx *ctx,
+ const u8 *message, size_t message_size)
+{
+ // Avoid undefined NULL pointer increments with empty messages
+ if (message_size == 0) {
+ return;
+ }
+
+ // Align ourselves with word boundaries
+ if ((ctx->input_idx & 7) != 0) {
+ size_t nb_bytes = MIN(align(ctx->input_idx, 8), message_size);
+ FOR (i, 0, nb_bytes) {
+ sha512_set_input(ctx, message[i]);
+ ctx->input_idx++;
+ }
+ message += nb_bytes;
+ message_size -= nb_bytes;
+ }
+
+ // Align ourselves with block boundaries
+ if ((ctx->input_idx & 127) != 0) {
+ size_t nb_words = MIN(align(ctx->input_idx, 128), message_size) >> 3;
+ load64_be_buf(ctx->input + (ctx->input_idx >> 3), message, nb_words);
+ ctx->input_idx += nb_words << 3;
+ message += nb_words << 3;
+ message_size -= nb_words << 3;
+ }
+
+ // Compress block if needed
+ if (ctx->input_idx == 128) {
+ sha512_incr(ctx->input_size, 1024); // size is in bits
+ sha512_compress(ctx);
+ ctx->input_idx = 0;
+ ZERO(ctx->input, 16);
+ }
+
+ // Process the message block by block
+ FOR (i, 0, message_size >> 7) { // number of blocks
+ load64_be_buf(ctx->input, message, 16);
+ sha512_incr(ctx->input_size, 1024); // size is in bits
+ sha512_compress(ctx);
+ ctx->input_idx = 0;
+ ZERO(ctx->input, 16);
+ message += 128;
+ }
+ message_size &= 127;
+
+ if (message_size != 0) {
+ // Remaining words
+ size_t nb_words = message_size >> 3;
+ load64_be_buf(ctx->input, message, nb_words);
+ ctx->input_idx += nb_words << 3;
+ message += nb_words << 3;
+ message_size -= nb_words << 3;
+
+ // Remaining bytes
+ FOR (i, 0, message_size) {
+ sha512_set_input(ctx, message[i]);
+ ctx->input_idx++;
+ }
+ }
+}
+
+void crypto_sha512_final(crypto_sha512_ctx *ctx, u8 hash[64])
+{
+ // Add padding bit
+ if (ctx->input_idx == 0) {
+ ZERO(ctx->input, 16);
+ }
+ sha512_set_input(ctx, 128);
+
+ // Update size
+ sha512_incr(ctx->input_size, ctx->input_idx * 8);
+
+ // Compress penultimate block (if any)
+ if (ctx->input_idx > 111) {
+ sha512_compress(ctx);
+ ZERO(ctx->input, 14);
+ }
+ // Compress last block
+ ctx->input[14] = ctx->input_size[0];
+ ctx->input[15] = ctx->input_size[1];
+ sha512_compress(ctx);
+
+ // Copy hash to output (big endian)
+ FOR (i, 0, 8) {
+ store64_be(hash + i*8, ctx->hash[i]);
+ }
+
+ WIPE_CTX(ctx);
+}
+
+void crypto_sha512(u8 hash[64], const u8 *message, size_t message_size)
+{
+ crypto_sha512_ctx ctx;
+ crypto_sha512_init (&ctx);
+ crypto_sha512_update(&ctx, message, message_size);
+ crypto_sha512_final (&ctx, hash);
+}
+
+////////////////////
+/// HMAC SHA 512 ///
+////////////////////
+void crypto_sha512_hmac_init(crypto_sha512_hmac_ctx *ctx,
+ const u8 *key, size_t key_size)
+{
+ // hash key if it is too long
+ if (key_size > 128) {
+ crypto_sha512(ctx->key, key, key_size);
+ key = ctx->key;
+ key_size = 64;
+ }
+ // Compute inner key: padded key XOR 0x36
+ FOR (i, 0, key_size) { ctx->key[i] = key[i] ^ 0x36; }
+ FOR (i, key_size, 128) { ctx->key[i] = 0x36; }
+ // Start computing inner hash
+ crypto_sha512_init (&ctx->ctx);
+ crypto_sha512_update(&ctx->ctx, ctx->key, 128);
+}
+
+void crypto_sha512_hmac_update(crypto_sha512_hmac_ctx *ctx,
+ const u8 *message, size_t message_size)
+{
+ crypto_sha512_update(&ctx->ctx, message, message_size);
+}
+
+void crypto_sha512_hmac_final(crypto_sha512_hmac_ctx *ctx, u8 hmac[64])
+{
+ // Finish computing inner hash
+ crypto_sha512_final(&ctx->ctx, hmac);
+ // Compute outer key: padded key XOR 0x5c
+ FOR (i, 0, 128) {
+ ctx->key[i] ^= 0x36 ^ 0x5c;
+ }
+ // Compute outer hash
+ crypto_sha512_init (&ctx->ctx);
+ crypto_sha512_update(&ctx->ctx, ctx->key , 128);
+ crypto_sha512_update(&ctx->ctx, hmac, 64);
+ crypto_sha512_final (&ctx->ctx, hmac); // outer hash
+ WIPE_CTX(ctx);
+}
+
+void crypto_sha512_hmac(u8 hmac[64], const u8 *key, size_t key_size,
+ const u8 *message, size_t message_size)
+{
+ crypto_sha512_hmac_ctx ctx;
+ crypto_sha512_hmac_init (&ctx, key, key_size);
+ crypto_sha512_hmac_update(&ctx, message, message_size);
+ crypto_sha512_hmac_final (&ctx, hmac);
+}
+
+////////////////////
+/// HKDF SHA 512 ///
+////////////////////
+void crypto_sha512_hkdf_expand(u8 *okm, size_t okm_size,
+ const u8 *prk, size_t prk_size,
+ const u8 *info, size_t info_size)
+{
+ int not_first = 0;
+ u8 ctr = 1;
+ u8 blk[64];
+
+ while (okm_size > 0) {
+ size_t out_size = MIN(okm_size, sizeof(blk));
+
+ crypto_sha512_hmac_ctx ctx;
+ crypto_sha512_hmac_init(&ctx, prk , prk_size);
+ if (not_first) {
+ // For some reason HKDF uses some kind of CBC mode.
+ // For some reason CTR mode alone wasn't enough.
+ // Like what, they didn't trust HMAC in 2010? Really??
+ crypto_sha512_hmac_update(&ctx, blk , sizeof(blk));
+ }
+ crypto_sha512_hmac_update(&ctx, info, info_size);
+ crypto_sha512_hmac_update(&ctx, &ctr, 1);
+ crypto_sha512_hmac_final(&ctx, blk);
+
+ COPY(okm, blk, out_size);
+
+ not_first = 1;
+ okm += out_size;
+ okm_size -= out_size;
+ ctr++;
+ }
+ WIPE_BUFFER(blk);
+}
+
+void crypto_sha512_hkdf(u8 *okm , size_t okm_size,
+ const u8 *ikm , size_t ikm_size,
+ const u8 *salt, size_t salt_size,
+ const u8 *info, size_t info_size)
+{
+ // Extract
+ u8 prk[64];
+ crypto_sha512_hmac(prk, salt, salt_size, ikm, ikm_size);
+
+ // Expand
+ crypto_sha512_hkdf_expand(okm, okm_size, prk, sizeof(prk), info, info_size);
+
+ WIPE_BUFFER(prk);
+}
+
+///////////////
+/// Ed25519 ///
+///////////////
+void crypto_ed25519_key_pair(u8 secret_key[64], u8 public_key[32], u8 seed[32])
+{
+ u8 a[64];
+ COPY(a, seed, 32); // a[ 0..31] = seed
+ crypto_wipe(seed, 32);
+ COPY(secret_key, a, 32); // secret key = seed
+ crypto_sha512(a, a, 32); // a[ 0..31] = scalar
+ crypto_eddsa_trim_scalar(a, a); // a[ 0..31] = trimmed scalar
+ crypto_eddsa_scalarbase(public_key, a); // public key = [trimmed scalar]B
+ COPY(secret_key + 32, public_key, 32); // secret key includes public half
+ WIPE_BUFFER(a);
+}
+
+static void hash_reduce(u8 h[32],
+ const u8 *a, size_t a_size,
+ const u8 *b, size_t b_size,
+ const u8 *c, size_t c_size,
+ const u8 *d, size_t d_size)
+{
+ u8 hash[64];
+ crypto_sha512_ctx ctx;
+ crypto_sha512_init (&ctx);
+ crypto_sha512_update(&ctx, a, a_size);
+ crypto_sha512_update(&ctx, b, b_size);
+ crypto_sha512_update(&ctx, c, c_size);
+ crypto_sha512_update(&ctx, d, d_size);
+ crypto_sha512_final (&ctx, hash);
+ crypto_eddsa_reduce(h, hash);
+}
+
+static void ed25519_dom_sign(u8 signature[64], const u8 secret_key[64],
+ const u8 *dom, size_t dom_size,
+ const u8 *message, size_t message_size)
+{
+ u8 a[64]; // secret scalar and prefix
+ u8 r[32]; // secret deterministic "random" nonce
+ u8 h[32]; // publically verifiable hash of the message (not wiped)
+ u8 R[32]; // first half of the signature (allows overlapping inputs)
+ const u8 *pk = secret_key + 32;
+
+ crypto_sha512(a, secret_key, 32);
+ crypto_eddsa_trim_scalar(a, a);
+ hash_reduce(r, dom, dom_size, a + 32, 32, message, message_size, 0, 0);
+ crypto_eddsa_scalarbase(R, r);
+ hash_reduce(h, dom, dom_size, R, 32, pk, 32, message, message_size);
+ COPY(signature, R, 32);
+ crypto_eddsa_mul_add(signature + 32, h, a, r);
+
+ WIPE_BUFFER(a);
+ WIPE_BUFFER(r);
+}
+
+void crypto_ed25519_sign(u8 signature [64], const u8 secret_key[64],
+ const u8 *message, size_t message_size)
+{
+ ed25519_dom_sign(signature, secret_key, 0, 0, message, message_size);
+}
+
+int crypto_ed25519_check(const u8 signature[64], const u8 public_key[32],
+ const u8 *msg, size_t msg_size)
+{
+ u8 h_ram[32];
+ hash_reduce(h_ram, signature, 32, public_key, 32, msg, msg_size, 0, 0);
+ return crypto_eddsa_check_equation(signature, public_key, h_ram);
+}
+
+static const u8 domain[34] = "SigEd25519 no Ed25519 collisions\1";
+
+void crypto_ed25519_ph_sign(uint8_t signature[64], const uint8_t secret_key[64],
+ const uint8_t message_hash[64])
+{
+ ed25519_dom_sign(signature, secret_key, domain, sizeof(domain),
+ message_hash, 64);
+}
+
+int crypto_ed25519_ph_check(const uint8_t sig[64], const uint8_t pk[32],
+ const uint8_t msg_hash[64])
+{
+ u8 h_ram[32];
+ hash_reduce(h_ram, domain, sizeof(domain), sig, 32, pk, 32, msg_hash, 64);
+ return crypto_eddsa_check_equation(sig, pk, h_ram);
+}
+
+
+#ifdef MONOCYPHER_CPP_NAMESPACE
+}
+#endif
diff --git a/src/monocypher/monocypher-ed25519.h b/src/monocypher/monocypher-ed25519.h
new file mode 100644
index 0000000..9ad1c54
--- /dev/null
+++ b/src/monocypher/monocypher-ed25519.h
@@ -0,0 +1,140 @@
+// Monocypher version 4.0.3
+//
+// This file is dual-licensed. Choose whichever licence you want from
+// the two licences listed below.
+//
+// The first licence is a regular 2-clause BSD licence. The second licence
+// is the CC-0 from Creative Commons. It is intended to release Monocypher
+// to the public domain. The BSD licence serves as a fallback option.
+//
+// SPDX-License-Identifier: BSD-2-Clause OR CC0-1.0
+//
+// ------------------------------------------------------------------------
+//
+// Copyright (c) 2017-2019, Loup Vaillant
+// All rights reserved.
+//
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// 1. Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the
+// distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+//
+// ------------------------------------------------------------------------
+//
+// Written in 2017-2019 by Loup Vaillant
+//
+// To the extent possible under law, the author(s) have dedicated all copyright
+// and related neighboring rights to this software to the public domain
+// worldwide. This software is distributed without any warranty.
+//
+// You should have received a copy of the CC0 Public Domain Dedication along
+// with this software. If not, see
+//
+
+#ifndef ED25519_H
+#define ED25519_H
+
+#include "monocypher.h"
+
+#ifdef MONOCYPHER_CPP_NAMESPACE
+namespace MONOCYPHER_CPP_NAMESPACE {
+#elif defined(__cplusplus)
+extern "C" {
+#endif
+
+////////////////////////
+/// Type definitions ///
+////////////////////////
+
+// Do not rely on the size or content on any of those types,
+// they may change without notice.
+typedef struct {
+ uint64_t hash[8];
+ uint64_t input[16];
+ uint64_t input_size[2];
+ size_t input_idx;
+} crypto_sha512_ctx;
+
+typedef struct {
+ uint8_t key[128];
+ crypto_sha512_ctx ctx;
+} crypto_sha512_hmac_ctx;
+
+
+// SHA 512
+// -------
+void crypto_sha512_init (crypto_sha512_ctx *ctx);
+void crypto_sha512_update(crypto_sha512_ctx *ctx,
+ const uint8_t *message, size_t message_size);
+void crypto_sha512_final (crypto_sha512_ctx *ctx, uint8_t hash[64]);
+void crypto_sha512(uint8_t hash[64],
+ const uint8_t *message, size_t message_size);
+
+// SHA 512 HMAC
+// ------------
+void crypto_sha512_hmac_init(crypto_sha512_hmac_ctx *ctx,
+ const uint8_t *key, size_t key_size);
+void crypto_sha512_hmac_update(crypto_sha512_hmac_ctx *ctx,
+ const uint8_t *message, size_t message_size);
+void crypto_sha512_hmac_final(crypto_sha512_hmac_ctx *ctx, uint8_t hmac[64]);
+void crypto_sha512_hmac(uint8_t hmac[64],
+ const uint8_t *key , size_t key_size,
+ const uint8_t *message, size_t message_size);
+
+// SHA 512 HKDF
+// ------------
+void crypto_sha512_hkdf_expand(uint8_t *okm, size_t okm_size,
+ const uint8_t *prk, size_t prk_size,
+ const uint8_t *info, size_t info_size);
+void crypto_sha512_hkdf(uint8_t *okm , size_t okm_size,
+ const uint8_t *ikm , size_t ikm_size,
+ const uint8_t *salt, size_t salt_size,
+ const uint8_t *info, size_t info_size);
+
+// Ed25519
+// -------
+// Signatures (EdDSA with curve25519 + SHA-512)
+// --------------------------------------------
+void crypto_ed25519_key_pair(uint8_t secret_key[64],
+ uint8_t public_key[32],
+ uint8_t seed[32]);
+void crypto_ed25519_sign(uint8_t signature [64],
+ const uint8_t secret_key[64],
+ const uint8_t *message, size_t message_size);
+int crypto_ed25519_check(const uint8_t signature [64],
+ const uint8_t public_key[32],
+ const uint8_t *message, size_t message_size);
+
+// Pre-hash variants
+void crypto_ed25519_ph_sign(uint8_t signature [64],
+ const uint8_t secret_key [64],
+ const uint8_t message_hash[64]);
+int crypto_ed25519_ph_check(const uint8_t signature [64],
+ const uint8_t public_key [32],
+ const uint8_t message_hash[64]);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // ED25519_H
diff --git a/src/monocypher/monocypher.c b/src/monocypher/monocypher.c
new file mode 100644
index 0000000..fe5bbdc
--- /dev/null
+++ b/src/monocypher/monocypher.c
@@ -0,0 +1,2988 @@
+// Monocypher version 4.0.3
+//
+// This file is dual-licensed. Choose whichever licence you want from
+// the two licences listed below.
+//
+// The first licence is a regular 2-clause BSD licence. The second licence
+// is the CC-0 from Creative Commons. It is intended to release Monocypher
+// to the public domain. The BSD licence serves as a fallback option.
+//
+// SPDX-License-Identifier: BSD-2-Clause OR CC0-1.0
+//
+// ------------------------------------------------------------------------
+//
+// Copyright (c) 2017-2020, Loup Vaillant
+// All rights reserved.
+//
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// 1. Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the
+// distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+//
+// ------------------------------------------------------------------------
+//
+// Written in 2017-2020 by Loup Vaillant
+//
+// To the extent possible under law, the author(s) have dedicated all copyright
+// and related neighboring rights to this software to the public domain
+// worldwide. This software is distributed without any warranty.
+//
+// You should have received a copy of the CC0 Public Domain Dedication along
+// with this software. If not, see
+//
+
+#include "monocypher.h"
+
+#ifdef MONOCYPHER_CPP_NAMESPACE
+namespace MONOCYPHER_CPP_NAMESPACE {
+#endif
+
+/////////////////
+/// Utilities ///
+/////////////////
+#define FOR_T(type, i, start, end) for (type i = (start); i < (end); i++)
+#define FOR(i, start, end) FOR_T(size_t, i, start, end)
+#define COPY(dst, src, size) FOR(_i_, 0, size) (dst)[_i_] = (src)[_i_]
+#define ZERO(buf, size) FOR(_i_, 0, size) (buf)[_i_] = 0
+#define WIPE_CTX(ctx) crypto_wipe(ctx , sizeof(*(ctx)))
+#define WIPE_BUFFER(buffer) crypto_wipe(buffer, sizeof(buffer))
+#define MIN(a, b) ((a) <= (b) ? (a) : (b))
+#define MAX(a, b) ((a) >= (b) ? (a) : (b))
+
+typedef int8_t i8;
+typedef uint8_t u8;
+typedef int16_t i16;
+typedef uint32_t u32;
+typedef int32_t i32;
+typedef int64_t i64;
+typedef uint64_t u64;
+
+static const u8 zero[128] = {0};
+
+// returns the smallest positive integer y such that
+// (x + y) % pow_2 == 0
+// Basically, y is the "gap" missing to align x.
+// Only works when pow_2 is a power of 2.
+// Note: we use ~x+1 instead of -x to avoid compiler warnings
+static size_t gap(size_t x, size_t pow_2)
+{
+ return (~x + 1) & (pow_2 - 1);
+}
+
+static u32 load24_le(const u8 s[3])
+{
+ return
+ ((u32)s[0] << 0) |
+ ((u32)s[1] << 8) |
+ ((u32)s[2] << 16);
+}
+
+static u32 load32_le(const u8 s[4])
+{
+ return
+ ((u32)s[0] << 0) |
+ ((u32)s[1] << 8) |
+ ((u32)s[2] << 16) |
+ ((u32)s[3] << 24);
+}
+
+static u64 load64_le(const u8 s[8])
+{
+ return load32_le(s) | ((u64)load32_le(s+4) << 32);
+}
+
+static void store32_le(u8 out[4], u32 in)
+{
+ out[0] = (u8)(in );
+ out[1] = (u8)(in >> 8);
+ out[2] = (u8)(in >> 16);
+ out[3] = (u8)(in >> 24);
+}
+
+static void store64_le(u8 out[8], u64 in)
+{
+ store32_le(out , (u32)(in ));
+ store32_le(out + 4, (u32)(in >> 32));
+}
+
+static void load32_le_buf (u32 *dst, const u8 *src, size_t size) {
+ FOR(i, 0, size) { dst[i] = load32_le(src + i*4); }
+}
+static void load64_le_buf (u64 *dst, const u8 *src, size_t size) {
+ FOR(i, 0, size) { dst[i] = load64_le(src + i*8); }
+}
+static void store32_le_buf(u8 *dst, const u32 *src, size_t size) {
+ FOR(i, 0, size) { store32_le(dst + i*4, src[i]); }
+}
+static void store64_le_buf(u8 *dst, const u64 *src, size_t size) {
+ FOR(i, 0, size) { store64_le(dst + i*8, src[i]); }
+}
+
+static u64 rotr64(u64 x, u64 n) { return (x >> n) ^ (x << (64 - n)); }
+static u32 rotl32(u32 x, u32 n) { return (x << n) ^ (x >> (32 - n)); }
+
+static int neq0(u64 diff)
+{
+ // constant time comparison to zero
+ // return diff != 0 ? -1 : 0
+ u64 half = (diff >> 32) | ((u32)diff); // half < 2^32
+ u64 eq0 = 1 & ((half - 1) >> 32); // half == 0 ? 1 : 0
+ return (int)eq0 - 1; // half == 0 ? 0 : -1
+}
+
+static u64 x16(const u8 a[16], const u8 b[16])
+{
+ return (load64_le(a + 0) ^ load64_le(b + 0))
+ | (load64_le(a + 8) ^ load64_le(b + 8));
+}
+static u64 x32(const u8 a[32],const u8 b[32]){return x16(a,b)| x16(a+16, b+16);}
+static u64 x64(const u8 a[64],const u8 b[64]){return x32(a,b)| x32(a+32, b+32);}
+int crypto_verify16(const u8 a[16], const u8 b[16]){ return neq0(x16(a, b)); }
+int crypto_verify32(const u8 a[32], const u8 b[32]){ return neq0(x32(a, b)); }
+int crypto_verify64(const u8 a[64], const u8 b[64]){ return neq0(x64(a, b)); }
+
+void crypto_wipe(void *secret, size_t size)
+{
+ volatile u8 *v_secret = (u8*)secret;
+ ZERO(v_secret, size);
+}
+
+/////////////////
+/// Chacha 20 ///
+/////////////////
+#define QUARTERROUND(a, b, c, d) \
+ a += b; d = rotl32(d ^ a, 16); \
+ c += d; b = rotl32(b ^ c, 12); \
+ a += b; d = rotl32(d ^ a, 8); \
+ c += d; b = rotl32(b ^ c, 7)
+
+static void chacha20_rounds(u32 out[16], const u32 in[16])
+{
+ // The temporary variables make Chacha20 10% faster.
+ u32 t0 = in[ 0]; u32 t1 = in[ 1]; u32 t2 = in[ 2]; u32 t3 = in[ 3];
+ u32 t4 = in[ 4]; u32 t5 = in[ 5]; u32 t6 = in[ 6]; u32 t7 = in[ 7];
+ u32 t8 = in[ 8]; u32 t9 = in[ 9]; u32 t10 = in[10]; u32 t11 = in[11];
+ u32 t12 = in[12]; u32 t13 = in[13]; u32 t14 = in[14]; u32 t15 = in[15];
+
+ FOR (i, 0, 10) { // 20 rounds, 2 rounds per loop.
+ QUARTERROUND(t0, t4, t8 , t12); // column 0
+ QUARTERROUND(t1, t5, t9 , t13); // column 1
+ QUARTERROUND(t2, t6, t10, t14); // column 2
+ QUARTERROUND(t3, t7, t11, t15); // column 3
+ QUARTERROUND(t0, t5, t10, t15); // diagonal 0
+ QUARTERROUND(t1, t6, t11, t12); // diagonal 1
+ QUARTERROUND(t2, t7, t8 , t13); // diagonal 2
+ QUARTERROUND(t3, t4, t9 , t14); // diagonal 3
+ }
+ out[ 0] = t0; out[ 1] = t1; out[ 2] = t2; out[ 3] = t3;
+ out[ 4] = t4; out[ 5] = t5; out[ 6] = t6; out[ 7] = t7;
+ out[ 8] = t8; out[ 9] = t9; out[10] = t10; out[11] = t11;
+ out[12] = t12; out[13] = t13; out[14] = t14; out[15] = t15;
+}
+
+static const u8 *chacha20_constant = (const u8*)"expand 32-byte k"; // 16 bytes
+
+void crypto_chacha20_h(u8 out[32], const u8 key[32], const u8 in [16])
+{
+ u32 block[16];
+ load32_le_buf(block , chacha20_constant, 4);
+ load32_le_buf(block + 4, key , 8);
+ load32_le_buf(block + 12, in , 4);
+
+ chacha20_rounds(block, block);
+
+ // prevent reversal of the rounds by revealing only half of the buffer.
+ store32_le_buf(out , block , 4); // constant
+ store32_le_buf(out+16, block+12, 4); // counter and nonce
+ WIPE_BUFFER(block);
+}
+
+u64 crypto_chacha20_djb(u8 *cipher_text, const u8 *plain_text,
+ size_t text_size, const u8 key[32], const u8 nonce[8],
+ u64 ctr)
+{
+ u32 input[16];
+ load32_le_buf(input , chacha20_constant, 4);
+ load32_le_buf(input + 4, key , 8);
+ load32_le_buf(input + 14, nonce , 2);
+ input[12] = (u32) ctr;
+ input[13] = (u32)(ctr >> 32);
+
+ // Whole blocks
+ u32 pool[16];
+ size_t nb_blocks = text_size >> 6;
+ FOR (i, 0, nb_blocks) {
+ chacha20_rounds(pool, input);
+ if (plain_text != NULL) {
+ FOR (j, 0, 16) {
+ u32 p = pool[j] + input[j];
+ store32_le(cipher_text, p ^ load32_le(plain_text));
+ cipher_text += 4;
+ plain_text += 4;
+ }
+ } else {
+ FOR (j, 0, 16) {
+ u32 p = pool[j] + input[j];
+ store32_le(cipher_text, p);
+ cipher_text += 4;
+ }
+ }
+ input[12]++;
+ if (input[12] == 0) {
+ input[13]++;
+ }
+ }
+ text_size &= 63;
+
+ // Last (incomplete) block
+ if (text_size > 0) {
+ if (plain_text == NULL) {
+ plain_text = zero;
+ }
+ chacha20_rounds(pool, input);
+ u8 tmp[64];
+ FOR (i, 0, 16) {
+ store32_le(tmp + i*4, pool[i] + input[i]);
+ }
+ FOR (i, 0, text_size) {
+ cipher_text[i] = tmp[i] ^ plain_text[i];
+ }
+ WIPE_BUFFER(tmp);
+ }
+ ctr = input[12] + ((u64)input[13] << 32) + (text_size > 0);
+
+ WIPE_BUFFER(pool);
+ WIPE_BUFFER(input);
+ return ctr;
+}
+
+u32 crypto_chacha20_ietf(u8 *cipher_text, const u8 *plain_text,
+ size_t text_size,
+ const u8 key[32], const u8 nonce[12], u32 ctr)
+{
+ u64 big_ctr = ctr + ((u64)load32_le(nonce) << 32);
+ return (u32)crypto_chacha20_djb(cipher_text, plain_text, text_size,
+ key, nonce + 4, big_ctr);
+}
+
+u64 crypto_chacha20_x(u8 *cipher_text, const u8 *plain_text,
+ size_t text_size,
+ const u8 key[32], const u8 nonce[24], u64 ctr)
+{
+ u8 sub_key[32];
+ crypto_chacha20_h(sub_key, key, nonce);
+ ctr = crypto_chacha20_djb(cipher_text, plain_text, text_size,
+ sub_key, nonce + 16, ctr);
+ WIPE_BUFFER(sub_key);
+ return ctr;
+}
+
+/////////////////
+/// Poly 1305 ///
+/////////////////
+
+// h = (h + c) * r
+// preconditions:
+// ctx->h <= 4_ffffffff_ffffffff_ffffffff_ffffffff
+// ctx->r <= 0ffffffc_0ffffffc_0ffffffc_0fffffff
+// end <= 1
+// Postcondition:
+// ctx->h <= 4_ffffffff_ffffffff_ffffffff_ffffffff
+static void poly_blocks(crypto_poly1305_ctx *ctx, const u8 *in,
+ size_t nb_blocks, unsigned end)
+{
+ // Local all the things!
+ const u32 r0 = ctx->r[0];
+ const u32 r1 = ctx->r[1];
+ const u32 r2 = ctx->r[2];
+ const u32 r3 = ctx->r[3];
+ const u32 rr0 = (r0 >> 2) * 5; // lose 2 bits...
+ const u32 rr1 = (r1 >> 2) + r1; // rr1 == (r1 >> 2) * 5
+ const u32 rr2 = (r2 >> 2) + r2; // rr1 == (r2 >> 2) * 5
+ const u32 rr3 = (r3 >> 2) + r3; // rr1 == (r3 >> 2) * 5
+ const u32 rr4 = r0 & 3; // ...recover 2 bits
+ u32 h0 = ctx->h[0];
+ u32 h1 = ctx->h[1];
+ u32 h2 = ctx->h[2];
+ u32 h3 = ctx->h[3];
+ u32 h4 = ctx->h[4];
+
+ FOR (i, 0, nb_blocks) {
+ // h + c, without carry propagation
+ const u64 s0 = (u64)h0 + load32_le(in); in += 4;
+ const u64 s1 = (u64)h1 + load32_le(in); in += 4;
+ const u64 s2 = (u64)h2 + load32_le(in); in += 4;
+ const u64 s3 = (u64)h3 + load32_le(in); in += 4;
+ const u32 s4 = h4 + end;
+
+ // (h + c) * r, without carry propagation
+ const u64 x0 = s0*r0+ s1*rr3+ s2*rr2+ s3*rr1+ s4*rr0;
+ const u64 x1 = s0*r1+ s1*r0 + s2*rr3+ s3*rr2+ s4*rr1;
+ const u64 x2 = s0*r2+ s1*r1 + s2*r0 + s3*rr3+ s4*rr2;
+ const u64 x3 = s0*r3+ s1*r2 + s2*r1 + s3*r0 + s4*rr3;
+ const u32 x4 = s4*rr4;
+
+ // partial reduction modulo 2^130 - 5
+ const u32 u5 = (u32)(x3 >> 32) + x4; // u5 <= 7ffffff5
+ const u64 u0 = (u32)(u5 >> 2) * 5 + (x0 & 0xffffffff);
+ const u64 u1 = (u32)(u0 >> 32) + (x1 & 0xffffffff) + (x0 >> 32);
+ const u64 u2 = (u32)(u1 >> 32) + (x2 & 0xffffffff) + (x1 >> 32);
+ const u64 u3 = (u32)(u2 >> 32) + (x3 & 0xffffffff) + (x2 >> 32);
+ const u32 u4 = (u32)(u3 >> 32) + (u5 & 3); // u4 <= 4
+
+ // Update the hash
+ h0 = u0 & 0xffffffff;
+ h1 = u1 & 0xffffffff;
+ h2 = u2 & 0xffffffff;
+ h3 = u3 & 0xffffffff;
+ h4 = u4;
+ }
+ ctx->h[0] = h0;
+ ctx->h[1] = h1;
+ ctx->h[2] = h2;
+ ctx->h[3] = h3;
+ ctx->h[4] = h4;
+}
+
+void crypto_poly1305_init(crypto_poly1305_ctx *ctx, const u8 key[32])
+{
+ ZERO(ctx->h, 5); // Initial hash is zero
+ ctx->c_idx = 0;
+ // load r and pad (r has some of its bits cleared)
+ load32_le_buf(ctx->r , key , 4);
+ load32_le_buf(ctx->pad, key+16, 4);
+ FOR (i, 0, 1) { ctx->r[i] &= 0x0fffffff; }
+ FOR (i, 1, 4) { ctx->r[i] &= 0x0ffffffc; }
+}
+
+void crypto_poly1305_update(crypto_poly1305_ctx *ctx,
+ const u8 *message, size_t message_size)
+{
+ // Avoid undefined NULL pointer increments with empty messages
+ if (message_size == 0) {
+ return;
+ }
+
+ // Align ourselves with block boundaries
+ size_t aligned = MIN(gap(ctx->c_idx, 16), message_size);
+ FOR (i, 0, aligned) {
+ ctx->c[ctx->c_idx] = *message;
+ ctx->c_idx++;
+ message++;
+ message_size--;
+ }
+
+ // If block is complete, process it
+ if (ctx->c_idx == 16) {
+ poly_blocks(ctx, ctx->c, 1, 1);
+ ctx->c_idx = 0;
+ }
+
+ // Process the message block by block
+ size_t nb_blocks = message_size >> 4;
+ poly_blocks(ctx, message, nb_blocks, 1);
+ message += nb_blocks << 4;
+ message_size &= 15;
+
+ // remaining bytes (we never complete a block here)
+ FOR (i, 0, message_size) {
+ ctx->c[ctx->c_idx] = message[i];
+ ctx->c_idx++;
+ }
+}
+
+void crypto_poly1305_final(crypto_poly1305_ctx *ctx, u8 mac[16])
+{
+ // Process the last block (if any)
+ // We move the final 1 according to remaining input length
+ // (this will add less than 2^130 to the last input block)
+ if (ctx->c_idx != 0) {
+ ZERO(ctx->c + ctx->c_idx, 16 - ctx->c_idx);
+ ctx->c[ctx->c_idx] = 1;
+ poly_blocks(ctx, ctx->c, 1, 0);
+ }
+
+ // check if we should subtract 2^130-5 by performing the
+ // corresponding carry propagation.
+ u64 c = 5;
+ FOR (i, 0, 4) {
+ c += ctx->h[i];
+ c >>= 32;
+ }
+ c += ctx->h[4];
+ c = (c >> 2) * 5; // shift the carry back to the beginning
+ // c now indicates how many times we should subtract 2^130-5 (0 or 1)
+ FOR (i, 0, 4) {
+ c += (u64)ctx->h[i] + ctx->pad[i];
+ store32_le(mac + i*4, (u32)c);
+ c = c >> 32;
+ }
+ WIPE_CTX(ctx);
+}
+
+void crypto_poly1305(u8 mac[16], const u8 *message,
+ size_t message_size, const u8 key[32])
+{
+ crypto_poly1305_ctx ctx;
+ crypto_poly1305_init (&ctx, key);
+ crypto_poly1305_update(&ctx, message, message_size);
+ crypto_poly1305_final (&ctx, mac);
+}
+
+////////////////
+/// BLAKE2 b ///
+////////////////
+static const u64 iv[8] = {
+ 0x6a09e667f3bcc908, 0xbb67ae8584caa73b,
+ 0x3c6ef372fe94f82b, 0xa54ff53a5f1d36f1,
+ 0x510e527fade682d1, 0x9b05688c2b3e6c1f,
+ 0x1f83d9abfb41bd6b, 0x5be0cd19137e2179,
+};
+
+static void blake2b_compress(crypto_blake2b_ctx *ctx, int is_last_block)
+{
+ static const u8 sigma[12][16] = {
+ { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 },
+ { 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3 },
+ { 11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4 },
+ { 7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8 },
+ { 9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13 },
+ { 2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9 },
+ { 12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11 },
+ { 13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10 },
+ { 6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5 },
+ { 10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0 },
+ { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 },
+ { 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3 },
+ };
+
+ // increment input offset
+ u64 *x = ctx->input_offset;
+ size_t y = ctx->input_idx;
+ x[0] += y;
+ if (x[0] < y) {
+ x[1]++;
+ }
+
+ // init work vector
+ u64 v0 = ctx->hash[0]; u64 v8 = iv[0];
+ u64 v1 = ctx->hash[1]; u64 v9 = iv[1];
+ u64 v2 = ctx->hash[2]; u64 v10 = iv[2];
+ u64 v3 = ctx->hash[3]; u64 v11 = iv[3];
+ u64 v4 = ctx->hash[4]; u64 v12 = iv[4] ^ ctx->input_offset[0];
+ u64 v5 = ctx->hash[5]; u64 v13 = iv[5] ^ ctx->input_offset[1];
+ u64 v6 = ctx->hash[6]; u64 v14 = iv[6] ^ (u64)~(is_last_block - 1);
+ u64 v7 = ctx->hash[7]; u64 v15 = iv[7];
+
+ // mangle work vector
+ u64 *input = ctx->input;
+#define BLAKE2_G(a, b, c, d, x, y) \
+ a += b + x; d = rotr64(d ^ a, 32); \
+ c += d; b = rotr64(b ^ c, 24); \
+ a += b + y; d = rotr64(d ^ a, 16); \
+ c += d; b = rotr64(b ^ c, 63)
+#define BLAKE2_ROUND(i) \
+ BLAKE2_G(v0, v4, v8 , v12, input[sigma[i][ 0]], input[sigma[i][ 1]]); \
+ BLAKE2_G(v1, v5, v9 , v13, input[sigma[i][ 2]], input[sigma[i][ 3]]); \
+ BLAKE2_G(v2, v6, v10, v14, input[sigma[i][ 4]], input[sigma[i][ 5]]); \
+ BLAKE2_G(v3, v7, v11, v15, input[sigma[i][ 6]], input[sigma[i][ 7]]); \
+ BLAKE2_G(v0, v5, v10, v15, input[sigma[i][ 8]], input[sigma[i][ 9]]); \
+ BLAKE2_G(v1, v6, v11, v12, input[sigma[i][10]], input[sigma[i][11]]); \
+ BLAKE2_G(v2, v7, v8 , v13, input[sigma[i][12]], input[sigma[i][13]]); \
+ BLAKE2_G(v3, v4, v9 , v14, input[sigma[i][14]], input[sigma[i][15]])
+
+#ifdef BLAKE2_NO_UNROLLING
+ FOR (i, 0, 12) {
+ BLAKE2_ROUND(i);
+ }
+#else
+ BLAKE2_ROUND(0); BLAKE2_ROUND(1); BLAKE2_ROUND(2); BLAKE2_ROUND(3);
+ BLAKE2_ROUND(4); BLAKE2_ROUND(5); BLAKE2_ROUND(6); BLAKE2_ROUND(7);
+ BLAKE2_ROUND(8); BLAKE2_ROUND(9); BLAKE2_ROUND(10); BLAKE2_ROUND(11);
+#endif
+
+ // update hash
+ ctx->hash[0] ^= v0 ^ v8; ctx->hash[1] ^= v1 ^ v9;
+ ctx->hash[2] ^= v2 ^ v10; ctx->hash[3] ^= v3 ^ v11;
+ ctx->hash[4] ^= v4 ^ v12; ctx->hash[5] ^= v5 ^ v13;
+ ctx->hash[6] ^= v6 ^ v14; ctx->hash[7] ^= v7 ^ v15;
+}
+
+void crypto_blake2b_keyed_init(crypto_blake2b_ctx *ctx, size_t hash_size,
+ const u8 *key, size_t key_size)
+{
+ // initial hash
+ COPY(ctx->hash, iv, 8);
+ ctx->hash[0] ^= 0x01010000 ^ (key_size << 8) ^ hash_size;
+
+ ctx->input_offset[0] = 0; // beginning of the input, no offset
+ ctx->input_offset[1] = 0; // beginning of the input, no offset
+ ctx->hash_size = hash_size;
+ ctx->input_idx = 0;
+ ZERO(ctx->input, 16);
+
+ // if there is a key, the first block is that key (padded with zeroes)
+ if (key_size > 0) {
+ u8 key_block[128] = {0};
+ COPY(key_block, key, key_size);
+ // same as calling crypto_blake2b_update(ctx, key_block , 128)
+ load64_le_buf(ctx->input, key_block, 16);
+ ctx->input_idx = 128;
+ WIPE_BUFFER(key_block);
+ }
+}
+
+void crypto_blake2b_init(crypto_blake2b_ctx *ctx, size_t hash_size)
+{
+ crypto_blake2b_keyed_init(ctx, hash_size, 0, 0);
+}
+
+void crypto_blake2b_update(crypto_blake2b_ctx *ctx,
+ const u8 *message, size_t message_size)
+{
+ // Avoid undefined NULL pointer increments with empty messages
+ if (message_size == 0) {
+ return;
+ }
+
+ // Align with word boundaries
+ if ((ctx->input_idx & 7) != 0) {
+ size_t nb_bytes = MIN(gap(ctx->input_idx, 8), message_size);
+ size_t word = ctx->input_idx >> 3;
+ size_t byte = ctx->input_idx & 7;
+ FOR (i, 0, nb_bytes) {
+ ctx->input[word] |= (u64)message[i] << ((byte + i) << 3);
+ }
+ ctx->input_idx += nb_bytes;
+ message += nb_bytes;
+ message_size -= nb_bytes;
+ }
+
+ // Align with block boundaries (faster than byte by byte)
+ if ((ctx->input_idx & 127) != 0) {
+ size_t nb_words = MIN(gap(ctx->input_idx, 128), message_size) >> 3;
+ load64_le_buf(ctx->input + (ctx->input_idx >> 3), message, nb_words);
+ ctx->input_idx += nb_words << 3;
+ message += nb_words << 3;
+ message_size -= nb_words << 3;
+ }
+
+ // Process block by block
+ size_t nb_blocks = message_size >> 7;
+ FOR (i, 0, nb_blocks) {
+ if (ctx->input_idx == 128) {
+ blake2b_compress(ctx, 0);
+ }
+ load64_le_buf(ctx->input, message, 16);
+ message += 128;
+ ctx->input_idx = 128;
+ }
+ message_size &= 127;
+
+ if (message_size != 0) {
+ // Compress block & flush input buffer as needed
+ if (ctx->input_idx == 128) {
+ blake2b_compress(ctx, 0);
+ ctx->input_idx = 0;
+ }
+ if (ctx->input_idx == 0) {
+ ZERO(ctx->input, 16);
+ }
+ // Fill remaining words (faster than byte by byte)
+ size_t nb_words = message_size >> 3;
+ load64_le_buf(ctx->input, message, nb_words);
+ ctx->input_idx += nb_words << 3;
+ message += nb_words << 3;
+ message_size -= nb_words << 3;
+
+ // Fill remaining bytes
+ FOR (i, 0, message_size) {
+ size_t word = ctx->input_idx >> 3;
+ size_t byte = ctx->input_idx & 7;
+ ctx->input[word] |= (u64)message[i] << (byte << 3);
+ ctx->input_idx++;
+ }
+ }
+}
+
+void crypto_blake2b_final(crypto_blake2b_ctx *ctx, u8 *hash)
+{
+ blake2b_compress(ctx, 1); // compress the last block
+ size_t hash_size = MIN(ctx->hash_size, 64);
+ size_t nb_words = hash_size >> 3;
+ store64_le_buf(hash, ctx->hash, nb_words);
+ FOR (i, nb_words << 3, hash_size) {
+ hash[i] = (ctx->hash[i >> 3] >> (8 * (i & 7))) & 0xff;
+ }
+ WIPE_CTX(ctx);
+}
+
+void crypto_blake2b_keyed(u8 *hash, size_t hash_size,
+ const u8 *key, size_t key_size,
+ const u8 *message, size_t message_size)
+{
+ crypto_blake2b_ctx ctx;
+ crypto_blake2b_keyed_init(&ctx, hash_size, key, key_size);
+ crypto_blake2b_update (&ctx, message, message_size);
+ crypto_blake2b_final (&ctx, hash);
+}
+
+void crypto_blake2b(u8 *hash, size_t hash_size, const u8 *msg, size_t msg_size)
+{
+ crypto_blake2b_keyed(hash, hash_size, 0, 0, msg, msg_size);
+}
+
+//////////////
+/// Argon2 ///
+//////////////
+// references to R, Z, Q etc. come from the spec
+
+// Argon2 operates on 1024 byte blocks.
+typedef struct { u64 a[128]; } blk;
+
+// updates a BLAKE2 hash with a 32 bit word, little endian.
+static void blake_update_32(crypto_blake2b_ctx *ctx, u32 input)
+{
+ u8 buf[4];
+ store32_le(buf, input);
+ crypto_blake2b_update(ctx, buf, 4);
+ WIPE_BUFFER(buf);
+}
+
+static void blake_update_32_buf(crypto_blake2b_ctx *ctx,
+ const u8 *buf, u32 size)
+{
+ blake_update_32(ctx, size);
+ crypto_blake2b_update(ctx, buf, size);
+}
+
+
+static void copy_block(blk *o,const blk*in){FOR(i, 0, 128) o->a[i] = in->a[i];}
+static void xor_block(blk *o,const blk*in){FOR(i, 0, 128) o->a[i] ^= in->a[i];}
+
+// Hash with a virtually unlimited digest size.
+// Doesn't extract more entropy than the base hash function.
+// Mainly used for filling a whole kilobyte block with pseudo-random bytes.
+// (One could use a stream cipher with a seed hash as the key, but
+// this would introduce another dependency —and point of failure.)
+static void extended_hash(u8 *digest, u32 digest_size,
+ const u8 *input , u32 input_size)
+{
+ crypto_blake2b_ctx ctx;
+ crypto_blake2b_init (&ctx, MIN(digest_size, 64));
+ blake_update_32 (&ctx, digest_size);
+ crypto_blake2b_update(&ctx, input, input_size);
+ crypto_blake2b_final (&ctx, digest);
+
+ if (digest_size > 64) {
+ // the conversion to u64 avoids integer overflow on
+ // ludicrously big hash sizes.
+ u32 r = (u32)(((u64)digest_size + 31) >> 5) - 2;
+ u32 i = 1;
+ u32 in = 0;
+ u32 out = 32;
+ while (i < r) {
+ // Input and output overlap. This is intentional
+ crypto_blake2b(digest + out, 64, digest + in, 64);
+ i += 1;
+ in += 32;
+ out += 32;
+ }
+ crypto_blake2b(digest + out, digest_size - (32 * r), digest + in , 64);
+ }
+}
+
+#define LSB(x) ((u64)(u32)x)
+#define G(a, b, c, d) \
+ a += b + ((LSB(a) * LSB(b)) << 1); d ^= a; d = rotr64(d, 32); \
+ c += d + ((LSB(c) * LSB(d)) << 1); b ^= c; b = rotr64(b, 24); \
+ a += b + ((LSB(a) * LSB(b)) << 1); d ^= a; d = rotr64(d, 16); \
+ c += d + ((LSB(c) * LSB(d)) << 1); b ^= c; b = rotr64(b, 63)
+#define ROUND(v0, v1, v2, v3, v4, v5, v6, v7, \
+ v8, v9, v10, v11, v12, v13, v14, v15) \
+ G(v0, v4, v8, v12); G(v1, v5, v9, v13); \
+ G(v2, v6, v10, v14); G(v3, v7, v11, v15); \
+ G(v0, v5, v10, v15); G(v1, v6, v11, v12); \
+ G(v2, v7, v8, v13); G(v3, v4, v9, v14)
+
+// Core of the compression function G. Computes Z from R in place.
+static void g_rounds(blk *b)
+{
+ // column rounds (work_block = Q)
+ for (int i = 0; i < 128; i += 16) {
+ ROUND(b->a[i ], b->a[i+ 1], b->a[i+ 2], b->a[i+ 3],
+ b->a[i+ 4], b->a[i+ 5], b->a[i+ 6], b->a[i+ 7],
+ b->a[i+ 8], b->a[i+ 9], b->a[i+10], b->a[i+11],
+ b->a[i+12], b->a[i+13], b->a[i+14], b->a[i+15]);
+ }
+ // row rounds (b = Z)
+ for (int i = 0; i < 16; i += 2) {
+ ROUND(b->a[i ], b->a[i+ 1], b->a[i+ 16], b->a[i+ 17],
+ b->a[i+32], b->a[i+33], b->a[i+ 48], b->a[i+ 49],
+ b->a[i+64], b->a[i+65], b->a[i+ 80], b->a[i+ 81],
+ b->a[i+96], b->a[i+97], b->a[i+112], b->a[i+113]);
+ }
+}
+
+const crypto_argon2_extras crypto_argon2_no_extras = { 0, 0, 0, 0 };
+
+void crypto_argon2(u8 *hash, u32 hash_size, void *work_area,
+ crypto_argon2_config config,
+ crypto_argon2_inputs inputs,
+ crypto_argon2_extras extras)
+{
+ const u32 segment_size = config.nb_blocks / config.nb_lanes / 4;
+ const u32 lane_size = segment_size * 4;
+ const u32 nb_blocks = lane_size * config.nb_lanes; // rounding down
+
+ // work area seen as blocks (must be suitably aligned)
+ blk *blocks = (blk*)work_area;
+ {
+ u8 initial_hash[72]; // 64 bytes plus 2 words for future hashes
+ crypto_blake2b_ctx ctx;
+ crypto_blake2b_init (&ctx, 64);
+ blake_update_32 (&ctx, config.nb_lanes ); // p: number of "threads"
+ blake_update_32 (&ctx, hash_size);
+ blake_update_32 (&ctx, config.nb_blocks);
+ blake_update_32 (&ctx, config.nb_passes);
+ blake_update_32 (&ctx, 0x13); // v: version number
+ blake_update_32 (&ctx, config.algorithm); // y: Argon2i, Argon2d...
+ blake_update_32_buf (&ctx, inputs.pass, inputs.pass_size);
+ blake_update_32_buf (&ctx, inputs.salt, inputs.salt_size);
+ blake_update_32_buf (&ctx, extras.key, extras.key_size);
+ blake_update_32_buf (&ctx, extras.ad, extras.ad_size);
+ crypto_blake2b_final(&ctx, initial_hash); // fill 64 first bytes only
+
+ // fill first 2 blocks of each lane
+ u8 hash_area[1024];
+ FOR_T(u32, l, 0, config.nb_lanes) {
+ FOR_T(u32, i, 0, 2) {
+ store32_le(initial_hash + 64, i); // first additional word
+ store32_le(initial_hash + 68, l); // second additional word
+ extended_hash(hash_area, 1024, initial_hash, 72);
+ load64_le_buf(blocks[l * lane_size + i].a, hash_area, 128);
+ }
+ }
+
+ WIPE_BUFFER(initial_hash);
+ WIPE_BUFFER(hash_area);
+ }
+
+ // Argon2i and Argon2id start with constant time indexing
+ int constant_time = config.algorithm != CRYPTO_ARGON2_D;
+
+ // Fill (and re-fill) the rest of the blocks
+ //
+ // Note: even though each segment within the same slice can be
+ // computed in parallel, (one thread per lane), we are computing
+ // them sequentially, because Monocypher doesn't support threads.
+ //
+ // Yet optimal performance (and therefore security) requires one
+ // thread per lane. The only reason Monocypher supports multiple
+ // lanes is compatibility.
+ blk tmp;
+ FOR_T(u32, pass, 0, config.nb_passes) {
+ FOR_T(u32, slice, 0, 4) {
+ // On the first slice of the first pass,
+ // blocks 0 and 1 are already filled, hence pass_offset.
+ u32 pass_offset = pass == 0 && slice == 0 ? 2 : 0;
+ u32 slice_offset = slice * segment_size;
+
+ // Argon2id switches back to non-constant time indexing
+ // after the first two slices of the first pass
+ if (slice == 2 && config.algorithm == CRYPTO_ARGON2_ID) {
+ constant_time = 0;
+ }
+
+ // Each iteration of the following loop may be performed in
+ // a separate thread. All segments must be fully completed
+ // before we start filling the next slice.
+ FOR_T(u32, segment, 0, config.nb_lanes) {
+ blk index_block;
+ u32 index_ctr = 1;
+ FOR_T (u32, block, pass_offset, segment_size) {
+ // Current and previous blocks
+ u32 lane_offset = segment * lane_size;
+ blk *segment_start = blocks + lane_offset + slice_offset;
+ blk *current = segment_start + block;
+ blk *previous =
+ block == 0 && slice_offset == 0
+ ? segment_start + lane_size - 1
+ : segment_start + block - 1;
+
+ u64 index_seed;
+ if (constant_time) {
+ if (block == pass_offset || (block % 128) == 0) {
+ // Fill or refresh deterministic indices block
+
+ // seed the beginning of the block...
+ ZERO(index_block.a, 128);
+ index_block.a[0] = pass;
+ index_block.a[1] = segment;
+ index_block.a[2] = slice;
+ index_block.a[3] = nb_blocks;
+ index_block.a[4] = config.nb_passes;
+ index_block.a[5] = config.algorithm;
+ index_block.a[6] = index_ctr;
+ index_ctr++;
+
+ // ... then shuffle it
+ copy_block(&tmp, &index_block);
+ g_rounds (&index_block);
+ xor_block (&index_block, &tmp);
+ copy_block(&tmp, &index_block);
+ g_rounds (&index_block);
+ xor_block (&index_block, &tmp);
+ }
+ index_seed = index_block.a[block % 128];
+ } else {
+ index_seed = previous->a[0];
+ }
+
+ // Establish the reference set. *Approximately* comprises:
+ // - The last 3 slices (if they exist yet)
+ // - The already constructed blocks in the current segment
+ u32 next_slice = ((slice + 1) % 4) * segment_size;
+ u32 window_start = pass == 0 ? 0 : next_slice;
+ u32 nb_segments = pass == 0 ? slice : 3;
+ u32 lane =
+ pass == 0 && slice == 0
+ ? segment
+ : (u32)(index_seed >> 32) % config.nb_lanes;
+ u32 window_size =
+ nb_segments * segment_size +
+ (lane == segment ? block-1 :
+ block == 0 ? (u32)-1 : 0);
+
+ // Find reference block
+ u64 j1 = index_seed & 0xffffffff; // block selector
+ u64 x = (j1 * j1) >> 32;
+ u64 y = (window_size * x) >> 32;
+ u64 z = (window_size - 1) - y;
+ u32 ref = (u32)((window_start + z) % lane_size);
+ u32 index = lane * lane_size + ref;
+ blk *reference = blocks + index;
+
+ // Shuffle the previous & reference block
+ // into the current block
+ copy_block(&tmp, previous);
+ xor_block (&tmp, reference);
+ if (pass == 0) { copy_block(current, &tmp); }
+ else { xor_block (current, &tmp); }
+ g_rounds (&tmp);
+ xor_block (current, &tmp);
+ }
+ }
+ }
+ }
+
+ // Wipe temporary block
+ volatile u64* p = tmp.a;
+ ZERO(p, 128);
+
+ // XOR last blocks of each lane
+ blk *last_block = blocks + lane_size - 1;
+ FOR_T (u32, lane, 1, config.nb_lanes) {
+ blk *next_block = last_block + lane_size;
+ xor_block(next_block, last_block);
+ last_block = next_block;
+ }
+
+ // Serialize last block
+ u8 final_block[1024];
+ store64_le_buf(final_block, last_block->a, 128);
+
+ // Wipe work area
+ p = (u64*)work_area;
+ ZERO(p, 128 * nb_blocks);
+
+ // Hash the very last block with H' into the output hash
+ extended_hash(hash, hash_size, final_block, 1024);
+ WIPE_BUFFER(final_block);
+}
+
+////////////////////////////////////
+/// Arithmetic modulo 2^255 - 19 ///
+////////////////////////////////////
+// Originally taken from SUPERCOP's ref10 implementation.
+// A bit bigger than TweetNaCl, over 4 times faster.
+
+// field element
+typedef i32 fe[10];
+
+// field constants
+//
+// fe_one : 1
+// sqrtm1 : sqrt(-1)
+// d : -121665 / 121666
+// D2 : 2 * -121665 / 121666
+// lop_x, lop_y: low order point in Edwards coordinates
+// ufactor : -sqrt(-1) * 2
+// A2 : 486662^2 (A squared)
+static const fe fe_one = {1};
+static const fe sqrtm1 = {
+ -32595792, -7943725, 9377950, 3500415, 12389472,
+ -272473, -25146209, -2005654, 326686, 11406482,
+};
+static const fe d = {
+ -10913610, 13857413, -15372611, 6949391, 114729,
+ -8787816, -6275908, -3247719, -18696448, -12055116,
+};
+static const fe D2 = {
+ -21827239, -5839606, -30745221, 13898782, 229458,
+ 15978800, -12551817, -6495438, 29715968, 9444199,
+};
+static const fe lop_x = {
+ 21352778, 5345713, 4660180, -8347857, 24143090,
+ 14568123, 30185756, -12247770, -33528939, 8345319,
+};
+static const fe lop_y = {
+ -6952922, -1265500, 6862341, -7057498, -4037696,
+ -5447722, 31680899, -15325402, -19365852, 1569102,
+};
+static const fe ufactor = {
+ -1917299, 15887451, -18755900, -7000830, -24778944,
+ 544946, -16816446, 4011309, -653372, 10741468,
+};
+static const fe A2 = {
+ 12721188, 3529, 0, 0, 0, 0, 0, 0, 0, 0,
+};
+
+static void fe_0(fe h) { ZERO(h , 10); }
+static void fe_1(fe h) { h[0] = 1; ZERO(h+1, 9); }
+
+static void fe_copy(fe h,const fe f ){FOR(i,0,10) h[i] = f[i]; }
+static void fe_neg (fe h,const fe f ){FOR(i,0,10) h[i] = -f[i]; }
+static void fe_add (fe h,const fe f,const fe g){FOR(i,0,10) h[i] = f[i] + g[i];}
+static void fe_sub (fe h,const fe f,const fe g){FOR(i,0,10) h[i] = f[i] - g[i];}
+
+// Some compilers, when inlining fe_cswap() or fe_ccopy(), may introduce
+// a timing leak. It happens when it notices `b` has only 2 possible
+// values, and either replace the arithmetic by a secret dependent
+// branch, or (as has been observed), swap pointers instead of values,
+// which intruduces a secret dependent index.
+//
+// We apply two mitigations here:
+// - Add `volatile` in the mask declaration.
+// - Unroll the copy loop (costs couple hundred bytes of binary code).
+//
+// As of June 2026, those mitigation work when applied separately or
+// together. Applying them both is currently overkill, but may help
+// delay the day compilers grow clever enough to defeat it. (The true
+// fix is in the semantics of the language itself: C currently has no
+// way to specify constant time code).
+//
+// Note (Loup): as of June 2026, the problem has yet to surface in
+// fe_cswap(), but since this is almost the same code as fe_ccopy() I
+// believe it is more prudent to apply the precaution there too.
+static void fe_cswap(fe f, fe g, int b)
+{
+ volatile i32 mask = -b; // -1 = 0xffffffff
+ i32 x0 = (f[0] ^ g[0]) & mask; f[0] = f[0] ^ x0; g[0] = g[0] ^ x0;
+ i32 x1 = (f[1] ^ g[1]) & mask; f[1] = f[1] ^ x1; g[1] = g[1] ^ x1;
+ i32 x2 = (f[2] ^ g[2]) & mask; f[2] = f[2] ^ x2; g[2] = g[2] ^ x2;
+ i32 x3 = (f[3] ^ g[3]) & mask; f[3] = f[3] ^ x3; g[3] = g[3] ^ x3;
+ i32 x4 = (f[4] ^ g[4]) & mask; f[4] = f[4] ^ x4; g[4] = g[4] ^ x4;
+ i32 x5 = (f[5] ^ g[5]) & mask; f[5] = f[5] ^ x5; g[5] = g[5] ^ x5;
+ i32 x6 = (f[6] ^ g[6]) & mask; f[6] = f[6] ^ x6; g[6] = g[6] ^ x6;
+ i32 x7 = (f[7] ^ g[7]) & mask; f[7] = f[7] ^ x7; g[7] = g[7] ^ x7;
+ i32 x8 = (f[8] ^ g[8]) & mask; f[8] = f[8] ^ x8; g[8] = g[8] ^ x8;
+ i32 x9 = (f[9] ^ g[9]) & mask; f[9] = f[9] ^ x9; g[9] = g[9] ^ x9;
+}
+
+static void fe_ccopy(fe f, const fe g, int b)
+{
+ volatile i32 mask = -b; // -1 = 0xffffffff
+ i32 x0 = (f[0] ^ g[0]) & mask; f[0] = f[0] ^ x0;
+ i32 x1 = (f[1] ^ g[1]) & mask; f[1] = f[1] ^ x1;
+ i32 x2 = (f[2] ^ g[2]) & mask; f[2] = f[2] ^ x2;
+ i32 x3 = (f[3] ^ g[3]) & mask; f[3] = f[3] ^ x3;
+ i32 x4 = (f[4] ^ g[4]) & mask; f[4] = f[4] ^ x4;
+ i32 x5 = (f[5] ^ g[5]) & mask; f[5] = f[5] ^ x5;
+ i32 x6 = (f[6] ^ g[6]) & mask; f[6] = f[6] ^ x6;
+ i32 x7 = (f[7] ^ g[7]) & mask; f[7] = f[7] ^ x7;
+ i32 x8 = (f[8] ^ g[8]) & mask; f[8] = f[8] ^ x8;
+ i32 x9 = (f[9] ^ g[9]) & mask; f[9] = f[9] ^ x9;
+}
+
+
+// Signed carry propagation
+// ------------------------
+//
+// Let t be a number. It can be uniquely decomposed thus:
+//
+// t = h*2^26 + l
+// such that -2^25 <= l < 2^25
+//
+// Let c = (t + 2^25) / 2^26 (rounded down)
+// c = (h*2^26 + l + 2^25) / 2^26 (rounded down)
+// c = h + (l + 2^25) / 2^26 (rounded down)
+// c = h (exactly)
+// Because 0 <= l + 2^25 < 2^26
+//
+// Let u = t - c*2^26
+// u = h*2^26 + l - h*2^26
+// u = l
+// Therefore, -2^25 <= u < 2^25
+//
+// Additionally, if |t| < x, then |h| < x/2^26 (rounded down)
+//
+// Notations:
+// - In C, 1<<25 means 2^25.
+// - In C, x>>25 means floor(x / (2^25)).
+// - All of the above applies with 25 & 24 as well as 26 & 25.
+//
+//
+// Note on negative right shifts
+// -----------------------------
+//
+// In C, x >> n, where x is a negative integer, is implementation
+// defined. In practice, all platforms do arithmetic shift, which is
+// equivalent to division by 2^26, rounded down. Some compilers, like
+// GCC, even guarantee it.
+//
+// If we ever stumble upon a platform that does not propagate the sign
+// bit (we won't), visible failures will show at the slightest test, and
+// the signed shifts can be replaced by the following:
+//
+// typedef struct { i64 x:39; } s25;
+// typedef struct { i64 x:38; } s26;
+// i64 shift25(i64 x) { s25 s; s.x = ((u64)x)>>25; return s.x; }
+// i64 shift26(i64 x) { s26 s; s.x = ((u64)x)>>26; return s.x; }
+//
+// Current compilers cannot optimise this, causing a 30% drop in
+// performance. Fairly expensive for something that never happens.
+//
+//
+// Precondition
+// ------------
+//
+// |t0| < 2^63
+// |t1|..|t9| < 2^62
+//
+// Algorithm
+// ---------
+// c = t0 + 2^25 / 2^26 -- |c| <= 2^36
+// t0 -= c * 2^26 -- |t0| <= 2^25
+// t1 += c -- |t1| <= 2^63
+//
+// c = t4 + 2^25 / 2^26 -- |c| <= 2^36
+// t4 -= c * 2^26 -- |t4| <= 2^25
+// t5 += c -- |t5| <= 2^63
+//
+// c = t1 + 2^24 / 2^25 -- |c| <= 2^38
+// t1 -= c * 2^25 -- |t1| <= 2^24
+// t2 += c -- |t2| <= 2^63
+//
+// c = t5 + 2^24 / 2^25 -- |c| <= 2^38
+// t5 -= c * 2^25 -- |t5| <= 2^24
+// t6 += c -- |t6| <= 2^63
+//
+// c = t2 + 2^25 / 2^26 -- |c| <= 2^37
+// t2 -= c * 2^26 -- |t2| <= 2^25 < 1.1 * 2^25 (final t2)
+// t3 += c -- |t3| <= 2^63
+//
+// c = t6 + 2^25 / 2^26 -- |c| <= 2^37
+// t6 -= c * 2^26 -- |t6| <= 2^25 < 1.1 * 2^25 (final t6)
+// t7 += c -- |t7| <= 2^63
+//
+// c = t3 + 2^24 / 2^25 -- |c| <= 2^38
+// t3 -= c * 2^25 -- |t3| <= 2^24 < 1.1 * 2^24 (final t3)
+// t4 += c -- |t4| <= 2^25 + 2^38 < 2^39
+//
+// c = t7 + 2^24 / 2^25 -- |c| <= 2^38
+// t7 -= c * 2^25 -- |t7| <= 2^24 < 1.1 * 2^24 (final t7)
+// t8 += c -- |t8| <= 2^63
+//
+// c = t4 + 2^25 / 2^26 -- |c| <= 2^13
+// t4 -= c * 2^26 -- |t4| <= 2^25 < 1.1 * 2^25 (final t4)
+// t5 += c -- |t5| <= 2^24 + 2^13 < 1.1 * 2^24 (final t5)
+//
+// c = t8 + 2^25 / 2^26 -- |c| <= 2^37
+// t8 -= c * 2^26 -- |t8| <= 2^25 < 1.1 * 2^25 (final t8)
+// t9 += c -- |t9| <= 2^63
+//
+// c = t9 + 2^24 / 2^25 -- |c| <= 2^38
+// t9 -= c * 2^25 -- |t9| <= 2^24 < 1.1 * 2^24 (final t9)
+// t0 += c * 19 -- |t0| <= 2^25 + 2^38*19 < 2^44
+//
+// c = t0 + 2^25 / 2^26 -- |c| <= 2^18
+// t0 -= c * 2^26 -- |t0| <= 2^25 < 1.1 * 2^25 (final t0)
+// t1 += c -- |t1| <= 2^24 + 2^18 < 1.1 * 2^24 (final t1)
+//
+// Postcondition
+// -------------
+// |t0|, |t2|, |t4|, |t6|, |t8| < 1.1 * 2^25
+// |t1|, |t3|, |t5|, |t7|, |t9| < 1.1 * 2^24
+#define FE_CARRY \
+ i64 c; \
+ c = (t0 + ((i64)1<<25)) >> 26; t0 -= c * ((i64)1 << 26); t1 += c; \
+ c = (t4 + ((i64)1<<25)) >> 26; t4 -= c * ((i64)1 << 26); t5 += c; \
+ c = (t1 + ((i64)1<<24)) >> 25; t1 -= c * ((i64)1 << 25); t2 += c; \
+ c = (t5 + ((i64)1<<24)) >> 25; t5 -= c * ((i64)1 << 25); t6 += c; \
+ c = (t2 + ((i64)1<<25)) >> 26; t2 -= c * ((i64)1 << 26); t3 += c; \
+ c = (t6 + ((i64)1<<25)) >> 26; t6 -= c * ((i64)1 << 26); t7 += c; \
+ c = (t3 + ((i64)1<<24)) >> 25; t3 -= c * ((i64)1 << 25); t4 += c; \
+ c = (t7 + ((i64)1<<24)) >> 25; t7 -= c * ((i64)1 << 25); t8 += c; \
+ c = (t4 + ((i64)1<<25)) >> 26; t4 -= c * ((i64)1 << 26); t5 += c; \
+ c = (t8 + ((i64)1<<25)) >> 26; t8 -= c * ((i64)1 << 26); t9 += c; \
+ c = (t9 + ((i64)1<<24)) >> 25; t9 -= c * ((i64)1 << 25); t0 += c * 19; \
+ c = (t0 + ((i64)1<<25)) >> 26; t0 -= c * ((i64)1 << 26); t1 += c; \
+ h[0]=(i32)t0; h[1]=(i32)t1; h[2]=(i32)t2; h[3]=(i32)t3; h[4]=(i32)t4; \
+ h[5]=(i32)t5; h[6]=(i32)t6; h[7]=(i32)t7; h[8]=(i32)t8; h[9]=(i32)t9
+
+// Decodes a field element from a byte buffer.
+// mask specifies how many bits we ignore.
+// Traditionally we ignore 1. It's useful for EdDSA,
+// which uses that bit to denote the sign of x.
+// Elligator however uses positive representatives,
+// which means ignoring 2 bits instead.
+static void fe_frombytes_mask(fe h, const u8 s[32], unsigned nb_mask)
+{
+ u32 mask = 0xffffff >> nb_mask;
+ i64 t0 = load32_le(s); // t0 < 2^32
+ i64 t1 = load24_le(s + 4) << 6; // t1 < 2^30
+ i64 t2 = load24_le(s + 7) << 5; // t2 < 2^29
+ i64 t3 = load24_le(s + 10) << 3; // t3 < 2^27
+ i64 t4 = load24_le(s + 13) << 2; // t4 < 2^26
+ i64 t5 = load32_le(s + 16); // t5 < 2^32
+ i64 t6 = load24_le(s + 20) << 7; // t6 < 2^31
+ i64 t7 = load24_le(s + 23) << 5; // t7 < 2^29
+ i64 t8 = load24_le(s + 26) << 4; // t8 < 2^28
+ i64 t9 = (load24_le(s + 29) & mask) << 2; // t9 < 2^25
+ FE_CARRY; // Carry precondition OK
+}
+
+static void fe_frombytes(fe h, const u8 s[32])
+{
+ fe_frombytes_mask(h, s, 1);
+}
+
+
+// Precondition
+// |h[0]|, |h[2]|, |h[4]|, |h[6]|, |h[8]| < 1.1 * 2^25
+// |h[1]|, |h[3]|, |h[5]|, |h[7]|, |h[9]| < 1.1 * 2^24
+//
+// Therefore, |h| < 2^255-19
+// There are two possibilities:
+//
+// - If h is positive, all we need to do is reduce its individual
+// limbs down to their tight positive range.
+// - If h is negative, we also need to add 2^255-19 to it.
+// Or just remove 19 and chop off any excess bit.
+static void fe_tobytes(u8 s[32], const fe h)
+{
+ i32 t[10];
+ COPY(t, h, 10);
+ i32 q = (19 * t[9] + (((i32) 1) << 24)) >> 25;
+ // |t9| < 1.1 * 2^24
+ // -1.1 * 2^24 < t9 < 1.1 * 2^24
+ // -21 * 2^24 < 19 * t9 < 21 * 2^24
+ // -2^29 < 19 * t9 + 2^24 < 2^29
+ // -2^29 / 2^25 < (19 * t9 + 2^24) / 2^25 < 2^29 / 2^25
+ // -16 < (19 * t9 + 2^24) / 2^25 < 16
+ FOR (i, 0, 5) {
+ q += t[2*i ]; q >>= 26; // q = 0 or -1
+ q += t[2*i+1]; q >>= 25; // q = 0 or -1
+ }
+ // q = 0 iff h >= 0
+ // q = -1 iff h < 0
+ // Adding q * 19 to h reduces h to its proper range.
+ q *= 19; // Shift carry back to the beginning
+ FOR (i, 0, 5) {
+ t[i*2 ] += q; q = t[i*2 ] >> 26; t[i*2 ] -= q * ((i32)1 << 26);
+ t[i*2+1] += q; q = t[i*2+1] >> 25; t[i*2+1] -= q * ((i32)1 << 25);
+ }
+ // h is now fully reduced, and q represents the excess bit.
+
+ store32_le(s + 0, ((u32)t[0] >> 0) | ((u32)t[1] << 26));
+ store32_le(s + 4, ((u32)t[1] >> 6) | ((u32)t[2] << 19));
+ store32_le(s + 8, ((u32)t[2] >> 13) | ((u32)t[3] << 13));
+ store32_le(s + 12, ((u32)t[3] >> 19) | ((u32)t[4] << 6));
+ store32_le(s + 16, ((u32)t[5] >> 0) | ((u32)t[6] << 25));
+ store32_le(s + 20, ((u32)t[6] >> 7) | ((u32)t[7] << 19));
+ store32_le(s + 24, ((u32)t[7] >> 13) | ((u32)t[8] << 12));
+ store32_le(s + 28, ((u32)t[8] >> 20) | ((u32)t[9] << 6));
+
+ WIPE_BUFFER(t);
+}
+
+// Precondition
+// -------------
+// |f0|, |f2|, |f4|, |f6|, |f8| < 1.65 * 2^26
+// |f1|, |f3|, |f5|, |f7|, |f9| < 1.65 * 2^25
+//
+// |g0|, |g2|, |g4|, |g6|, |g8| < 1.65 * 2^26
+// |g1|, |g3|, |g5|, |g7|, |g9| < 1.65 * 2^25
+static void fe_mul_small(fe h, const fe f, i32 g)
+{
+ i64 t0 = f[0] * (i64) g; i64 t1 = f[1] * (i64) g;
+ i64 t2 = f[2] * (i64) g; i64 t3 = f[3] * (i64) g;
+ i64 t4 = f[4] * (i64) g; i64 t5 = f[5] * (i64) g;
+ i64 t6 = f[6] * (i64) g; i64 t7 = f[7] * (i64) g;
+ i64 t8 = f[8] * (i64) g; i64 t9 = f[9] * (i64) g;
+ // |t0|, |t2|, |t4|, |t6|, |t8| < 1.65 * 2^26 * 2^31 < 2^58
+ // |t1|, |t3|, |t5|, |t7|, |t9| < 1.65 * 2^25 * 2^31 < 2^57
+
+ FE_CARRY; // Carry precondition OK
+}
+
+// Precondition
+// -------------
+// |f0|, |f2|, |f4|, |f6|, |f8| < 1.65 * 2^26
+// |f1|, |f3|, |f5|, |f7|, |f9| < 1.65 * 2^25
+//
+// |g0|, |g2|, |g4|, |g6|, |g8| < 1.65 * 2^26
+// |g1|, |g3|, |g5|, |g7|, |g9| < 1.65 * 2^25
+static void fe_mul(fe h, const fe f, const fe g)
+{
+ // Everything is unrolled and put in temporary variables.
+ // We could roll the loop, but that would make curve25519 twice as slow.
+ i32 f0 = f[0]; i32 f1 = f[1]; i32 f2 = f[2]; i32 f3 = f[3]; i32 f4 = f[4];
+ i32 f5 = f[5]; i32 f6 = f[6]; i32 f7 = f[7]; i32 f8 = f[8]; i32 f9 = f[9];
+ i32 g0 = g[0]; i32 g1 = g[1]; i32 g2 = g[2]; i32 g3 = g[3]; i32 g4 = g[4];
+ i32 g5 = g[5]; i32 g6 = g[6]; i32 g7 = g[7]; i32 g8 = g[8]; i32 g9 = g[9];
+ i32 F1 = f1*2; i32 F3 = f3*2; i32 F5 = f5*2; i32 F7 = f7*2; i32 F9 = f9*2;
+ i32 G1 = g1*19; i32 G2 = g2*19; i32 G3 = g3*19;
+ i32 G4 = g4*19; i32 G5 = g5*19; i32 G6 = g6*19;
+ i32 G7 = g7*19; i32 G8 = g8*19; i32 G9 = g9*19;
+ // |F1|, |F3|, |F5|, |F7|, |F9| < 1.65 * 2^26
+ // |G0|, |G2|, |G4|, |G6|, |G8| < 2^31
+ // |G1|, |G3|, |G5|, |G7|, |G9| < 2^30
+
+ i64 t0 = f0*(i64)g0 + F1*(i64)G9 + f2*(i64)G8 + F3*(i64)G7 + f4*(i64)G6
+ + F5*(i64)G5 + f6*(i64)G4 + F7*(i64)G3 + f8*(i64)G2 + F9*(i64)G1;
+ i64 t1 = f0*(i64)g1 + f1*(i64)g0 + f2*(i64)G9 + f3*(i64)G8 + f4*(i64)G7
+ + f5*(i64)G6 + f6*(i64)G5 + f7*(i64)G4 + f8*(i64)G3 + f9*(i64)G2;
+ i64 t2 = f0*(i64)g2 + F1*(i64)g1 + f2*(i64)g0 + F3*(i64)G9 + f4*(i64)G8
+ + F5*(i64)G7 + f6*(i64)G6 + F7*(i64)G5 + f8*(i64)G4 + F9*(i64)G3;
+ i64 t3 = f0*(i64)g3 + f1*(i64)g2 + f2*(i64)g1 + f3*(i64)g0 + f4*(i64)G9
+ + f5*(i64)G8 + f6*(i64)G7 + f7*(i64)G6 + f8*(i64)G5 + f9*(i64)G4;
+ i64 t4 = f0*(i64)g4 + F1*(i64)g3 + f2*(i64)g2 + F3*(i64)g1 + f4*(i64)g0
+ + F5*(i64)G9 + f6*(i64)G8 + F7*(i64)G7 + f8*(i64)G6 + F9*(i64)G5;
+ i64 t5 = f0*(i64)g5 + f1*(i64)g4 + f2*(i64)g3 + f3*(i64)g2 + f4*(i64)g1
+ + f5*(i64)g0 + f6*(i64)G9 + f7*(i64)G8 + f8*(i64)G7 + f9*(i64)G6;
+ i64 t6 = f0*(i64)g6 + F1*(i64)g5 + f2*(i64)g4 + F3*(i64)g3 + f4*(i64)g2
+ + F5*(i64)g1 + f6*(i64)g0 + F7*(i64)G9 + f8*(i64)G8 + F9*(i64)G7;
+ i64 t7 = f0*(i64)g7 + f1*(i64)g6 + f2*(i64)g5 + f3*(i64)g4 + f4*(i64)g3
+ + f5*(i64)g2 + f6*(i64)g1 + f7*(i64)g0 + f8*(i64)G9 + f9*(i64)G8;
+ i64 t8 = f0*(i64)g8 + F1*(i64)g7 + f2*(i64)g6 + F3*(i64)g5 + f4*(i64)g4
+ + F5*(i64)g3 + f6*(i64)g2 + F7*(i64)g1 + f8*(i64)g0 + F9*(i64)G9;
+ i64 t9 = f0*(i64)g9 + f1*(i64)g8 + f2*(i64)g7 + f3*(i64)g6 + f4*(i64)g5
+ + f5*(i64)g4 + f6*(i64)g3 + f7*(i64)g2 + f8*(i64)g1 + f9*(i64)g0;
+ // t0 < 0.67 * 2^61
+ // t1 < 0.41 * 2^61
+ // t2 < 0.52 * 2^61
+ // t3 < 0.32 * 2^61
+ // t4 < 0.38 * 2^61
+ // t5 < 0.22 * 2^61
+ // t6 < 0.23 * 2^61
+ // t7 < 0.13 * 2^61
+ // t8 < 0.09 * 2^61
+ // t9 < 0.03 * 2^61
+
+ FE_CARRY; // Everything below 2^62, Carry precondition OK
+}
+
+// Precondition
+// -------------
+// |f0|, |f2|, |f4|, |f6|, |f8| < 1.65 * 2^26
+// |f1|, |f3|, |f5|, |f7|, |f9| < 1.65 * 2^25
+//
+// Note: we could use fe_mul() for this, but this is significantly faster
+static void fe_sq(fe h, const fe f)
+{
+ i32 f0 = f[0]; i32 f1 = f[1]; i32 f2 = f[2]; i32 f3 = f[3]; i32 f4 = f[4];
+ i32 f5 = f[5]; i32 f6 = f[6]; i32 f7 = f[7]; i32 f8 = f[8]; i32 f9 = f[9];
+ i32 f0_2 = f0*2; i32 f1_2 = f1*2; i32 f2_2 = f2*2; i32 f3_2 = f3*2;
+ i32 f4_2 = f4*2; i32 f5_2 = f5*2; i32 f6_2 = f6*2; i32 f7_2 = f7*2;
+ i32 f5_38 = f5*38; i32 f6_19 = f6*19; i32 f7_38 = f7*38;
+ i32 f8_19 = f8*19; i32 f9_38 = f9*38;
+ // |f0_2| , |f2_2| , |f4_2| , |f6_2| , |f8_2| < 1.65 * 2^27
+ // |f1_2| , |f3_2| , |f5_2| , |f7_2| , |f9_2| < 1.65 * 2^26
+ // |f5_38|, |f6_19|, |f7_38|, |f8_19|, |f9_38| < 2^31
+
+ i64 t0 = f0 *(i64)f0 + f1_2*(i64)f9_38 + f2_2*(i64)f8_19
+ + f3_2*(i64)f7_38 + f4_2*(i64)f6_19 + f5 *(i64)f5_38;
+ i64 t1 = f0_2*(i64)f1 + f2 *(i64)f9_38 + f3_2*(i64)f8_19
+ + f4 *(i64)f7_38 + f5_2*(i64)f6_19;
+ i64 t2 = f0_2*(i64)f2 + f1_2*(i64)f1 + f3_2*(i64)f9_38
+ + f4_2*(i64)f8_19 + f5_2*(i64)f7_38 + f6 *(i64)f6_19;
+ i64 t3 = f0_2*(i64)f3 + f1_2*(i64)f2 + f4 *(i64)f9_38
+ + f5_2*(i64)f8_19 + f6 *(i64)f7_38;
+ i64 t4 = f0_2*(i64)f4 + f1_2*(i64)f3_2 + f2 *(i64)f2
+ + f5_2*(i64)f9_38 + f6_2*(i64)f8_19 + f7 *(i64)f7_38;
+ i64 t5 = f0_2*(i64)f5 + f1_2*(i64)f4 + f2_2*(i64)f3
+ + f6 *(i64)f9_38 + f7_2*(i64)f8_19;
+ i64 t6 = f0_2*(i64)f6 + f1_2*(i64)f5_2 + f2_2*(i64)f4
+ + f3_2*(i64)f3 + f7_2*(i64)f9_38 + f8 *(i64)f8_19;
+ i64 t7 = f0_2*(i64)f7 + f1_2*(i64)f6 + f2_2*(i64)f5
+ + f3_2*(i64)f4 + f8 *(i64)f9_38;
+ i64 t8 = f0_2*(i64)f8 + f1_2*(i64)f7_2 + f2_2*(i64)f6
+ + f3_2*(i64)f5_2 + f4 *(i64)f4 + f9 *(i64)f9_38;
+ i64 t9 = f0_2*(i64)f9 + f1_2*(i64)f8 + f2_2*(i64)f7
+ + f3_2*(i64)f6 + f4 *(i64)f5_2;
+ // t0 < 0.67 * 2^61
+ // t1 < 0.41 * 2^61
+ // t2 < 0.52 * 2^61
+ // t3 < 0.32 * 2^61
+ // t4 < 0.38 * 2^61
+ // t5 < 0.22 * 2^61
+ // t6 < 0.23 * 2^61
+ // t7 < 0.13 * 2^61
+ // t8 < 0.09 * 2^61
+ // t9 < 0.03 * 2^61
+
+ FE_CARRY;
+}
+
+// Parity check. Returns 0 if even, 1 if odd
+static int fe_isodd(const fe f)
+{
+ u8 s[32];
+ fe_tobytes(s, f);
+ u8 isodd = s[0] & 1;
+ WIPE_BUFFER(s);
+ return isodd;
+}
+
+// Returns 1 if equal, 0 if not equal
+static int fe_isequal(const fe f, const fe g)
+{
+ u8 fs[32];
+ u8 gs[32];
+ fe_tobytes(fs, f);
+ fe_tobytes(gs, g);
+ int isdifferent = crypto_verify32(fs, gs);
+ WIPE_BUFFER(fs);
+ WIPE_BUFFER(gs);
+ return 1 + isdifferent;
+}
+
+// Inverse square root.
+// Returns true if x is a square, false otherwise.
+// After the call:
+// isr = sqrt(1/x) if x is a non-zero square.
+// isr = sqrt(sqrt(-1)/x) if x is not a square.
+// isr = 0 if x is zero.
+// We do not guarantee the sign of the square root.
+//
+// Notes:
+// Let quartic = x^((p-1)/4)
+//
+// x^((p-1)/2) = chi(x)
+// quartic^2 = chi(x)
+// quartic = sqrt(chi(x))
+// quartic = 1 or -1 or sqrt(-1) or -sqrt(-1)
+//
+// Note that x is a square if quartic is 1 or -1
+// There are 4 cases to consider:
+//
+// if quartic = 1 (x is a square)
+// then x^((p-1)/4) = 1
+// x^((p-5)/4) * x = 1
+// x^((p-5)/4) = 1/x
+// x^((p-5)/8) = sqrt(1/x) or -sqrt(1/x)
+//
+// if quartic = -1 (x is a square)
+// then x^((p-1)/4) = -1
+// x^((p-5)/4) * x = -1
+// x^((p-5)/4) = -1/x
+// x^((p-5)/8) = sqrt(-1) / sqrt(x)
+// x^((p-5)/8) * sqrt(-1) = sqrt(-1)^2 / sqrt(x)
+// x^((p-5)/8) * sqrt(-1) = -1/sqrt(x)
+// x^((p-5)/8) * sqrt(-1) = -sqrt(1/x) or sqrt(1/x)
+//
+// if quartic = sqrt(-1) (x is not a square)
+// then x^((p-1)/4) = sqrt(-1)
+// x^((p-5)/4) * x = sqrt(-1)
+// x^((p-5)/4) = sqrt(-1)/x
+// x^((p-5)/8) = sqrt(sqrt(-1)/x) or -sqrt(sqrt(-1)/x)
+//
+// Note that the product of two non-squares is always a square:
+// For any non-squares a and b, chi(a) = -1 and chi(b) = -1.
+// Since chi(x) = x^((p-1)/2), chi(a)*chi(b) = chi(a*b) = 1.
+// Therefore a*b is a square.
+//
+// Since sqrt(-1) and x are both non-squares, their product is a
+// square, and we can compute their square root.
+//
+// if quartic = -sqrt(-1) (x is not a square)
+// then x^((p-1)/4) = -sqrt(-1)
+// x^((p-5)/4) * x = -sqrt(-1)
+// x^((p-5)/4) = -sqrt(-1)/x
+// x^((p-5)/8) = sqrt(-sqrt(-1)/x)
+// x^((p-5)/8) = sqrt( sqrt(-1)/x) * sqrt(-1)
+// x^((p-5)/8) * sqrt(-1) = sqrt( sqrt(-1)/x) * sqrt(-1)^2
+// x^((p-5)/8) * sqrt(-1) = sqrt( sqrt(-1)/x) * -1
+// x^((p-5)/8) * sqrt(-1) = -sqrt(sqrt(-1)/x) or sqrt(sqrt(-1)/x)
+static int invsqrt(fe isr, const fe x)
+{
+ fe t0, t1, t2;
+
+ // t0 = x^((p-5)/8)
+ // Can be achieved with a simple double & add ladder,
+ // but it would be slower.
+ fe_sq(t0, x);
+ fe_sq(t1,t0); fe_sq(t1, t1); fe_mul(t1, x, t1);
+ fe_mul(t0, t0, t1);
+ fe_sq(t0, t0); fe_mul(t0, t1, t0);
+ fe_sq(t1, t0); FOR (i, 1, 5) { fe_sq(t1, t1); } fe_mul(t0, t1, t0);
+ fe_sq(t1, t0); FOR (i, 1, 10) { fe_sq(t1, t1); } fe_mul(t1, t1, t0);
+ fe_sq(t2, t1); FOR (i, 1, 20) { fe_sq(t2, t2); } fe_mul(t1, t2, t1);
+ fe_sq(t1, t1); FOR (i, 1, 10) { fe_sq(t1, t1); } fe_mul(t0, t1, t0);
+ fe_sq(t1, t0); FOR (i, 1, 50) { fe_sq(t1, t1); } fe_mul(t1, t1, t0);
+ fe_sq(t2, t1); FOR (i, 1, 100) { fe_sq(t2, t2); } fe_mul(t1, t2, t1);
+ fe_sq(t1, t1); FOR (i, 1, 50) { fe_sq(t1, t1); } fe_mul(t0, t1, t0);
+ fe_sq(t0, t0); FOR (i, 1, 2) { fe_sq(t0, t0); } fe_mul(t0, t0, x);
+
+ // quartic = x^((p-1)/4)
+ i32 *quartic = t1;
+ fe_sq (quartic, t0);
+ fe_mul(quartic, quartic, x);
+
+ i32 *check = t2;
+ fe_0 (check); int z0 = fe_isequal(x , check);
+ fe_1 (check); int p1 = fe_isequal(quartic, check);
+ fe_neg(check, check ); int m1 = fe_isequal(quartic, check);
+ fe_neg(check, sqrtm1); int ms = fe_isequal(quartic, check);
+
+ // if quartic == -1 or sqrt(-1)
+ // then isr = x^((p-1)/4) * sqrt(-1)
+ // else isr = x^((p-1)/4)
+ fe_mul(isr, t0, sqrtm1);
+ fe_ccopy(isr, t0, 1 - (m1 | ms));
+
+ WIPE_BUFFER(t0);
+ WIPE_BUFFER(t1);
+ WIPE_BUFFER(t2);
+ return p1 | m1 | z0;
+}
+
+// Inverse in terms of inverse square root.
+// Requires two additional squarings to get rid of the sign.
+//
+// 1/x = x * (+invsqrt(x^2))^2
+// = x * (-invsqrt(x^2))^2
+//
+// A fully optimised exponentiation by p-1 would save 6 field
+// multiplications, but it would require more code.
+static void fe_invert(fe out, const fe x)
+{
+ fe tmp;
+ fe_sq(tmp, x);
+ invsqrt(tmp, tmp);
+ fe_sq(tmp, tmp);
+ fe_mul(out, tmp, x);
+ WIPE_BUFFER(tmp);
+}
+
+// trim a scalar for scalar multiplication
+void crypto_eddsa_trim_scalar(u8 out[32], const u8 in[32])
+{
+ COPY(out, in, 32);
+ out[ 0] &= 248;
+ out[31] &= 127;
+ out[31] |= 64;
+}
+
+// get bit from scalar at position i
+static int scalar_bit(const u8 s[32], int i)
+{
+ if (i < 0) { return 0; } // handle -1 for sliding windows
+ return (s[i>>3] >> (i&7)) & 1;
+}
+
+///////////////
+/// X-25519 /// Taken from SUPERCOP's ref10 implementation.
+///////////////
+static void scalarmult(u8 q[32], const u8 scalar[32], const u8 p[32],
+ int nb_bits)
+{
+ // computes the scalar product
+ fe x1;
+ fe_frombytes(x1, p);
+
+ // computes the actual scalar product (the result is in x2 and z2)
+ fe x2, z2, x3, z3, t0, t1;
+ // Montgomery ladder
+ // In projective coordinates, to avoid divisions: x = X / Z
+ // We don't care about the y coordinate, it's only 1 bit of information
+ fe_1(x2); fe_0(z2); // "zero" point
+ fe_copy(x3, x1); fe_1(z3); // "one" point
+ int swap = 0;
+ for (int pos = nb_bits-1; pos >= 0; --pos) {
+ // constant time conditional swap before ladder step
+ int b = scalar_bit(scalar, pos);
+ swap ^= b; // xor trick avoids swapping at the end of the loop
+ fe_cswap(x2, x3, swap);
+ fe_cswap(z2, z3, swap);
+ swap = b; // anticipates one last swap after the loop
+
+ // Montgomery ladder step: replaces (P2, P3) by (P2*2, P2+P3)
+ // with differential addition
+ fe_sub(t0, x3, z3);
+ fe_sub(t1, x2, z2);
+ fe_add(x2, x2, z2);
+ fe_add(z2, x3, z3);
+ fe_mul(z3, t0, x2);
+ fe_mul(z2, z2, t1);
+ fe_sq (t0, t1 );
+ fe_sq (t1, x2 );
+ fe_add(x3, z3, z2);
+ fe_sub(z2, z3, z2);
+ fe_mul(x2, t1, t0);
+ fe_sub(t1, t1, t0);
+ fe_sq (z2, z2 );
+ fe_mul_small(z3, t1, 121666);
+ fe_sq (x3, x3 );
+ fe_add(t0, t0, z3);
+ fe_mul(z3, x1, z2);
+ fe_mul(z2, t1, t0);
+ }
+ // last swap is necessary to compensate for the xor trick
+ // Note: after this swap, P3 == P2 + P1.
+ fe_cswap(x2, x3, swap);
+ fe_cswap(z2, z3, swap);
+
+ // normalises the coordinates: x == X / Z
+ fe_invert(z2, z2);
+ fe_mul(x2, x2, z2);
+ fe_tobytes(q, x2);
+
+ WIPE_BUFFER(x1);
+ WIPE_BUFFER(x2); WIPE_BUFFER(z2); WIPE_BUFFER(t0);
+ WIPE_BUFFER(x3); WIPE_BUFFER(z3); WIPE_BUFFER(t1);
+}
+
+void crypto_x25519(u8 raw_shared_secret[32],
+ const u8 your_secret_key [32],
+ const u8 their_public_key [32])
+{
+ // restrict the possible scalar values
+ u8 e[32];
+ crypto_eddsa_trim_scalar(e, your_secret_key);
+ scalarmult(raw_shared_secret, e, their_public_key, 255);
+ WIPE_BUFFER(e);
+}
+
+void crypto_x25519_public_key(u8 public_key[32],
+ const u8 secret_key[32])
+{
+ static const u8 base_point[32] = {9};
+ crypto_x25519(public_key, secret_key, base_point);
+}
+
+///////////////////////////
+/// Arithmetic modulo L ///
+///////////////////////////
+static const u32 L[8] = {
+ 0x5cf5d3ed, 0x5812631a, 0xa2f79cd6, 0x14def9de,
+ 0x00000000, 0x00000000, 0x00000000, 0x10000000,
+};
+
+// p = a*b + p
+static void multiply(u32 p[16], const u32 a[8], const u32 b[8])
+{
+ FOR (i, 0, 8) {
+ u64 carry = 0;
+ FOR (j, 0, 8) {
+ carry += p[i+j] + (u64)a[i] * b[j];
+ p[i+j] = (u32)carry;
+ carry >>= 32;
+ }
+ p[i+8] = (u32)carry;
+ }
+}
+
+static int is_above_l(const u32 x[8])
+{
+ // We work with L directly, in a 2's complement encoding
+ // (-L == ~L + 1)
+ u64 carry = 1;
+ FOR (i, 0, 8) {
+ carry += (u64)x[i] + (~L[i] & 0xffffffff);
+ carry >>= 32;
+ }
+ return (int)carry; // carry is either 0 or 1
+}
+
+// Final reduction modulo L, by conditionally removing L.
+// if x < l , then r = x
+// if l <= x 2*l, then r = x-l
+// otherwise the result will be wrong
+static void remove_l(u32 r[8], const u32 x[8])
+{
+ u64 carry = (u64)is_above_l(x);
+ u32 mask = ~(u32)carry + 1; // carry == 0 or 1
+ FOR (i, 0, 8) {
+ carry += (u64)x[i] + (~L[i] & mask);
+ r[i] = (u32)carry;
+ carry >>= 32;
+ }
+}
+
+// Full reduction modulo L (Barrett reduction)
+static void mod_l(u8 reduced[32], const u32 x[16])
+{
+ static const u32 r[9] = {
+ 0x0a2c131b,0xed9ce5a3,0x086329a7,0x2106215d,
+ 0xffffffeb,0xffffffff,0xffffffff,0xffffffff,0xf,
+ };
+ // xr = x * r
+ u32 xr[25] = {0};
+ FOR (i, 0, 9) {
+ u64 carry = 0;
+ FOR (j, 0, 16) {
+ carry += xr[i+j] + (u64)r[i] * x[j];
+ xr[i+j] = (u32)carry;
+ carry >>= 32;
+ }
+ xr[i+16] = (u32)carry;
+ }
+ // xr = floor(xr / 2^512) * L
+ // Since the result is guaranteed to be below 2*L,
+ // it is enough to only compute the first 256 bits.
+ // The division is performed by saying xr[i+16]. (16 * 32 = 512)
+ ZERO(xr, 8);
+ FOR (i, 0, 8) {
+ u64 carry = 0;
+ FOR (j, 0, 8-i) {
+ carry += xr[i+j] + (u64)xr[i+16] * L[j];
+ xr[i+j] = (u32)carry;
+ carry >>= 32;
+ }
+ }
+ // xr = x - xr
+ u64 carry = 1;
+ FOR (i, 0, 8) {
+ carry += (u64)x[i] + (~xr[i] & 0xffffffff);
+ xr[i] = (u32)carry;
+ carry >>= 32;
+ }
+ // Final reduction modulo L (conditional subtraction)
+ remove_l(xr, xr);
+ store32_le_buf(reduced, xr, 8);
+
+ WIPE_BUFFER(xr);
+}
+
+void crypto_eddsa_reduce(u8 reduced[32], const u8 expanded[64])
+{
+ u32 x[16];
+ load32_le_buf(x, expanded, 16);
+ mod_l(reduced, x);
+ WIPE_BUFFER(x);
+}
+
+// r = (a * b) + c
+void crypto_eddsa_mul_add(u8 r[32],
+ const u8 a[32], const u8 b[32], const u8 c[32])
+{
+ u32 A[8]; load32_le_buf(A, a, 8);
+ u32 B[8]; load32_le_buf(B, b, 8);
+ u32 p[16]; load32_le_buf(p, c, 8); ZERO(p + 8, 8);
+ multiply(p, A, B);
+ mod_l(r, p);
+ WIPE_BUFFER(p);
+ WIPE_BUFFER(A);
+ WIPE_BUFFER(B);
+}
+
+///////////////
+/// Ed25519 ///
+///////////////
+
+// Point (group element, ge) in a twisted Edwards curve,
+// in extended projective coordinates.
+// ge : x = X/Z, y = Y/Z, T = XY/Z
+// ge_cached : Yp = X+Y, Ym = X-Y, T2 = T*D2
+// ge_precomp: Z = 1
+typedef struct { fe X; fe Y; fe Z; fe T; } ge;
+typedef struct { fe Yp; fe Ym; fe Z; fe T2; } ge_cached;
+typedef struct { fe Yp; fe Ym; fe T2; } ge_precomp;
+
+static void ge_zero(ge *p)
+{
+ fe_0(p->X);
+ fe_1(p->Y);
+ fe_1(p->Z);
+ fe_0(p->T);
+}
+
+static void ge_tobytes(u8 s[32], const ge *h)
+{
+ fe recip, x, y;
+ fe_invert(recip, h->Z);
+ fe_mul(x, h->X, recip);
+ fe_mul(y, h->Y, recip);
+ fe_tobytes(s, y);
+ s[31] ^= (u8)fe_isodd(x) << 7;
+
+ WIPE_BUFFER(recip);
+ WIPE_BUFFER(x);
+ WIPE_BUFFER(y);
+}
+
+// h = -s, where s is a point encoded in 32 bytes
+//
+// Variable time! Inputs must not be secret!
+// => Use only to *check* signatures.
+//
+// From the specifications:
+// The encoding of s contains y and the sign of x
+// x = sqrt((y^2 - 1) / (d*y^2 + 1))
+// In extended coordinates:
+// X = x, Y = y, Z = 1, T = x*y
+//
+// Note that num * den is a square iff num / den is a square
+// If num * den is not a square, the point was not on the curve.
+// From the above:
+// Let num = y^2 - 1
+// Let den = d*y^2 + 1
+// x = sqrt((y^2 - 1) / (d*y^2 + 1))
+// x = sqrt(num / den)
+// x = sqrt(num^2 / (num * den))
+// x = num * sqrt(1 / (num * den))
+//
+// Therefore, we can just compute:
+// num = y^2 - 1
+// den = d*y^2 + 1
+// isr = invsqrt(num * den) // abort if not square
+// x = num * isr
+// Finally, negate x if its sign is not as specified.
+static int ge_frombytes_neg_vartime(ge *h, const u8 s[32])
+{
+ fe_frombytes(h->Y, s);
+ fe_1(h->Z);
+ fe_sq (h->T, h->Y); // t = y^2
+ fe_mul(h->X, h->T, d ); // x = d*y^2
+ fe_sub(h->T, h->T, h->Z); // t = y^2 - 1
+ fe_add(h->X, h->X, h->Z); // x = d*y^2 + 1
+ fe_mul(h->X, h->T, h->X); // x = (y^2 - 1) * (d*y^2 + 1)
+ int is_square = invsqrt(h->X, h->X);
+ if (!is_square) {
+ return -1; // Not on the curve, abort
+ }
+ fe_mul(h->X, h->T, h->X); // x = sqrt((y^2 - 1) / (d*y^2 + 1))
+ if (fe_isodd(h->X) == (s[31] >> 7)) {
+ fe_neg(h->X, h->X);
+ }
+ fe_mul(h->T, h->X, h->Y);
+ return 0;
+}
+
+static void ge_cache(ge_cached *c, const ge *p)
+{
+ fe_add (c->Yp, p->Y, p->X);
+ fe_sub (c->Ym, p->Y, p->X);
+ fe_copy(c->Z , p->Z );
+ fe_mul (c->T2, p->T, D2 );
+}
+
+// Internal buffers are not wiped! Inputs must not be secret!
+// => Use only to *check* signatures.
+static void ge_add(ge *s, const ge *p, const ge_cached *q)
+{
+ fe a, b;
+ fe_add(a , p->Y, p->X );
+ fe_sub(b , p->Y, p->X );
+ fe_mul(a , a , q->Yp);
+ fe_mul(b , b , q->Ym);
+ fe_add(s->Y, a , b );
+ fe_sub(s->X, a , b );
+
+ fe_add(s->Z, p->Z, p->Z );
+ fe_mul(s->Z, s->Z, q->Z );
+ fe_mul(s->T, p->T, q->T2);
+ fe_add(a , s->Z, s->T );
+ fe_sub(b , s->Z, s->T );
+
+ fe_mul(s->T, s->X, s->Y);
+ fe_mul(s->X, s->X, b );
+ fe_mul(s->Y, s->Y, a );
+ fe_mul(s->Z, a , b );
+}
+
+// Internal buffers are not wiped! Inputs must not be secret!
+// => Use only to *check* signatures.
+static void ge_sub(ge *s, const ge *p, const ge_cached *q)
+{
+ ge_cached neg;
+ fe_copy(neg.Ym, q->Yp);
+ fe_copy(neg.Yp, q->Ym);
+ fe_copy(neg.Z , q->Z );
+ fe_neg (neg.T2, q->T2);
+ ge_add(s, p, &neg);
+}
+
+static void ge_madd(ge *s, const ge *p, const ge_precomp *q, fe a, fe b)
+{
+ fe_add(a , p->Y, p->X );
+ fe_sub(b , p->Y, p->X );
+ fe_mul(a , a , q->Yp);
+ fe_mul(b , b , q->Ym);
+ fe_add(s->Y, a , b );
+ fe_sub(s->X, a , b );
+
+ fe_add(s->Z, p->Z, p->Z );
+ fe_mul(s->T, p->T, q->T2);
+ fe_add(a , s->Z, s->T );
+ fe_sub(b , s->Z, s->T );
+
+ fe_mul(s->T, s->X, s->Y);
+ fe_mul(s->X, s->X, b );
+ fe_mul(s->Y, s->Y, a );
+ fe_mul(s->Z, a , b );
+}
+
+// Internal buffers are not wiped! Inputs must not be secret!
+// => Use only to *check* signatures.
+static void ge_msub(ge *s, const ge *p, const ge_precomp *q, fe a, fe b)
+{
+ ge_precomp neg;
+ fe_copy(neg.Ym, q->Yp);
+ fe_copy(neg.Yp, q->Ym);
+ fe_neg (neg.T2, q->T2);
+ ge_madd(s, p, &neg, a, b);
+}
+
+static void ge_double(ge *s, const ge *p, ge *q)
+{
+ fe_sq (q->X, p->X);
+ fe_sq (q->Y, p->Y);
+ fe_sq (q->Z, p->Z); // qZ = pZ^2
+ fe_mul_small(q->Z, q->Z, 2); // qZ = pZ^2 * 2
+ fe_add(q->T, p->X, p->Y);
+ fe_sq (s->T, q->T);
+ fe_add(q->T, q->Y, q->X);
+ fe_sub(q->Y, q->Y, q->X);
+ fe_sub(q->X, s->T, q->T);
+ fe_sub(q->Z, q->Z, q->Y);
+
+ fe_mul(s->X, q->X , q->Z);
+ fe_mul(s->Y, q->T , q->Y);
+ fe_mul(s->Z, q->Y , q->Z);
+ fe_mul(s->T, q->X , q->T);
+}
+
+// 5-bit signed window in cached format (Niels coordinates, Z=1)
+static const ge_precomp b_window[8] = {
+ {{25967493,-14356035,29566456,3660896,-12694345,
+ 4014787,27544626,-11754271,-6079156,2047605,},
+ {-12545711,934262,-2722910,3049990,-727428,
+ 9406986,12720692,5043384,19500929,-15469378,},
+ {-8738181,4489570,9688441,-14785194,10184609,
+ -12363380,29287919,11864899,-24514362,-4438546,},},
+ {{15636291,-9688557,24204773,-7912398,616977,
+ -16685262,27787600,-14772189,28944400,-1550024,},
+ {16568933,4717097,-11556148,-1102322,15682896,
+ -11807043,16354577,-11775962,7689662,11199574,},
+ {30464156,-5976125,-11779434,-15670865,23220365,
+ 15915852,7512774,10017326,-17749093,-9920357,},},
+ {{10861363,11473154,27284546,1981175,-30064349,
+ 12577861,32867885,14515107,-15438304,10819380,},
+ {4708026,6336745,20377586,9066809,-11272109,
+ 6594696,-25653668,12483688,-12668491,5581306,},
+ {19563160,16186464,-29386857,4097519,10237984,
+ -4348115,28542350,13850243,-23678021,-15815942,},},
+ {{5153746,9909285,1723747,-2777874,30523605,
+ 5516873,19480852,5230134,-23952439,-15175766,},
+ {-30269007,-3463509,7665486,10083793,28475525,
+ 1649722,20654025,16520125,30598449,7715701,},
+ {28881845,14381568,9657904,3680757,-20181635,
+ 7843316,-31400660,1370708,29794553,-1409300,},},
+ {{-22518993,-6692182,14201702,-8745502,-23510406,
+ 8844726,18474211,-1361450,-13062696,13821877,},
+ {-6455177,-7839871,3374702,-4740862,-27098617,
+ -10571707,31655028,-7212327,18853322,-14220951,},
+ {4566830,-12963868,-28974889,-12240689,-7602672,
+ -2830569,-8514358,-10431137,2207753,-3209784,},},
+ {{-25154831,-4185821,29681144,7868801,-6854661,
+ -9423865,-12437364,-663000,-31111463,-16132436,},
+ {25576264,-2703214,7349804,-11814844,16472782,
+ 9300885,3844789,15725684,171356,6466918,},
+ {23103977,13316479,9739013,-16149481,817875,
+ -15038942,8965339,-14088058,-30714912,16193877,},},
+ {{-33521811,3180713,-2394130,14003687,-16903474,
+ -16270840,17238398,4729455,-18074513,9256800,},
+ {-25182317,-4174131,32336398,5036987,-21236817,
+ 11360617,22616405,9761698,-19827198,630305,},
+ {-13720693,2639453,-24237460,-7406481,9494427,
+ -5774029,-6554551,-15960994,-2449256,-14291300,},},
+ {{-3151181,-5046075,9282714,6866145,-31907062,
+ -863023,-18940575,15033784,25105118,-7894876,},
+ {-24326370,15950226,-31801215,-14592823,-11662737,
+ -5090925,1573892,-2625887,2198790,-15804619,},
+ {-3099351,10324967,-2241613,7453183,-5446979,
+ -2735503,-13812022,-16236442,-32461234,-12290683,},},
+};
+
+// Incremental sliding windows (left to right)
+// Based on Roberto Maria Avanzi[2005]
+typedef struct {
+ i16 next_index; // position of the next signed digit
+ i8 next_digit; // next signed digit (odd number below 2^window_width)
+ u8 next_check; // point at which we must check for a new window
+} slide_ctx;
+
+static void slide_init(slide_ctx *ctx, const u8 scalar[32])
+{
+ // scalar is guaranteed to be below L, either because we checked (s),
+ // or because we reduced it modulo L (h_ram). L is under 2^253, so
+ // so bits 253 to 255 are guaranteed to be zero. No need to test them.
+ //
+ // Note however that L is very close to 2^252, so bit 252 is almost
+ // always zero. If we were to start at bit 251, the tests wouldn't
+ // catch the off-by-one error (constructing one that does would be
+ // prohibitively expensive).
+ //
+ // We should still check bit 252, though.
+ int i = 252;
+ while (i > 0 && scalar_bit(scalar, i) == 0) {
+ i--;
+ }
+ ctx->next_check = (u8)(i + 1);
+ ctx->next_index = -1;
+ ctx->next_digit = -1;
+}
+
+static int slide_step(slide_ctx *ctx, int width, int i, const u8 scalar[32])
+{
+ if (i == ctx->next_check) {
+ if (scalar_bit(scalar, i) == scalar_bit(scalar, i - 1)) {
+ ctx->next_check--;
+ } else {
+ // compute digit of next window
+ int w = MIN(width, i + 1);
+ int v = -(scalar_bit(scalar, i) << (w-1));
+ FOR_T (int, j, 0, w-1) {
+ v += scalar_bit(scalar, i-(w-1)+j) << j;
+ }
+ v += scalar_bit(scalar, i-w);
+ int lsb = v & (~v + 1); // smallest bit of v
+ int s = // log2(lsb)
+ (((lsb & 0xAA) != 0) << 0) |
+ (((lsb & 0xCC) != 0) << 1) |
+ (((lsb & 0xF0) != 0) << 2);
+ ctx->next_index = (i16)(i-(w-1)+s);
+ ctx->next_digit = (i8) (v >> s );
+ ctx->next_check -= (u8) w;
+ }
+ }
+ return i == ctx->next_index ? ctx->next_digit: 0;
+}
+
+#define P_W_WIDTH 3 // Affects the size of the stack
+#define B_W_WIDTH 5 // Affects the size of the binary
+#define P_W_SIZE (1<<(P_W_WIDTH-2))
+
+int crypto_eddsa_check_equation(const u8 signature[64], const u8 public_key[32],
+ const u8 h[32])
+{
+ ge minus_A; // -public_key
+ ge minus_R; // -first_half_of_signature
+ const u8 *s = signature + 32;
+
+ // Check that A and R are on the curve
+ // Check that 0 <= S < L (prevents malleability)
+ // *Allow* non-cannonical encoding for A and R
+ {
+ u32 s32[8];
+ load32_le_buf(s32, s, 8);
+ if (ge_frombytes_neg_vartime(&minus_A, public_key) ||
+ ge_frombytes_neg_vartime(&minus_R, signature) ||
+ is_above_l(s32)) {
+ return -1;
+ }
+ }
+
+ // look-up table for minus_A
+ ge_cached lutA[P_W_SIZE];
+ {
+ ge minus_A2, tmp;
+ ge_double(&minus_A2, &minus_A, &tmp);
+ ge_cache(&lutA[0], &minus_A);
+ FOR (i, 1, P_W_SIZE) {
+ ge_add(&tmp, &minus_A2, &lutA[i-1]);
+ ge_cache(&lutA[i], &tmp);
+ }
+ }
+
+ // sum = [s]B - [h]A
+ // Merged double and add ladder, fused with sliding
+ slide_ctx h_slide; slide_init(&h_slide, h);
+ slide_ctx s_slide; slide_init(&s_slide, s);
+ int i = MAX(h_slide.next_check, s_slide.next_check);
+ ge *sum = &minus_A; // reuse minus_A for the sum
+ ge_zero(sum);
+ while (i >= 0) {
+ ge tmp;
+ ge_double(sum, sum, &tmp);
+ int h_digit = slide_step(&h_slide, P_W_WIDTH, i, h);
+ int s_digit = slide_step(&s_slide, B_W_WIDTH, i, s);
+ if (h_digit > 0) { ge_add(sum, sum, &lutA[ h_digit / 2]); }
+ if (h_digit < 0) { ge_sub(sum, sum, &lutA[-h_digit / 2]); }
+ fe t1, t2;
+ if (s_digit > 0) { ge_madd(sum, sum, b_window + s_digit/2, t1, t2); }
+ if (s_digit < 0) { ge_msub(sum, sum, b_window + -s_digit/2, t1, t2); }
+ i--;
+ }
+
+ // Compare [8](sum-R) and the zero point
+ // The multiplication by 8 eliminates any low-order component
+ // and ensures consistency with batched verification.
+ ge_cached cached;
+ u8 check[32];
+ static const u8 zero_point[32] = {1}; // Point of order 1
+ ge_cache(&cached, &minus_R);
+ ge_add(sum, sum, &cached);
+ ge_double(sum, sum, &minus_R); // reuse minus_R as temporary
+ ge_double(sum, sum, &minus_R); // reuse minus_R as temporary
+ ge_double(sum, sum, &minus_R); // reuse minus_R as temporary
+ ge_tobytes(check, sum);
+ return crypto_verify32(check, zero_point);
+}
+
+// 5-bit signed comb in cached format (Niels coordinates, Z=1)
+static const ge_precomp b_comb_low[8] = {
+ {{-6816601,-2324159,-22559413,124364,18015490,
+ 8373481,19993724,1979872,-18549925,9085059,},
+ {10306321,403248,14839893,9633706,8463310,
+ -8354981,-14305673,14668847,26301366,2818560,},
+ {-22701500,-3210264,-13831292,-2927732,-16326337,
+ -14016360,12940910,177905,12165515,-2397893,},},
+ {{-12282262,-7022066,9920413,-3064358,-32147467,
+ 2927790,22392436,-14852487,2719975,16402117,},
+ {-7236961,-4729776,2685954,-6525055,-24242706,
+ -15940211,-6238521,14082855,10047669,12228189,},
+ {-30495588,-12893761,-11161261,3539405,-11502464,
+ 16491580,-27286798,-15030530,-7272871,-15934455,},},
+ {{17650926,582297,-860412,-187745,-12072900,
+ -10683391,-20352381,15557840,-31072141,-5019061,},
+ {-6283632,-2259834,-4674247,-4598977,-4089240,
+ 12435688,-31278303,1060251,6256175,10480726,},
+ {-13871026,2026300,-21928428,-2741605,-2406664,
+ -8034988,7355518,15733500,-23379862,7489131,},},
+ {{6883359,695140,23196907,9644202,-33430614,
+ 11354760,-20134606,6388313,-8263585,-8491918,},
+ {-7716174,-13605463,-13646110,14757414,-19430591,
+ -14967316,10359532,-11059670,-21935259,12082603,},
+ {-11253345,-15943946,10046784,5414629,24840771,
+ 8086951,-6694742,9868723,15842692,-16224787,},},
+ {{9639399,11810955,-24007778,-9320054,3912937,
+ -9856959,996125,-8727907,-8919186,-14097242,},
+ {7248867,14468564,25228636,-8795035,14346339,
+ 8224790,6388427,-7181107,6468218,-8720783,},
+ {15513115,15439095,7342322,-10157390,18005294,
+ -7265713,2186239,4884640,10826567,7135781,},},
+ {{-14204238,5297536,-5862318,-6004934,28095835,
+ 4236101,-14203318,1958636,-16816875,3837147,},
+ {-5511166,-13176782,-29588215,12339465,15325758,
+ -15945770,-8813185,11075932,-19608050,-3776283,},
+ {11728032,9603156,-4637821,-5304487,-7827751,
+ 2724948,31236191,-16760175,-7268616,14799772,},},
+ {{-28842672,4840636,-12047946,-9101456,-1445464,
+ 381905,-30977094,-16523389,1290540,12798615,},
+ {27246947,-10320914,14792098,-14518944,5302070,
+ -8746152,-3403974,-4149637,-27061213,10749585,},
+ {25572375,-6270368,-15353037,16037944,1146292,
+ 32198,23487090,9585613,24714571,-1418265,},},
+ {{19844825,282124,-17583147,11004019,-32004269,
+ -2716035,6105106,-1711007,-21010044,14338445,},
+ {8027505,8191102,-18504907,-12335737,25173494,
+ -5923905,15446145,7483684,-30440441,10009108,},
+ {-14134701,-4174411,10246585,-14677495,33553567,
+ -14012935,23366126,15080531,-7969992,7663473,},},
+};
+
+static const ge_precomp b_comb_high[8] = {
+ {{33055887,-4431773,-521787,6654165,951411,
+ -6266464,-5158124,6995613,-5397442,-6985227,},
+ {4014062,6967095,-11977872,3960002,8001989,
+ 5130302,-2154812,-1899602,-31954493,-16173976,},
+ {16271757,-9212948,23792794,731486,-25808309,
+ -3546396,6964344,-4767590,10976593,10050757,},},
+ {{2533007,-4288439,-24467768,-12387405,-13450051,
+ 14542280,12876301,13893535,15067764,8594792,},
+ {20073501,-11623621,3165391,-13119866,13188608,
+ -11540496,-10751437,-13482671,29588810,2197295,},
+ {-1084082,11831693,6031797,14062724,14748428,
+ -8159962,-20721760,11742548,31368706,13161200,},},
+ {{2050412,-6457589,15321215,5273360,25484180,
+ 124590,-18187548,-7097255,-6691621,-14604792,},
+ {9938196,2162889,-6158074,-1711248,4278932,
+ -2598531,-22865792,-7168500,-24323168,11746309,},
+ {-22691768,-14268164,5965485,9383325,20443693,
+ 5854192,28250679,-1381811,-10837134,13717818,},},
+ {{-8495530,16382250,9548884,-4971523,-4491811,
+ -3902147,6182256,-12832479,26628081,10395408,},
+ {27329048,-15853735,7715764,8717446,-9215518,
+ -14633480,28982250,-5668414,4227628,242148,},
+ {-13279943,-7986904,-7100016,8764468,-27276630,
+ 3096719,29678419,-9141299,3906709,11265498,},},
+ {{11918285,15686328,-17757323,-11217300,-27548967,
+ 4853165,-27168827,6807359,6871949,-1075745,},
+ {-29002610,13984323,-27111812,-2713442,28107359,
+ -13266203,6155126,15104658,3538727,-7513788,},
+ {14103158,11233913,-33165269,9279850,31014152,
+ 4335090,-1827936,4590951,13960841,12787712,},},
+ {{1469134,-16738009,33411928,13942824,8092558,
+ -8778224,-11165065,1437842,22521552,-2792954,},
+ {31352705,-4807352,-25327300,3962447,12541566,
+ -9399651,-27425693,7964818,-23829869,5541287,},
+ {-25732021,-6864887,23848984,3039395,-9147354,
+ 6022816,-27421653,10590137,25309915,-1584678,},},
+ {{-22951376,5048948,31139401,-190316,-19542447,
+ -626310,-17486305,-16511925,-18851313,-12985140,},
+ {-9684890,14681754,30487568,7717771,-10829709,
+ 9630497,30290549,-10531496,-27798994,-13812825,},
+ {5827835,16097107,-24501327,12094619,7413972,
+ 11447087,28057551,-1793987,-14056981,4359312,},},
+ {{26323183,2342588,-21887793,-1623758,-6062284,
+ 2107090,-28724907,9036464,-19618351,-13055189,},
+ {-29697200,14829398,-4596333,14220089,-30022969,
+ 2955645,12094100,-13693652,-5941445,7047569,},
+ {-3201977,14413268,-12058324,-16417589,-9035655,
+ -7224648,9258160,1399236,30397584,-5684634,},},
+};
+
+static void lookup_add(ge *p, ge_precomp *tmp_c, fe tmp_a, fe tmp_b,
+ const ge_precomp comb[8], const u8 scalar[32], int i)
+{
+ u8 teeth = (u8)((scalar_bit(scalar, i) ) +
+ (scalar_bit(scalar, i + 32) << 1) +
+ (scalar_bit(scalar, i + 64) << 2) +
+ (scalar_bit(scalar, i + 96) << 3));
+ u8 high = teeth >> 3;
+ u8 index = (teeth ^ (high - 1)) & 7;
+ FOR (j, 0, 8) {
+ i32 select = 1 & (((j ^ index) - 1) >> 8);
+ fe_ccopy(tmp_c->Yp, comb[j].Yp, select);
+ fe_ccopy(tmp_c->Ym, comb[j].Ym, select);
+ fe_ccopy(tmp_c->T2, comb[j].T2, select);
+ }
+ fe_neg(tmp_a, tmp_c->T2);
+ fe_cswap(tmp_c->T2, tmp_a , high ^ 1);
+ fe_cswap(tmp_c->Yp, tmp_c->Ym, high ^ 1);
+ ge_madd(p, p, tmp_c, tmp_a, tmp_b);
+}
+
+// p = [scalar]B, where B is the base point
+static void ge_scalarmult_base(ge *p, const u8 scalar[32])
+{
+ // twin 4-bits signed combs, from Mike Hamburg's
+ // Fast and compact elliptic-curve cryptography (2012)
+ // 1 / 2 modulo L
+ static const u8 half_mod_L[32] = {
+ 247,233,122,46,141,49,9,44,107,206,123,81,239,124,111,10,
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,
+ };
+ // (2^256 - 1) / 2 modulo L
+ static const u8 half_ones[32] = {
+ 142,74,204,70,186,24,118,107,184,231,190,57,250,173,119,99,
+ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,7,
+ };
+
+ // All bits set form: 1 means 1, 0 means -1
+ u8 s_scalar[32];
+ crypto_eddsa_mul_add(s_scalar, scalar, half_mod_L, half_ones);
+
+ // Double and add ladder
+ fe tmp_a, tmp_b; // temporaries for addition
+ ge_precomp tmp_c; // temporary for comb lookup
+ ge tmp_d; // temporary for doubling
+ fe_1(tmp_c.Yp);
+ fe_1(tmp_c.Ym);
+ fe_0(tmp_c.T2);
+
+ // Save a double on the first iteration
+ ge_zero(p);
+ lookup_add(p, &tmp_c, tmp_a, tmp_b, b_comb_low , s_scalar, 31);
+ lookup_add(p, &tmp_c, tmp_a, tmp_b, b_comb_high, s_scalar, 31+128);
+ // Regular double & add for the rest
+ for (int i = 30; i >= 0; i--) {
+ ge_double(p, p, &tmp_d);
+ lookup_add(p, &tmp_c, tmp_a, tmp_b, b_comb_low , s_scalar, i);
+ lookup_add(p, &tmp_c, tmp_a, tmp_b, b_comb_high, s_scalar, i+128);
+ }
+ // Note: we could save one addition at the end if we assumed the
+ // scalar fit in 252 bits. Which it does in practice if it is
+ // selected at random. However, non-random, non-hashed scalars
+ // *can* overflow 252 bits in practice. Better account for that
+ // than leaving that kind of subtle corner case.
+
+ WIPE_BUFFER(tmp_a); WIPE_CTX(&tmp_d);
+ WIPE_BUFFER(tmp_b); WIPE_CTX(&tmp_c);
+ WIPE_BUFFER(s_scalar);
+}
+
+void crypto_eddsa_scalarbase(u8 point[32], const u8 scalar[32])
+{
+ ge P;
+ ge_scalarmult_base(&P, scalar);
+ ge_tobytes(point, &P);
+ WIPE_CTX(&P);
+}
+
+void crypto_eddsa_key_pair(u8 secret_key[64], u8 public_key[32], u8 seed[32])
+{
+ // To allow overlaps, observable writes happen in this order:
+ // 1. seed
+ // 2. secret_key
+ // 3. public_key
+ u8 a[64];
+ COPY(a, seed, 32);
+ crypto_wipe(seed, 32);
+ COPY(secret_key, a, 32);
+ crypto_blake2b(a, 64, a, 32);
+ crypto_eddsa_trim_scalar(a, a);
+ crypto_eddsa_scalarbase(secret_key + 32, a);
+ COPY(public_key, secret_key + 32, 32);
+ WIPE_BUFFER(a);
+}
+
+static void hash_reduce(u8 h[32],
+ const u8 *a, size_t a_size,
+ const u8 *b, size_t b_size,
+ const u8 *c, size_t c_size)
+{
+ u8 hash[64];
+ crypto_blake2b_ctx ctx;
+ crypto_blake2b_init (&ctx, 64);
+ crypto_blake2b_update(&ctx, a, a_size);
+ crypto_blake2b_update(&ctx, b, b_size);
+ crypto_blake2b_update(&ctx, c, c_size);
+ crypto_blake2b_final (&ctx, hash);
+ crypto_eddsa_reduce(h, hash);
+}
+
+// Digital signature of a message with from a secret key.
+//
+// The secret key comprises two parts:
+// - The seed that generates the key (secret_key[ 0..31])
+// - The public key (secret_key[32..63])
+//
+// The seed and the public key are bundled together to make sure users
+// don't use mismatched seeds and public keys, which would instantly
+// leak the secret scalar and allow forgeries (allowing this to happen
+// has resulted in critical vulnerabilities in the wild).
+//
+// The seed is hashed to derive the secret scalar and a secret prefix.
+// The sole purpose of the prefix is to generate a secret random nonce.
+// The properties of that nonce must be as follows:
+// - Unique: we need a different one for each message.
+// - Secret: third parties must not be able to predict it.
+// - Random: any detectable bias would break all security.
+//
+// There are two ways to achieve these properties. The obvious one is
+// to simply generate a random number. Here that would be a parameter
+// (Monocypher doesn't have an RNG). It works, but then users may reuse
+// the nonce by accident, which _also_ leaks the secret scalar and
+// allows forgeries. This has happened in the wild too.
+//
+// This is no good, so instead we generate that nonce deterministically
+// by reducing modulo L a hash of the secret prefix and the message.
+// The secret prefix makes the nonce unpredictable, the message makes it
+// unique, and the hash/reduce removes all bias.
+//
+// The cost of that safety is hashing the message twice. If that cost
+// is unacceptable, there are two alternatives:
+//
+// - Signing a hash of the message instead of the message itself. This
+// is fine as long as the hash is collision resistant. It is not
+// compatible with existing "pure" signatures, but at least it's safe.
+//
+// - Using a random nonce. Please exercise **EXTREME CAUTION** if you
+// ever do that. It is absolutely **critical** that the nonce is
+// really an unbiased random number between 0 and L-1, never reused,
+// and wiped immediately.
+//
+// To lower the likelihood of complete catastrophe if the RNG is
+// either flawed or misused, you can hash the RNG output together with
+// the secret prefix and the beginning of the message, and use the
+// reduction of that hash instead of the RNG output itself. It's not
+// foolproof (you'd need to hash the whole message) but it helps.
+//
+// Signing a message involves the following operations:
+//
+// scalar, prefix = HASH(secret_key)
+// r = HASH(prefix || message) % L
+// R = [r]B
+// h = HASH(R || public_key || message) % L
+// S = ((h * a) + r) % L
+// signature = R || S
+void crypto_eddsa_sign(u8 signature [64], const u8 secret_key[64],
+ const u8 *message, size_t message_size)
+{
+ u8 a[64]; // secret scalar and prefix
+ u8 r[32]; // secret deterministic "random" nonce
+ u8 h[32]; // publically verifiable hash of the message (not wiped)
+ u8 R[32]; // first half of the signature (allows overlapping inputs)
+
+ crypto_blake2b(a, 64, secret_key, 32);
+ crypto_eddsa_trim_scalar(a, a);
+ hash_reduce(r, a + 32, 32, message, message_size, 0, 0);
+ crypto_eddsa_scalarbase(R, r);
+ hash_reduce(h, R, 32, secret_key + 32, 32, message, message_size);
+ COPY(signature, R, 32);
+ crypto_eddsa_mul_add(signature + 32, h, a, r);
+
+ WIPE_BUFFER(a);
+ WIPE_BUFFER(r);
+}
+
+// To check the signature R, S of the message M with the public key A,
+// there are 3 steps:
+//
+// compute h = HASH(R || A || message) % L
+// check that A is on the curve.
+// check that R == [s]B - [h]A
+//
+// The last two steps are done in crypto_eddsa_check_equation()
+int crypto_eddsa_check(const u8 signature[64], const u8 public_key[32],
+ const u8 *message, size_t message_size)
+{
+ u8 h[32];
+ hash_reduce(h, signature, 32, public_key, 32, message, message_size);
+ return crypto_eddsa_check_equation(signature, public_key, h);
+}
+
+/////////////////////////
+/// EdDSA <--> X25519 ///
+/////////////////////////
+void crypto_eddsa_to_x25519(u8 x25519[32], const u8 eddsa[32])
+{
+ // (u, v) = ((1+y)/(1-y), sqrt(-486664)*u/x)
+ // Only converting y to u, the sign of x is ignored.
+ fe t1, t2;
+ fe_frombytes(t2, eddsa);
+ fe_add(t1, fe_one, t2);
+ fe_sub(t2, fe_one, t2);
+ fe_invert(t2, t2);
+ fe_mul(t1, t1, t2);
+ fe_tobytes(x25519, t1);
+ WIPE_BUFFER(t1);
+ WIPE_BUFFER(t2);
+}
+
+void crypto_x25519_to_eddsa(u8 eddsa[32], const u8 x25519[32])
+{
+ // (x, y) = (sqrt(-486664)*u/v, (u-1)/(u+1))
+ // Only converting u to y, x is assumed positive.
+ fe t1, t2;
+ fe_frombytes(t2, x25519);
+ fe_sub(t1, t2, fe_one);
+ fe_add(t2, t2, fe_one);
+ fe_invert(t2, t2);
+ fe_mul(t1, t1, t2);
+ fe_tobytes(eddsa, t1);
+ WIPE_BUFFER(t1);
+ WIPE_BUFFER(t2);
+}
+
+/////////////////////////////////////////////
+/// Dirty ephemeral public key generation ///
+/////////////////////////////////////////////
+
+// Those functions generates a public key, *without* clearing the
+// cofactor. Sending that key over the network leaks 3 bits of the
+// private key. Use only to generate ephemeral keys that will be hidden
+// with crypto_curve_to_hidden().
+//
+// The public key is otherwise compatible with crypto_x25519(), which
+// properly clears the cofactor.
+//
+// Note that the distribution of the resulting public keys is almost
+// uniform. Flipping the sign of the v coordinate (not provided by this
+// function), covers the entire key space almost perfectly, where
+// "almost" means a 2^-128 bias (undetectable). This uniformity is
+// needed to ensure the proper randomness of the resulting
+// representatives (once we apply crypto_curve_to_hidden()).
+//
+// Recall that Curve25519 has order C = 2^255 + e, with e < 2^128 (not
+// to be confused with the prime order of the main subgroup, L, which is
+// 8 times less than that).
+//
+// Generating all points would require us to multiply a point of order C
+// (the base point plus any point of order 8) by all scalars from 0 to
+// C-1. Clamping limits us to scalars between 2^254 and 2^255 - 1. But
+// by negating the resulting point at random, we also cover scalars from
+// -2^255 + 1 to -2^254 (which modulo C is congruent to e+1 to 2^254 + e).
+//
+// In practice:
+// - Scalars from 0 to e + 1 are never generated
+// - Scalars from 2^255 to 2^255 + e are never generated
+// - Scalars from 2^254 + 1 to 2^254 + e are generated twice
+//
+// Since e < 2^128, detecting this bias requires observing over 2^100
+// representatives from a given source (this will never happen), *and*
+// recovering enough of the private key to determine that they do, or do
+// not, belong to the biased set (this practically requires solving
+// discrete logarithm, which is conjecturally intractable).
+//
+// In practice, this means the bias is impossible to detect.
+
+// s + (x*L) % 8*L
+// Guaranteed to fit in 256 bits iff s fits in 255 bits.
+// L < 2^253
+// x%8 < 2^3
+// L * (x%8) < 2^255
+// s < 2^255
+// s + L * (x%8) < 2^256
+static void add_xl(u8 s[32], u8 x)
+{
+ u64 mod8 = x & 7;
+ u64 carry = 0;
+ FOR (i , 0, 8) {
+ carry = carry + load32_le(s + 4*i) + L[i] * mod8;
+ store32_le(s + 4*i, (u32)carry);
+ carry >>= 32;
+ }
+}
+
+// "Small" dirty ephemeral key.
+// Use if you need to shrink the size of the binary, and can afford to
+// slow down by a factor of two (compared to the fast version)
+//
+// This version works by decoupling the cofactor from the main factor.
+//
+// - The trimmed scalar determines the main factor
+// - The clamped bits of the scalar determine the cofactor.
+//
+// Cofactor and main factor are combined into a single scalar, which is
+// then multiplied by a point of order 8*L (unlike the base point, which
+// has prime order). That "dirty" base point is the addition of the
+// regular base point (9), and a point of order 8.
+void crypto_x25519_dirty_small(u8 public_key[32], const u8 secret_key[32])
+{
+ // Base point of order 8*L
+ // Raw scalar multiplication with it does not clear the cofactor,
+ // and the resulting public key will reveal 3 bits of the scalar.
+ //
+ // The low order component of this base point has been chosen
+ // to yield the same results as crypto_x25519_dirty_fast().
+ static const u8 dirty_base_point[32] = {
+ 0xd8, 0x86, 0x1a, 0xa2, 0x78, 0x7a, 0xd9, 0x26,
+ 0x8b, 0x74, 0x74, 0xb6, 0x82, 0xe3, 0xbe, 0xc3,
+ 0xce, 0x36, 0x9a, 0x1e, 0x5e, 0x31, 0x47, 0xa2,
+ 0x6d, 0x37, 0x7c, 0xfd, 0x20, 0xb5, 0xdf, 0x75,
+ };
+ // separate the main factor & the cofactor of the scalar
+ u8 scalar[32];
+ crypto_eddsa_trim_scalar(scalar, secret_key);
+
+ // Separate the main factor and the cofactor
+ //
+ // The scalar is trimmed, so its cofactor is cleared. The three
+ // least significant bits however still have a main factor. We must
+ // remove it for X25519 compatibility.
+ //
+ // cofactor = lsb * L (modulo 8*L)
+ // combined = scalar + cofactor (modulo 8*L)
+ add_xl(scalar, secret_key[0]);
+ scalarmult(public_key, scalar, dirty_base_point, 256);
+ WIPE_BUFFER(scalar);
+}
+
+// Select low order point
+// We're computing the [cofactor]lop scalar multiplication, where:
+//
+// cofactor = tweak & 7.
+// lop = (lop_x, lop_y)
+// lop_x = sqrt((sqrt(d + 1) + 1) / d)
+// lop_y = -lop_x * sqrtm1
+//
+// The low order point has order 8. There are 4 such points. We've
+// chosen the one whose both coordinates are positive (below p/2).
+// The 8 low order points are as follows:
+//
+// [0]lop = ( 0 , 1 )
+// [1]lop = ( lop_x , lop_y)
+// [2]lop = ( sqrt(-1), -0 )
+// [3]lop = ( lop_x , -lop_y)
+// [4]lop = (-0 , -1 )
+// [5]lop = (-lop_x , -lop_y)
+// [6]lop = (-sqrt(-1), 0 )
+// [7]lop = (-lop_x , lop_y)
+//
+// The x coordinate is either 0, sqrt(-1), lop_x, or their opposite.
+// The y coordinate is either 0, -1 , lop_y, or their opposite.
+// The pattern for both is the same, except for a rotation of 2 (modulo 8)
+//
+// This helper function captures the pattern, and we can use it thus:
+//
+// select_lop(x, lop_x, sqrtm1, cofactor);
+// select_lop(y, lop_y, fe_one, cofactor + 2);
+//
+// This is faster than an actual scalar multiplication,
+// and requires less code than naive constant time look up.
+static void select_lop(fe out, const fe x, const fe k, u8 cofactor)
+{
+ fe tmp;
+ fe_0(out);
+ fe_ccopy(out, k , (cofactor >> 1) & 1); // bit 1
+ fe_ccopy(out, x , (cofactor >> 0) & 1); // bit 0
+ fe_neg (tmp, out);
+ fe_ccopy(out, tmp, (cofactor >> 2) & 1); // bit 2
+ WIPE_BUFFER(tmp);
+}
+
+// "Fast" dirty ephemeral key
+// We use this one by default.
+//
+// This version works by performing a regular scalar multiplication,
+// then add a low order point. The scalar multiplication is done in
+// Edwards space for more speed (*2 compared to the "small" version).
+// The cost is a bigger binary for programs that don't also sign messages.
+void crypto_x25519_dirty_fast(u8 public_key[32], const u8 secret_key[32])
+{
+ // Compute clean scalar multiplication
+ u8 scalar[32];
+ ge pk;
+ crypto_eddsa_trim_scalar(scalar, secret_key);
+ ge_scalarmult_base(&pk, scalar);
+
+ // Compute low order point
+ fe t1, t2;
+ select_lop(t1, lop_x, sqrtm1, secret_key[0]);
+ select_lop(t2, lop_y, fe_one, secret_key[0] + 2);
+ ge_precomp low_order_point;
+ fe_add(low_order_point.Yp, t2, t1);
+ fe_sub(low_order_point.Ym, t2, t1);
+ fe_mul(low_order_point.T2, t2, t1);
+ fe_mul(low_order_point.T2, low_order_point.T2, D2);
+
+ // Add low order point to the public key
+ ge_madd(&pk, &pk, &low_order_point, t1, t2);
+
+ // Convert to Montgomery u coordinate (we ignore the sign)
+ fe_add(t1, pk.Z, pk.Y);
+ fe_sub(t2, pk.Z, pk.Y);
+ fe_invert(t2, t2);
+ fe_mul(t1, t1, t2);
+
+ fe_tobytes(public_key, t1);
+
+ WIPE_BUFFER(t1); WIPE_CTX(&pk);
+ WIPE_BUFFER(t2); WIPE_CTX(&low_order_point);
+ WIPE_BUFFER(scalar);
+}
+
+///////////////////
+/// Elligator 2 ///
+///////////////////
+static const fe A = {486662};
+
+// Elligator direct map
+//
+// Computes the point corresponding to a representative, encoded in 32
+// bytes (little Endian). Since positive representatives fits in 254
+// bits, The two most significant bits are ignored.
+//
+// From the paper:
+// w = -A / (fe(1) + non_square * r^2)
+// e = chi(w^3 + A*w^2 + w)
+// u = e*w - (fe(1)-e)*(A//2)
+// v = -e * sqrt(u^3 + A*u^2 + u)
+//
+// We ignore v because we don't need it for X25519 (the Montgomery
+// ladder only uses u).
+//
+// Note that e is either 0, 1 or -1
+// if e = 0 u = 0 and v = 0
+// if e = 1 u = w
+// if e = -1 u = -w - A = w * non_square * r^2
+//
+// Let r1 = non_square * r^2
+// Let r2 = 1 + r1
+// Note that r2 cannot be zero, -1/non_square is not a square.
+// We can (tediously) verify that:
+// w^3 + A*w^2 + w = (A^2*r1 - r2^2) * A / r2^3
+// Therefore:
+// chi(w^3 + A*w^2 + w) = chi((A^2*r1 - r2^2) * (A / r2^3))
+// chi(w^3 + A*w^2 + w) = chi((A^2*r1 - r2^2) * (A / r2^3)) * 1
+// chi(w^3 + A*w^2 + w) = chi((A^2*r1 - r2^2) * (A / r2^3)) * chi(r2^6)
+// chi(w^3 + A*w^2 + w) = chi((A^2*r1 - r2^2) * (A / r2^3) * r2^6)
+// chi(w^3 + A*w^2 + w) = chi((A^2*r1 - r2^2) * A * r2^3)
+// Corollary:
+// e = 1 if (A^2*r1 - r2^2) * A * r2^3) is a non-zero square
+// e = -1 if (A^2*r1 - r2^2) * A * r2^3) is not a square
+// Note that w^3 + A*w^2 + w (and therefore e) can never be zero:
+// w^3 + A*w^2 + w = w * (w^2 + A*w + 1)
+// w^3 + A*w^2 + w = w * (w^2 + A*w + A^2/4 - A^2/4 + 1)
+// w^3 + A*w^2 + w = w * (w + A/2)^2 - A^2/4 + 1)
+// which is zero only if:
+// w = 0 (impossible)
+// (w + A/2)^2 = A^2/4 - 1 (impossible, because A^2/4-1 is not a square)
+//
+// Let isr = invsqrt((A^2*r1 - r2^2) * A * r2^3)
+// isr = sqrt(1 / ((A^2*r1 - r2^2) * A * r2^3)) if e = 1
+// isr = sqrt(sqrt(-1) / ((A^2*r1 - r2^2) * A * r2^3)) if e = -1
+//
+// if e = 1
+// let u1 = -A * (A^2*r1 - r2^2) * A * r2^2 * isr^2
+// u1 = w
+// u1 = u
+//
+// if e = -1
+// let ufactor = -non_square * sqrt(-1) * r^2
+// let vfactor = sqrt(ufactor)
+// let u2 = -A * (A^2*r1 - r2^2) * A * r2^2 * isr^2 * ufactor
+// u2 = w * -1 * -non_square * r^2
+// u2 = w * non_square * r^2
+// u2 = u
+void crypto_elligator_map(u8 curve[32], const u8 hidden[32])
+{
+ fe r, u, t1, t2, t3;
+ fe_frombytes_mask(r, hidden, 2); // r is encoded in 254 bits.
+ fe_sq(r, r);
+ fe_add(t1, r, r);
+ fe_add(u, t1, fe_one);
+ fe_sq (t2, u);
+ fe_mul(t3, A2, t1);
+ fe_sub(t3, t3, t2);
+ fe_mul(t3, t3, A);
+ fe_mul(t1, t2, u);
+ fe_mul(t1, t3, t1);
+ int is_square = invsqrt(t1, t1);
+ fe_mul(u, r, ufactor);
+ fe_ccopy(u, fe_one, is_square);
+ fe_sq (t1, t1);
+ fe_mul(u, u, A);
+ fe_mul(u, u, t3);
+ fe_mul(u, u, t2);
+ fe_mul(u, u, t1);
+ fe_neg(u, u);
+ fe_tobytes(curve, u);
+
+ WIPE_BUFFER(t1); WIPE_BUFFER(r);
+ WIPE_BUFFER(t2); WIPE_BUFFER(u);
+ WIPE_BUFFER(t3);
+}
+
+// Elligator inverse map
+//
+// Computes the representative of a point, if possible. If not, it does
+// nothing and returns -1. Note that the success of the operation
+// depends only on the point (more precisely its u coordinate). The
+// tweak parameter is used only upon success
+//
+// The tweak should be a random byte. Beyond that, its contents are an
+// implementation detail. Currently, the tweak comprises:
+// - Bit 1 : sign of the v coordinate (0 if positive, 1 if negative)
+// - Bit 2-5: not used
+// - Bits 6-7: random padding
+//
+// From the paper:
+// Let sq = -non_square * u * (u+A)
+// if sq is not a square, or u = -A, there is no mapping
+// Assuming there is a mapping:
+// if v is positive: r = sqrt(-u / (non_square * (u+A)))
+// if v is negative: r = sqrt(-(u+A) / (non_square * u ))
+//
+// We compute isr = invsqrt(-non_square * u * (u+A))
+// if it wasn't a square, abort.
+// else, isr = sqrt(-1 / (non_square * u * (u+A))
+//
+// If v is positive, we return isr * u:
+// isr * u = sqrt(-1 / (non_square * u * (u+A)) * u
+// isr * u = sqrt(-u / (non_square * (u+A))
+//
+// If v is negative, we return isr * (u+A):
+// isr * (u+A) = sqrt(-1 / (non_square * u * (u+A)) * (u+A)
+// isr * (u+A) = sqrt(-(u+A) / (non_square * u)
+int crypto_elligator_rev(u8 hidden[32], const u8 public_key[32], u8 tweak)
+{
+ fe t1, t2, t3;
+ fe_frombytes(t1, public_key); // t1 = u
+
+ fe_add(t2, t1, A); // t2 = u + A
+ fe_mul(t3, t1, t2);
+ fe_mul_small(t3, t3, -2);
+ int is_square = invsqrt(t3, t3); // t3 = sqrt(-1 / non_square * u * (u+A))
+ if (is_square) {
+ // The only variable time bit. This ultimately reveals how many
+ // tries it took us to find a representable key.
+ // This does not affect security as long as we try keys at random.
+
+ fe_ccopy (t1, t2, tweak & 1); // multiply by u if v is positive,
+ fe_mul (t3, t1, t3); // multiply by u+A otherwise
+ fe_mul_small(t1, t3, 2);
+ fe_neg (t2, t3);
+ fe_ccopy (t3, t2, fe_isodd(t1));
+ fe_tobytes(hidden, t3);
+
+ // Pad with two random bits
+ hidden[31] |= tweak & 0xc0;
+ }
+
+ WIPE_BUFFER(t1);
+ WIPE_BUFFER(t2);
+ WIPE_BUFFER(t3);
+ return is_square - 1;
+}
+
+void crypto_elligator_key_pair(u8 hidden[32], u8 secret_key[32], u8 seed[32])
+{
+ u8 pk [32]; // public key
+ u8 buf[64]; // seed + representative
+ COPY(buf + 32, seed, 32);
+ do {
+ crypto_chacha20_djb(buf, 0, 64, buf+32, zero, 0);
+ crypto_x25519_dirty_fast(pk, buf); // or the "small" version
+ } while(crypto_elligator_rev(buf+32, pk, buf[32]));
+ // Note that the return value of crypto_elligator_rev() is
+ // independent from its tweak parameter.
+ // Therefore, buf[32] is not actually reused. Either we loop one
+ // more time and buf[32] is used for the new seed, or we succeeded,
+ // and buf[32] becomes the tweak parameter.
+
+ crypto_wipe(seed, 32);
+ COPY(hidden , buf + 32, 32);
+ COPY(secret_key, buf , 32);
+ WIPE_BUFFER(buf);
+ WIPE_BUFFER(pk);
+}
+
+///////////////////////
+/// Scalar division ///
+///////////////////////
+
+// Montgomery reduction.
+// Divides x by (2^256), and reduces the result modulo L
+//
+// Precondition:
+// x < L * 2^256
+// Constants:
+// r = 2^256 (makes division by r trivial)
+// k = (r * (1/r) - 1) // L (1/r is computed modulo L )
+// Algorithm:
+// s = (x * k) % r
+// t = x + s*L (t is always a multiple of r)
+// u = (t/r) % L (u is always below 2*L, conditional subtraction is enough)
+static void redc(u32 u[8], u32 x[16])
+{
+ static const u32 k[8] = {
+ 0x12547e1b, 0xd2b51da3, 0xfdba84ff, 0xb1a206f2,
+ 0xffa36bea, 0x14e75438, 0x6fe91836, 0x9db6c6f2,
+ };
+
+ // s = x * k (modulo 2^256)
+ // This is cheaper than the full multiplication.
+ u32 s[8] = {0};
+ FOR (i, 0, 8) {
+ u64 carry = 0;
+ FOR (j, 0, 8-i) {
+ carry += s[i+j] + (u64)x[i] * k[j];
+ s[i+j] = (u32)carry;
+ carry >>= 32;
+ }
+ }
+ u32 t[16] = {0};
+ multiply(t, s, L);
+
+ // t = t + x
+ u64 carry = 0;
+ FOR (i, 0, 16) {
+ carry += (u64)t[i] + x[i];
+ t[i] = (u32)carry;
+ carry >>= 32;
+ }
+
+ // u = (t / 2^256) % L
+ // Note that t / 2^256 is always below 2*L,
+ // So a constant time conditional subtraction is enough
+ remove_l(u, t+8);
+
+ WIPE_BUFFER(s);
+ WIPE_BUFFER(t);
+}
+
+void crypto_x25519_inverse(u8 blind_salt [32], const u8 private_key[32],
+ const u8 curve_point[32])
+{
+ static const u8 Lm2[32] = { // L - 2
+ 0xeb, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58,
+ 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
+ };
+ // 1 in Montgomery form
+ u32 m_inv [8] = {
+ 0x8d98951d, 0xd6ec3174, 0x737dcf70, 0xc6ef5bf4,
+ 0xfffffffe, 0xffffffff, 0xffffffff, 0x0fffffff,
+ };
+
+ u8 scalar[32];
+ crypto_eddsa_trim_scalar(scalar, private_key);
+
+ // Convert the scalar in Montgomery form
+ // m_scl = scalar * 2^256 (modulo L)
+ u32 m_scl[8];
+ {
+ u32 tmp[16];
+ ZERO(tmp, 8);
+ load32_le_buf(tmp+8, scalar, 8);
+ mod_l(scalar, tmp);
+ load32_le_buf(m_scl, scalar, 8);
+ WIPE_BUFFER(tmp); // Wipe ASAP to save stack space
+ }
+
+ // Compute the inverse
+ u32 product[16];
+ for (int i = 252; i >= 0; i--) {
+ ZERO(product, 16);
+ multiply(product, m_inv, m_inv);
+ redc(m_inv, product);
+ if (scalar_bit(Lm2, i)) {
+ ZERO(product, 16);
+ multiply(product, m_inv, m_scl);
+ redc(m_inv, product);
+ }
+ }
+ // Convert the inverse *out* of Montgomery form
+ // scalar = m_inv / 2^256 (modulo L)
+ COPY(product, m_inv, 8);
+ ZERO(product + 8, 8);
+ redc(m_inv, product);
+ store32_le_buf(scalar, m_inv, 8); // the *inverse* of the scalar
+
+ // Clear the cofactor of scalar:
+ // cleared = scalar * (3*L + 1) (modulo 8*L)
+ // cleared = scalar + scalar * 3 * L (modulo 8*L)
+ // Note that (scalar * 3) is reduced modulo 8, so we only need the
+ // first byte.
+ add_xl(scalar, scalar[0] * 3);
+
+ // Recall that 8*L < 2^256. However it is also very close to
+ // 2^255. If we spanned the ladder over 255 bits, random tests
+ // wouldn't catch the off-by-one error.
+ scalarmult(blind_salt, scalar, curve_point, 256);
+
+ WIPE_BUFFER(scalar); WIPE_BUFFER(m_scl);
+ WIPE_BUFFER(product); WIPE_BUFFER(m_inv);
+}
+
+////////////////////////////////
+/// Authenticated encryption ///
+////////////////////////////////
+static void lock_auth(u8 mac[16], const u8 auth_key[32],
+ const u8 *ad , size_t ad_size,
+ const u8 *cipher_text, size_t text_size)
+{
+ u8 sizes[16]; // Not secret, not wiped
+ store64_le(sizes + 0, ad_size);
+ store64_le(sizes + 8, text_size);
+ crypto_poly1305_ctx poly_ctx; // auto wiped...
+ crypto_poly1305_init (&poly_ctx, auth_key);
+ crypto_poly1305_update(&poly_ctx, ad , ad_size);
+ crypto_poly1305_update(&poly_ctx, zero , gap(ad_size, 16));
+ crypto_poly1305_update(&poly_ctx, cipher_text, text_size);
+ crypto_poly1305_update(&poly_ctx, zero , gap(text_size, 16));
+ crypto_poly1305_update(&poly_ctx, sizes , 16);
+ crypto_poly1305_final (&poly_ctx, mac); // ...here
+}
+
+void crypto_aead_init_x(crypto_aead_ctx *ctx,
+ u8 const key[32], const u8 nonce[24])
+{
+ crypto_chacha20_h(ctx->key, key, nonce);
+ COPY(ctx->nonce, nonce + 16, 8);
+ ctx->counter = 0;
+}
+
+void crypto_aead_init_djb(crypto_aead_ctx *ctx,
+ const u8 key[32], const u8 nonce[8])
+{
+ COPY(ctx->key , key , 32);
+ COPY(ctx->nonce, nonce, 8);
+ ctx->counter = 0;
+}
+
+void crypto_aead_init_ietf(crypto_aead_ctx *ctx,
+ const u8 key[32], const u8 nonce[12])
+{
+ COPY(ctx->key , key , 32);
+ COPY(ctx->nonce, nonce + 4, 8);
+ ctx->counter = (u64)load32_le(nonce) << 32;
+}
+
+void crypto_aead_write(crypto_aead_ctx *ctx, u8 *cipher_text, u8 mac[16],
+ const u8 *ad, size_t ad_size,
+ const u8 *plain_text, size_t text_size)
+{
+ u8 auth_key[64]; // the last 32 bytes are used for rekeying.
+ crypto_chacha20_djb(auth_key, 0, 64, ctx->key, ctx->nonce, ctx->counter);
+ crypto_chacha20_djb(cipher_text, plain_text, text_size,
+ ctx->key, ctx->nonce, ctx->counter + 1);
+ lock_auth(mac, auth_key, ad, ad_size, cipher_text, text_size);
+ COPY(ctx->key, auth_key + 32, 32);
+ WIPE_BUFFER(auth_key);
+}
+
+int crypto_aead_read(crypto_aead_ctx *ctx, u8 *plain_text, const u8 mac[16],
+ const u8 *ad, size_t ad_size,
+ const u8 *cipher_text, size_t text_size)
+{
+ u8 auth_key[64]; // the last 32 bytes are used for rekeying.
+ u8 real_mac[16];
+ crypto_chacha20_djb(auth_key, 0, 64, ctx->key, ctx->nonce, ctx->counter);
+ lock_auth(real_mac, auth_key, ad, ad_size, cipher_text, text_size);
+ int mismatch = crypto_verify16(mac, real_mac);
+ if (!mismatch) {
+ crypto_chacha20_djb(plain_text, cipher_text, text_size,
+ ctx->key, ctx->nonce, ctx->counter + 1);
+ COPY(ctx->key, auth_key + 32, 32);
+ }
+ WIPE_BUFFER(auth_key);
+ WIPE_BUFFER(real_mac);
+ return mismatch;
+}
+
+void crypto_aead_lock(u8 *cipher_text, u8 mac[16], const u8 key[32],
+ const u8 nonce[24], const u8 *ad, size_t ad_size,
+ const u8 *plain_text, size_t text_size)
+{
+ crypto_aead_ctx ctx;
+ crypto_aead_init_x(&ctx, key, nonce);
+ crypto_aead_write(&ctx, cipher_text, mac, ad, ad_size,
+ plain_text, text_size);
+ crypto_wipe(&ctx, sizeof(ctx));
+}
+
+int crypto_aead_unlock(u8 *plain_text, const u8 mac[16], const u8 key[32],
+ const u8 nonce[24], const u8 *ad, size_t ad_size,
+ const u8 *cipher_text, size_t text_size)
+{
+ crypto_aead_ctx ctx;
+ crypto_aead_init_x(&ctx, key, nonce);
+ int mismatch = crypto_aead_read(&ctx, plain_text, mac, ad, ad_size,
+ cipher_text, text_size);
+ crypto_wipe(&ctx, sizeof(ctx));
+ return mismatch;
+}
+
+#ifdef MONOCYPHER_CPP_NAMESPACE
+}
+#endif
diff --git a/src/monocypher/monocypher.h b/src/monocypher/monocypher.h
new file mode 100644
index 0000000..291b8b9
--- /dev/null
+++ b/src/monocypher/monocypher.h
@@ -0,0 +1,321 @@
+// Monocypher version 4.0.3
+//
+// This file is dual-licensed. Choose whichever licence you want from
+// the two licences listed below.
+//
+// The first licence is a regular 2-clause BSD licence. The second licence
+// is the CC-0 from Creative Commons. It is intended to release Monocypher
+// to the public domain. The BSD licence serves as a fallback option.
+//
+// SPDX-License-Identifier: BSD-2-Clause OR CC0-1.0
+//
+// ------------------------------------------------------------------------
+//
+// Copyright (c) 2017-2019, Loup Vaillant
+// All rights reserved.
+//
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// 1. Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the
+// distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+//
+// ------------------------------------------------------------------------
+//
+// Written in 2017-2019 by Loup Vaillant
+//
+// To the extent possible under law, the author(s) have dedicated all copyright
+// and related neighboring rights to this software to the public domain
+// worldwide. This software is distributed without any warranty.
+//
+// You should have received a copy of the CC0 Public Domain Dedication along
+// with this software. If not, see
+//
+
+#ifndef MONOCYPHER_H
+#define MONOCYPHER_H
+
+#include
+#include
+
+#ifdef MONOCYPHER_CPP_NAMESPACE
+namespace MONOCYPHER_CPP_NAMESPACE {
+#elif defined(__cplusplus)
+extern "C" {
+#endif
+
+// Constant time comparisons
+// -------------------------
+
+// Return 0 if a and b are equal, -1 otherwise
+int crypto_verify16(const uint8_t a[16], const uint8_t b[16]);
+int crypto_verify32(const uint8_t a[32], const uint8_t b[32]);
+int crypto_verify64(const uint8_t a[64], const uint8_t b[64]);
+
+
+// Erase sensitive data
+// --------------------
+void crypto_wipe(void *secret, size_t size);
+
+
+// Authenticated encryption
+// ------------------------
+void crypto_aead_lock(uint8_t *cipher_text,
+ uint8_t mac [16],
+ const uint8_t key [32],
+ const uint8_t nonce[24],
+ const uint8_t *ad, size_t ad_size,
+ const uint8_t *plain_text, size_t text_size);
+int crypto_aead_unlock(uint8_t *plain_text,
+ const uint8_t mac [16],
+ const uint8_t key [32],
+ const uint8_t nonce[24],
+ const uint8_t *ad, size_t ad_size,
+ const uint8_t *cipher_text, size_t text_size);
+
+// Authenticated stream
+// --------------------
+typedef struct {
+ uint64_t counter;
+ uint8_t key[32];
+ uint8_t nonce[8];
+} crypto_aead_ctx;
+
+void crypto_aead_init_x(crypto_aead_ctx *ctx,
+ const uint8_t key[32], const uint8_t nonce[24]);
+void crypto_aead_init_djb(crypto_aead_ctx *ctx,
+ const uint8_t key[32], const uint8_t nonce[8]);
+void crypto_aead_init_ietf(crypto_aead_ctx *ctx,
+ const uint8_t key[32], const uint8_t nonce[12]);
+
+void crypto_aead_write(crypto_aead_ctx *ctx,
+ uint8_t *cipher_text,
+ uint8_t mac[16],
+ const uint8_t *ad , size_t ad_size,
+ const uint8_t *plain_text, size_t text_size);
+int crypto_aead_read(crypto_aead_ctx *ctx,
+ uint8_t *plain_text,
+ const uint8_t mac[16],
+ const uint8_t *ad , size_t ad_size,
+ const uint8_t *cipher_text, size_t text_size);
+
+
+// General purpose hash (BLAKE2b)
+// ------------------------------
+
+// Direct interface
+void crypto_blake2b(uint8_t *hash, size_t hash_size,
+ const uint8_t *message, size_t message_size);
+
+void crypto_blake2b_keyed(uint8_t *hash, size_t hash_size,
+ const uint8_t *key, size_t key_size,
+ const uint8_t *message, size_t message_size);
+
+// Incremental interface
+typedef struct {
+ // Do not rely on the size or contents of this type,
+ // for they may change without notice.
+ uint64_t hash[8];
+ uint64_t input_offset[2];
+ uint64_t input[16];
+ size_t input_idx;
+ size_t hash_size;
+} crypto_blake2b_ctx;
+
+void crypto_blake2b_init(crypto_blake2b_ctx *ctx, size_t hash_size);
+void crypto_blake2b_keyed_init(crypto_blake2b_ctx *ctx, size_t hash_size,
+ const uint8_t *key, size_t key_size);
+void crypto_blake2b_update(crypto_blake2b_ctx *ctx,
+ const uint8_t *message, size_t message_size);
+void crypto_blake2b_final(crypto_blake2b_ctx *ctx, uint8_t *hash);
+
+
+// Password key derivation (Argon2)
+// --------------------------------
+#define CRYPTO_ARGON2_D 0
+#define CRYPTO_ARGON2_I 1
+#define CRYPTO_ARGON2_ID 2
+
+typedef struct {
+ uint32_t algorithm; // Argon2d, Argon2i, Argon2id
+ uint32_t nb_blocks; // memory hardness, >= 8 * nb_lanes
+ uint32_t nb_passes; // CPU hardness, >= 1 (>= 3 recommended for Argon2i)
+ uint32_t nb_lanes; // parallelism level (single threaded anyway)
+} crypto_argon2_config;
+
+typedef struct {
+ const uint8_t *pass;
+ const uint8_t *salt;
+ uint32_t pass_size;
+ uint32_t salt_size; // 16 bytes recommended
+} crypto_argon2_inputs;
+
+typedef struct {
+ const uint8_t *key; // may be NULL if no key
+ const uint8_t *ad; // may be NULL if no additional data
+ uint32_t key_size; // 0 if no key (32 bytes recommended otherwise)
+ uint32_t ad_size; // 0 if no additional data
+} crypto_argon2_extras;
+
+extern const crypto_argon2_extras crypto_argon2_no_extras;
+
+void crypto_argon2(uint8_t *hash, uint32_t hash_size, void *work_area,
+ crypto_argon2_config config,
+ crypto_argon2_inputs inputs,
+ crypto_argon2_extras extras);
+
+
+// Key exchange (X-25519)
+// ----------------------
+
+// Shared secrets are not quite random.
+// Hash them to derive an actual shared key.
+void crypto_x25519_public_key(uint8_t public_key[32],
+ const uint8_t secret_key[32]);
+void crypto_x25519(uint8_t raw_shared_secret[32],
+ const uint8_t your_secret_key [32],
+ const uint8_t their_public_key [32]);
+
+// Conversion to EdDSA
+void crypto_x25519_to_eddsa(uint8_t eddsa[32], const uint8_t x25519[32]);
+
+// scalar "division"
+// Used for OPRF. Be aware that exponential blinding is less secure
+// than Diffie-Hellman key exchange.
+void crypto_x25519_inverse(uint8_t blind_salt [32],
+ const uint8_t private_key[32],
+ const uint8_t curve_point[32]);
+
+// "Dirty" versions of x25519_public_key().
+// Use with crypto_elligator_rev().
+// Leaks 3 bits of the private key.
+void crypto_x25519_dirty_small(uint8_t pk[32], const uint8_t sk[32]);
+void crypto_x25519_dirty_fast (uint8_t pk[32], const uint8_t sk[32]);
+
+
+// Signatures
+// ----------
+
+// EdDSA with curve25519 + BLAKE2b
+void crypto_eddsa_key_pair(uint8_t secret_key[64],
+ uint8_t public_key[32],
+ uint8_t seed[32]);
+void crypto_eddsa_sign(uint8_t signature [64],
+ const uint8_t secret_key[64],
+ const uint8_t *message, size_t message_size);
+int crypto_eddsa_check(const uint8_t signature [64],
+ const uint8_t public_key[32],
+ const uint8_t *message, size_t message_size);
+
+// Conversion to X25519
+void crypto_eddsa_to_x25519(uint8_t x25519[32], const uint8_t eddsa[32]);
+
+// EdDSA building blocks
+void crypto_eddsa_trim_scalar(uint8_t out[32], const uint8_t in[32]);
+void crypto_eddsa_reduce(uint8_t reduced[32], const uint8_t expanded[64]);
+void crypto_eddsa_mul_add(uint8_t r[32],
+ const uint8_t a[32],
+ const uint8_t b[32],
+ const uint8_t c[32]);
+void crypto_eddsa_scalarbase(uint8_t point[32], const uint8_t scalar[32]);
+int crypto_eddsa_check_equation(const uint8_t signature[64],
+ const uint8_t public_key[32],
+ const uint8_t h_ram[32]);
+
+
+// Chacha20
+// --------
+
+// Specialised hash.
+// Used to hash X25519 shared secrets.
+void crypto_chacha20_h(uint8_t out[32],
+ const uint8_t key[32],
+ const uint8_t in [16]);
+
+// Unauthenticated stream cipher.
+// Don't forget to add authentication.
+uint64_t crypto_chacha20_djb(uint8_t *cipher_text,
+ const uint8_t *plain_text,
+ size_t text_size,
+ const uint8_t key[32],
+ const uint8_t nonce[8],
+ uint64_t ctr);
+uint32_t crypto_chacha20_ietf(uint8_t *cipher_text,
+ const uint8_t *plain_text,
+ size_t text_size,
+ const uint8_t key[32],
+ const uint8_t nonce[12],
+ uint32_t ctr);
+uint64_t crypto_chacha20_x(uint8_t *cipher_text,
+ const uint8_t *plain_text,
+ size_t text_size,
+ const uint8_t key[32],
+ const uint8_t nonce[24],
+ uint64_t ctr);
+
+
+// Poly 1305
+// ---------
+
+// This is a *one time* authenticator.
+// Disclosing the mac reveals the key.
+// See crypto_lock() on how to use it properly.
+
+// Direct interface
+void crypto_poly1305(uint8_t mac[16],
+ const uint8_t *message, size_t message_size,
+ const uint8_t key[32]);
+
+// Incremental interface
+typedef struct {
+ // Do not rely on the size or contents of this type,
+ // for they may change without notice.
+ uint8_t c[16]; // chunk of the message
+ size_t c_idx; // How many bytes are there in the chunk.
+ uint32_t r [4]; // constant multiplier (from the secret key)
+ uint32_t pad[4]; // random number added at the end (from the secret key)
+ uint32_t h [5]; // accumulated hash
+} crypto_poly1305_ctx;
+
+void crypto_poly1305_init (crypto_poly1305_ctx *ctx, const uint8_t key[32]);
+void crypto_poly1305_update(crypto_poly1305_ctx *ctx,
+ const uint8_t *message, size_t message_size);
+void crypto_poly1305_final (crypto_poly1305_ctx *ctx, uint8_t mac[16]);
+
+
+// Elligator 2
+// -----------
+
+// Elligator mappings proper
+void crypto_elligator_map(uint8_t curve [32], const uint8_t hidden[32]);
+int crypto_elligator_rev(uint8_t hidden[32], const uint8_t curve [32],
+ uint8_t tweak);
+
+// Easy to use key pair generation
+void crypto_elligator_key_pair(uint8_t hidden[32], uint8_t secret_key[32],
+ uint8_t seed[32]);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // MONOCYPHER_H
diff --git a/src/pkg.c b/src/pkg.c
index 0616bd6..f5c487c 100644
--- a/src/pkg.c
+++ b/src/pkg.c
@@ -15,63 +15,35 @@
#include "common.h"
#include "pkg.h"
-/* null dependencies/conflicts for error throwing */
-dep_t null_dep = { "null", {-1, -1, -1, NULL} };
-conf_t null_conf = { "null", {-1, -1, -1, NULL} };
-
-char
-*join_with_cwd(const char *path)
-{
- char cwd[PATH_MAX];
-
- if (!getcwd(cwd, sizeof(cwd))) {
- perror("getcwd");
- return NULL;
- }
-
- size_t length = strlen(cwd + strlen(path) + 2);
- char *result = malloc(length);
- if (!result) { return NULL; }
-
- snprintf(result, length, "%s/%s", cwd, path);
- return result;
-}
-
-/*
- * gl_pkgver
- * DESCRIPTION: takes three integers and formats them as a semver_t struct
- * PARAMETERS:
- * ~ int major (the MAJOR number)
- * ~ int minor (the MINOR number)
- * ~ int patch (the PATCH number)
- * RETURNS:
- * ~ a pointer to a semver_t struct
- */
-semver_t
-gl_pkgver(int major, int minor, int patch)
-{
- semver_t pkgver = {major, minor, patch, NULL};
- return pkgver;
-}
-
+/* "MAJ.MIN.PAT[-TAG]". Each number is read with strtol and clamped to
+ * 0..INT_MAX (a manifest is untrusted: sscanf's %d overflowing is
+ * undefined); anything missing or unparseable is 0. */
semver_t
gl_parse_semver(const char *str)
{
semver_t v = {0, 0, 0, NULL};
if (!str) { return v; }
-
+
char buffer[64];
- strncpy(buffer, str, sizeof(buffer) - 1);
- buffer[sizeof(buffer) - 1] = '\0';
+ snprintf(buffer, sizeof(buffer), "%s", str);
char *dash = strchr(buffer, '-');
if (dash) {
*dash = '\0';
- dash++;
- v.tag = strdup(dash);
+ v.tag = strdup(dash + 1);
}
- sscanf(buffer, "%d.%d.%d", &v.maj, &v.min, &v.pat);
+ int *parts[] = { &v.maj, &v.min, &v.pat };
+ const char *p = buffer;
+ for (int i = 0; i < 3 && *p; i++) {
+ char *end;
+ errno = 0;
+ long n = strtol(p, &end, 10);
+ if (end == p) { break; }
+ *parts[i] = (errno || n < 0) ? 0 : n > INT_MAX ? INT_MAX : (int)n;
+ if (*end != '.') { break; }
+ p = end + 1;
+ }
return v;
}
@@ -81,47 +53,6 @@ gl_free_semver(semver_t *v)
if (v && v->tag) { free (v->tag); };
}
-gl_pkg_t empty = {
- "null",
- {-1, -1, -1, NULL},
- "Null package for internal use. Returned upon an error.",
- "null@null.null",
- "null",
- "null",
- &null_dep,
- &null_conf
-};
-
-int
-gl_mkdir(dir_t *dir)
-{
- if (mkdir(dir -> path, dir -> mode) == 0) {
- return 0;
- }
-
- if (errno == EEXIST) { return 0; }
- perror("libglacier: mkdir");
- return -1;
-}
-
-gl_pkg_t
-*gl_spawn_pkg(char *name, semver_t *ver, char *desc, char *maint, char *copy, char *repo, dep_t *dep, conf_t *conf)
-{
- gl_pkg_t *new_pkg = malloc(sizeof(gl_pkg_t));
- if (!new_pkg) { return NULL; }
-
- new_pkg -> pkg_name = name;
- new_pkg -> pkg_ver = *ver;
- new_pkg -> pkg_desc = desc;
- new_pkg -> pkg_maint = maint;
- new_pkg -> pkg_copyright = copy;
- new_pkg -> pkg_repo = repo;
- new_pkg -> pkg_dependencies = dep;
- new_pkg -> pkg_conflicts = conf;
-
- return new_pkg;
-}
-
/*
* gl_valid_pkg_ident
* DESCRIPTION: checks that a package name or repo name is safe to use as a
@@ -151,26 +82,103 @@ gl_valid_pkg_ident(const char *s)
return true;
}
+/* A manifest comes out of an untrusted archive. libconfig honours
+ * `@include "FILE"` at the start of a line, which would have it read any
+ * file (as root, under syspkg): such a manifest is refused. */
+static char *
+read_manifest_text(const char *filename)
+{
+ FILE *f = fopen(filename, "rb");
+ if (!f) { return NULL; }
+ enum { MAX_MANIFEST = 1024 * 1024 };
+ char *buf = malloc(MAX_MANIFEST + 1);
+ size_t n = buf ? fread(buf, 1, MAX_MANIFEST + 1, f) : 0;
+ bool bad = !buf || ferror(f) || n > MAX_MANIFEST || memchr(buf, '\0', n);
+ fclose(f);
+ if (bad) {
+ free(buf);
+ return NULL;
+ }
+ buf[n] = '\0';
+ for (const char *line = buf; line; line = strchr(line, '\n')) {
+ if (*line == '\n') { line++; }
+ while (*line == ' ' || *line == '\t') { line++; }
+ if (strncmp(line, "@include", 8) == 0) {
+ free(buf);
+ return NULL;
+ }
+ }
+ return buf;
+}
+
+/* PKG_DEPS / PKG_CONFS: a list of { PKG_NAME; PKG_VER; }, each name a
+ * gl_valid_pkg_ident, into a NULL-name-terminated array of
+ * (name, version) pairs, names and vers parallel. false if malformed. */
+static bool
+manifest_relations(config_t *cfg, const char *key, char ***names, semver_t **vers)
+{
+ *names = NULL;
+ *vers = NULL;
+ config_setting_t *l = config_lookup(cfg, key);
+ if (!l) { return true; }
+ if (!config_setting_is_list(l)) { return false; }
+
+ int count = config_setting_length(l);
+ *names = calloc((size_t)count + 1, sizeof(**names));
+ *vers = calloc((size_t)count + 1, sizeof(**vers));
+ if (!*names || !*vers) { return false; }
+ for (int i = 0; i < count; i++) {
+ config_setting_t *d = config_setting_get_elem(l, (unsigned)i);
+ const char *name = NULL, *ver = NULL;
+ config_setting_lookup_string(d, "PKG_NAME", &name);
+ config_setting_lookup_string(d, "PKG_VER", &ver);
+ if (!gl_valid_pkg_ident(name) || !((*names)[i] = strdup(name))) {
+ return false;
+ }
+ (*vers)[i] = gl_parse_semver(ver);
+ }
+ return true;
+}
+
+static void
+free_relations(char **names, semver_t *vers)
+{
+ for (size_t i = 0; names && names[i]; i++) {
+ free(names[i]);
+ gl_free_semver(&vers[i]);
+ }
+ free(names);
+ free(vers);
+}
+
int
gl_gpm2gpkg(const char *filename, gl_pkg_t *pkg)
{
+ char *text = read_manifest_text(filename);
+ if (!text) { return ENOENT; }
+
config_t cfg;
- config_setting_t *deps, *confs;
- const char *tmp_str;
- const char *name = NULL, *repo = NULL;
-
config_init(&cfg);
-
- if (!config_read_file(&cfg, filename)) {
+ int parsed = config_read_string(&cfg, text);
+ free(text);
+ if (!parsed) {
config_destroy(&cfg);
return ENOENT;
}
/* Validate before anything is strdup'd, so a rejected manifest leaves
* pkg untouched and the caller has nothing to free. */
+ const char *tmp_str, *name = NULL, *repo = NULL;
+ char **dnames, **cnames;
+ semver_t *dvers, *cvers;
config_lookup_string(&cfg, "PKG_NAME", &name);
config_lookup_string(&cfg, "PKG_REPO", &repo);
- if (!gl_valid_pkg_ident(name) || !gl_valid_pkg_ident(repo)) {
+ bool ok = gl_valid_pkg_ident(name) && gl_valid_pkg_ident(repo);
+ bool rels = manifest_relations(&cfg, "PKG_DEPS", &dnames, &dvers);
+ rels = manifest_relations(&cfg, "PKG_CONFS", &cnames, &cvers) && rels;
+ if (!ok || !rels) {
+ free_relations(dnames, dvers);
+ free_relations(cnames, cvers);
config_destroy(&cfg);
return EINVAL;
}
@@ -182,102 +190,37 @@ gl_gpm2gpkg(const char *filename, gl_pkg_t *pkg)
if (config_lookup_string(&cfg, "PKG_COPYRIGHT", &tmp_str)) { pkg->pkg_copyright = strdup(tmp_str); }
if (config_lookup_string(&cfg, "PKG_VER", &tmp_str)) { pkg->pkg_ver = gl_parse_semver(tmp_str); }
- deps = config_lookup(&cfg, "PKG_DEPS");
- if (deps && config_setting_is_list(deps)) {
- int count = config_setting_length(deps);
- pkg->pkg_dependencies = calloc((size_t)(count + 1), sizeof(dep_t));
-
- for (int i = 0; i < count; i++) {
- config_setting_t *d = config_setting_get_elem(deps, (unsigned int)i);
- const char *dep_name = NULL;
- const char *dep_ver = NULL;
-
- config_setting_lookup_string(d, "PKG_NAME", &dep_name);
- config_setting_lookup_string(d, "PKG_VER", &dep_ver);
-
- pkg->pkg_dependencies[i].dep_name = dep_name ? strdup(dep_name) : NULL;
- pkg->pkg_dependencies[i].dep_ver = gl_parse_semver(dep_ver);
+ if (dnames) {
+ size_t n = 0;
+ while (dnames[n]) { n++; }
+ pkg->pkg_dependencies = calloc(n + 1, sizeof(dep_t));
+ for (size_t i = 0; pkg->pkg_dependencies && i < n; i++) {
+ pkg->pkg_dependencies[i].dep_name = dnames[i];
+ pkg->pkg_dependencies[i].dep_ver = dvers[i];
+ dnames[i] = NULL;
}
}
-
- confs = config_lookup(&cfg, "PKG_CONFS");
- if (confs && config_setting_is_list(confs)) {
- int count = config_setting_length(confs);
- pkg->pkg_conflicts = calloc((size_t)(count + 1), sizeof(conf_t));
-
- for (int i = 0; i < count; i++) {
- config_setting_t *c = config_setting_get_elem(confs, (unsigned int)i);
- const char *conf_name = NULL;
- const char *conf_ver = NULL;
-
- config_setting_lookup_string(c, "PKG_NAME", &conf_name);
- config_setting_lookup_string(c, "PKG_VER", &conf_ver);
-
- pkg->pkg_conflicts[i].conf_name = conf_name ? strdup(conf_name) : NULL;
- pkg->pkg_conflicts[i].conf_ver = gl_parse_semver(conf_ver);
+ if (cnames) {
+ size_t n = 0;
+ while (cnames[n]) { n++; }
+ pkg->pkg_conflicts = calloc(n + 1, sizeof(conf_t));
+ for (size_t i = 0; pkg->pkg_conflicts && i < n; i++) {
+ pkg->pkg_conflicts[i].conf_name = cnames[i];
+ pkg->pkg_conflicts[i].conf_ver = cvers[i];
+ cnames[i] = NULL;
}
}
-
+ free_relations(dnames, dvers);
+ free_relations(cnames, cvers);
config_destroy(&cfg);
- return 0;
-}
-char
-*gl_cat_dir(const dir_t *a, const dir_t *b)
-{
- size_t length = strlen(a->path) + strlen(b->path) + 2;
- char *result = malloc(length);
-
- if (!result) { return NULL; }
-
- snprintf(result, length, "%s/%s", a->path, b-> path);
- return result;
-}
-
-char
-*gl_cat_dir_and_ver(const dir_t *base, const gl_pkg_t *pkg)
-{
- char version[32];
- snprintf(version, sizeof(version), "%d.%d.%d",
- pkg->pkg_ver.maj, pkg->pkg_ver.min, pkg->pkg_ver.pat);
-
- size_t length = strlen(base->path) + strlen(pkg->pkg_name) + strlen(version) + 3;
- char *result = malloc(length);
- if (!result) { return NULL; }
-
- snprintf(result, length, "%s/%s-%s", base->path, pkg->pkg_name, version);
- return result;
-}
-
-dir_t
-gl_return_dir(const dir_t *a, const dir_t *b, mode_t mode)
-{
- char *path = gl_cat_dir(a, b);
- dir_t result = { path, mode };
- return result;
-}
-
-dir_t
-gl_return_dir_and_ver(const dir_t *base, gl_pkg_t *pkg, mode_t mode)
-{
- char *path = gl_cat_dir_and_ver(base, pkg);
- dir_t result = { path, mode };
- return result;
-}
-
-void
-gl_free_path(char *path)
-{
- free(path);
-}
-
-void
-gl_free_dir(dir_t *dir)
-{
- if (dir && dir->path) {
- free((char *)dir->path);
- dir->path = NULL;
+ if (!pkg->pkg_name || !pkg->pkg_repo || (dnames && !pkg->pkg_dependencies) ||
+ (cnames && !pkg->pkg_conflicts)) {
+ gl_free_pkg(pkg);
+ memset(pkg, 0, sizeof(*pkg));
+ return ENOMEM;
}
+ return 0;
}
void
@@ -310,43 +253,6 @@ gl_free_pkg(gl_pkg_t *pkg)
}
}
-void
-gl_show_pkg_props(const gl_pkg_t *pkg)
-{
- if (!pkg) {
- printf("(null pkg)\n");
- return;
- }
-
- printf("Package: %s\n", pkg->pkg_name);
- printf("Version: %d.%d.%d\n", pkg->pkg_ver.maj, pkg->pkg_ver.min, pkg->pkg_ver.pat);
- printf("Description: %s\n", pkg->pkg_desc);
- printf("Maintainer: %s\n", pkg->pkg_maint);
- printf("Copyright: %s\n", pkg->pkg_copyright);
-
- if (pkg->pkg_dependencies) {
- printf("Dependencies:\n");
- dep_t *dep = pkg->pkg_dependencies;
- printf("\t~ %s (%d.%d.%d)\n",
- dep->dep_name,
- dep->dep_ver.maj, dep->dep_ver.min, dep->dep_ver.pat);
- }
- else {
- printf("Dependencies: (none)\n");
- }
-
- if (pkg->pkg_conflicts) {
- printf("Conflicting packages:\n");
- conf_t *conf = pkg->pkg_conflicts;
- printf("\t~ %s (%d.%d.%d)\n",
- conf->conf_name,
- conf->conf_ver.maj, conf->conf_ver.min, conf->conf_ver.pat);
- }
- else {
- printf("Conflicting packages: (none)\n");
- }
-}
-
/*
* Read integer `key` from a libconfig file (glacier.conf). Distinguishes
* the failures a caller has to report differently:
@@ -383,3 +289,418 @@ gl_conf_get_int(const char *path, const char *key, int *out)
config_destroy(&cfg);
return GL_CONF_OK;
}
+
+/* A repos.conf url: https:// or file://, then at least one printable,
+ * non-space ASCII character. http:// and everything else are refused. */
+static bool
+valid_repo_url(const char *u)
+{
+ const char *rest;
+ if (strncmp(u, "https://", 8) == 0)
+ rest = u + 8;
+ else if (strncmp(u, "file://", 7) == 0)
+ rest = u + 7;
+ else
+ return false;
+ if (*rest == '\0')
+ return false;
+ for (; *rest; rest++)
+ if (*rest < 0x21 || *rest > 0x7e)
+ return false;
+ return true;
+}
+
+/*
+ * Read and check a whole repos.conf (format: gl_conf_repo_keys). On
+ * GL_CONF_OK, *cfg is open (the caller config_destroy()s it) and *repos
+ * is its `repos` list; on anything else nothing is left open.
+ */
+static gl_conf_status_t
+read_repos_conf(const char *path, config_t *cfg, config_setting_t **repos)
+{
+ config_init(cfg);
+ if (!config_read_file(cfg, path)) {
+ config_destroy(cfg);
+ return GL_CONF_ERR_READ;
+ }
+
+ config_setting_t *root = config_root_setting(cfg);
+ config_setting_t *list = config_setting_get_member(root, "repos");
+ if (config_setting_length(root) != 1 || !list ||
+ config_setting_type(list) != CONFIG_TYPE_LIST)
+ goto invalid;
+
+ int n = config_setting_length(list);
+ for (int i = 0; i < n; i++) {
+ config_setting_t *e = config_setting_get_elem(list, (unsigned)i);
+ if (config_setting_type(e) != CONFIG_TYPE_GROUP)
+ goto invalid;
+
+ config_setting_t *name = config_setting_get_member(e, "name");
+ config_setting_t *url = config_setting_get_member(e, "url");
+ config_setting_t *keys = config_setting_get_member(e, "keys");
+ if (!name || !keys ||
+ config_setting_length(e) != (url ? 3 : 2))
+ goto invalid;
+ const char *nm = config_setting_get_string(name);
+ if (!nm || !gl_valid_pkg_ident(nm))
+ goto invalid;
+ if (url) {
+ const char *u = config_setting_get_string(url);
+ if (!u || !valid_repo_url(u))
+ goto invalid;
+ }
+ for (int j = 0; j < i; j++) {
+ config_setting_t *prev = config_setting_get_member(
+ config_setting_get_elem(list, (unsigned)j), "name");
+ if (strcmp(config_setting_get_string(prev), nm) == 0)
+ goto invalid;
+ }
+ if (config_setting_type(keys) != CONFIG_TYPE_ARRAY &&
+ config_setting_type(keys) != CONFIG_TYPE_LIST)
+ goto invalid;
+ int nk = config_setting_length(keys);
+ for (int k = 0; k < nk; k++) {
+ const char *key = config_setting_get_string(
+ config_setting_get_elem(keys, (unsigned)k));
+ if (!key || !gl_valid_pkg_ident(key))
+ goto invalid;
+ }
+ }
+ *repos = list;
+ return GL_CONF_OK;
+invalid:
+ config_destroy(cfg);
+ return GL_CONF_ERR_INVALID;
+}
+
+/*
+ * The names of the keys repos.conf trusts for `repo` (REPO_DESIGN.txt
+ * section 3), '\n'-separated (each followed by '\n') into out.
+ * GL_CONF_OK out set (possibly "": the repo lists no keys)
+ * GL_CONF_ERR_READ file missing, unreadable, or not valid libconfig
+ * GL_CONF_ERR_MISSING the file is valid, but doesn't list `repo`
+ * GL_CONF_ERR_INVALID anything else than the format below
+ * GL_CONF_ERR_SPACE the names don't fit in outlen
+ * The WHOLE file is checked on every call, not only `repo`'s entry, and
+ * nothing unknown is ignored (a setting that silently never parses is
+ * CLAUDE.md bug 10's class):
+ * repos = ( { name = "base"; url = "https://..."; keys = [ "k" ]; }, ... );
+ * `repos` is the only top-level setting; each entry is a group with a
+ * name (a gl_valid_pkg_ident, unique), an optional url (valid_repo_url)
+ * and keys (an array or list of gl_valid_pkg_ident strings).
+ */
+gl_conf_status_t
+gl_conf_repo_keys(const char *path, const char *repo, char *out, size_t outlen)
+{
+ if (!out || outlen == 0)
+ return GL_CONF_ERR_SPACE;
+ out[0] = '\0';
+
+ config_t cfg;
+ config_setting_t *repos;
+ gl_conf_status_t st = read_repos_conf(path, &cfg, &repos);
+ if (st != GL_CONF_OK)
+ return st;
+
+ st = GL_CONF_ERR_MISSING;
+ int n = config_setting_length(repos);
+ for (int i = 0; i < n; i++) {
+ config_setting_t *e = config_setting_get_elem(repos, (unsigned)i);
+ if (strcmp(config_setting_get_string(
+ config_setting_get_member(e, "name")), repo) != 0)
+ continue;
+ config_setting_t *keys = config_setting_get_member(e, "keys");
+ size_t used = 0;
+ int nk = config_setting_length(keys);
+ for (int k = 0; k < nk; k++) {
+ const char *key = config_setting_get_string(
+ config_setting_get_elem(keys, (unsigned)k));
+ size_t len = strlen(key);
+ if (used + len + 2 > outlen) {
+ out[0] = '\0';
+ st = GL_CONF_ERR_SPACE;
+ goto out;
+ }
+ memcpy(out + used, key, len);
+ out[used + len] = '\n';
+ used += len + 1;
+ out[used] = '\0';
+ }
+ st = GL_CONF_OK;
+ break;
+ }
+out:
+ config_destroy(&cfg);
+ return st;
+}
+
+/*
+ * Every repository repos.conf lists, in its order (= priority), one
+ * "NAME\tURL\n" line each into out; URL is empty for an entry without
+ * one. The whole file is checked as by gl_conf_repo_keys.
+ * GL_CONF_OK out set (possibly "": no repositories)
+ * GL_CONF_ERR_READ file missing, unreadable, or not valid libconfig
+ * GL_CONF_ERR_INVALID not the format gl_conf_repo_keys describes
+ * GL_CONF_ERR_SPACE the lines don't fit in outlen
+ */
+gl_conf_status_t
+gl_conf_repos(const char *path, char *out, size_t outlen)
+{
+ if (!out || outlen == 0)
+ return GL_CONF_ERR_SPACE;
+ out[0] = '\0';
+
+ config_t cfg;
+ config_setting_t *repos;
+ gl_conf_status_t st = read_repos_conf(path, &cfg, &repos);
+ if (st != GL_CONF_OK)
+ return st;
+
+ size_t used = 0;
+ int n = config_setting_length(repos);
+ for (int i = 0; i < n; i++) {
+ config_setting_t *e = config_setting_get_elem(repos, (unsigned)i);
+ const char *nm = config_setting_get_string(
+ config_setting_get_member(e, "name"));
+ config_setting_t *url = config_setting_get_member(e, "url");
+ const char *u = url ? config_setting_get_string(url) : "";
+ int len = snprintf(out + used, outlen - used, "%s\t%s\n", nm, u);
+ if (len < 0 || (size_t)len >= outlen - used) {
+ out[0] = '\0';
+ st = GL_CONF_ERR_SPACE;
+ break;
+ }
+ used += (size_t)len;
+ }
+ config_destroy(&cfg);
+ return st;
+}
+
+/* ------------------------------------------------------------------ */
+/* Repository indexes (REPO_DESIGN.txt section 4) */
+/* ------------------------------------------------------------------ */
+
+/* A version as the store names it: "%d.%d.%d", each part decimal
+ * without leading zeros and fitting an int. Anything else would name a
+ * version directory the store never makes. */
+static bool
+valid_index_ver(const char *v)
+{
+ for (int part = 0; part < 3; part++) {
+ const char *start = v;
+ long n = 0;
+ while (*v >= '0' && *v <= '9') {
+ n = n * 10 + (*v - '0');
+ if (n > INT_MAX)
+ return false;
+ v++;
+ }
+ if (v == start || (*start == '0' && v - start > 1))
+ return false;
+ if (part < 2 && *v++ != '.')
+ return false;
+ }
+ return *v == '\0';
+}
+
+static bool
+valid_sha256_field(const char *s)
+{
+ if (strncmp(s, "sha256:", 7) != 0 || strlen(s) != 7 + 64)
+ return false;
+ for (s += 7; *s; s++)
+ if (!((*s >= '0' && *s <= '9') || (*s >= 'a' && *s <= 'f')))
+ return false;
+ return true;
+}
+
+/* A group with exactly the members named in `names` (NULL-terminated). */
+static bool
+has_exactly(config_setting_t *g, const char *const *names)
+{
+ if (!g || config_setting_type(g) != CONFIG_TYPE_GROUP)
+ return false;
+ int n = 0;
+ for (; names[n]; n++)
+ if (!config_setting_get_member(g, names[n]))
+ return false;
+ return config_setting_length(g) == n;
+}
+
+static const char *
+str_member(config_setting_t *g, const char *name)
+{
+ config_setting_t *m = config_setting_get_member(g, name);
+ return m && config_setting_type(m) == CONFIG_TYPE_STRING ?
+ config_setting_get_string(m) : NULL;
+}
+
+/* An integer member (int or int64), or false. */
+static bool
+int_member(config_setting_t *g, const char *name, int64_t *out)
+{
+ config_setting_t *m = config_setting_get_member(g, name);
+ if (!m || (config_setting_type(m) != CONFIG_TYPE_INT &&
+ config_setting_type(m) != CONFIG_TYPE_INT64))
+ return false;
+ *out = (int64_t)config_setting_get_int64(m);
+ return true;
+}
+
+/* DEPS or CONFS: a list of { NAME; VER; } groups, as "NAME VER\n" lines
+ * into a new string (*out; "" for an empty list). */
+static bool
+read_relations(config_setting_t *pkg, const char *name, char **out)
+{
+ static const char *const members[] = { "NAME", "VER", NULL };
+ config_setting_t *l = config_setting_get_member(pkg, name);
+ if (!l || config_setting_type(l) != CONFIG_TYPE_LIST)
+ return false;
+ int n = config_setting_length(l);
+ size_t cap = 1, used = 0;
+ for (int i = 0; i < n; i++) {
+ config_setting_t *r = config_setting_get_elem(l, (unsigned)i);
+ const char *nm, *ver;
+ if (!has_exactly(r, members) || !(nm = str_member(r, "NAME")) ||
+ !(ver = str_member(r, "VER")) || !gl_valid_pkg_ident(nm) ||
+ !valid_index_ver(ver))
+ return false;
+ cap += strlen(nm) + strlen(ver) + 2;
+ }
+ if (!(*out = malloc(cap)))
+ return false;
+ (*out)[0] = '\0';
+ for (int i = 0; i < n; i++) {
+ config_setting_t *r = config_setting_get_elem(l, (unsigned)i);
+ used += (size_t)snprintf(*out + used, cap - used, "%s %s\n",
+ str_member(r, "NAME"), str_member(r, "VER"));
+ }
+ return true;
+}
+
+void
+gl_index_free(gl_index_t *idx)
+{
+ if (!idx)
+ return;
+ for (size_t i = 0; i < idx->npkgs; i++) {
+ gl_index_pkg_t *p = &idx->pkgs[i];
+ free(p->name);
+ free(p->ver);
+ free(p->file);
+ free(p->sha256);
+ free(p->deps);
+ free(p->confs);
+ }
+ free(idx->pkgs);
+ free(idx->repo);
+ memset(idx, 0, sizeof(*idx));
+}
+
+/*
+ * Parse an index's bytes (text[0..len)) into *out, which is zeroed
+ * first; free it with gl_index_free on GL_INDEX_OK. The caller checks
+ * the signature over the SAME bytes first: nothing here is trusted
+ * otherwise, and nothing here checks SERIAL or VALID_UNTIL against
+ * anything (that is the caller's rollback/expiry policy).
+ *
+ * Strict, like repos.conf: exactly REPO, SERIAL (> 0), VALID_UNTIL
+ * (> 0) and PKGS at the top; each package exactly NAME, VER, FILE,
+ * SHA256, SIZE (>= 0), DEPS, CONFS; NAME a gl_valid_pkg_ident; VER as
+ * the store names versions; FILE exactly "NAME-VER.gpkg"; SHA256
+ * "sha256:" + 64 lowercase hex; DEPS/CONFS lists of { NAME; VER; }; no
+ * NAME + VER twice. A NUL or '@' anywhere is refused before parsing:
+ * an index never has one, and '@' is how libconfig spells @include.
+ * GL_INDEX_OK, GL_INDEX_ERR_FORMAT, GL_INDEX_ERR_REPO (well formed,
+ * but REPO is not `repo`), GL_INDEX_ERR_NOMEM
+ */
+gl_index_status_t
+gl_index_parse(const char *text, size_t len, const char *repo, gl_index_t *out)
+{
+ static const char *const top[] = { "REPO", "SERIAL", "VALID_UNTIL", "PKGS", NULL };
+ static const char *const fields[] = { "NAME", "VER", "FILE", "SHA256",
+ "SIZE", "DEPS", "CONFS", NULL };
+
+ if (!out)
+ return GL_INDEX_ERR_FORMAT;
+ memset(out, 0, sizeof(*out));
+ if (!text || !repo || memchr(text, '\0', len) || memchr(text, '@', len))
+ return GL_INDEX_ERR_FORMAT;
+
+ char *buf = malloc(len + 1);
+ if (!buf)
+ return GL_INDEX_ERR_NOMEM;
+ memcpy(buf, text, len);
+ buf[len] = '\0';
+
+ config_t cfg;
+ config_init(&cfg);
+ int ok = config_read_string(&cfg, buf);
+ free(buf);
+ if (!ok) {
+ config_destroy(&cfg);
+ return GL_INDEX_ERR_FORMAT;
+ }
+
+ gl_index_status_t st = GL_INDEX_ERR_FORMAT;
+ config_setting_t *root = config_root_setting(&cfg);
+ const char *r = str_member(root, "REPO");
+ config_setting_t *pkgs = config_setting_get_member(root, "PKGS");
+ if (config_setting_length(root) != 4 || !has_exactly(root, top) ||
+ !r || !gl_valid_pkg_ident(r) ||
+ !int_member(root, "SERIAL", &out->serial) || out->serial <= 0 ||
+ !int_member(root, "VALID_UNTIL", &out->valid_until) ||
+ out->valid_until <= 0 ||
+ config_setting_type(pkgs) != CONFIG_TYPE_LIST)
+ goto fail;
+
+ size_t n = (size_t)config_setting_length(pkgs);
+ if (n && !(out->pkgs = calloc(n, sizeof(*out->pkgs)))) {
+ st = GL_INDEX_ERR_NOMEM;
+ goto fail;
+ }
+ for (size_t i = 0; i < n; i++) {
+ config_setting_t *e = config_setting_get_elem(pkgs, (unsigned)i);
+ gl_index_pkg_t *p = &out->pkgs[i];
+ out->npkgs = i + 1;
+ const char *nm, *ver, *file, *sha;
+ if (!has_exactly(e, fields) ||
+ !(nm = str_member(e, "NAME")) || !gl_valid_pkg_ident(nm) ||
+ !(ver = str_member(e, "VER")) || !valid_index_ver(ver) ||
+ !(file = str_member(e, "FILE")) ||
+ !(sha = str_member(e, "SHA256")) || !valid_sha256_field(sha) ||
+ !int_member(e, "SIZE", &p->size) || p->size < 0)
+ goto fail;
+ char want[PATH_MAX];
+ snprintf(want, sizeof want, "%s-%s.gpkg", nm, ver);
+ if (strcmp(file, want) != 0)
+ goto fail;
+ for (size_t j = 0; j < i; j++)
+ if (strcmp(out->pkgs[j].name, nm) == 0 &&
+ strcmp(out->pkgs[j].ver, ver) == 0)
+ goto fail;
+ if (!(p->name = strdup(nm)) || !(p->ver = strdup(ver)) ||
+ !(p->file = strdup(file)) || !(p->sha256 = strdup(sha))) {
+ st = GL_INDEX_ERR_NOMEM;
+ goto fail;
+ }
+ if (!read_relations(e, "DEPS", &p->deps) ||
+ !read_relations(e, "CONFS", &p->confs))
+ goto fail;
+ }
+ if (strcmp(r, repo) != 0) {
+ st = GL_INDEX_ERR_REPO;
+ goto fail;
+ }
+ if (!(out->repo = strdup(r))) {
+ st = GL_INDEX_ERR_NOMEM;
+ goto fail;
+ }
+ config_destroy(&cfg);
+ return GL_INDEX_OK;
+fail:
+ config_destroy(&cfg);
+ gl_index_free(out);
+ return st;
+}
diff --git a/src/pkg.h b/src/pkg.h
index f93b625..f41bca5 100644
--- a/src/pkg.h
+++ b/src/pkg.h
@@ -7,6 +7,7 @@
#include
#include
#include
+#include
#include
#include "common.h"
@@ -43,44 +44,57 @@ typedef struct {
}
gl_pkg_t;
-extern dep_t null_dep;
-extern conf_t null_conf;
-extern gl_pkg_t empty;
-
-semver_t gl_pkgver(int major, int minor, int patch);
semver_t gl_parse_semver(const char *str);
void gl_free_semver(semver_t *v);
-
-gl_pkg_t *gl_spawn_pkg(
- char *name, semver_t *ver, char *desc,
- char *maint, char *copy, char *repo,
- dep_t *dep, conf_t *conf
-);
-
-void gl_show_pkg_props(const gl_pkg_t *pkg);
void gl_free_pkg(gl_pkg_t *pkg);
-int gl_mkdir(dir_t *dir);
-char *gl_cat_dir(const dir_t *a, const dir_t *b);
-char *gl_cat_dir_and_ver(const dir_t *base, const gl_pkg_t *pkg);
-dir_t gl_return_dir(const dir_t *a, const dir_t *b, mode_t mode);
-dir_t gl_return_dir_and_ver(const dir_t *base, gl_pkg_t *pkg, mode_t mode);
-void gl_free_path(char *path);
-void gl_free_dir(dir_t *dir);
-
typedef enum {
GL_CONF_OK = 0,
GL_CONF_ERR_READ = 1,
GL_CONF_ERR_MISSING = 2,
- GL_CONF_ERR_TYPE = 3
+ GL_CONF_ERR_TYPE = 3,
+ GL_CONF_ERR_INVALID = 4,
+ GL_CONF_ERR_SPACE = 5
} gl_conf_status_t;
-#define GL_CONF_PATH "/etc/glacier/glacier.conf"
gl_conf_status_t gl_conf_get_int(const char *path, const char *key, int *out);
+gl_conf_status_t gl_conf_repo_keys(const char *path, const char *repo,
+ char *out, size_t outlen);
+gl_conf_status_t gl_conf_repos(const char *path, char *out, size_t outlen);
+
+/* A repository index (REPO_DESIGN.txt section 4), as gl_index_parse
+ * reads it. deps/confs are "NAME VER\n" lines ("" for none). */
+typedef struct {
+ char *name;
+ char *ver;
+ char *file;
+ char *sha256;
+ int64_t size;
+ char *deps;
+ char *confs;
+} gl_index_pkg_t;
+
+typedef struct {
+ char *repo;
+ int64_t serial;
+ int64_t valid_until;
+ size_t npkgs;
+ gl_index_pkg_t *pkgs;
+} gl_index_t;
+
+typedef enum {
+ GL_INDEX_OK = 0,
+ GL_INDEX_ERR_FORMAT = 1,
+ GL_INDEX_ERR_REPO = 2,
+ GL_INDEX_ERR_NOMEM = 3
+} gl_index_status_t;
+
+gl_index_status_t gl_index_parse(const char *text, size_t len,
+ const char *repo, gl_index_t *out);
+void gl_index_free(gl_index_t *idx);
bool gl_valid_pkg_ident(const char *s);
int gl_gpm2gpkg(const char *filename, gl_pkg_t *pkg);
-char *join_with_cwd(const char *path);
#endif
diff --git a/src/runtime.c b/src/runtime.c
deleted file mode 100644
index 35c9de5..0000000
--- a/src/runtime.c
+++ /dev/null
@@ -1,45 +0,0 @@
-#define _POSIX_C_SOURCE 200809L
-#define _DEFAULT_SOURCE
-#define _XOPEN_SOURCE 700
-
-#include
-#include
-#include
-
-#include "color.h"
-
-bool
-lg_is_root(void)
-{
- if (getuid() != 0) {
- return false;
- }
- else if (getuid() == 0) {
- return true;
- }
- else {
- return false;
- }
-}
-
-bool
-lg_runtime_exists(void)
-{
- const char *rt_files[] = {
- "/etc/glacier/glacier.conf",
- "/etc/glacier/make.conf"
- };
-
- size_t num_of_files = sizeof(rt_files) / sizeof(rt_files[0]);
-
- for (size_t f_index = 0; f_index < num_of_files; f_index++) {
- if (access(rt_files[f_index], F_OK) == 0) {
- continue;
- }
- else {
- return false;
- }
- }
-
- return true;
-}
diff --git a/src/sign.c b/src/sign.c
new file mode 100644
index 0000000..6b0263c
--- /dev/null
+++ b/src/sign.c
@@ -0,0 +1,428 @@
+/*
+ * gl_sign: Ed25519 signatures in signify's file formats. See sign.h.
+ *
+ * Parsing is strict: exactly two lines, the "untrusted comment: "
+ * prefix, canonical base64 of exactly the expected length, the right
+ * algorithm bytes. Anything else is GL_SIGN_ERR_FORMAT.
+ */
+
+#define _POSIX_C_SOURCE 200809L
+#define _DEFAULT_SOURCE
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "sign.h"
+#include "monocypher/monocypher.h"
+#include "monocypher/monocypher-ed25519.h"
+
+#define COMMENT_PREFIX "untrusted comment: "
+#define COMMENT_PREFIX_LEN (sizeof COMMENT_PREFIX - 1)
+
+/* Decoded sizes of the three kinds of file. */
+#define PUBKEY_BYTES 42
+#define SIG_BYTES 74
+#define SECKEY_BYTES 104
+
+/* A key or signature file is a few hundred bytes at most. */
+#define MAX_KEYFILE (COMMENT_PREFIX_LEN + GL_SIGN_MAX_COMMENT + 1 + 4 * ((SECKEY_BYTES + 2) / 3) + 1)
+
+/* Reads all of a regular file (at most `max` bytes) into a malloc'd
+ * buffer. */
+static int
+read_all(const char *path, size_t max, uint8_t **out, size_t *len)
+{
+ int fd = open(path, O_RDONLY | O_CLOEXEC);
+ if (fd < 0)
+ return GL_SIGN_ERR_IO;
+
+ struct stat st;
+ if (fstat(fd, &st) != 0 || !S_ISREG(st.st_mode)) {
+ close(fd);
+ return GL_SIGN_ERR_IO;
+ }
+ if (st.st_size < 0 || (uintmax_t)st.st_size > max) {
+ close(fd);
+ return GL_SIGN_ERR_TOOBIG;
+ }
+
+ size_t size = (size_t)st.st_size;
+ uint8_t *buf = malloc(size ? size : 1);
+ if (!buf) {
+ close(fd);
+ return GL_SIGN_ERR_IO;
+ }
+
+ size_t got = 0;
+ while (got < size) {
+ ssize_t n = read(fd, buf + got, size - got);
+ if (n < 0 && errno == EINTR)
+ continue;
+ if (n <= 0)
+ break;
+ got += (size_t)n;
+ }
+ /* The file must not have grown or shrunk under us. */
+ uint8_t extra;
+ ssize_t more = read(fd, &extra, 1);
+ close(fd);
+ if (got != size || more != 0) {
+ free(buf);
+ return GL_SIGN_ERR_IO;
+ }
+
+ *out = buf;
+ *len = size;
+ return GL_SIGN_OK;
+}
+
+static int
+b64_value(uint8_t c)
+{
+ if (c >= 'A' && c <= 'Z') return c - 'A';
+ if (c >= 'a' && c <= 'z') return c - 'a' + 26;
+ if (c >= '0' && c <= '9') return c - '0' + 52;
+ if (c == '+') return 62;
+ if (c == '/') return 63;
+ return -1;
+}
+
+/* Decodes canonical, padded base64 that must yield exactly `outlen`
+ * bytes. Returns 0 on success. */
+static int
+b64_decode_exact(const uint8_t *in, size_t n, uint8_t *out, size_t outlen)
+{
+ if (n != (outlen + 2) / 3 * 4)
+ return -1;
+
+ size_t o = 0;
+ for (size_t i = 0; i < n; i += 4) {
+ uint32_t t = 0;
+ size_t pad = 0;
+ for (size_t j = 0; j < 4; j++) {
+ uint8_t c = in[i + j];
+ int v;
+ if (c == '=') {
+ /* Padding only in the last quartet's last two places. */
+ if (i + 4 != n || j < 2)
+ return -1;
+ pad++;
+ v = 0;
+ } else {
+ if (pad)
+ return -1;
+ v = b64_value(c);
+ if (v < 0)
+ return -1;
+ }
+ t = (t << 6) | (uint32_t)v;
+ }
+ /* Canonical: the bits padding drops must be zero. */
+ if ((pad == 1 && (t & 0xffu)) || (pad == 2 && (t & 0xffffu)))
+ return -1;
+
+ size_t bytes = 3 - pad;
+ if (o + bytes > outlen)
+ return -1;
+ uint8_t b[3] = { (uint8_t)(t >> 16), (uint8_t)(t >> 8), (uint8_t)t };
+ memcpy(out + o, b, bytes);
+ o += bytes;
+ }
+ return o == outlen ? 0 : -1;
+}
+
+static void
+b64_encode(const uint8_t *in, size_t n, char *out)
+{
+ static const char tab[] =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+ size_t o = 0;
+ for (size_t i = 0; i < n; i += 3) {
+ uint32_t t = (uint32_t)in[i] << 16;
+ if (i + 1 < n) t |= (uint32_t)in[i + 1] << 8;
+ if (i + 2 < n) t |= in[i + 2];
+ out[o++] = tab[(t >> 18) & 63];
+ out[o++] = tab[(t >> 12) & 63];
+ out[o++] = i + 1 < n ? tab[(t >> 6) & 63] : '=';
+ out[o++] = i + 2 < n ? tab[t & 63] : '=';
+ }
+ out[o] = '\0';
+}
+
+/* Reads a signify file and decodes its payload into out[0..outlen). */
+static int
+read_signify(const char *path, uint8_t *out, size_t outlen)
+{
+ uint8_t *buf;
+ size_t len;
+ int st = read_all(path, MAX_KEYFILE, &buf, &len);
+ if (st == GL_SIGN_ERR_TOOBIG)
+ return GL_SIGN_ERR_FORMAT;
+ if (st != GL_SIGN_OK)
+ return st;
+
+ st = GL_SIGN_ERR_FORMAT;
+ if (len < COMMENT_PREFIX_LEN || memcmp(buf, COMMENT_PREFIX, COMMENT_PREFIX_LEN) != 0)
+ goto out;
+
+ uint8_t *nl = memchr(buf, '\n', len);
+ if (!nl || memchr(buf, '\0', (size_t)(nl - buf)))
+ goto out;
+ if ((size_t)(nl - buf) - COMMENT_PREFIX_LEN > GL_SIGN_MAX_COMMENT)
+ goto out;
+
+ /* The base64 line runs to a final newline that ends the file. */
+ uint8_t *b64 = nl + 1;
+ uint8_t *end = buf + len;
+ if (b64 >= end || end[-1] != '\n')
+ goto out;
+ if (b64_decode_exact(b64, (size_t)(end - 1 - b64), out, outlen) != 0)
+ goto out;
+ if (out[0] != 'E' || out[1] != 'd')
+ goto out;
+ st = GL_SIGN_OK;
+out:
+ crypto_wipe(buf, len);
+ free(buf);
+ return st;
+}
+
+int
+gl_sign_read_pubkey(const char *path, gl_sign_pubkey_t *out)
+{
+ uint8_t raw[PUBKEY_BYTES];
+ int st = read_signify(path, raw, sizeof raw);
+ if (st != GL_SIGN_OK)
+ return st;
+ memcpy(out->keynum, raw + 2, GL_SIGN_KEYNUM_SIZE);
+ memcpy(out->key, raw + 10, sizeof out->key);
+ return GL_SIGN_OK;
+}
+
+int
+gl_sign_read_sig(const char *path, gl_sign_sig_t *out)
+{
+ uint8_t raw[SIG_BYTES];
+ int st = read_signify(path, raw, sizeof raw);
+ if (st != GL_SIGN_OK)
+ return st;
+ memcpy(out->keynum, raw + 2, GL_SIGN_KEYNUM_SIZE);
+ memcpy(out->sig, raw + 10, sizeof out->sig);
+ return GL_SIGN_OK;
+}
+
+/* sec[0..64) = seed | public key, as signify and Monocypher both lay
+ * it out. */
+static int
+read_seckey(const char *path, uint8_t keynum[GL_SIGN_KEYNUM_SIZE], uint8_t sec[64])
+{
+ uint8_t raw[SECKEY_BYTES];
+ int st = read_signify(path, raw, sizeof raw);
+ if (st != GL_SIGN_OK)
+ goto out;
+
+ st = GL_SIGN_ERR_FORMAT;
+ if (raw[2] != 'B' || raw[3] != 'K')
+ goto out;
+ uint32_t rounds = (uint32_t)raw[4] << 24 | (uint32_t)raw[5] << 16
+ | (uint32_t)raw[6] << 8 | raw[7];
+ if (rounds != 0) {
+ st = GL_SIGN_ERR_ENCRYPTED;
+ goto out;
+ }
+
+ /* checksum = the first 8 bytes of SHA-512(seckey) */
+ uint8_t digest[64];
+ crypto_sha512(digest, raw + 40, 64);
+ int ok = memcmp(digest, raw + 24, 8) == 0;
+ crypto_wipe(digest, sizeof digest);
+ if (!ok)
+ goto out;
+
+ memcpy(keynum, raw + 32, GL_SIGN_KEYNUM_SIZE);
+ memcpy(sec, raw + 40, 64);
+ st = GL_SIGN_OK;
+out:
+ crypto_wipe(raw, sizeof raw);
+ return st;
+}
+
+int
+gl_sign_check_seckey(const char *path)
+{
+ uint8_t keynum[GL_SIGN_KEYNUM_SIZE], sec[64];
+ int st = read_seckey(path, keynum, sec);
+ crypto_wipe(sec, sizeof sec);
+ return st;
+}
+
+int
+gl_sign_verify(const gl_sign_pubkey_t *pub, const gl_sign_sig_t *sig,
+ const uint8_t *msg, size_t len)
+{
+ if (memcmp(pub->keynum, sig->keynum, GL_SIGN_KEYNUM_SIZE) != 0)
+ return GL_SIGN_ERR_KEYNUM;
+ return crypto_ed25519_check(sig->sig, pub->key, msg, len) == 0
+ ? GL_SIGN_OK : GL_SIGN_ERR_BAD;
+}
+
+int
+gl_sign_verify_file(const char *pubkey_path, const char *msg_path,
+ const char *sig_path)
+{
+ gl_sign_pubkey_t pub;
+ gl_sign_sig_t sig;
+ int st = gl_sign_read_pubkey(pubkey_path, &pub);
+ if (st != GL_SIGN_OK)
+ return st;
+ st = gl_sign_read_sig(sig_path, &sig);
+ if (st != GL_SIGN_OK)
+ return st;
+ /* Cheap mismatch first: no need to read the message. */
+ if (memcmp(pub.keynum, sig.keynum, GL_SIGN_KEYNUM_SIZE) != 0)
+ return GL_SIGN_ERR_KEYNUM;
+
+ uint8_t *msg;
+ size_t len;
+ st = read_all(msg_path, GL_SIGN_MAX_MSG, &msg, &len);
+ if (st != GL_SIGN_OK)
+ return st;
+ st = gl_sign_verify(&pub, &sig, msg, len);
+ free(msg);
+ return st;
+}
+
+/* Writes "untrusted comment: COMMENT\n" + base64(raw) + "\n" to an fd
+ * opened with `flags`. */
+static int
+write_signify(const char *path, int flags, mode_t mode, const char *comment,
+ const uint8_t *raw, size_t rawlen)
+{
+ char b64[4 * ((SECKEY_BYTES + 2) / 3) + 1];
+ if (rawlen > SECKEY_BYTES)
+ return GL_SIGN_ERR_WRITE;
+ b64_encode(raw, rawlen, b64);
+
+ int fd = open(path, O_WRONLY | O_CREAT | O_NOFOLLOW | O_CLOEXEC | flags, mode);
+ if (fd < 0)
+ return errno == EEXIST ? GL_SIGN_ERR_EXISTS : GL_SIGN_ERR_WRITE;
+ /* Exactly `mode`, whatever the umask: a secret key 0600, a public
+ * key 0644 (users' gpkg reads the system's keys). */
+ if (fchmod(fd, mode) != 0) {
+ close(fd);
+ return GL_SIGN_ERR_WRITE;
+ }
+
+ FILE *f = fdopen(fd, "w");
+ if (!f) {
+ close(fd);
+ return GL_SIGN_ERR_WRITE;
+ }
+ int bad = fprintf(f, COMMENT_PREFIX "%s\n%s\n", comment, b64) < 0;
+ crypto_wipe(b64, sizeof b64);
+ if (fclose(f) != 0 || bad)
+ return GL_SIGN_ERR_WRITE;
+ return GL_SIGN_OK;
+}
+
+int
+gl_sign_file(const char *seckey_path, const char *msg_path, const char *sig_path)
+{
+ uint8_t keynum[GL_SIGN_KEYNUM_SIZE], sec[64];
+ int st = read_seckey(seckey_path, keynum, sec);
+ if (st != GL_SIGN_OK)
+ return st;
+
+ uint8_t *msg;
+ size_t len;
+ st = read_all(msg_path, GL_SIGN_MAX_MSG, &msg, &len);
+ if (st != GL_SIGN_OK) {
+ crypto_wipe(sec, sizeof sec);
+ return st;
+ }
+
+ uint8_t raw[SIG_BYTES] = { 'E', 'd' };
+ memcpy(raw + 2, keynum, GL_SIGN_KEYNUM_SIZE);
+ crypto_ed25519_sign(raw + 10, sec, msg, len);
+ crypto_wipe(sec, sizeof sec);
+ free(msg);
+
+ /* "verify with NAME.pub", NAME = the secret key's name minus .sec */
+ const char *base = strrchr(seckey_path, '/');
+ base = base ? base + 1 : seckey_path;
+ size_t blen = strlen(base);
+ if (blen > 4 && strcmp(base + blen - 4, ".sec") == 0)
+ blen -= 4;
+ if (blen > GL_SIGN_MAX_COMMENT - 16)
+ blen = GL_SIGN_MAX_COMMENT - 16;
+ char comment[GL_SIGN_MAX_COMMENT + 1];
+ snprintf(comment, sizeof comment, "verify with %.*s.pub", (int)blen, base);
+
+ return write_signify(sig_path, O_TRUNC, 0644, comment, raw, sizeof raw);
+}
+
+static int
+random_bytes(uint8_t *buf, size_t n)
+{
+ while (n > 0) {
+ ssize_t got = getrandom(buf, n, 0);
+ if (got < 0 && errno == EINTR)
+ continue;
+ if (got <= 0)
+ return -1;
+ buf += got;
+ n -= (size_t)got;
+ }
+ return 0;
+}
+
+int
+gl_sign_generate(const char *pubkey_path, const char *seckey_path,
+ const char *comment)
+{
+ if (strchr(comment, '\n') || strlen(comment) > GL_SIGN_MAX_COMMENT - 11)
+ return GL_SIGN_ERR_FORMAT;
+
+ /* rand = seed[32] | keynum[8] | salt[16] */
+ uint8_t rand[56];
+ if (random_bytes(rand, sizeof rand) != 0)
+ return GL_SIGN_ERR_RANDOM;
+
+ uint8_t sec[64], pub[32];
+ crypto_ed25519_key_pair(sec, pub, rand); /* wipes the seed */
+
+ uint8_t sraw[SECKEY_BYTES] = { 'E', 'd', 'B', 'K', 0, 0, 0, 0 };
+ memcpy(sraw + 8, rand + 40, 16); /* salt (unused at 0 rounds) */
+ uint8_t digest[64];
+ crypto_sha512(digest, sec, sizeof sec);
+ memcpy(sraw + 24, digest, 8); /* checksum */
+ memcpy(sraw + 32, rand + 32, GL_SIGN_KEYNUM_SIZE);
+ memcpy(sraw + 40, sec, sizeof sec);
+
+ uint8_t praw[PUBKEY_BYTES] = { 'E', 'd' };
+ memcpy(praw + 2, rand + 32, GL_SIGN_KEYNUM_SIZE);
+ memcpy(praw + 10, pub, sizeof pub);
+
+ char c[GL_SIGN_MAX_COMMENT + 1];
+ snprintf(c, sizeof c, "%s secret key", comment);
+ int st = write_signify(seckey_path, O_EXCL, 0600, c, sraw, sizeof sraw);
+ if (st == GL_SIGN_OK) {
+ snprintf(c, sizeof c, "%s public key", comment);
+ st = write_signify(pubkey_path, O_EXCL, 0644, c, praw, sizeof praw);
+ /* Never leave a secret key without its public half. */
+ if (st != GL_SIGN_OK)
+ unlink(seckey_path);
+ }
+
+ crypto_wipe(rand, sizeof rand);
+ crypto_wipe(sec, sizeof sec);
+ crypto_wipe(sraw, sizeof sraw);
+ crypto_wipe(digest, sizeof digest);
+ return st;
+}
diff --git a/src/sign.h b/src/sign.h
new file mode 100644
index 0000000..6b1f7fe
--- /dev/null
+++ b/src/sign.h
@@ -0,0 +1,82 @@
+#ifndef GL_SIGN_H_
+#define GL_SIGN_H_
+
+/*
+ * gl_sign: Ed25519 signatures in OpenBSD signify's file formats
+ * (REPO_DESIGN.txt section 2). Private to libglacier_verify.so, like
+ * sha256.h: no public header is installed.
+ *
+ * Every file is two lines, "untrusted comment: ..." and base64 of:
+ * public key "Ed" | keynum[8] | pubkey[32]
+ * signature "Ed" | keynum[8] | sig[64]
+ * secret key "Ed" | "BK" | rounds[4, big-endian] | salt[16] |
+ * checksum[8] | keynum[8] | seckey[64]
+ * Only unencrypted secret keys (rounds == 0, `signify -G -n`) are
+ * read; an encrypted one is GL_SIGN_ERR_ENCRYPTED.
+ *
+ * Signatures are pure Ed25519 over the whole message, as signify's, so
+ * `signify -V -p KEY.pub -m FILE` checks what gl_sign_file makes and
+ * gl_sign_verify_file checks what signify makes.
+ */
+
+#include
+#include
+
+#define GL_SIGN_KEYNUM_SIZE 8
+/* Largest message signed or verified: the whole file is read. */
+#define GL_SIGN_MAX_MSG (64u * 1024u * 1024u)
+/* Longest comment signify accepts. */
+#define GL_SIGN_MAX_COMMENT 1024
+
+typedef enum {
+ GL_SIGN_OK = 0,
+ GL_SIGN_ERR_IO = 1, /* a file can't be opened or read */
+ GL_SIGN_ERR_FORMAT = 2, /* not a well-formed file of the expected kind */
+ GL_SIGN_ERR_ENCRYPTED = 3, /* a passphrase-protected secret key */
+ GL_SIGN_ERR_KEYNUM = 4, /* the signature was made by another key */
+ GL_SIGN_ERR_BAD = 5, /* the signature does not verify */
+ GL_SIGN_ERR_TOOBIG = 6, /* message larger than GL_SIGN_MAX_MSG */
+ GL_SIGN_ERR_RANDOM = 7, /* no randomness for a new key */
+ GL_SIGN_ERR_EXISTS = 8, /* a key file to create already exists */
+ GL_SIGN_ERR_WRITE = 9, /* a file to write can't be created or written */
+} gl_sign_status_t;
+
+typedef struct {
+ uint8_t keynum[GL_SIGN_KEYNUM_SIZE];
+ uint8_t key[32];
+} gl_sign_pubkey_t;
+
+typedef struct {
+ uint8_t keynum[GL_SIGN_KEYNUM_SIZE];
+ uint8_t sig[64];
+} gl_sign_sig_t;
+
+int gl_sign_read_pubkey(const char *path, gl_sign_pubkey_t *out);
+/* Reads a secret key only to check it (then wipes it). */
+int gl_sign_check_seckey(const char *path);
+int gl_sign_read_sig(const char *path, gl_sign_sig_t *out);
+
+/* Checks `sig` over msg[0..len) with `pub`: GL_SIGN_ERR_KEYNUM before
+ * any crypto if the keynums differ, else GL_SIGN_OK or GL_SIGN_ERR_BAD. */
+int gl_sign_verify(const gl_sign_pubkey_t *pub, const gl_sign_sig_t *sig,
+ const uint8_t *msg, size_t len);
+
+/* Reads all three files and verifies. The status names the first
+ * problem found, in the order: public key, signature, message, check. */
+int gl_sign_verify_file(const char *pubkey_path, const char *msg_path,
+ const char *sig_path);
+
+/* Signs msg_path with the secret key, writing sig_path (replaced if it
+ * exists, never followed if it's a symlink). The signature's comment
+ * is "verify with NAME.pub", NAME being the secret key's file name
+ * without ".sec", as signify writes it. */
+int gl_sign_file(const char *seckey_path, const char *msg_path,
+ const char *sig_path);
+
+/* Makes a new keypair. Neither file may exist (GL_SIGN_ERR_EXISTS).
+ * The secret key is created 0600 and unencrypted; the comments are
+ * "COMMENT public key" / "COMMENT secret key". */
+int gl_sign_generate(const char *pubkey_path, const char *seckey_path,
+ const char *comment);
+
+#endif
diff --git a/src/transaction.c b/src/transaction.c
index 9245590..668f5e5 100644
--- a/src/transaction.c
+++ b/src/transaction.c
@@ -12,33 +12,76 @@
#include
#include "common.h"
+#include "deps.h"
#include "istoreutils.h"
#include "log.h"
#include "transaction.h"
/* The one directory a scope owns; everything below derives from it.
* See the layout comment in common.h. */
+/* `sysroot` is the install root ("" for the running system, see
+ * gl_context_t). User scope never has one (set_sysroot refuses it). */
static void
-scope_base(gl_ctx_scope_t scope, uid_t uid, char *out)
+scope_base(gl_ctx_scope_t scope, uid_t uid, const char *sysroot, char *out)
{
switch (scope) {
case GL_SCOPE_USR:
gl_usr_base(uid, out, PATH_MAX);
break;
case GL_SCOPE_SYS:
- snprintf(out, PATH_MAX, "%s", GL_SYS_ROOT);
+ snprintf(out, PATH_MAX, "%s%s", sysroot, GL_SYS_ROOT);
break;
}
}
static void
-scope_subdir(gl_ctx_scope_t scope, uid_t uid, const char *name, char *out)
+scope_subdir(gl_ctx_scope_t scope, uid_t uid, const char *sysroot,
+ const char *name, char *out)
{
char base[PATH_MAX];
- scope_base(scope, uid, base);
+ scope_base(scope, uid, sysroot, base);
snprintf(out, PATH_MAX, "%s/%s", base, name);
}
+int
+gl_check_sysroot(const char *sysroot)
+{
+ char root[PATH_MAX];
+ if (gl_normalize_sysroot(sysroot, root, sizeof(root)) != 0) {
+ lg_printf(2, "Refusing install root %s: not an absolute path without . or .. components.",
+ sysroot ? sysroot : "(null)");
+ return -1;
+ }
+ if (root[0] == '\0') { return 0; }
+
+ char bad[PATH_MAX];
+ const char *why = "";
+ if (gl_sysroot_trusted(root, bad, sizeof(bad), &why) != 0) {
+ lg_printf(2, "Refusing install root %s: %s is %s.", root, bad, why);
+ return -1;
+ }
+ return 0;
+}
+
+/* Store the install root (gl_normalize_sysroot). Only system scope may
+ * have one (IMAGE_DESIGN section 3): per-user scope has no business in
+ * an image. */
+static int
+set_sysroot(gl_context_t *ctx, gl_ctx_scope_t scope, const char *sysroot)
+{
+ ctx->sysroot[0] = '\0';
+ if (gl_check_sysroot(sysroot) != 0) {
+ return -1;
+ }
+ gl_normalize_sysroot(sysroot, ctx->sysroot, sizeof(ctx->sysroot));
+ if (scope == GL_SCOPE_USR && ctx->sysroot[0] != '\0') {
+ lg_printf(2, "Refusing a user-scope context with install root %s.", ctx->sysroot);
+ ctx->sysroot[0] = '\0';
+ return -1;
+ }
+ return 0;
+}
+
static int
fsync_dir(const char *path)
{
@@ -67,11 +110,11 @@ atomic_swap(const char *live, const char *stage, const char *backup)
}
static void
-derive_base_paths(gl_ctx_scope_t scope, uid_t uid, char *index_out,
- char *store_out, char *links_out)
+derive_base_paths(gl_ctx_scope_t scope, uid_t uid, const char *sysroot,
+ char *index_out, char *store_out, char *links_out)
{
- scope_subdir(scope, uid, "index", index_out);
- scope_subdir(scope, uid, "store", store_out);
+ scope_subdir(scope, uid, sysroot, "index", index_out);
+ scope_subdir(scope, uid, sysroot, "store", store_out);
/* For system scope this is the DEFAULT links location — for
* packages the operator explicitly installs under system scope that
* aren't part of the base system. Base-repo packages get routed to
@@ -79,7 +122,7 @@ derive_base_paths(gl_ctx_scope_t scope, uid_t uid, char *index_out,
* gl_link_pkg/gl_relink_store (which know a package's repo), not
* something the context can decide up front. See GL_BASE_REPO in
* istoreutils.c. */
- scope_subdir(scope, uid, "links", links_out);
+ scope_subdir(scope, uid, sysroot, "links", links_out);
}
static int
@@ -118,7 +161,9 @@ hardlink_tree(const char *src, const char *dest)
if(lstat(s, &st) != 0) { continue; }
if (S_ISDIR(st.st_mode)) {
- if (mkdir(d, st.st_mode & 0777) != 0 && errno != EEXIST) {
+ /* Exact mode, not umask-filtered: the stage becomes
+ * live, so seeding must not tighten what it copies. */
+ if (gl_mkdir_exact(d, st.st_mode & 07777) != 0) {
closedir(dir);
return -1;
}
@@ -148,6 +193,13 @@ hardlink_tree(const char *src, const char *dest)
int
gl_init_live_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid)
+{
+ return gl_init_live_context_at(ctx, scope, uid, "/");
+}
+
+int
+gl_init_live_context_at(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid,
+ const char *sysroot)
{
if (!ctx) {
return -1;
@@ -158,16 +210,28 @@ gl_init_live_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid)
ctx->scope = scope;
ctx->lock_fd = -1;
ctx->overwrite = 0;
+ ctx->link_refusals = 0;
+ ctx->strict = 0;
+ if (set_sysroot(ctx, scope, sysroot) != 0) {
+ return -1;
+ }
- strncpy(ctx->root_path, "/glacier", PATH_MAX - 1);
- derive_base_paths(scope, uid, ctx->index_path, ctx->store_path,
- ctx->links_path);
+ snprintf(ctx->root_path, PATH_MAX, "%s/glacier", ctx->sysroot);
+ derive_base_paths(scope, uid, ctx->sysroot, ctx->index_path,
+ ctx->store_path, ctx->links_path);
return 0;
}
gl_txn_status_t
gl_init_stage_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid)
+{
+ return gl_init_stage_context_at(ctx, scope, uid, "/");
+}
+
+gl_txn_status_t
+gl_init_stage_context_at(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid,
+ const char *sysroot)
{
if (!ctx) {
return GL_TXN_ERR_NO_TXN;
@@ -178,8 +242,13 @@ gl_init_stage_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid)
ctx->scope = scope;
ctx->lock_fd = -1;
ctx->overwrite = 0;
+ ctx->link_refusals = 0;
+ ctx->strict = 0;
+ if (set_sysroot(ctx, scope, sysroot) != 0) {
+ return GL_TXN_ERR_PERM;
+ }
- strncpy(ctx->root_path, "/glacier", PATH_MAX - 1);
+ snprintf(ctx->root_path, PATH_MAX, "%s/glacier", ctx->sysroot);
if (scope == GL_SCOPE_USR) {
switch (gl_usr_base_check(uid)) {
@@ -200,7 +269,7 @@ gl_init_stage_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid)
}
char stage_base[PATH_MAX - 7];
- scope_subdir(scope, uid, "stage", stage_base);
+ scope_subdir(scope, uid, ctx->sysroot, "stage", stage_base);
snprintf(ctx->index_path, PATH_MAX, "%s/index", stage_base);
snprintf(ctx->store_path, PATH_MAX, "%s/store", stage_base);
@@ -226,17 +295,23 @@ gl_init_stage_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid)
}
}
- if (gl_mkdirp(ctx->index_path, 0700) != 0) {
+ /* chmod, not just mkdir's mode: the stage becomes live on commit,
+ * and a system store must be 0755 whatever syspkg's umask is. */
+ mode_t dir_mode = gl_scope_dir_mode(scope);
+ if (gl_mkdirp(ctx->index_path, dir_mode) != 0 ||
+ chmod(ctx->index_path, dir_mode) != 0) {
gl_abort_transaction(ctx);
return GL_TXN_ERR_MKDIR;
}
- if (gl_mkdirp(ctx->store_path, 0700) != 0) {
+ if (gl_mkdirp(ctx->store_path, dir_mode) != 0 ||
+ chmod(ctx->store_path, dir_mode) != 0) {
gl_abort_transaction(ctx);
return GL_TXN_ERR_MKDIR;
}
- if (gl_mkdirp(ctx->links_path, 0700) != 0) {
+ if (gl_mkdirp(ctx->links_path, dir_mode) != 0 ||
+ chmod(ctx->links_path, dir_mode) != 0) {
gl_abort_transaction(ctx);
return GL_TXN_ERR_MKDIR;
}
@@ -247,7 +322,7 @@ gl_init_stage_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid)
}
char live_index[PATH_MAX], live_store[PATH_MAX], live_links[PATH_MAX];
- derive_base_paths(scope, uid, live_index, live_store, live_links);
+ derive_base_paths(scope, uid, ctx->sysroot, live_index, live_store, live_links);
/* live_links is populated (derive_base_paths always fills all
* three) but intentionally unused below — see the comment on the
* hardlink_tree call. */
@@ -275,6 +350,85 @@ gl_init_stage_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid)
return GL_TXN_OK;
}
+/* parent/name into out (PATH_MAX), created with `mode` if missing, then
+ * refused unless it is a real directory owned by `owner` with exactly
+ * that mode. 0, or -1 having printed why. */
+static int
+checked_subdir(const char *parent, const char *name, uid_t owner, mode_t mode,
+ char *out)
+{
+ struct stat st;
+ snprintf(out, PATH_MAX, "%s/%s", parent, name);
+ if (gl_mkdir_exact(out, mode) != 0 && errno != EEXIST) {
+ lg_printf(2, "Cannot create %s: %s", out, strerror(errno));
+ return -1;
+ }
+ if (lstat(out, &st) != 0 || !S_ISDIR(st.st_mode) || st.st_uid != owner ||
+ (st.st_mode & 07777) != mode) {
+ lg_printf(2, "Refusing %s: not a directory owned by UID %u with mode %o.",
+ out, owner, (unsigned)mode);
+ return -1;
+ }
+ return 0;
+}
+
+/*
+ * /NAME: a directory for state a scope keeps beside its index and
+ * store ("repos": the accepted repository indexes, REPO_DESIGN.txt
+ * section 5). The base must already exist and be the scope's (owned by
+ * root, or by the uid for user scope; no group/world write). NAME is
+ * created if missing with the scope's mode, and is then refused unless
+ * it is a real directory owned like the base with exactly that mode.
+ * The path goes to out (PATH_MAX). 0, or -1 having printed why.
+ */
+int
+gl_scope_state_dir(const gl_context_t *ctx, const char *name, char *out)
+{
+ if (!ctx || !out || !gl_valid_pkg_ident(name)) {
+ return -1;
+ }
+ uid_t owner = ctx->scope == GL_SCOPE_SYS ? 0 : ctx->uid;
+ mode_t mode = gl_scope_dir_mode(ctx->scope);
+
+ char base[PATH_MAX];
+ scope_base(ctx->scope, ctx->uid, ctx->sysroot, base);
+ struct stat st;
+ if (ctx->scope == GL_SCOPE_USR && gl_usr_base_check(ctx->uid) != GL_USR_BASE_OK) {
+ lg_printf(2, "Cannot open %s: not a private directory owned by UID %u.",
+ base, ctx->uid);
+ return -1;
+ }
+ if (lstat(base, &st) != 0) {
+ lg_printf(2, "Cannot open %s: %s", base, strerror(errno));
+ return -1;
+ }
+ if (!S_ISDIR(st.st_mode) || st.st_uid != owner || (st.st_mode & 022)) {
+ lg_printf(2, "Refusing %s: not a directory owned by UID %u, or group/world-writable.",
+ base, owner);
+ return -1;
+ }
+
+ return checked_subdir(base, name, owner, mode, out);
+}
+
+/*
+ * /cache/REPO: where packages fetched from REPO are downloaded
+ * (REPO_DESIGN.txt section 5). Both levels are made and checked as
+ * gl_scope_state_dir makes and checks NAME. 0, or -1 having printed why.
+ */
+int
+gl_scope_cache_dir(const gl_context_t *ctx, const char *repo, char *out)
+{
+ char cache[PATH_MAX];
+ if (!ctx || !out || !gl_valid_pkg_ident(repo) ||
+ gl_scope_state_dir(ctx, "cache", cache) != 0) {
+ return -1;
+ }
+ return checked_subdir(cache, repo,
+ ctx->scope == GL_SCOPE_SYS ? 0 : ctx->uid,
+ gl_scope_dir_mode(ctx->scope), out);
+}
+
gl_txn_status_t
gl_begin_transaction(gl_context_t *ctx)
{
@@ -283,7 +437,7 @@ gl_begin_transaction(gl_context_t *ctx)
}
char lock_path[PATH_MAX];
- scope_subdir(ctx->scope, ctx->uid, "lock", lock_path);
+ scope_subdir(ctx->scope, ctx->uid, ctx->sysroot, "lock", lock_path);
int fd = open(lock_path, O_WRONLY | O_CREAT, 0600);
if (fd < 0) {
@@ -309,8 +463,19 @@ gl_commit_transaction(gl_context_t *ctx)
return GL_TXN_ERR_NO_TXN;
}
+ /* Dependencies and conflicts (ROADMAP 2.3). The CLIs check the plan
+ * before prompting; this holds every other caller of the library to
+ * the same rules. gl_check_txn reports each problem itself. */
+ int dep_problems = gl_check_txn(ctx);
+ if (dep_problems != 0) {
+ if (dep_problems < 0) {
+ lg_printf(2, "Cannot read the package store to check dependencies.");
+ }
+ return GL_TXN_ERR_DEPS;
+ }
+
char stage_base[PATH_MAX];
- scope_subdir(ctx->scope, ctx->uid, "stage", stage_base);
+ scope_subdir(ctx->scope, ctx->uid, ctx->sysroot, "stage", stage_base);
int same = check_same_filesystem(ctx->root_path, stage_base);
if (same < 0) {
@@ -322,12 +487,12 @@ gl_commit_transaction(gl_context_t *ctx)
/* Derive live paths the same way gl_init_live_context would */
char live_index[PATH_MAX], live_store[PATH_MAX], live_links[PATH_MAX];
- derive_base_paths(ctx->scope, ctx->uid, live_index, live_store,
- live_links);
+ derive_base_paths(ctx->scope, ctx->uid, ctx->sysroot, live_index,
+ live_store, live_links);
/* Derive old/backup paths */
char old_base[PATH_MAX];
- scope_subdir(ctx->scope, ctx->uid, "old", old_base);
+ scope_subdir(ctx->scope, ctx->uid, ctx->sysroot, "old", old_base);
char old_index[PATH_MAX], old_store[PATH_MAX], old_links[PATH_MAX];
snprintf(old_index, PATH_MAX, "%s/index", old_base);
@@ -363,18 +528,22 @@ gl_commit_transaction(gl_context_t *ctx)
/* fsync the directory the renames happened in */
char base[PATH_MAX];
- scope_base(ctx->scope, ctx->uid, base);
+ scope_base(ctx->scope, ctx->uid, ctx->sysroot, base);
fsync_dir(base);
if (ctx->scope == GL_SCOPE_SYS) {
- fsync_dir("/usr"); /* base-repo packages land here instead */
+ char usr[PATH_MAX];
+ snprintf(usr, sizeof(usr), "%s/usr", ctx->sysroot);
+ fsync_dir(usr); /* base-repo packages land here instead */
}
gl_context_t live_ctx;
- gl_init_live_context(&live_ctx, ctx->scope, ctx->uid);
+ gl_init_live_context_at(&live_ctx, ctx->scope, ctx->uid,
+ ctx->sysroot[0] ? ctx->sysroot : "/");
if (gl_relink_store(&live_ctx) != 0) {
lg_printf(2, "Post-commit relink incomplete for uid %u", ctx->uid);
}
+ ctx->link_refusals = live_ctx.link_refusals;
gl_rmdir_recursive(old_base);
@@ -390,13 +559,15 @@ gl_commit_transaction(gl_context_t *ctx)
gl_txn_status_t
gl_abort_transaction(gl_context_t *ctx)
{
- if (!ctx) {
+ /* Only the lock holder's stage is its own to remove: without the
+ * lock, this would wipe another process's running transaction. */
+ if (!ctx || ctx->lock_fd < 0) {
return GL_TXN_ERR_NO_TXN;
}
/* Remove stage area — live is untouched */
char stage_uid[PATH_MAX];
- scope_subdir(ctx->scope, ctx->uid, "stage", stage_uid);
+ scope_subdir(ctx->scope, ctx->uid, ctx->sysroot, "stage", stage_uid);
gl_rmdir_recursive(stage_uid); /* best-effort; ignore errors */
diff --git a/src/transaction.h b/src/transaction.h
index fd61fca..63eb3f9 100644
--- a/src/transaction.h
+++ b/src/transaction.h
@@ -5,6 +5,8 @@
#include
#include
+#include
+#include
#include
#define GL_UID_NONE ((uid_t)0)
@@ -19,6 +21,22 @@ typedef enum {
GL_SCOPE_SYS
} gl_ctx_scope_t;
+/* Modes for what glacier creates in a scope. System scope is
+ * world-readable, like /usr: its store is where /usr's links point, so
+ * every user must be able to traverse it, and user-scope dependency
+ * checks read its index. User scope stays private to its uid. */
+static inline mode_t
+gl_scope_dir_mode(gl_ctx_scope_t scope)
+{
+ return scope == GL_SCOPE_SYS ? 0755 : 0700;
+}
+
+static inline mode_t
+gl_scope_file_mode(gl_ctx_scope_t scope)
+{
+ return scope == GL_SCOPE_SYS ? 0644 : 0600;
+}
+
typedef struct {
uid_t uid;
@@ -37,8 +55,53 @@ typedef struct {
* existing non-glacier files at their link destinations; see
* gl_link_pkg. */
int overwrite;
+
+ /* Install root (IMAGE_DESIGN section 3), set by the *_at init
+ * functions: "" for the running system, else e.g. "/mnt/everest".
+ * The paths above are INSTALL paths, already under it: every
+ * filesystem operation uses them as they are. What a path MEANS
+ * once the image is booted (a symlink target, a .displaced key)
+ * is its runtime path, gl_runtime_path(). System scope only. */
+ char sysroot[PATH_MAX];
+
+ /* Paths the link pass refused to link (a foreign file in the
+ * way, say), counted only for packages installed in this
+ * transaction. Zeroed by the init functions; after
+ * gl_commit_transaction, the post-commit relink's count. A
+ * non-zero count means the transaction committed but did not
+ * fully succeed. */
+ int link_refusals;
+
+ /* Set by the caller after gl_init_*_context (which zero it).
+ * Non-zero: a path the link pass would refuse (a foreign file in
+ * the way, say) fails the transaction before anything is
+ * committed, instead of being counted in link_refusals after.
+ * See deps.h. */
+ int strict;
} gl_context_t;
+/* An install path's runtime meaning: sysroot stripped. A path not under
+ * the sysroot is returned unchanged. */
+static inline const char *
+gl_runtime_path(const gl_context_t *ctx, const char *install_path)
+{
+ size_t n = strlen(ctx->sysroot);
+ if (n > 0 && strncmp(install_path, ctx->sysroot, n) == 0 &&
+ install_path[n] == '/') {
+ return install_path + n;
+ }
+ return install_path;
+}
+
+/* The install path of a runtime path (a symlink's absolute target, say):
+ * sysroot prepended. Returns -1 if it doesn't fit. */
+static inline int
+gl_at(const gl_context_t *ctx, const char *runtime_path, char *out, size_t outsz)
+{
+ int n = snprintf(out, outsz, "%s%s", ctx->sysroot, runtime_path);
+ return (n < 0 || (size_t)n >= outsz) ? -1 : 0;
+}
+
typedef enum {
GL_TXN_OK = 0, /* success */
GL_TXN_ERR_LOCK = 1, /* unable to acquire lock */
@@ -48,13 +111,25 @@ typedef enum {
GL_TXN_ERR_SYNC = 5, /* fsync failed */
GL_TXN_ERR_CLEANUP = 6, /* cleanup failed */
GL_TXN_ERR_NO_TXN = 7, /* aborted without beginning */
- GL_TXN_ERR_XDEV = 8 /* stage and live on different filesystems */
+ GL_TXN_ERR_XDEV = 8, /* stage and live on different filesystems */
+ GL_TXN_ERR_DEPS = 9 /* unmet dependency, conflict, file collision,
+ * or (strict) a link refusal: see deps.h */
} gl_txn_status_t;
+/* Is `sysroot` acceptable as an install root (absolute, no . or ..,
+ * trusted: see gl_sysroot_trusted)? Logs why not. 0 if so. "/" is. */
+int gl_check_sysroot(const char *sysroot);
+
int gl_init_live_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid);
+int gl_init_live_context_at(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid,
+ const char *sysroot);
gl_txn_status_t gl_init_stage_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid);
+gl_txn_status_t gl_init_stage_context_at(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid,
+ const char *sysroot);
gl_txn_status_t gl_begin_transaction(gl_context_t *ctx);
+int gl_scope_state_dir(const gl_context_t *ctx, const char *name, char *out);
+int gl_scope_cache_dir(const gl_context_t *ctx, const char *repo, char *out);
gl_txn_status_t gl_commit_transaction(gl_context_t *ctx);
gl_txn_status_t gl_abort_transaction(gl_context_t *ctx);
diff --git a/src/x_log b/src/x_log
deleted file mode 100755
index 2550482..0000000
Binary files a/src/x_log and /dev/null differ
diff --git a/src/x_pkg b/src/x_pkg
deleted file mode 100755
index 09e71be..0000000
Binary files a/src/x_pkg and /dev/null differ
diff --git a/test_backup b/test_backup
deleted file mode 100755
index 941572c..0000000
Binary files a/test_backup and /dev/null differ