Skip to content

security(release): rotate the Ed25519 release signing key - #32

Merged
Jaro-c merged 1 commit into
mainfrom
security/rotate-release-key
Jul 15, 2026
Merged

security(release): rotate the Ed25519 release signing key#32
Jaro-c merged 1 commit into
mainfrom
security/rotate-release-key

Conversation

@Jaro-c

@Jaro-c Jaro-c commented Jul 4, 2026

Copy link
Copy Markdown
Member

Unifies the signing key with podup, helmly and helmly-agent for the first time -- this repo had its own, third distinct embedded key despite the shared-key design. Requires the org secret GLYNDOR_RELEASE_ED25519_KEY (scoped to epistle, helmly, helmly-agent, podup, unitpm; RELEASE_SIGN_KEY retired). Breaks in-band self-update for every existing install; reinstall required.

Jaro-c added a commit that referenced this pull request Jul 15, 2026
The signer required a 64-byte seed+pub and rejected anything else. The
org secret GLYNDOR_RELEASE_ED25519_KEY is a raw 32-byte seed -- that is
what podup's signer reads, and what signed podup 1.9.1 -- so pointing
this workflow at the shared key (#32) is not enough on its own: the sign
step dies with "key size 32, want 64" and the release never publishes.

Verified by running it, not by reading it: with a 32-byte seed the signer
previously exited 1; it now signs, and the signature verifies against the
public key derived from that seed -- which is exactly the check
updater.go performs in the field.

Accept both forms. ed25519.NewKeyFromSeed expands a 32-byte seed into the
64-byte private key, and the 64-byte form still works untouched, so no
existing caller breaks. A wrong length is still refused.

This mismatch is likely why unitpm drifted onto a release key of its own
while the docs described one shared key: the shared key could not be used
here at all.

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
Unifies the signing key with podup, helmly and helmly-agent for the
first time -- despite the documented shared-key design, this repo had
its own, third distinct embedded key. Every binary from before this
rotation cannot update in-band; reinstall required.

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
@Jaro-c
Jaro-c force-pushed the security/rotate-release-key branch from 4d41a20 to f67a5c6 Compare July 15, 2026 13:42
@Jaro-c

Jaro-c commented Jul 15, 2026

Copy link
Copy Markdown
Member Author

Rebased onto current main and reduced to the one commit that matters. Two things had gone stale in the eleven days this sat open:

The secret rename is already on main. It landed separately, so main today signs with GLYNDOR_RELEASE_ED25519_KEY while updater.go still embeds 3eSCGskG…. That is exactly the trap podup hit last night: signing with the new key, verifying against the old one. This PR is now the only thing standing between that state and a release nothing can verify.

The README commit had to go. It conflicted, and rebasing it would have reverted #35docs: drop the persuasive competitor framing from the README Architecture table, merged 2026-07-07, three days after this branch was cut. Landing it as written would have re-added the Lynx | PM2 | Supervisor comparison table, which the anti-advertising rule now explicitly forbids. main's README is untouched by this PR.

Ordering matters, and it is counterintuitive: #38 (making the signer accept the org's 32-byte seed) must not merge before this one. Today the size mismatch makes the release fail closed at the signing step — accidental protection. Fix the signer first and unitpm goes from "cannot release" to "publishes a release its own updater rejects". #32 before or with #38, never #38 alone.

Verified the key itself, running Go rather than reading it: HFv7vg5FCY7YyKUDbJhaQSfB9SboJGSblJtFbLmLHzM= decodes under base64.StdEncoding to 32 bytes = ed25519.PublicKeySize. The trailing = is load-bearing — without it Go returns illegal base64 data at input byte 40 and the updater dies. This branch has it right.

@Jaro-c
Jaro-c merged commit 09c1942 into main Jul 15, 2026
20 checks passed
@Jaro-c
Jaro-c deleted the security/rotate-release-key branch July 15, 2026 13:46
Jaro-c added a commit that referenced this pull request Jul 15, 2026
The signer required a 64-byte seed+pub; the org secret GLYNDOR_RELEASE_ED25519_KEY is a raw 32-byte seed, so the sign step died with 'key size 32, want 64' and no release could publish. This mismatch is likely why unitpm drifted onto a release key of its own while the docs described one shared key.

Accept both forms via ed25519.NewKeyFromSeed; the 64-byte form still works. Verified by running it: with a 32-byte seed the signer now signs, and the signature verifies against the public key derived from that seed — the same check updater.go performs in the field.

Merged after #32 deliberately: fixing the signer before the key rotation would have taken unitpm from 'cannot release' to 'publishes a release its own updater rejects'.

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant