Herald Package Manager
Herald — Aegis's capability-gated package manager — and Chancery, its Debian-style signed repository system: the .hpkg format, the ECDSA-P256 trust chain, dependency resolution, and kernel-enforced CAP_KIND_INSTALL.
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 signedPackageslist, which is itself pinned and signed byRelease. 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/— verifiedRelease/Packagesmetadata, per source.cache/— downloaded.hpkgfiles 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:
- Fetches
dists/<suite>/ReleaseandRelease.sig. - Verifies the Release signature against the embedded trust anchor. A failure aborts the source — an untrusted repository is rejected.
- Reads, from the now-trusted
Release, the expected SHA-256 of the<component>/binary-<arch>/Packageslist for Herald’s architecture. - Fetches that
Packageslist and verifies its hash matches. - 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:
- Dependency resolution. Herald walks the
dependsgraph 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. - Download + verify. Each package is downloaded from its source’s
pool/, and its SHA-256 is checked against the value pinned by the signedPackageslist. A mismatch — a substituted or rolled-back package — is refused. - 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 toheraldin 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.