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).
113 lines
3.0 KiB
Go
113 lines
3.0 KiB
Go
// Package tfsign loads a GPG signing key and produces the detached signatures
|
|
// the Terraform provider registry protocol requires over SHA256SUMS files.
|
|
package tfsign
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"golang.org/x/crypto/openpgp"
|
|
"golang.org/x/crypto/openpgp/armor"
|
|
)
|
|
|
|
// Signer holds a decrypted GPG entity and exposes what the registry download
|
|
// response needs: a detached signature, the armored public key, and the key ID.
|
|
type Signer struct {
|
|
entity *openpgp.Entity
|
|
publicASCII string
|
|
keyID string
|
|
}
|
|
|
|
// Load reads an armored private key from path, decrypting it with passphrase if
|
|
// the key is protected. A blank path returns (nil, nil): signing is optional, and
|
|
// a nil *Signer means the terraform registry is disabled.
|
|
func Load(path, passphrase string) (*Signer, error) {
|
|
if path == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open signing key: %w", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
keyring, err := openpgp.ReadArmoredKeyRing(f)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read signing key: %w", err)
|
|
}
|
|
if len(keyring) == 0 {
|
|
return nil, fmt.Errorf("signing key %q contains no entities", path)
|
|
}
|
|
entity := keyring[0]
|
|
|
|
if entity.PrivateKey == nil {
|
|
return nil, fmt.Errorf("signing key %q has no private key material", path)
|
|
}
|
|
if entity.PrivateKey.Encrypted {
|
|
if err := decrypt(entity, passphrase); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
pub, err := armorPublicKey(entity)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Signer{
|
|
entity: entity,
|
|
publicASCII: pub,
|
|
keyID: entity.PrimaryKey.KeyIdString(),
|
|
}, nil
|
|
}
|
|
|
|
// decrypt unlocks the entity's private key and all subkeys with the passphrase.
|
|
func decrypt(entity *openpgp.Entity, passphrase string) error {
|
|
pw := []byte(passphrase)
|
|
if err := entity.PrivateKey.Decrypt(pw); err != nil {
|
|
return fmt.Errorf("decrypt signing key: %w", err)
|
|
}
|
|
for _, sub := range entity.Subkeys {
|
|
if sub.PrivateKey != nil && sub.PrivateKey.Encrypted {
|
|
_ = sub.PrivateKey.Decrypt(pw)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func armorPublicKey(entity *openpgp.Entity) (string, error) {
|
|
var buf bytes.Buffer
|
|
w, err := armor.Encode(&buf, openpgp.PublicKeyType, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if err := entity.Serialize(w); err != nil {
|
|
return "", err
|
|
}
|
|
if err := w.Close(); err != nil {
|
|
return "", err
|
|
}
|
|
return buf.String(), nil
|
|
}
|
|
|
|
// Sign returns a binary detached signature over message, matching the
|
|
// SHA256SUMS.sig format Terraform verifies.
|
|
func (s *Signer) Sign(message []byte) ([]byte, error) {
|
|
var buf bytes.Buffer
|
|
if err := openpgp.DetachSign(&buf, s.entity, bytes.NewReader(message), nil); err != nil {
|
|
return nil, err
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// PublicKeyArmor returns the ASCII-armored public key for the registry's
|
|
// signing_keys response.
|
|
func (s *Signer) PublicKeyArmor() string { return s.publicASCII }
|
|
|
|
// KeyID returns the 16-hex-char uppercase key ID Terraform matches against the
|
|
// signature's issuer.
|
|
func (s *Signer) KeyID() string { return strings.ToUpper(s.keyID) }
|