From a11d54a87654d71ad1a0e67c2152bfe2c5f8cbfc Mon Sep 17 00:00:00 2001 From: Tomaz Zaman Date: Mon, 10 Aug 2026 23:30:11 +0200 Subject: [PATCH 30/59] mono: OTA key rotation support A single signing key was a single point of failure: lose it and the fleet can no longer be sent signed updates (and you cannot push a new key through the signed channel it no longer trusts). Devices now trust a directory of keys (/etc/mono-keys) and the checker accepts a signature from ANY of them. Ships two: the primary (used nightly) and an offline rotation key whose private half is held off-machine. If the primary is ever lost, sign with the rotation key - already trusted by every device - and updates resume with no per-device action. Co-Authored-By: Claude Fable 5 --- package/mono/mono-update-check/Makefile | 11 +++++++---- .../mono-update-check/files/mono-rotation.pub | 2 ++ .../mono-update-check/files/mono-update-check | 16 ++++++++++++---- 3 files changed, 21 insertions(+), 8 deletions(-) create mode 100644 package/mono/mono-update-check/files/mono-rotation.pub diff --git a/package/mono/mono-update-check/Makefile b/package/mono/mono-update-check/Makefile index 0c9085a9b5..ed5c9b5ecc 100644 --- a/package/mono/mono-update-check/Makefile +++ b/package/mono/mono-update-check/Makefile @@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=mono-update-check PKG_VERSION:=1.0 -PKG_RELEASE:=1 +PKG_RELEASE:=2 PKG_LICENSE:=GPL-2.0 include $(INCLUDE_DIR)/package.mk @@ -52,9 +52,12 @@ define Package/mono-update-check/install $(INSTALL_BIN) ./files/mono-update-check $(1)/usr/sbin/ $(INSTALL_DIR) $(1)/etc/uci-defaults $(INSTALL_BIN) ./files/90-mono-update-cron $(1)/etc/uci-defaults/ - # Public half of the offline release-signing key; the trust anchor - # for auto-mode. Private key never leaves the release host. - $(INSTALL_DATA) ./files/mono-release.pub $(1)/etc/mono-release.pub + # Trusted release signing keys (public halves): the primary key used + # nightly, plus an offline rotation key so a lost primary never strands + # the fleet. Private halves never leave the release host / offline media. + $(INSTALL_DIR) $(1)/etc/mono-keys + $(INSTALL_DATA) ./files/mono-release.pub $(1)/etc/mono-keys/mono-release.pub + $(INSTALL_DATA) ./files/mono-rotation.pub $(1)/etc/mono-keys/mono-rotation.pub endef $(eval $(call BuildPackage,mono-update-check)) diff --git a/package/mono/mono-update-check/files/mono-rotation.pub b/package/mono/mono-update-check/files/mono-rotation.pub new file mode 100644 index 0000000000..c53d82eaa6 --- /dev/null +++ b/package/mono/mono-update-check/files/mono-rotation.pub @@ -0,0 +1,2 @@ +untrusted comment: Mono Gateway ROTATION key (offline backup) +RWQy8Eacpkg85yWvoFH4lkjIwZ1E/+6we77naPIvpDyUys36W1Fx/R2L diff --git a/package/mono/mono-update-check/files/mono-update-check b/package/mono/mono-update-check/files/mono-update-check index 8146ed689b..16471d1c23 100644 --- a/package/mono/mono-update-check/files/mono-update-check +++ b/package/mono/mono-update-check/files/mono-update-check @@ -10,7 +10,7 @@ URL=$(uci -q get mono-update.check.url) MODE=$(uci -q get mono-update.check.mode) STATE=/tmp/mono-update-available -PUBKEY=/etc/mono-release.pub +KEYDIR=/etc/mono-keys # every trusted release pubkey (primary + rotation) [ -n "$URL" ] || exit 0 @@ -68,7 +68,7 @@ echo "$TAG $SYSUPGRADE_URL" > "$STATE" [ "$MODE" = "auto" ] || exit 0 # --- auto mode: signature-gated, RAM-guarded flash --------------------- -[ -f "$PUBKEY" ] || { logger -t mono-update "no $PUBKEY, refusing auto-flash"; exit 1; } +[ -n "$(ls "$KEYDIR"/*.pub 2>/dev/null)" ] || { logger -t mono-update "no keys in $KEYDIR, refusing auto-flash"; exit 1; } command -v usign >/dev/null 2>&1 || { logger -t mono-update "usign missing, refusing"; exit 1; } # The whole image lives in a ramdisk during sysupgrade; don't start if RAM @@ -89,8 +89,16 @@ fail() { logger -t mono-update "$1"; rm -rf "$TMP"; exit 1; } fetch "$DIR/sha256sums" "$TMP/sha256sums" || fail "cannot fetch sha256sums" fetch "$DIR/sha256sums.sig" "$TMP/sha256sums.sig" || fail "cannot fetch signature" -usign -V -m "$TMP/sha256sums" -x "$TMP/sha256sums.sig" -p "$PUBKEY" \ - || fail "signature verification FAILED - refusing $TAG" +# Accept the signature if ANY trusted key verifies it (primary or a +# pre-baked rotation key), so losing the primary key never strands devices. +ok=0 +for k in "$KEYDIR"/*.pub; do + [ -f "$k" ] || continue + if usign -V -m "$TMP/sha256sums" -x "$TMP/sha256sums.sig" -p "$k" 2>/dev/null; then + ok=1; break + fi +done +[ "$ok" = 1 ] || fail "signature verification FAILED against all trusted keys - refusing $TAG" WANT=$(awk -v n="$IMG_NAME" '$2==n || $2=="*"n {print $1}' "$TMP/sha256sums" | head -1) [ -n "$WANT" ] || fail "image $IMG_NAME not in signed hash list" -- 2.47.3