Herald Package Manager

Herald is Aegis’s package manager. It installs, lists, and removes software the Aegis way: every package is cryptographically verified before a single byte is written, and the authority to install software into the system is an unforgeable kernel capability — not something even uid 0 holds by default.

Herald has two halves:

  • herald — the on-device client (/bin/herald), a static musl binary that verifies, installs, and removes packages, and fetches them from a repository.
  • Chancery — the host-side tool that builds and signs a repository. It runs on a developer’s machine, not on Aegis, and produces a static, signed file tree that any web server can host.

The repository format and trust model deliberately mirror Debian’s: one signature over a top-level Release file anchors a hash chain down to every package. Aegis ships the repository management software (Chancery) as a first-class tool rather than leaving it to ad-hoc scripts.

Packages: the .hpkg format

A package is an uncompressed POSIX ustar archive with the extension .hpkg. There is intentionally no compression: Aegis has no in-kernel inflate, and a package’s payload (a single application bundle) is small. A .hpkg contains:

manifest                 # ini, MUST be the first entry
apps/<id>/<exec>         # the application ELF (mode 0755)
apps/<id>/app.ini        # name= / exec= (the /apps bundle descriptor)

The manifest is a simple key=value ini:

Key Required Meaning
id yes bundle id; the install name and the /apps/<id> dir
name yes human-readable display name
version yes package version (x.y.z)
exec yes ELF basename inside apps/<id>/
arch no target architecture (default x86_64)
caps no capabilities the program needs (space-separated)
depends no other package ids this one needs

Installation extracts the bundle into the /apps tree, so an installed package is indistinguishable from a built-in application: it appears in the launcher and dock, and is resolved by the same glyph_apps_scan/glyph_apps_find machinery as everything else.

Herald binds extraction to the package’s own bundle directory — every archive entry must be the manifest or live under apps/<id>/. A package therefore cannot write into another bundle, into /etc, or anywhere else under /, even though it is being unpacked as root.

Herald also enforces that exec equals id. Because the kernel’s /etc/aegis/caps.d/ policy files are keyed by the installed binary’s basename, allowing a package to set a different exec would let it write a policy file for another binary (e.g., id=evil exec=reboot clobbers /etc/aegis/caps.d/reboot). The exec == id invariant ensures a package can only own its own cap policy. Every legitimate package already satisfies this; Chancery enforces it when building .hpkg files.

Trust model

Cryptography is ECDSA P-256 over SHA-256, implemented with the in-tree BearSSL (br_ecdsa_i31_vrfy_asn1). Signatures are ASN.1 DER; public keys are the 65-byte uncompressed point. Herald embeds a single trust anchor — the repository’s public key — compiled into the binary as a C array, so an attacker who already has filesystem write access cannot swap it on disk.

There are two verification paths:

  • Local file install (herald install foo.hpkg) verifies a detached per-package signature (foo.hpkg.sig) against the trust anchor.
  • Repository install (herald install <name>) uses the Debian-style signed-metadata chain: the package’s SHA-256 is pinned by a signed Packages list, which is itself pinned and signed by Release. No per-package signature is needed — authenticating the metadata authenticates the package.

Capability enforcement

The authority to install is the kernel capability CAP_KIND_INSTALL (kind 17). The kernel enforces it on every filesystem mutation under /apps/ and /etc/aegis/: creating, writing, renaming, chmod-ing, symlink-ing, or unlinking anything in those trees from a ring-3 process requires the capability, in addition to the usual VFS_WRITE and DAC checks. A root shell without CAP_KIND_INSTALL gets EPERM:

$ touch /apps/evil
touch: /apps/evil: Operation not permitted

Crucially, the check runs on the resolved inode, not the path string — mirroring the /etc/shadow gate in vfs_open. The ext2 resolver records the inodes of /apps and /etc/aegis at mount, and the enforcement flags any resolution that passes through them. This defeats the obvious bypasses:

ln -s /apps /tmp/x
echo evil > /tmp/x/settings/settings   # still EPERM — resolves into /apps

Symlinks, .., //, and operations on the bare directory names are all caught because they all resolve to the protected inodes. Herald receives CAP_KIND_INSTALL as an admin-tier capability via /etc/aegis/caps.d/herald, meaning it is granted only inside an authenticated session. herald install therefore requires a real login; an unauthenticated context fails closed with a clear message.

Capability allow-list

A package’s caps field is written into the policy engine as that program’s capability grant. To stop a package from granting itself — or, via the basename-keyed policy, another binary — dangerous authority, Herald enforces an allow-list at install time. A package may request only:

FB  IPC  NET_SOCKET  PROC_READ  THREAD_CREATE  VFS_OPEN  VFS_READ  VFS_WRITE

