229 lines
7.1 KiB
Go
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
|
|
}
|