// SPDX-FileCopyrightText: 2026 Echolot contributors // SPDX-License-Identifier: GPL-3.0-or-later // release-sign signs a release manifest (SHA256SUMS) with the project's ed25519 key, producing // the detached .sig that self-updating servers verify before trusting the checksums. // // release-sign -gen mint a keypair (seed on stdout — store it as the CI // secret RELEASE_SIGNING_KEY; publish the public key) // release-sign sign; key read from $RELEASE_SIGNING_KEY, writes .sig // release-sign -verify -pub check against .sig — what the updater will do // // Run from CI (build-server.yml); the private key exists only in the Actions secret store, never // on the release host, which is the property that makes the signature worth having. package main import ( "flag" "fmt" "os" "echo-lot.app/server/internal/relsign" ) func main() { gen := flag.Bool("gen", false, "generate a keypair and exit") verify := flag.Bool("verify", false, "verify against .sig instead of signing") pub := flag.String("pub", "", "public key (base64) for -verify") flag.Parse() if err := run(*gen, *verify, *pub, flag.Args()); err != nil { fmt.Fprintln(os.Stderr, "release-sign:", err) os.Exit(1) } } func run(gen, verify bool, pub string, args []string) error { if gen { pubB64, seedB64, err := relsign.GenerateKey() if err != nil { return err } fmt.Printf("public key (embed / ECHOLOT_SELF_UPDATE_PUBKEY):\n%s\n\n"+ "private key (CI secret RELEASE_SIGNING_KEY — this is the only copy):\n%s\n", pubB64, seedB64) return nil } if len(args) != 1 { return fmt.Errorf("usage: release-sign [-gen | -verify -pub ] ") } file := args[0] data, err := os.ReadFile(file) if err != nil { return err } if verify { if pub == "" { return fmt.Errorf("-verify needs -pub") } sig, err := os.ReadFile(file + ".sig") if err != nil { return err } if err := relsign.Verify(pub, data, string(sig)); err != nil { return err } fmt.Printf("%s: signature OK\n", file) return nil } seed := os.Getenv("RELEASE_SIGNING_KEY") if seed == "" { return fmt.Errorf("RELEASE_SIGNING_KEY is not set — refusing to produce an unsigned release") } sig, err := relsign.Sign(seed, data) if err != nil { return err } if err := os.WriteFile(file+".sig", []byte(sig+"\n"), 0o644); err != nil { return err } fmt.Printf("wrote %s.sig\n", file) return nil }