mirror of
https://github.com/goreleaser/nfpm
synced 2025-04-21 01:07:59 +02:00
264 lines
6.6 KiB
Go
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
|
|
}
|