dehub/sigcred/pgp.go
mediocregopher 23fa9da972 properly check for gpg not returning a matching pgp key
---
type: change
description: properly check for gpg not returning a matching pgp key
fingerprint: AC2l2tYnpJAkvDzaRR5QQ2MSa4lsVbDO7I1ilcMKFX61
credentials:
- type: pgp_signature
  pub_key_id: 95C46FA6A41148AC
  body: iQIzBAABAgAdFiEEJ6tQKp6olvZKJ0lwlcRvpqQRSKwFAl6t03YACgkQlcRvpqQRSKyawxAArg76IfshpyGZgGFkfcQz6eyEj9bxwQ8zp1LU+sWch5HxHmsWFFPBrXBcmWVJuSsPZzNNDyIb2tM23NqjnZIq55BigmhX1iX7NDDfoRcMsXi6ibvY7bS3zJ0PRAWplTKUpkQp0cRnq8/Awy9NpUHoop+0HbJ65XmE94kHqboQj7DLG/ZWa1r8xQmKM+1Ffob6u0A7AOhUkWeghfzzjWmAkmjpybD+RZ90pQrwLRdA4+3mrVR0+cDxe70c0B9rPOoKdMsYx4PGfpyk6eIwpE+N7mgXQkmUbiVzA6oVUDw9MkSo17E6qU1/b2NWy5x0BAGdka4NlL7IEYZ3a/iryqrFU58jpmxD9sfZfVJbEhEyUENHvZIMa7t7ysDJa1B9ZngVFcGT06btO+55aSAAWzn60+gtpDLrwbiPMBKQFYwPsXRhiUOum/yfyhXiILqrOdktps+TvfxAan2WzSyHaXWTAuYeqddkDxqVhKZo00bMeSNWPWDJgUfBj1jeJMlnCzV8zDRxTYU1CWiW6EpCNIy0hH137s3fQuWsJdkhNdS4QguxQYY5EE5fF1VKNBXou4coCpEIAgrDzphjxaufRqVP3lvivpfkA5INU2XsIc+hGTI3nTjYxvi5PMsguN1KuKVeZtsIPDUlxUbwNfV7A+TLDOxJTVVCx9r3x5OzcRmpSvM=
  account: mediocregopher
2020-05-02 14:09:33 -06:00

321 lines
8.8 KiB
Go

