RFC-0013: Signed Releases + Verified Downloads #168
Closed
kn4oqw-clint
announced in
RFCs
Replies: 1 comment
|
This has already been implemented. |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Summary
Two things a hotspot pulls from the network must be tamper-evident: the
software it updates to, and the reference data it downloads (host lists, talkgroup
names, ID databases). Today both are fetched over TLS and used verbatim — TLS
proves transport, not provenance, so a compromised mirror or a malicious
operator-configured URL can feed a node bad data or a bad binary. This RFC adds a
single signature-verification primitive (minisign / Ed25519) and applies it in
both places: release artifacts are signed and verified before they are applied,
and reference-data refreshes verify a checksum or signature before the download
replaces the cache. The failure mode is uniform and loud — a tampered artifact
or database file is rejected with a clear error, which is the requirement's
acceptance.
Motivation
Pi-Star sbin #60 asked, years ago, for MD5 sums on the DMRId / host downloads;
it never shipped, and unsigned rolling updates remain the incumbent norm — a node
trusts whatever a mirror serves. That is two distinct risks:
databases on a schedule and feeds them to the gateways and the dashboard. A
poisoned list can misroute traffic or mislabel it; the review of a live node
already surfaced these downloads (and one where the upstream was simply
unreachable). Verifying a checksum/signature makes a swapped file a rejection,
not a silent adoption.
is the highest-value attack on the whole fleet. Before Waypoint ships atomic
updates (Atomic updates with rollback — an update completes or the old version boots #13), the primitive that verifies a release must exist and be trusted.
minisign (Ed25519, a tiny well-specified format) is the right tool for an
appliance: no PKI, no CA, a 56-byte public key an operator can pin, and a standard
CLI (
minisign) for the release side. Sigstore is powerful but its keyless/OIDCmodel and transparency-log dependency are disproportionate for a Pi that may be
offline. The primitive is minisign-compatible so releases can be signed with the
stock tool and verified by a few hundred lines of Go with no new heavy dependency
(Ed25519 is stdlib; BLAKE2b is already in
x/crypto).Design
The verification primitive (
internal/minisign)A self-contained minisign verifier:
algorithm + 8-byte key id + 32-byte Ed25519 public key).
.minisig(the untrusted-comment line, the base64signature line = algorithm + key id + 64-byte Ed25519 signature, the
trusted-comment line, and the base64 global signature over
signature ‖ trusted comment).Ed(Ed25519 over the raw file)and modern prehashed
ED(Ed25519 over BLAKE2b-512 of the file — the variantused for large files, and what
minisign -Sproduces by default on recentversions). It checks the key id matches the trusted key, verifies the file
signature, and verifies the global signature over the trusted comment, so a
trusted comment (e.g. the release version) cannot be forged.
Verify(pub PublicKey, message []byte, sig Signature) errorfor in-memory data,and
VerifyFile(pub, path, sigPath)for a file on disk. Every failure is adistinct, wrapped, human-readable error (
bad key id,signature mismatch,malformed .minisig, …) — never a barefalse.The primitive is pure and has no I/O beyond reading the named files, so it is
exhaustively unit-tested against real minisign vectors (a key + a signed file
generated by the
minisigntool, checked into the test as fixtures), and againsttampered variants that must each fail.
Verified reference-data downloads (
internal/verifydl)A thin download helper wraps the refreshers' existing HTTP fetch:
or one fetched from a sidecar URL (
<url>.sha256). (sbin Retire per-bridge cross-mode surface; close YSF parity gap G1 #60's MD5 request,modernized — MD5 is broken; SHA-256 is the floor.)
<url>.minisigand verifies it against a trustedminisign public key via
internal/minisign.Downloadreturns a typed error and no body, so the callerkeeps its previous cache — a tampered or corrupt download can never replace good
data. The error names the URL and the reason.
Verification is opt-in per source, because the community hostfile mirrors do
not publish sums/signatures today: a refresher fetches plain (as now) unless a
checksum/signature source and — for signatures — a trusted key is configured,
in which case verification is mandatory for that source and a failure rejects
the download. This ships the mechanism without breaking today's unsigned upstreams,
and lights up fully the moment Waypoint hosts signed mirrors of the reference data
(the natural home for signed lists, and the fix for the "mirror unreachable" case a
Waypoint mirror also addresses). A
-require-signed-hostfilesglobal switch (off bydefault) makes verification mandatory for every source for an operator who runs
only signed mirrors.
The refreshers (
dmrhosts,dmrtg, and the reflector lists) gain an optionalverification config threaded through their
Fetch/Run; this RFC wiresdmrhostsanddmrtgas the pattern, and the reflector refreshers adopt the samehelper as a mechanical follow-up.
Signed releases
Release artifacts (the
waypointdbinary per arch, and the eventual image/.deb)are signed at build time and verified before they are applied:
minisign -Sover eachartifact with the Waypoint release key, whose secret half lives only as a CI
secret (never in the repo), emitting a
<artifact>.minisigalongside each file.The trusted comment carries the version/commit so it is bound into the signature.
docs/waypoint-release.pub) and bundledso any client can verify a release offline, and is also settable via
-release-pubkeyfor an operator running their own build channel.internal/minisign.VerifyFile(releasePub, artifact, artifact+".minisig")andrefuses to switch unless it passes — the "updater verifies before applying"
half of Signed releases; checksummed hostfile/ID database downloads #12. Because the updater lands in Atomic updates with rollback — an update completes or the old version boots #13, this RFC delivers the primitive,
the signing pipeline, and the trusted key; Atomic updates with rollback — an update completes or the old version boots #13 wires the pre-apply gate onto them.
Until then the acceptance is demonstrable directly: verifying a signed artifact
passes and a byte-flipped one is rejected with a clear error (a unit test, and a
waypointd -verify <file> <sig>maintenance flag operators/scripts can call).Key management is an operator/maintainer concern, documented in
docs/signing.md:generating the release keypair, storing the secret key as a CI secret, and how a
downstream re-builder swaps in their own key.
The contract (test harness)
Automated (Go):
minisigntool verify; the same file with one byte flipped, a wrong key id, atruncated
.minisig, and a forged trusted comment each fail with their distincterror. Both
Ed(legacy) andED(prehashed BLAKE2b) modes covered.returned; a tampered body is rejected with a clear error and no bytes.
.minisigis returned; atampered body (sig no longer matches) is rejected; a missing sidecar when
verification is required is rejected.
dmrhosts/dmrtgFetchwrites the cache only on a verified download and leaves the prior cacheintact on a verification failure (mirrors the existing failure-safe fetch).
VerifyFileaccepts a correctly-signedartifact and rejects a byte-flipped one with a clear, wrapped error — the "a
tampered artifact … is rejected with a clear error" acceptance, as a test.
Alternatives considered
transparency log at verify time, and a Pi may be offline. minisign's pinned key is
simpler, offline-verifiable, and standard. (A Sigstore attestation in addition
is a possible future for the public download surface.)
the server, not the content: a compromised or malicious mirror serves signed-
looking bytes over a valid cert. Provenance needs a signature over the artifact.
both are broken for tamper resistance. SHA-256 for checksums, Ed25519 for
signatures. A checksum alone (no signature) still only detects corruption, not a
malicious swap where the attacker controls the checksum too; signatures are the
real protection, checksums the cheap floor for sources that only publish sums.
strips the sidecar. Verification is opt-in per source, but once opted in it is
mandatory (a missing/failing signature is a rejection), and
-require-signed- hostfilesmakes it fleet-wide.Open questions
that signs. Waypoint hosting signed mirrors of the host/TG/ID lists (re-published
from upstream with a Waypoint signature) both enables mandatory verification and
fixes the reachability gap a review already found. Scope of that mirror
(which lists, refresh cadence) is its own small design.
DMRIds.dat). MMDVM-Host consumes a callsign↔ID databaseWaypoint does not yet refresh; adding a verified refresher for it is the direct
sbin Retire per-bridge cross-mode surface; close YSF parity gap G1 #60 ask and a natural extension of
verifydl. Deferred to when the ID DBrefresher lands.
for a small set of trusted keys (rotation, a backup signer) is a config-shape
question deferred until a rotation is actually needed.
updater; this RFC intentionally stops at the primitive + signing + trusted key so
Atomic updates with rollback — an update completes or the old version boots #13 has a verified foundation to build the switch-or-rollback on.
Migrated from
docs/rfcs/0013-signed-releases-verified-downloads.md; the drafting history is in the git log.All reactions