dehub/sigcred/pgp.go
mediocregopher aff3daab19 Modify how SignifierInterface is produced so it always sets AccountID on Credentials
---
type: change
message: |-
  Modify how SignifierInterface is produced so it always sets AccountID on Credentials

  Previously it was the responsibility of the caller of the Sign method to set the
  AccountID on the produced Credential, but this didn't really make sense. This
  commit makes it so that all SignifierInterface's produced by Signifier
  implicitly set the AccountID field.

  The solution here is still a bit hacky, and ultimately the real solution will
  probably be to refactor the structore of Credential, so that it doesn't have
  AccountID.
change_hash: ADPuz04GuyxWwjo/0/jc7DcsPMl5rK0osSpaqmUxv818
credentials:
- type: pgp_signature
  pub_key_id: 95C46FA6A41148AC
  body: iQIzBAABAgAdFiEEJ6tQKp6olvZKJ0lwlcRvpqQRSKwFAl5r+hgACgkQlcRvpqQRSKzwYBAAsY4tj+E5xtJSZ1TvrS0mwJ/lSHYWE4rS3eDMY3JUJLE1tr5k3OTRtUhh2UHCsArXSVF4sU8cBSCtf2noaThQm8KQghPMgoZ1LnPd4BnxxlE2gPik4FMcv+mCv9OgUh0AUO+rSXeYJA3oWunaW9kYollUdX/mVTQTmmbLBqBpeXF/TQO/bJTEEzA853j5QDT8//onfSIlzUw0UB57IZZZImp5/XrggHBbKdfhUTJ75LGMgDEDvDNIdV8lBys+RnMzK0Yj6EvLQhsw426+0Sf9vX3jtzj6WKhmi8QyYvcxIbcrWUScEfA/RAgf0A8KhqKq91bicSHjvyK1TZRSSWcS43ewamgvVWx0KSYYoIn7PPwOTmpHP8u6RzGEQFjOhP1EaGytQJKMXidU6CPTh+pYVtPZc8oLAwk+DyMquqfUSbzN/63t90HpTm7uycuzOnQxilYe2HKlbMJCId0a0DyAFrA+0pNRz0tyd3DvF4svCdEy82rzlUGEhq7aIJKoXIut+fKGEBd6Znz6oX15CyQq0oPthZcCqgFR0oTqufvV2iWo+26cd9dVTPVbJA9kSbaFchgdAqCkPA5wDVuNJJtMftf7STW8Lm6dnU6q9YFjZVdR55WtvUCINxBUtOirRzG1jcS0VNhhtb+SMNATEvDGJmt6neHM6Z17MAdwGS+s/hA=
  account: mediocregopher
2020-03-13 15:24:46 -06:00

284 lines
7.3 KiB
Go