package sigcred
import (
"bytes"
"crypto"
"crypto/sha256"
"errors"
"fmt"
"io"
"io/ioutil"
"os/exec"
"path/filepath"
"strings"
"dehub.dev/src/dehub.git/fs"
"dehub.dev/src/dehub.git/yamlutil"
"golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/armor"
"golang.org/x/crypto/openpgp/packet"
)
// CredentialPGPSignature describes a PGP signature which has been used to sign
// a commit.
type CredentialPGPSignature struct {
PubKeyID string `yaml:"pub_key_id"`
PubKeyBody string `yaml:"pub_key_body,omitempty"`
Body yamlutil.Blob `yaml:"body"`
}
// SelfVerify will only work if PubKeyBody is filled in. If so, Body will
// attempt to be verified by that public key.
func (c *CredentialPGPSignature) SelfVerify(data []byte) error {
if c.PubKeyBody == "" {
return ErrNotSelfVerifying{
Subject: "PGP signature Credential with no pub_key_body field",
}
}
sig := SignifierPGP{Body: c.PubKeyBody}
return sig.Verify(nil, data, CredentialUnion{PGPSignature: c})
}
type pgpKey struct {
entity *openpgp.Entity
}
func newPGPPubKey(r io.Reader) (pgpKey, error) {
// TODO support non-armored keys as well
block, err := armor.Decode(r)
if err != nil {
return pgpKey{}, fmt.Errorf("could not decode armored PGP public key: %w", err)
}
entity, err := openpgp.ReadEntity(packet.NewReader(block.Body))
if err != nil {
return pgpKey{}, fmt.Errorf("could not read PGP public key: %w", err)
}
return pgpKey{entity: entity}, nil
}
func (s pgpKey) Sign(_ fs.FS, data []byte) (CredentialUnion, error) {
if s.entity.PrivateKey == nil {
return CredentialUnion{}, errors.New("private key not loaded")
}
h := sha256.New()
h.Write(data)
var sig packet.Signature
sig.Hash = crypto.SHA256
sig.PubKeyAlgo = s.entity.PrimaryKey.PubKeyAlgo
if err := sig.Sign(h, s.entity.PrivateKey, nil); err != nil {
return CredentialUnion{}, fmt.Errorf("signing data: %w", err)
}
body := new(bytes.Buffer)
if err := sig.Serialize(body); err != nil {
return CredentialUnion{}, fmt.Errorf("serializing signature: %w", err)
}
return CredentialUnion{
PGPSignature: &CredentialPGPSignature{
PubKeyID: s.entity.PrimaryKey.KeyIdString(),
Body: body.Bytes(),
},
}, nil
}
func (s pgpKey) Signed(_ fs.FS, cred CredentialUnion) (bool, error) {
if cred.PGPSignature == nil {
return false, nil
}
return cred.PGPSignature.PubKeyID == s.entity.PrimaryKey.KeyIdString(), nil
}
func (s pgpKey) Verify(_ fs.FS, data []byte, cred CredentialUnion) error {
credSig := cred.PGPSignature
if credSig == nil {
return fmt.Errorf("SignifierPGPFile cannot verify %+v", cred)
}
pkt, err := packet.Read(bytes.NewBuffer(credSig.Body))
if err != nil {
return fmt.Errorf("could not read signature packet: %w", err)
}
sigPkt, ok := pkt.(*packet.Signature)
if !ok {
return fmt.Errorf("signature bytes were parsed as a %T, not a signature", pkt)
}
// The gpg process which is invoked during normal signing automatically
// hashes whatever is piped to it. The VerifySignature method in the openpgp
// package expects you to do it yourself.
h := sigPkt.Hash.New()
h.Write(data)
return s.entity.PrimaryKey.VerifySignature(h, sigPkt)
}
func (s pgpKey) MarshalBinary() ([]byte, error) {
body := new(bytes.Buffer)
armorEncoder, err := armor.Encode(body, "PGP PUBLIC KEY", nil)
if err != nil {
return nil, fmt.Errorf("initializing armor encoder: %w", err)
} else if err := s.entity.Serialize(armorEncoder); err != nil {
return nil, fmt.Errorf("encoding public key: %w", err)
} else if err := armorEncoder.Close(); err != nil {
return nil, fmt.Errorf("closing armor encoder: %w", err)
}
return body.Bytes(), nil
}
func (s pgpKey) userID() (*packet.UserId, error) {
if l := len(s.entity.Identities); l == 0 {
return nil, errors.New("pgp key has no identity information")
} else if l > 1 {
return nil, errors.New("multiple identities on a single pgp key is unsupported")
}
var identity *openpgp.Identity
for _, identity = range s.entity.Identities {
break
}
return identity.UserId, nil
}
func anonPGPSignifier(pgpKey pgpKey, sig Signifier) (Signifier, error) {
keyID := pgpKey.entity.PrimaryKey.KeyIdString()
userID, err := pgpKey.userID()
if err != nil {
return nil, err
}
pubKeyBody, err := pgpKey.MarshalBinary()
if err != nil {
return nil, err
}
return signifierMiddleware{
Signifier: sig,
signCallback: func(cred *CredentialUnion) {
cred.PGPSignature.PubKeyBody = string(pubKeyBody)
cred.AnonID = fmt.Sprintf("%s %q", keyID, userID.Email)
},
}, nil
}
// TestSignifierPGP returns a direct implementation of Signifier which uses a
// random private key generated in memory, as well as an armored version of its
// public key.
//
// NOTE that the key returned is very weak, and should only be used for tests.
func TestSignifierPGP(name string, anon bool, randReader io.Reader) (Signifier, []byte) {
entity, err := openpgp.NewEntity(name, "", name+"@example.com", &packet.Config{
Rand: randReader,
RSABits: 512,
})
if err != nil {
panic(err)
}
pgpKey := pgpKey{entity: entity}
pubKeyBody, err := pgpKey.MarshalBinary()
if err != nil {
panic(err)
}
if anon {
sigInt, err := anonPGPSignifier(pgpKey, pgpKey)
if err != nil {
panic(err)
}
return sigInt, pubKeyBody
}
return accountSignifier(name, pgpKey), pubKeyBody
}
// SignifierPGP describes a pgp public key whose corresponding private key will
// be used as a signing key. The public key can be described by one of multiple
// fields, each being a different method of loading the public key. Only one
// field should be set.
type SignifierPGP struct {
// An armored string encoding of the public key, as exported via
// `gpg -a --export <key-id>`
Body string `yaml:"body,omitempty"`
// Path, relative to the root of the repo, of the armored public key file.
Path string `yaml:"path,omitempty"`
}
var _ Signifier = SignifierPGP{}
func cmdGPG(stdin []byte, args ...string) ([]byte, error) {
args = append([]string{"--openpgp"}, args...)
stderr := new(bytes.Buffer)
cmd := exec.Command("gpg", args...)
cmd.Stdin = bytes.NewBuffer(stdin)
cmd.Stderr = stderr
out, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("calling gpg command (%v): %s", err, stderr.String())
}
return out, nil
}
// LoadSignifierPGP loads a pgp key using the given identifier. The key is
// assumed to be stored in the client's keyring already.
//
// If this is being called for an anonymous user to use, then anon can be set to
// true. This will have the effect of setting the PubKeyBody and AnonID of all
// produced credentials.
func LoadSignifierPGP(keyID string, anon bool) (Signifier, error) {
pubKey, err := cmdGPG(nil, "-a", "--export", keyID)
if err != nil {
return nil, fmt.Errorf("loading public key: %w", err)
} else if len(pubKey) == 0 {
return nil, fmt.Errorf("no public key found for %q", keyID)
}
sig := &SignifierPGP{Body: string(pubKey)}
if !anon {
return sig, nil
}
pgpKey, err := sig.load(nil)
if err != nil {
return nil, err
}
return anonPGPSignifier(pgpKey, sig)
}
func (s SignifierPGP) load(fs fs.FS) (pgpKey, error) {
if s.Body != "" {
return newPGPPubKey(strings.NewReader(s.Body))
}
path := filepath.Clean(s.Path)
fr, err := fs.Open(path)
if err != nil {
return pgpKey{}, fmt.Errorf("opening PGP public key file at %q: %w", path, err)
}
defer fr.Close()
pubKeyB, err := ioutil.ReadAll(fr)
if err != nil {
return pgpKey{}, fmt.Errorf("reading PGP public key from file at %q: %w", s.Path, err)
}
return SignifierPGP{Body: string(pubKeyB)}.load(fs)
}
// Sign will sign the given arbitrary bytes using the private key corresponding
// to the pgp public key embedded in this Signifier.
func (s SignifierPGP) Sign(fs fs.FS, data []byte) (CredentialUnion, error) {
sigPGP, err := s.load(fs)
if err != nil {
return CredentialUnion{}, err
}
keyID := sigPGP.entity.PrimaryKey.KeyIdString()
sig, err := cmdGPG(data, "--detach-sign", "--local-user", keyID)
if err != nil {
return CredentialUnion{}, fmt.Errorf("signing with pgp key: %w", err)
}
return CredentialUnion{
PGPSignature: &CredentialPGPSignature{
PubKeyID: keyID,
Body: sig,
},
}, nil
}
// Signed returns true if the private key corresponding to the pgp public key
// embedded in this Signifier was used to produce the given Credential.
func (s SignifierPGP) Signed(fs fs.FS, cred CredentialUnion) (bool, error) {
sigPGP, err := s.load(fs)
if err != nil {
return false, err
}
return sigPGP.Signed(fs, cred)
}
// Verify asserts that the given signature was produced by this key signing the
// given piece of data.
func (s SignifierPGP) Verify(fs fs.FS, data []byte, cred CredentialUnion) error {
sigPGP, err := s.load(fs)
if err != nil {
return err
}
return sigPGP.Verify(fs, data, cred)
}
// SignifierPGPFile is deprecated and should not be used, use the Path field of
// SignifierPGP instead.
type SignifierPGPFile struct {
Path string `yaml:"path"`
}