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