// 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) }