1
1
mirror of https://github.com/goreleaser/nfpm synced 2025-04-21 01:07:59 +02:00
nfpm/internal/sign/pgp.go
2025-03-29 08:57:48 -03:00

264 lines
6.6 KiB
Go

package sign
import (
"bytes"
"crypto"
"errors"
"fmt"
"io"
"os"
"strconv"
"unicode"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/clearsign"
"github.com/ProtonMail/go-crypto/openpgp/packet"
"github.com/goreleaser/nfpm/v2"
)
// PGPSignerWithKeyID returns a PGP signer that creates a detached non-ASCII-armored
// signature and is compatible with rpmpack's signature API.
func PGPSignerWithKeyID(keyFile, passphrase string, hexKeyID *string) func([]byte) ([]byte, error) {
return func(data []byte) ([]byte, error) {
keyID, err := parseKeyID(hexKeyID)
if err != nil {
return nil, fmt.Errorf("%v is not a valid key id: %w", hexKeyID, err)
}
key, err := readSigningKey(keyFile, passphrase)
if err != nil {
return nil, &nfpm.ErrSigningFailure{Err: err}
}
var signature bytes.Buffer
if err := openpgp.DetachSign(
&signature,
key,
bytes.NewReader(data),
&packet.Config{
SigningKeyId: keyID,
DefaultHash: crypto.SHA256,
},
); err != nil {
return nil, &nfpm.ErrSigningFailure{Err: err}
}
return signature.Bytes(), nil
}
}
// PGPArmoredDetachSign creates an ASCII-armored detached signature.
func PGPArmoredDetachSign(message io.Reader, keyFile, passphrase string) ([]byte, error) {
return PGPArmoredDetachSignWithKeyID(message, keyFile, passphrase, nil)
}
// PGPArmoredDetachSignWithKeyID creates an ASCII-armored detached signature.
func PGPArmoredDetachSignWithKeyID(message io.Reader, keyFile, passphrase string, hexKeyID *string) ([]byte, error) {
keyID, err := parseKeyID(hexKeyID)
if err != nil {
return nil, fmt.Errorf("%v is not a valid key id: %w", hexKeyID, err)
}
key, err := readSigningKey(keyFile, passphrase)
if err != nil {
return nil, fmt.Errorf("armored detach sign: %w", err)
}
var signature bytes.Buffer
err = openpgp.ArmoredDetachSign(&signature, key, message, &packet.Config{
SigningKeyId: keyID,
DefaultHash: crypto.SHA256,
})
if err != nil {
return nil, fmt.Errorf("armored detach sign: %w", err)
}
return signature.Bytes(), nil
}
func PGPClearSignWithKeyID(message io.Reader, keyFile, passphrase string, hexKeyID *string) ([]byte, error) {
keyID, err := parseKeyID(hexKeyID)
if err != nil {
return nil, fmt.Errorf("%v is not a valid key id: %w", hexKeyID, err)
}
key, err := readSigningKey(keyFile, passphrase)
if err != nil {
return nil, fmt.Errorf("clear sign: %w", err)
}
var signature bytes.Buffer
writeCloser, err := clearsign.Encode(
&signature,
key.PrivateKey,
&packet.Config{
SigningKeyId: keyID,
DefaultHash: crypto.SHA256,
},
)
if err != nil {
return nil, fmt.Errorf("clear sign: %w", err)
}
if _, err := io.Copy(writeCloser, message); err != nil {
return nil, fmt.Errorf("clear sign: %w", err)
}
if err := writeCloser.Close(); err != nil {
return nil, fmt.Errorf("clear sign: %w", err)
}
return signature.Bytes(), nil
}
// PGPVerify is exported for use in tests and verifies an ASCII-armored or non-ASCII-armored
// signature using an ASCII-armored or non-ASCII-armored public key file. The signer
// identity is not explicitly checked, other that the obvious fact that the signer's key must
// be in the armoredPubKeyFile.
func PGPVerify(message io.Reader, signature []byte, armoredPubKeyFile string) error {
keyFileContent, err := os.ReadFile(armoredPubKeyFile)
if err != nil {
return fmt.Errorf("reading armored public key file: %w", err)
}
var keyring openpgp.EntityList
if isASCII(keyFileContent) {
keyring, err = openpgp.ReadArmoredKeyRing(bytes.NewReader(keyFileContent))
if err != nil {
return fmt.Errorf("decoding armored public key file: %w", err)
}
} else {
keyring, err = openpgp.ReadKeyRing(bytes.NewReader(keyFileContent))
if err != nil {
return fmt.Errorf("decoding public key file: %w", err)
}
}
if isASCII(signature) {
_, err = openpgp.CheckArmoredDetachedSignature(keyring, message, bytes.NewReader(signature), nil)
return err
}
_, err = openpgp.CheckDetachedSignature(keyring, message, bytes.NewReader(signature), nil)
return err
}
func PGPReadMessage(message []byte, armoredPubKeyFile string) (plaintext []byte, err error) {
keyFileContent, err := os.ReadFile(armoredPubKeyFile)
if err != nil {
return nil, fmt.Errorf("reading armored public key file: %w", err)
}
var keyring openpgp.EntityList
if isASCII(keyFileContent) {
keyring, err = openpgp.ReadArmoredKeyRing(bytes.NewReader(keyFileContent))
if err != nil {
return nil, fmt.Errorf("decoding armored public key file: %w", err)
}
} else {
keyring, err = openpgp.ReadKeyRing(bytes.NewReader(keyFileContent))
if err != nil {
return nil, fmt.Errorf("decoding public key file: %w", err)
}
}
block, _ := clearsign.Decode(message)
_, err = block.VerifySignature(keyring, nil)
return block.Plaintext, err
}
func parseKeyID(hexKeyID *string) (uint64, error) {
if hexKeyID == nil || *hexKeyID == "" {
return 0, nil
}
result, err := strconv.ParseUint(*hexKeyID, 16, 64)
if err != nil {
return 0, err
}
return result, nil
}
var (
errMoreThanOneKey = errors.New("more than one signing key in keyring")
errNoKeys = errors.New("no signing key in keyring")
errNoPassword = errors.New("key is encrypted but no passphrase was provided")
)
func readSigningKey(keyFile, passphrase string) (*openpgp.Entity, error) {
fileContent, err := os.ReadFile(keyFile)
if err != nil {
return nil, fmt.Errorf("reading PGP key file: %w", err)
}
var entityList openpgp.EntityList
if isASCII(fileContent) {
entityList, err = openpgp.ReadArmoredKeyRing(bytes.NewReader(fileContent))
if err != nil {
return nil, fmt.Errorf("decoding armored PGP keyring: %w", err)
}
} else {
entityList, err = openpgp.ReadKeyRing(bytes.NewReader(fileContent))
if err != nil {
return nil, fmt.Errorf("decoding PGP keyring: %w", err)
}
}
var key *openpgp.Entity
for _, candidate := range entityList {
if candidate.PrivateKey == nil {
continue
}
if !candidate.PrivateKey.CanSign() {
continue
}
if key != nil {
return nil, errMoreThanOneKey
}
key = candidate
}
if key == nil {
return nil, errNoKeys
}
if key.PrivateKey.Encrypted {
if passphrase == "" {
return nil, errNoPassword
}
pw := []byte(passphrase)
err = key.PrivateKey.Decrypt(pw)
if err != nil {
return nil, fmt.Errorf("decrypt secret signing key: %w", err)
}
for _, sub := range key.Subkeys {
if sub.PrivateKey != nil {
if err := sub.PrivateKey.Decrypt(pw); err != nil {
return nil, fmt.Errorf("gopenpgp: error in unlocking sub key: %w", err)
}
}
}
}
return key, nil
}
func isASCII(s []byte) bool {
for i := range s {
if s[i] > unicode.MaxASCII {
return false
}
}
return true
}