edb6c7c0f7
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).
87 lines
2.0 KiB
Go
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")
|
|
}
|
|
}
|