Files
artifactapi/internal/tfsign/signer_test.go
T
unkinben 97cdb9c6b5
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
feat: self-generate and store the terraform registry signing key
Rather than requiring an operator to create a GPG key and a K8s secret, the
registry now provisions itself: on first start artifactapi generates a signing
keypair and persists it in a new signing_keys table, so all replicas share one
key and there is nothing to set up. TF_SIGNING_KEY_PATH still overrides with a
bring-your-own key when set.

- signing_keys table + GetSigningKey / InsertSigningKeyIfAbsent (ON CONFLICT DO
  NOTHING so a replica race converges on one key)
- tfsign.Generate, LoadArmored, and LoadOrCreate(store, purpose)
- server prefers a configured key file, else LoadOrCreate against the DB
- tests: generate/load round-trip, load-or-create generates once then reuses,
  DB insert idempotency
2026-07-03 18:46:09 +10:00

156 lines
3.9 KiB
Go

package tfsign
import (
"bytes"
"context"
"os"
"path/filepath"
"regexp"
"testing"
"golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/armor"
)
// armoredPrivateKey generates a throwaway armored private key for tests.
func armoredPrivateKey(t *testing.T) string {
t.Helper()
e, err := openpgp.NewEntity("artifactapi test", "tf registry", "tf@example.com", nil)
if err != nil {
t.Fatal(err)
}
var buf bytes.Buffer
w, err := armor.Encode(&buf, openpgp.PrivateKeyType, nil)
if err != nil {
t.Fatal(err)
}
if err := e.SerializePrivate(w, nil); err != nil {
t.Fatal(err)
}
w.Close()
return buf.String()
}
func writeKey(t *testing.T, contents string) string {
t.Helper()
p := filepath.Join(t.TempDir(), "private-key.asc")
if err := os.WriteFile(p, []byte(contents), 0o600); err != nil {
t.Fatal(err)
}
return p
}
func TestLoadSignAndVerify(t *testing.T) {
path := writeKey(t, armoredPrivateKey(t))
s, err := Load(path, "")
if err != nil {
t.Fatal(err)
}
if s == nil {
t.Fatal("expected a signer")
}
if !regexp.MustCompile(`^[0-9A-F]{16}$`).MatchString(s.KeyID()) {
t.Errorf("key id %q is not 16 uppercase hex chars", s.KeyID())
}
msg := []byte("deadbeef terraform-provider-x_1.0.0_linux_amd64.zip\n")
sig, err := s.Sign(msg)
if err != nil {
t.Fatal(err)
}
// The advertised public key must verify the signature over the same bytes.
keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader([]byte(s.PublicKeyArmor())))
if err != nil {
t.Fatal(err)
}
if _, err := openpgp.CheckDetachedSignature(keyring, bytes.NewReader(msg), bytes.NewReader(sig)); err != nil {
t.Errorf("signature did not verify: %v", err)
}
}
func TestGenerateAndLoadArmored(t *testing.T) {
priv, keyID, err := Generate()
if err != nil {
t.Fatal(err)
}
if !regexp.MustCompile(`^[0-9A-F]{16}$`).MatchString(keyID) {
t.Errorf("generated key id %q malformed", keyID)
}
s, err := LoadArmored(priv, "")
if err != nil {
t.Fatal(err)
}
if s.KeyID() != keyID {
t.Errorf("loaded key id %q != generated %q", s.KeyID(), keyID)
}
msg := []byte("abc terraform-provider-x_1.0.0_linux_amd64.zip\n")
sig, err := s.Sign(msg)
if err != nil {
t.Fatal(err)
}
keyring, _ := openpgp.ReadArmoredKeyRing(bytes.NewReader([]byte(s.PublicKeyArmor())))
if _, err := openpgp.CheckDetachedSignature(keyring, bytes.NewReader(msg), bytes.NewReader(sig)); err != nil {
t.Errorf("signature did not verify: %v", err)
}
}
// memStore is an in-memory KeyStore that records how many keys it accepted.
type memStore struct {
armor, keyID string
found bool
inserts int
}
func (m *memStore) GetSigningKey(_ context.Context, _ string) (string, string, bool, error) {
return m.armor, m.keyID, m.found, nil
}
func (m *memStore) InsertSigningKeyIfAbsent(_ context.Context, _, armor, keyID string) error {
if !m.found { // ON CONFLICT DO NOTHING
m.armor, m.keyID, m.found = armor, keyID, true
m.inserts++
}
return nil
}
func TestLoadOrCreateGeneratesOnceThenReuses(t *testing.T) {
store := &memStore{}
first, err := LoadOrCreate(context.Background(), store, "terraform-provider")
if err != nil || first == nil {
t.Fatalf("first LoadOrCreate: signer=%v err=%v", first, err)
}
second, err := LoadOrCreate(context.Background(), store, "terraform-provider")
if err != nil || second == nil {
t.Fatalf("second LoadOrCreate: signer=%v err=%v", second, err)
}
if store.inserts != 1 {
t.Errorf("expected exactly one key generated, got %d", store.inserts)
}
if first.KeyID() != second.KeyID() {
t.Errorf("key id changed between loads: %q vs %q", first.KeyID(), second.KeyID())
}
}
func TestLoadEmptyPathDisabled(t *testing.T) {
s, err := Load("", "")
if err != nil {
t.Fatal(err)
}
if s != nil {
t.Error("empty path should yield a nil (disabled) signer")
}
}
func TestLoadMissingFile(t *testing.T) {
if _, err := Load(filepath.Join(t.TempDir(), "nope.asc"), ""); err == nil {
t.Error("expected an error for a missing key file")
}
}