2020-02-15 22:13:50 +00:00
|
|
|
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.
|
2020-03-13 21:24:46 +00:00
|
|
|
func SignifierPGPTmp(accountID string, randReader io.Reader) (SignifierInterface, []byte) {
|
2020-02-15 22:13:50 +00:00
|
|
|
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)
|
|
|
|
}
|
2020-03-13 21:24:46 +00:00
|
|
|
|
|
|
|
return accountSignifier{
|
|
|
|
accountID: accountID,
|
|
|
|
SignifierInterface: privKey,
|
|
|
|
}, pubKeyBody
|
2020-02-15 22:13:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|