Files

229 lines
7.1 KiB
Go

// Package gpg wraps OpenPGP keypair generation and import for codit's signing
// keys. It uses ProtonMail/go-crypto/openpgp so the produced keys are
// type-compatible with the RPM signing/verification path (go-rpmutils).
package gpg
import "bytes"
import "crypto"
import "errors"
import "fmt"
import "io"
import "strings"
import "github.com/ProtonMail/go-crypto/openpgp"
import "github.com/ProtonMail/go-crypto/openpgp/armor"
import "github.com/ProtonMail/go-crypto/openpgp/packet"
const publicKeyBlockType = "PGP PUBLIC KEY BLOCK"
const privateKeyBlockType = "PGP PRIVATE KEY BLOCK"
const signatureBlockType = "PGP SIGNATURE"
// compatConfig produces keys and signatures that older OpenPGP stacks (notably
// EL6/7 yum's pure-Python repo_gpgcheck parser) can read: plain RSA-2048 +
// SHA-256, and crucially NO salted-signature notation. go-crypto adds a
// "salt@notations.openpgpjs.org" notation to signatures by default, which that
// old parser chokes on (yum reports "Gpg Keys not imported").
func compatConfig() *packet.Config {
var noSaltNotation bool = false
return &packet.Config{
Algorithm: packet.PubKeyAlgoRSA,
RSABits: 2048,
DefaultHash: crypto.SHA256,
NonDeterministicSignaturesViaNotation: &noSaltNotation,
}
}
// KeyMaterial holds the derived/serialized material for a GPG key.
type KeyMaterial struct {
Fingerprint string
KeyID string
PublicArmored string
PrivateArmored string
}
// Generate creates a new OpenPGP signing keypair for the given name/email.
func Generate(name string, email string) (KeyMaterial, error) {
var km KeyMaterial
var entity *openpgp.Entity
var err error
entity, err = openpgp.NewEntity(strings.TrimSpace(name), "codit signing key", strings.TrimSpace(email), compatConfig())
if err != nil {
return km, err
}
return materialFromEntity(entity, true)
}
// Parse reads an armored private key and derives its public material.
func Parse(privateArmored string) (KeyMaterial, error) {
var km KeyMaterial
var list openpgp.EntityList
var err error
list, err = openpgp.ReadArmoredKeyRing(strings.NewReader(privateArmored))
if err != nil {
return km, err
}
if len(list) == 0 {
return km, errors.New("no key found in input")
}
if list[0].PrivateKey == nil {
return km, errors.New("input does not contain a private key")
}
if list[0].PrivateKey.Encrypted {
return km, errors.New("signing key is passphrase-protected; cannot sign unattended")
}
km, err = materialFromEntity(list[0], false)
if err != nil {
return km, err
}
km.PrivateArmored = strings.TrimSpace(privateArmored)
return km, nil
}
// VerifyDetached checks an armored detached signature over signedData against a
// set of armored public keys. On success it returns the signer's primary-key
// fingerprint (uppercase hex) and true; otherwise "" and false. Keys that fail
// to parse are skipped, and signing subkeys are matched via their primary.
func VerifyDetached(publicKeys []string, signedData []byte, armoredSig string) (string, bool) {
var keyring openpgp.EntityList
var list openpgp.EntityList
var signer *openpgp.Entity
var pub string
var err error
for _, pub = range publicKeys {
list, err = openpgp.ReadArmoredKeyRing(strings.NewReader(pub))
if err != nil { continue }
keyring = append(keyring, list...)
}
if len(keyring) == 0 { return "", false }
signer, err = openpgp.CheckArmoredDetachedSignature(keyring, bytes.NewReader(signedData), strings.NewReader(armoredSig), nil)
if err != nil || signer == nil || signer.PrimaryKey == nil { return "", false }
return fmt.Sprintf("%X", signer.PrimaryKey.Fingerprint), true
}
// SignatureIssuer extracts issuer metadata from an armored detached signature.
// Either value may be empty because older signatures do not always carry both
// subpackets.
func SignatureIssuer(armoredSig string) (string, string, bool) {
var block *armor.Block
var reader *packet.Reader
var pkt packet.Packet
var sig *packet.Signature
var fingerprint string
var keyID string
var err error
block, err = armor.Decode(strings.NewReader(armoredSig))
if err != nil || block == nil || block.Type != signatureBlockType {
return "", "", false
}
reader = packet.NewReader(block.Body)
pkt, err = reader.Next()
if err != nil {
return "", "", false
}
sig, _ = pkt.(*packet.Signature)
if sig == nil {
return "", "", false
}
if len(sig.IssuerFingerprint) > 0 {
fingerprint = fmt.Sprintf("%X", sig.IssuerFingerprint)
}
if sig.IssuerKeyId != nil {
keyID = fmt.Sprintf("%016X", *sig.IssuerKeyId)
}
return fingerprint, keyID, fingerprint != "" || keyID != ""
}
// ParsePublic reads an armored PUBLIC key and derives its material. It rejects
// input that carries a private key, since personal keys are registered
// public-only (the user keeps their private half locally).
func ParsePublic(publicArmored string) (KeyMaterial, error) {
var km KeyMaterial
var list openpgp.EntityList
var err error
list, err = openpgp.ReadArmoredKeyRing(strings.NewReader(publicArmored))
if err != nil {
return km, err
}
if len(list) == 0 {
return km, errors.New("no key found in input")
}
if list[0].PrivateKey != nil {
return km, errors.New("please provide a public key, not a private key")
}
return materialFromEntity(list[0], false)
}
// SignDetached produces an ASCII-armored detached signature of message using
// the given armored private key (e.g. for repodata/repomd.xml.asc).
func SignDetached(privateArmored string, message []byte) (string, error) {
var list openpgp.EntityList
var buf bytes.Buffer
var err error
list, err = openpgp.ReadArmoredKeyRing(strings.NewReader(privateArmored))
if err != nil {
return "", err
}
if len(list) == 0 || list[0].PrivateKey == nil {
return "", errors.New("input does not contain a private key")
}
if list[0].PrivateKey.Encrypted {
return "", errors.New("signing key is passphrase-protected; cannot sign unattended")
}
err = openpgp.ArmoredDetachSign(&buf, list[0], bytes.NewReader(message), compatConfig())
if err != nil {
return "", err
}
return buf.String(), nil
}
func materialFromEntity(entity *openpgp.Entity, includePrivate bool) (KeyMaterial, error) {
var km KeyMaterial
var pubBuf bytes.Buffer
var w io.WriteCloser
var err error
w, err = armor.Encode(&pubBuf, publicKeyBlockType, nil)
if err != nil {
return km, err
}
err = entity.Serialize(w)
if err != nil {
w.Close()
return km, err
}
err = w.Close()
if err != nil {
return km, err
}
km.PublicArmored = pubBuf.String()
if includePrivate {
var privBuf bytes.Buffer
var pw io.WriteCloser
pw, err = armor.Encode(&privBuf, privateKeyBlockType, nil)
if err != nil {
return km, err
}
// SerializePrivate re-signs the self/subkey signatures with the given
// config; pass compatConfig so the salt notation is not re-introduced.
err = entity.SerializePrivate(pw, compatConfig())
if err != nil {
pw.Close()
return km, err
}
err = pw.Close()
if err != nil {
return km, err
}
km.PrivateArmored = privBuf.String()
}
km.Fingerprint = fmt.Sprintf("%X", entity.PrimaryKey.Fingerprint)
km.KeyID = entity.PrimaryKey.KeyIdString()
return km, nil
}