CI / test (push) Successful in 4m6s
ed25519 over checksums.txt: llmgw release keygen makes the pair, CI signs with the RELEASE_SIGNING_KEY secret when it is set, and release.Install refuses an unsigned or wrongly signed release when the build embeds a trusted key (internal/release/trusted.pub, empty for now) or LLMGW_RELEASE_KEY names one. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
73 lines
2.6 KiB
Go
73 lines
2.6 KiB
Go
package release
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
_ "embed"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// trustedPub is the public key releases are signed with, base64, built into
|
|
// every binary from the repository. Empty — as it is until a key is made —
|
|
// trusts a release on its checksums alone, as every release before this did.
|
|
//
|
|
// Built in rather than fetched, because the point of a signature is that the
|
|
// server the release comes from cannot vouch for it: a machine that already
|
|
// runs llmgw checks the next release against the key the release it runs was
|
|
// built with, so replacing both the binaries and the checksums on the server
|
|
// is no longer enough.
|
|
//
|
|
//go:embed trusted.pub
|
|
var trustedPub string
|
|
|
|
// TrustedKey is the key releases are verified with: LLMGW_RELEASE_KEY when set
|
|
// (for rotating a key, or testing), the built-in one otherwise; empty is none.
|
|
func TrustedKey() string {
|
|
if k := strings.TrimSpace(os.Getenv("LLMGW_RELEASE_KEY")); k != "" {
|
|
return k
|
|
}
|
|
return strings.TrimSpace(trustedPub)
|
|
}
|
|
|
|
// SignatureName is the file a release's checksums are signed in.
|
|
const SignatureName = "checksums.txt.sig"
|
|
|
|
// GenerateKey makes a signing key pair, both base64: the private key is the
|
|
// 32-byte seed, which is what goes into the CI secret.
|
|
func GenerateKey() (public, private string, err error) {
|
|
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
return base64.StdEncoding.EncodeToString(pub), base64.StdEncoding.EncodeToString(priv.Seed()), nil
|
|
}
|
|
|
|
// Sign signs data with a base64 seed, returning the signature in base64.
|
|
func Sign(privateSeed string, data []byte) (string, error) {
|
|
seed, err := base64.StdEncoding.DecodeString(strings.TrimSpace(privateSeed))
|
|
if err != nil || len(seed) != ed25519.SeedSize {
|
|
return "", errors.New("the signing key is not a base64 ed25519 seed")
|
|
}
|
|
return base64.StdEncoding.EncodeToString(ed25519.Sign(ed25519.NewKeyFromSeed(seed), data)), nil
|
|
}
|
|
|
|
// Verify checks a base64 signature of data against a base64 public key.
|
|
func Verify(public string, data []byte, signature string) error {
|
|
pub, err := base64.StdEncoding.DecodeString(strings.TrimSpace(public))
|
|
if err != nil || len(pub) != ed25519.PublicKeySize {
|
|
return errors.New("the trusted release key is not a base64 ed25519 public key")
|
|
}
|
|
sig, err := base64.StdEncoding.DecodeString(strings.TrimSpace(signature))
|
|
if err != nil {
|
|
return fmt.Errorf("the release signature is not base64: %w", err)
|
|
}
|
|
if !ed25519.Verify(pub, data, sig) {
|
|
return errors.New("the release's checksums are not signed by the trusted key")
|
|
}
|
|
return nil
|
|
}
|