Files
artifactapi/internal/tfsign/signer_test.go
T
unkinben edb6c7c0f7
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
feat: serve local terraform repos as a provider registry
Local terraform repos already spoke the network mirror protocol, which needs
per-consumer .terraformrc config. This adds the provider registry protocol so
`terraform init` installs from a bare source address
(artifactapi.k8s.../{repo}/{type}) with no client setup.

- serve /.well-known/terraform.json service discovery and the providers.v1
  versions/download endpoints under /terraform/v1/providers
- map the Terraform namespace to the artifactapi repo name and locate the
  provider by type; download_url points back at the existing local file path
- generate SHA256SUMS per version and sign it with a GPG key loaded from
  TF_SIGNING_KEY_PATH; advertise the public key + key id in the download
  response. No key configured -> registry stays disabled (endpoints 404)
- new internal/tfsign (key loading + detached signing) and
  internal/api/terraform (registry handler); export ParseProviderZip for reuse
- add TF_SIGNING_KEY_PATH/PASSPHRASE and TF_PROVIDER_PROTOCOLS config
- unit test signing + verification; dockerised test of the full flow incl.
  signature verification against the advertised key

Also anchor the terraform/ gitignore to the repo root so it stops swallowing
internal/api/terraform and internal/provider/terraform test files (the latter
had gone silently untracked).
2026-07-03 17:46:55 +10:00

87 lines
2.0 KiB
Go

package tfsign
import (
"bytes"
"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 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")
}
}