package sigcred
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/sha256"
"dehub/fs"
"dehub/yamlutil"
"fmt"
"io"
"io/ioutil"
"os/exec"
"path/filepath"
"strings"
"time"
"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"`
Body yamlutil.Blob `yaml:"body"`
}
type pgpPubKey struct {
pubKey *packet.PublicKey
}
func newPGPPubKey(r io.Reader) (pgpPubKey, error) {
// TODO support non-armored keys as well
block, err := armor.Decode(r)
if err != nil {
return pgpPubKey{}, fmt.Errorf("could not decode armored PGP public key: %w", err)
}
pkt, err := packet.Read(block.Body)
if err != nil {
return pgpPubKey{}, fmt.Errorf("could not read PGP public key: %w", err)
}
pubKey, ok := pkt.(*packet.PublicKey)
if !ok {
return pgpPubKey{}, fmt.Errorf("packet is not a public key, it's a %T", pkt)
}
return pgpPubKey{pubKey: pubKey}, nil
}
func (s pgpPubKey) Signed(_ fs.FS, cred Credential) (bool, error) {
if cred.PGPSignature == nil {
return false, nil
}
return cred.PGPSignature.PubKeyID == s.pubKey.KeyIdString(), nil
}
func (s pgpPubKey) 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.pubKey.VerifySignature(h, sigPkt)
}
func (s pgpPubKey) encode() ([]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.pubKey.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
}
func (s pgpPubKey) asSignfier() (SignifierPGP, error) {
body, err := s.encode()
if err != nil {
return SignifierPGP{}, err
}
return SignifierPGP{
Body: string(body),
}, nil
}
type pgpPrivKey struct {
pgpPubKey
privKey *packet.PrivateKey
}
// SignifierPGPTmp 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.
func SignifierPGPTmp(accountID string, randReader io.Reader) (SignifierInterface, []byte) {
rawPrivKey, err := ecdsa.GenerateKey(elliptic.P521(), randReader)
if err != nil {
panic(err)
}
privKeyRaw := packet.NewECDSAPrivateKey(time.Now(), rawPrivKey)
privKey := pgpPrivKey{
pgpPubKey: pgpPubKey{
pubKey: &privKeyRaw.PublicKey,
},
privKey: privKeyRaw,
}
pubKeyBody, err := privKey.pgpPubKey.encode()
if err != nil {
panic(err)
}
return accountSignifier{
accountID: accountID,
SignifierInterface: privKey,
}, pubKeyBody
}
func (s pgpPrivKey) Sign(_ fs.FS, data []byte) (Credential, error) {
h := sha256.New()
h.Write(data)
var sig packet.Signature
sig.Hash = crypto.SHA256
sig.PubKeyAlgo = s.pubKey.PubKeyAlgo
if err := sig.Sign(h, s.privKey, nil); err != nil {
return Credential{}, fmt.Errorf("failed to sign data: %w", err)
}
body := new(bytes.Buffer)
if err := sig.Serialize(body); err != nil {
return Credential{}, fmt.Errorf("failed to serialize signature: %w", err)
}
return Credential{
PGPSignature: &CredentialPGPSignature{
PubKeyID: s.pubKey.KeyIdString(),
Body: body.Bytes(),
},
}, nil
}
// SignifierPGP describes a pgp public key whose corresponding private key will
// be used as a signing key.
type SignifierPGP struct {
Body string `yaml:"body"`
}
var _ SignifierInterface = SignifierPGP{}
func (s SignifierPGP) load() (pgpPubKey, error) {
return newPGPPubKey(strings.NewReader(s.Body))
}
// 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()
if err != nil {
return Credential{}, err
}
stderr := new(bytes.Buffer)
cmd := exec.Command("gpg",
"--openpgp",
"--detach-sign",
"--local-user", sigPGP.pubKey.KeyIdString())
cmd.Stdin = bytes.NewBuffer(data)
cmd.Stderr = stderr
sig, err := cmd.Output()
if err != nil {
return Credential{}, fmt.Errorf("error signing with gpg (%v): %s", err, stderr.String())
}
return Credential{
PGPSignature: &CredentialPGPSignature{
PubKeyID: sigPGP.pubKey.KeyIdString(),
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()
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()
if err != nil {
return err
}
return sigPGP.Verify(fs, data, cred)
}
// SignifierPGPFile is the same as SignifierPGP, except that the public key is
// found in the repo rather than encoded into the object.
type SignifierPGPFile struct {
Path string `yaml:"path"`
}
var _ SignifierInterface = SignifierPGPFile{}
func (s SignifierPGPFile) load(fs fs.FS) (SignifierPGP, error) {
path := filepath.Clean(s.Path)
fr, err := fs.Open(path)
if err != nil {
return SignifierPGP{}, fmt.Errorf("could not open PGP public key file at %q: %w", path, err)
}
defer fr.Close()
pubKeyB, err := ioutil.ReadAll(fr)
if err != nil {
return SignifierPGP{}, fmt.Errorf("could not read PGP public key from file blob at %q: %w", s.Path, err)
}
return SignifierPGP{Body: string(pubKeyB)}, nil
}
// Sign will sign the given arbitrary bytes using the private key corresponding
// to the pgp public key located by this Signifier.
func (s SignifierPGPFile) Sign(fs fs.FS, data []byte) (Credential, error) {
sigPGP, err := s.load(fs)
if err != nil {
return Credential{}, err
}
return sigPGP.Sign(fs, data)
}
// Signed returns true if the private key corresponding to the pgp public key
// located by this Signifier was used to produce the given Credential.
func (s SignifierPGPFile) Signed(fs fs.FS, cred Credential) (bool, error) {
if cred.PGPSignature == nil {
return false, nil
}
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 SignifierPGPFile) 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)
}