dehub/sigcred/pgp.go
mediocregopher 43b564e711 refactor pgp types a bit to use the openpgp.Entity type
---
type: change
message: |-
  refactor pgp types a bit to use the openpgp.Entity type

  By using Entity the pgp code will have access to the public key's identity
  information, which will help in making more readable commit messages from
  anonymous users.
change_hash: AAJCn1/lybheXWv8Rfy2Y2r4UF9seGptC6uvFn894c8Y
credentials:
- type: pgp_signature
  pub_key_id: 95C46FA6A41148AC
  body: iQIzBAABAgAdFiEEJ6tQKp6olvZKJ0lwlcRvpqQRSKwFAl6TSX4ACgkQlcRvpqQRSKzCpQ/+I/8BmEtsbwepiGZiIFg0HZK+z4CR5DWLpKQg3w5+NDSfzY9dkdwqbt0ohPyFmLXZz7s1cn/iU0OOb59fUPmH0TklMedY+l1q5XDNZ160l5494QXNXZDKMGC2QIjst+Fu9Qwys7U+1pTu8rN3K1x/ZQCCJvDVFH13jj7bIna2ltxhsqNY6OiVUc5EJsZHbNAgFXqYJNt4XbC+9QcOyDyyYytmJnjEX2oquj7skac9uyBjjJGkqA70sjD4yn44iFqEpFuJAqFuEJDj1D4bkt7Ib+syi/eMPZgDvHRtMGAxl+fBU2OAG8GIlxveYqeUejCp8SYoat+0Tdoi3lT/wj2lTQRtMBeOGUccpsOMRI1t+g5ggKx3YcQgbKLixuAgl649fnX7fmBLB3IuG5PdgnTqjQBPkTBYhkR80fDSg3nfuh+1TVDGiW6C8HtJRlFWWDRcyaYhcWZwKXVTMm7xtgNJYwpLeDmuTCJV6kvldxmBZEmfrQ/1u+wKG2/kCun9stSHdEuuZVM8sv4ECME4C+1fTNnEkzJhHvULV39b41ODZolxNj8ZRa1qt8emC1Hw6ZUjdkA0gXS70l8tfOtQunPTPLDGzNZNYNw8H2JqEM3O6NLIzwEUJVYOA6BmibQj5I7QMSvOS2PpD9KDKIR2vXNsk1yUqdpu9PmLhcclrMqEGOc=
  account: mediocregopher
2020-04-12 11:02:05 -06:00

275 lines
7.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, Credential{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) (Credential, error) {
if s.entity.PrivateKey == nil {
return Credential{}, 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 Credential{}, fmt.Errorf("signing data: %w", err)
}
body := new(bytes.Buffer)
if err := sig.Serialize(body); err != nil {
return Credential{}, fmt.Errorf("serializing signature: %w", err)
}
return Credential{
PGPSignature: &CredentialPGPSignature{
PubKeyID: s.entity.PrimaryKey.KeyIdString(),
Body: body.Bytes(),
},
}, nil
}
func (s pgpKey) Signed(_ fs.FS, cred Credential) (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 Credential) 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("error initializing armor encoder: %w", err)
} else if err := s.entity.Serialize(armorEncoder); err != nil {
return nil, fmt.Errorf("error encoding public key: %w", err)
} else if err := armorEncoder.Close(); err != nil {
return nil, fmt.Errorf("error closing armor encoder: %w", err)
}
return body.Bytes(), nil
}
// TestSignifierPGP returns a direct implementation of the SignifierInterface
// 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(accountID string, randReader io.Reader) (SignifierInterface, []byte) {
entity, err := openpgp.NewEntity(accountID, "", accountID+"@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)
}
return accountSignifier(accountID, 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 _ SignifierInterface = 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 setPubKeyBody is true, then CredentialPGPSignature instances produced by
// the returned Signifier will have their PubKeyBody field set.
func LoadSignifierPGP(keyID string, setPubKeyBody bool) (SignifierInterface, error) {
pubKey, err := cmdGPG(nil, "-a", "--export", keyID)
if err != nil {
return nil, fmt.Errorf("loading public key: %w", err)
}
var sigInt SignifierInterface = &SignifierPGP{Body: string(pubKey)}
if setPubKeyBody {
sigInt = signifierMiddleware{
SignifierInterface: sigInt,
signCallback: func(cred *Credential) {
cred.PGPSignature.PubKeyBody = string(pubKey)
},
}
}
return sigInt, nil
}
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) (Credential, error) {
sigPGP, err := s.load(fs)
if err != nil {
return Credential{}, err
}
keyID := sigPGP.entity.PrimaryKey.KeyIdString()
sig, err := cmdGPG(data, "--detach-sign", "--local-user", keyID)
if err != nil {
return Credential{}, fmt.Errorf("signing with pgp key: %w", err)
}
return Credential{
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 Credential) (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 Credential) 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"`
}