// SPDX-FileCopyrightText: 2026 Echolot contributors // SPDX-License-Identifier: GPL-3.0-or-later // Package relsign signs and verifies release manifests (detached ed25519 over SHA256SUMS). // // The checksum file alone protects download integrity, not authenticity: SHA256SUMS and the // binaries come from the same Gitea release, so whoever can alter one can alter both. The // signature is what separates "the file arrived intact" from "the project published this file" — // its private key lives in the CI secret store, not on the release host, so a compromised Gitea // can serve corrupted binaries but cannot make a self-updating server accept them. // // Formats, chosen to be reproducible with nothing but a stock library in any language: // the private key is the base64 of the 32-byte ed25519 seed, the public key the base64 of the // 32-byte public key, and the signature file the base64 of the 64-byte signature over the exact // bytes of the signed file. package relsign import ( "crypto/ed25519" "encoding/base64" "fmt" "strings" ) // GenerateKey mints a fresh signing keypair. func GenerateKey() (pubB64, seedB64 string, err error) { pub, priv, err := ed25519.GenerateKey(nil) if err != nil { return "", "", err } return base64.StdEncoding.EncodeToString(pub), base64.StdEncoding.EncodeToString(priv.Seed()), nil } // Sign produces the detached signature (base64) for data. func Sign(seedB64 string, data []byte) (string, error) { seed, err := base64.StdEncoding.DecodeString(strings.TrimSpace(seedB64)) if err != nil { return "", fmt.Errorf("signing key is not valid base64: %w", err) } if len(seed) != ed25519.SeedSize { return "", fmt.Errorf("signing key must be %d bytes, got %d", ed25519.SeedSize, len(seed)) } priv := ed25519.NewKeyFromSeed(seed) return base64.StdEncoding.EncodeToString(ed25519.Sign(priv, data)), nil } // Verify checks a detached signature. A nil error means the holder of the private key matching // pubB64 signed exactly these bytes. func Verify(pubB64 string, data []byte, sigB64 string) error { pub, err := base64.StdEncoding.DecodeString(strings.TrimSpace(pubB64)) if err != nil { return fmt.Errorf("public key is not valid base64: %w", err) } if len(pub) != ed25519.PublicKeySize { return fmt.Errorf("public key must be %d bytes, got %d", ed25519.PublicKeySize, len(pub)) } sig, err := base64.StdEncoding.DecodeString(strings.TrimSpace(sigB64)) if err != nil { return fmt.Errorf("signature is not valid base64: %w", err) } if !ed25519.Verify(ed25519.PublicKey(pub), data, sig) { return fmt.Errorf("signature does not verify: the file was not signed by this key, or was altered after signing") } return nil }