Operational procedures for Loran maintainers. This document is the canonical reference for the trust-pinned publisher key lifecycle — how the upstream catalog is built and signed (§2), how to plan a normal rotation, run a parallel-key transition window, and recover from an emergency compromise.
It resolves PRD Open Question 1 ("How do we rotate the publisher's signing key?") and operationalises Spec §2 decision #6 (minisign + ed25519 over a trust-pinned key baked into the binary).
- Loran release engineers: every section.
- Spacecraft Software Operations: §4 (emergency rotation).
- Downstream operators (Bravais OS, Ferrite OS, …): §1.4 reading list so an in-the-wild compromise is recognisable from the user side.
Every Loran release embeds the trust-pinned minisign public key(s)
directly in the binary via include_str!. The set is exposed as the
UpdateOpts::public_keys: Vec<String> field in loran-core and is
consulted by signing::verify_any during every loran update.
For releases that ship a single active publisher key, the slice is
length-one. For rotation windows, the slice contains both the
outgoing and incoming keys — verify_any accepts a tarball whose
signature matches any of them (§3.2).
A trust-pinned key that's fetched at runtime trades cryptographic guarantee for first-use trust. Loran refuses that trade: the only way to change the trust root is to ship a new Loran release. The consequence — and the reason §3 and §4 exist — is that the rotation procedure must be planned well before the trust root has to change.
Constant in loran-core::pipeline |
Used for | Notes |
|---|---|---|
PUBLISHER_PUBLIC_KEY |
Upstream pages tarball | Placeholder; replace with the real key at first launch (§2.4) |
When parallel keys are active the default_publisher constructor
returns both via the LORAN_PAGES_PUBLIC_KEY env var (comma-separated)
or by editing the embedded constant set to a Vec of base64 strings.
- An unexpected
TARBALL_VERIFY_FAILED(exit 11) fromloran updateis the first signal of a key mismatch. Compare the running Loran version against the release advisories — a parallel-key window means a stale Loran has been left running past the cut-over. - A signed Spacecraft Software release announcement is the only authoritative source for the active publisher key. Operators should not import keys obtained any other way.
The upstream catalog is authored in the loran-pages content repo
(https://github.com/Spacecraft-Software/loran-pages), not in this
repository. That repo's CI is the producer side of the
loran update mechanism; this section documents how it builds and signs
the artifacts that update_pages consumes.
scripts/build-pages.sh in loran-pages produces three files under
dist/, and publish.yml uploads them to the rolling pages-latest
GitHub Release:
| Asset | Role |
|---|---|
pages.tar.gz |
gzip tarball; category dirs at the archive root, extracted verbatim into $XDG_DATA_HOME/loran/pages/. |
pages.json |
manifest — {version, etag, sha256, size}. |
pages.tar.gz.minisig |
detached minisign signature over pages.tar.gz. |
They are served from
https://github.com/Spacecraft-Software/loran-pages/releases/latest/download/<asset>
— exactly the URLs baked into
loran-core::pipeline::PUBLISHER_PAGES_{MANIFEST,TARBALL,SIG}_URL. The
build is deterministic (sorted entries, pinned mtime, gzip -n), so
unchanged content yields identical bytes; the published asset's ETag
stays stable across rebuilds and keeps client 304s cheap.
The publisher secret key is held in the Spacecraft Software release
vault (§1.1) and mirrored into the loran-pages repository's Actions
secrets so publish.yml can sign unattended:
MINISIGN_SECRET_KEY— the minisign secret-key file contents.MINISIGN_PASSWORD— its password (may be empty).
The matching public key is baked into the Loran binary as
PUBLISHER_PUBLIC_KEY (§1.1). The two are always a pair: changing
either is a key rotation (§3 / §4), never an isolated edit.
publish.yml runs on every push to loran-pages main that touches
pages/** (and on manual dispatch). It validates (loran validate pages/), packs, signs, and rolls the pages-latest release. Clients
pick the change up on their next loran update — manual, or the opt-in
auto-update once the cached catalog passes its staleness interval.
The pipeline currently ships a development placeholder key (§1.3, §5). To go live:
- Generate the production keypair:
nix shell nixpkgs#minisign -c minisign -G - Store the secret key in the release vault; add
MINISIGN_SECRET_KEYandMINISIGN_PASSWORDto theloran-pagesrepo's Actions secrets. - Replace
PUBLISHER_PUBLIC_KEYinloran-core::pipelinewith the new public key (and keepsigning::tests::TEST_PUBLIC_KEYdistinct — §5). - Cut a Loran release carrying the real key, so installed clients trust the publisher (the baked placeholder never validated a real tarball, so there is no breakage for existing users).
- Trigger
publish.yml(push toloran-pages, or manual dispatch) to cut the first signedpages-latestrelease. - Verify end-to-end against a clean
$XDG_DATA_HOME:loran update, thenloran show <a-tool>.
Loran rotates the publisher key on a planned annual cadence as the default. The rotation is announced in advance, executed during a parallel-key transition window, and concluded with a release that drops the outgoing key.
- Generate the new keypair with the upstream-blessed
minisignbinary (Nix-provided to avoid supply-chain drift):nix shell nixpkgs#minisign -c minisign -G - Store the secret key in the Spacecraft Software release vault. The public key is committed verbatim to a new release-notes draft.
- Open a tracking issue. Announce the planned cut-over date and the parallel-key window length (≥14 days recommended).
- Cut a Loran release whose embedded
PUBLISHER_PUBLIC_KEYarray contains both the outgoing and incoming public keys. Theverify_anyprimitive accepts a tarball signed by either. - Sign every new upstream tarball with the incoming secret key from the moment that release ships.
- The outgoing key remains a valid trust root for the duration of the window so that operators who haven't yet upgraded Loran continue to receive updates.
- Publish a release advisory naming the parallel-key window's start and the announced cut-over date.
- Cut a follow-up Loran release whose embedded
PUBLISHER_PUBLIC_KEYarray contains only the incoming key. - Announce the cut-over via the same release-advisory channel; the outgoing key is now retired.
- Tarballs signed by the outgoing key will fail
verify_any → SignError::Mismatchfrom the cut-over release forward, which surfaces as exit code 11 (TARBALL_VERIFY_FAILED).
- Destroy the outgoing secret key from the release vault. The advisory should reference the destruction timestamp.
- Schedule the next rotation's tracking issue.
Emergency rotation is used when the outgoing key is known or strongly suspected to be compromised. The procedure differs from a normal rotation in two ways: there is no parallel-key window, and the advisory is published before the new release ships.
- Pause every signing pipeline. Do not sign any further tarball with the compromised key.
- Publish a security advisory naming the compromised key and
instructing operators to halt
loran updateuntil further notice. - Open an incident channel; assign an incident commander.
- Generate a new keypair (§3.1).
- Cut an emergency Loran release whose embedded
PUBLISHER_PUBLIC_KEYarray contains only the new key. The compromised key is omitted — every tarball it signed becomes un-verifiable. - Sign a fresh upstream tarball with the new key.
- Update the advisory with the new release version and the new public key fingerprint.
- Operators upgrade Loran to the emergency release; this is the only safe way to obtain a tarball signed by the new key.
- Investigate the root cause of the compromise; publish a post-incident report.
- Destroy the compromised secret key if it hasn't already been; verify backups don't retain it.
crates/loran-core/src/signing.rs::tests::TEST_PUBLIC_KEYis a test-only key with a documented purpose. It must never be the active publisher key. Phase 2 reuses the same string as the placeholderPUBLISHER_PUBLIC_KEYprecisely so an operator who accidentally runsloran updateagainst the placeholder URL gets a clean diagnostic rather than a surprising decode failure.- At first launch (§2.4), the release engineer must swap
PUBLISHER_PUBLIC_KEYto the real key and updatesigning::tests::TEST_PUBLIC_KEYto remain distinct.
All publisher-key advisories — planned and emergency — are published to:
- The Loran GitHub repository's
Securitytab. - The Spacecraft Software release-notes feed.
- The Bravais OS and Ferrite OS distribution advisory channels for downstream amplification.
Each advisory must include:
- Affected key (full base64).
- Reason for the advisory (planned rotation, emergency, post-incident report).
- Affected Loran versions (the version range for which the named key is the trust root).
- Recommended operator action (parallel-key window timeline, or upgrade-now-and-halt-updates).
- Spec §2 decision #6 — trust-pinned key constraint.
- Spec §11 — tldr-pages explicitly not covered by this procedure (tldr-pages is unsigned by upstream, so there is no rotation surface).
loran-core::signing::verify_any— the parallel-key acceptance primitive.loran-core::pipeline::UpdateOpts::public_keys— the runtime trust-pinned set consulted byupdate_pages.- PRD NG-09 — the publisher pipeline this section realizes (originally scoped as a separate, out-of-PRD project).
loran-pagesrepo (scripts/build-pages.sh,.github/workflows/{validate,publish}.yml) — the producer implementation described in §2.