Anything else (POWER, INSTALL, DISK_ADMIN, SETUID, NET_ADMIN, the CAP_* delegation caps, …) causes Herald to refuse the install outright. The list is an allow-list, not a deny-list, so unknown or future capabilities default to denied.

Client commands

herald sync                    fetch + verify repository metadata
herald search <term>           search the synced package lists
herald install <name>          install from a repository (with dependencies)
herald install <file.hpkg>     install a local package file
herald verify  <file.hpkg>     check a package's signature only
herald info    <file.hpkg|id>  show package / installed info
herald list                    list installed packages
herald remove  <id>            uninstall a package

Local state lives under /var/lib/herald/:

  • db — the installed-package database (one TAB-separated record per package).
  • lists/ — verified Release / Packages metadata, per source.
  • cache/ — downloaded .hpkg files awaiting install.

Repositories

Herald subscribes to repositories through a Debian-style /etc/herald/sources.list, one source per line:

# <base-url> <suite> <component>
https://repo.example.com  stable  main

herald sync

For each source, sync:

  1. Fetches dists/<suite>/Release and Release.sig.
  2. Verifies the Release signature against the embedded trust anchor. A failure aborts the source — an untrusted repository is rejected.
  3. Reads, from the now-trusted Release, the expected SHA-256 of the <component>/binary-<arch>/Packages list for Herald’s architecture.
  4. Fetches that Packages list and verifies its hash matches.
  5. Caches the verified list under /var/lib/herald/lists/.

Downloads are performed by forking /bin/curl, which carries the NET_SOCKET capability; Herald itself holds no network capability. Because every artifact is signature- or hash-verified after the fetch, the transport does not have to be trusted — plain HTTP works as safely as HTTPS.

herald install <name>

Repository install resolves a full set, then installs it atomically per package:

  1. Dependency resolution. Herald walks the depends graph depth-first, skipping packages already installed or already queued, and builds an install order with dependencies first. A dependency that is neither installed nor present in any synced list is a hard error.
  2. Download + verify. Each package is downloaded from its source’s pool/, and its SHA-256 is checked against the value pinned by the signed Packages list. A mismatch — a substituted or rolled-back package — is refused.
  3. Install. The bundle is extracted, the cap policy written, and the package recorded in the database — the same code path as a local install, reached only after the package is authenticated.

Architecture is carried end to end: Herald requests binary-x86_64/Packages and ignores stanzas for other architectures, so a single repository can serve multiple Aegis targets.

Chancery: building a repository

Chancery is the host-side repository manager — the analogue of Debian’s reprepro/aptly. It is a Rust tool that manages files and signs metadata; it does not serve the repository. The published tree is static and can be hosted by nginx, Caddy, object storage, GitHub Pages, or a CDN. (A built-in chancery serve provides a static server for local testing only.)

chancery init <dir> [--import-key <key>]   scaffold a repo + signing key
chancery add <pkg.hpkg> --suite stable --component main
chancery remove <name> [--version V] [--suite S]
chancery promote <name> <from-suite> <to-suite>
chancery publish                            regenerate + sign all metadata
chancery list
chancery key [--header]                     export the trust anchor
chancery serve [--port N]                   dev static server

A published repository has the Debian layout:

pool/<p>/<name>/<name>_<version>_<arch>.hpkg     # the packages
dists/<suite>/Release                            # signed top metadata
dists/<suite>/Release.sig                         #   (detached ECDSA-P256)
dists/<suite>/<component>/binary-<arch>/Packages  # per-arch stanzas
key.pub                                          # public trust anchor

Release records the suite, components, architectures, date, and the SHA-256 of each Packages file. Each Packages stanza carries the fields Herald needs — Package, Version, Architecture, Depends, Filename, Size, SHA256, Display-Name, Exec, Caps. Signing once over Release therefore authenticates the entire repository.

chancery init --import-key lets a repository sign with an existing key — for example the key whose public half is already embedded in herald, so that local-file installs and repository installs share a single trust anchor. chancery key --header emits that anchor as a C header ready to compile into the client.

Security properties summary

  • No unsigned software is ever installed. Local packages need a valid detached signature; repository packages need a valid signed-metadata chain.
  • No ambient install authority. Mutating the system app/config trees requires CAP_KIND_INSTALL, enforced on the resolved inode against symlink and path-traversal bypasses, and granted only to herald in an authenticated session.
  • Packages cannot escalate. The capability allow-list refuses any package requesting privileged capabilities, and extraction is confined to the package’s own /apps/<id> bundle.
  • No trusted transport required. Verification happens after download, so the network is never part of the trusted computing base.