2020-02-15 22:13:50 +00:00
|
|
|
package sigcred
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"crypto"
|
|
|
|
"crypto/sha256"
|
2020-04-12 17:02:05 +00:00
|
|
|
"errors"
|
2020-02-15 22:13:50 +00:00
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"os/exec"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
|
2020-04-11 23:10:18 +00:00
|
|
|
"dehub.dev/src/dehub.git/fs"
|
|
|
|
"dehub.dev/src/dehub.git/yamlutil"
|
|
|
|
|
2020-04-12 17:02:05 +00:00
|
|
|
"golang.org/x/crypto/openpgp"
|
2020-02-15 22:13:50 +00:00
|
|
|
"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 {
|
2020-04-11 23:10:18 +00:00
|
|
|
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}
|
2020-04-26 20:23:03 +00:00
|
|
|
return sig.Verify(nil, data, CredentialUnion{PGPSignature: c})
|
2020-02-15 22:13:50 +00:00
|
|
|
}
|
|
|
|
|
2020-04-12 17:02:05 +00:00
|
|
|
type pgpKey struct {
|
|
|
|
entity *openpgp.Entity
|
2020-02-15 22:13:50 +00:00
|
|
|
}
|
|
|
|
|
2020-04-12 17:02:05 +00:00
|
|
|
func newPGPPubKey(r io.Reader) (pgpKey, error) {
|
2020-02-15 22:13:50 +00:00
|
|
|
// TODO support non-armored keys as well
|
|
|
|
block, err := armor.Decode(r)
|
|
|
|
if err != nil {
|
2020-04-12 17:02:05 +00:00
|
|
|
return pgpKey{}, fmt.Errorf("could not decode armored PGP public key: %w", err)
|
2020-02-15 22:13:50 +00:00
|
|
|
}
|
|
|
|
|
2020-04-12 17:02:05 +00:00
|
|
|
entity, err := openpgp.ReadEntity(packet.NewReader(block.Body))
|
2020-02-15 22:13:50 +00:00
|
|
|
if err != nil {
|
2020-04-12 17:02:05 +00:00
|
|
|
return pgpKey{}, fmt.Errorf("could not read PGP public key: %w", err)
|
2020-02-15 22:13:50 +00:00
|
|
|
}
|
2020-04-12 17:02:05 +00:00
|
|
|
return pgpKey{entity: entity}, nil
|
|
|
|
}
|
2020-02-15 22:13:50 +00:00
|
|
|
|
2020-04-26 20:23:03 +00:00
|
|
|
func (s pgpKey) Sign(_ fs.FS, data []byte) (CredentialUnion, error) {
|
2020-04-12 17:02:05 +00:00
|
|
|
if s.entity.PrivateKey == nil {
|
2020-04-26 20:23:03 +00:00
|
|
|
return CredentialUnion{}, errors.New("private key not loaded")
|
2020-04-12 17:02:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2020-04-26 20:23:03 +00:00
|
|
|
return CredentialUnion{}, fmt.Errorf("signing data: %w", err)
|
2020-04-12 17:02:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
body := new(bytes.Buffer)
|
|
|
|
if err := sig.Serialize(body); err != nil {
|
2020-04-26 20:23:03 +00:00
|
|
|
return CredentialUnion{}, fmt.Errorf("serializing signature: %w", err)
|
2020-02-15 22:13:50 +00:00
|
|
|
}
|
|
|
|
|
2020-04-26 20:23:03 +00:00
|
|
|
return CredentialUnion{
|
2020-04-12 17:02:05 +00:00
|
|
|
PGPSignature: &CredentialPGPSignature{
|
|
|
|
PubKeyID: s.entity.PrimaryKey.KeyIdString(),
|
|
|
|
Body: body.Bytes(),
|
|
|
|
},
|
|
|
|
}, nil
|
2020-02-15 22:13:50 +00:00
|
|
|
}
|
|
|
|
|
2020-04-26 20:23:03 +00:00
|
|
|
func (s pgpKey) Signed(_ fs.FS, cred CredentialUnion) (bool, error) {
|
2020-02-15 22:13:50 +00:00
|
|
|
if cred.PGPSignature == nil {
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
|
2020-04-12 17:02:05 +00:00
|
|
|
return cred.PGPSignature.PubKeyID == s.entity.PrimaryKey.KeyIdString(), nil
|
2020-02-15 22:13:50 +00:00
|
|
|
}
|
|
|
|
|
2020-04-26 20:23:03 +00:00
|
|
|
func (s pgpKey) Verify(_ fs.FS, data []byte, cred CredentialUnion) error {
|
2020-02-15 22:13:50 +00:00
|
|
|
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)
|
2020-04-12 17:02:05 +00:00
|
|
|
return s.entity.PrimaryKey.VerifySignature(h, sigPkt)
|
2020-02-15 22:13:50 +00:00
|
|
|
}
|
|
|
|
|
2020-04-12 17:02:05 +00:00
|
|
|
func (s pgpKey) MarshalBinary() ([]byte, error) {
|
2020-02-15 22:13:50 +00:00
|
|
|
body := new(bytes.Buffer)
|
|
|
|
armorEncoder, err := armor.Encode(body, "PGP PUBLIC KEY", nil)
|
|
|
|
if err != nil {
|
2020-04-12 17:59:23 +00:00
|
|
|
return nil, fmt.Errorf("initializing armor encoder: %w", err)
|
2020-04-12 17:02:05 +00:00
|
|
|
} else if err := s.entity.Serialize(armorEncoder); err != nil {
|
2020-04-12 17:59:23 +00:00
|
|
|
return nil, fmt.Errorf("encoding public key: %w", err)
|
2020-02-15 22:13:50 +00:00
|
|
|
} else if err := armorEncoder.Close(); err != nil {
|
2020-04-12 17:59:23 +00:00
|
|
|
return nil, fmt.Errorf("closing armor encoder: %w", err)
|
2020-02-15 22:13:50 +00:00
|
|
|
}
|
|
|
|
return body.Bytes(), nil
|
|
|
|
}
|
|
|
|
|
2020-04-12 17:59:23 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-04-26 20:23:03 +00:00
|
|
|
func anonPGPSignifier(pgpKey pgpKey, sig Signifier) (Signifier, error) {
|
2020-04-12 17:59:23 +00:00
|
|
|
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{
|
2020-04-26 20:23:03 +00:00
|
|
|
Signifier: sig,
|
|
|
|
signCallback: func(cred *CredentialUnion) {
|
2020-04-12 17:59:23 +00:00
|
|
|
cred.PGPSignature.PubKeyBody = string(pubKeyBody)
|
|
|
|
cred.AnonID = fmt.Sprintf("%s %q", keyID, userID.Email)
|
|
|
|
},
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2020-04-26 20:23:03 +00:00
|
|
|
// 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.
|
2020-04-12 17:02:05 +00:00
|
|
|
//
|
|
|
|
// NOTE that the key returned is very weak, and should only be used for tests.
|
2020-04-26 20:23:03 +00:00
|
|
|
func TestSignifierPGP(name string, anon bool, randReader io.Reader) (Signifier, []byte) {
|
2020-04-12 17:59:23 +00:00
|
|
|
entity, err := openpgp.NewEntity(name, "", name+"@example.com", &packet.Config{
|
2020-04-12 17:02:05 +00:00
|
|
|
Rand: randReader,
|
|
|
|
RSABits: 512,
|
|
|
|
})
|
2020-02-15 22:13:50 +00:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
2020-04-12 17:02:05 +00:00
|
|
|
pgpKey := pgpKey{entity: entity}
|
|
|
|
pubKeyBody, err := pgpKey.MarshalBinary()
|
2020-02-15 22:13:50 +00:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
2020-03-13 21:24:46 +00:00
|
|
|
|
2020-04-12 17:59:23 +00:00
|
|
|
if anon {
|
|
|
|
sigInt, err := anonPGPSignifier(pgpKey, pgpKey)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return sigInt, pubKeyBody
|
|
|
|
}
|
|
|
|
return accountSignifier(name, pgpKey), pubKeyBody
|
2020-02-15 22:13:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// SignifierPGP describes a pgp public key whose corresponding private key will
|
2020-04-11 23:10:18 +00:00
|
|
|
// 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.
|
2020-02-15 22:13:50 +00:00
|
|
|
type SignifierPGP struct {
|
2020-04-11 23:10:18 +00:00
|
|
|
// An armored string encoding of the public key, as exported via
|
|
|
|
// `gpg -a --export <key-id>`
|
|
|
|
Body string `yaml:"body,omitempty"`
|
2020-02-15 22:13:50 +00:00
|
|
|
|
2020-04-11 23:10:18 +00:00
|
|
|
// Path, relative to the root of the repo, of the armored public key file.
|
|
|
|
Path string `yaml:"path,omitempty"`
|
2020-02-15 22:13:50 +00:00
|
|
|
}
|
|
|
|
|
2020-04-26 20:23:03 +00:00
|
|
|
var _ Signifier = SignifierPGP{}
|
2020-02-15 22:13:50 +00:00
|
|
|
|
2020-04-11 23:10:18 +00:00
|
|
|
func cmdGPG(stdin []byte, args ...string) ([]byte, error) {
|
|
|
|
args = append([]string{"--openpgp"}, args...)
|
2020-02-15 22:13:50 +00:00
|
|
|
stderr := new(bytes.Buffer)
|
2020-04-11 23:10:18 +00:00
|
|
|
cmd := exec.Command("gpg", args...)
|
|
|
|
cmd.Stdin = bytes.NewBuffer(stdin)
|
2020-02-15 22:13:50 +00:00
|
|
|
cmd.Stderr = stderr
|
2020-04-11 23:10:18 +00:00
|
|
|
out, err := cmd.Output()
|
2020-02-15 22:13:50 +00:00
|
|
|
if err != nil {
|
2020-04-11 23:10:18 +00:00
|
|
|
return nil, fmt.Errorf("calling gpg command (%v): %s", err, stderr.String())
|
2020-02-15 22:13:50 +00:00
|
|
|
}
|
2020-04-11 23:10:18 +00:00
|
|
|
return out, nil
|
2020-02-15 22:13:50 +00:00
|
|
|
}
|
|
|
|
|
2020-04-12 17:02:05 +00:00
|
|
|
// LoadSignifierPGP loads a pgp key using the given identifier. The key is
|
|
|
|
// assumed to be stored in the client's keyring already.
|
2020-04-11 23:10:18 +00:00
|
|
|
//
|
2020-04-12 17:59:23 +00:00
|
|
|
// 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
|
2020-04-26 20:23:03 +00:00
|
|
|
// produced credentials.
|
|
|
|
func LoadSignifierPGP(keyID string, anon bool) (Signifier, error) {
|
2020-04-11 23:10:18 +00:00
|
|
|
pubKey, err := cmdGPG(nil, "-a", "--export", keyID)
|
2020-02-15 22:13:50 +00:00
|
|
|
if err != nil {
|
2020-04-11 23:10:18 +00:00
|
|
|
return nil, fmt.Errorf("loading public key: %w", err)
|
2020-02-15 22:13:50 +00:00
|
|
|
}
|
2020-04-12 17:59:23 +00:00
|
|
|
|
|
|
|
sig := &SignifierPGP{Body: string(pubKey)}
|
|
|
|
if !anon {
|
|
|
|
return sig, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
pgpKey, err := sig.load(nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2020-02-15 22:13:50 +00:00
|
|
|
}
|
2020-04-12 17:59:23 +00:00
|
|
|
return anonPGPSignifier(pgpKey, sig)
|
2020-02-15 22:13:50 +00:00
|
|
|
}
|
|
|
|
|
2020-04-12 17:02:05 +00:00
|
|
|
func (s SignifierPGP) load(fs fs.FS) (pgpKey, error) {
|
2020-04-11 23:10:18 +00:00
|
|
|
if s.Body != "" {
|
|
|
|
return newPGPPubKey(strings.NewReader(s.Body))
|
|
|
|
}
|
2020-02-15 22:13:50 +00:00
|
|
|
|
|
|
|
path := filepath.Clean(s.Path)
|
|
|
|
fr, err := fs.Open(path)
|
|
|
|
if err != nil {
|
2020-04-12 17:02:05 +00:00
|
|
|
return pgpKey{}, fmt.Errorf("opening PGP public key file at %q: %w", path, err)
|
2020-02-15 22:13:50 +00:00
|
|
|
}
|
|
|
|
defer fr.Close()
|
|
|
|
|
|
|
|
pubKeyB, err := ioutil.ReadAll(fr)
|
|
|
|
if err != nil {
|
2020-04-12 17:02:05 +00:00
|
|
|
return pgpKey{}, fmt.Errorf("reading PGP public key from file at %q: %w", s.Path, err)
|
2020-02-15 22:13:50 +00:00
|
|
|
}
|
|
|
|
|
2020-04-11 23:10:18 +00:00
|
|
|
return SignifierPGP{Body: string(pubKeyB)}.load(fs)
|
2020-02-15 22:13:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Sign will sign the given arbitrary bytes using the private key corresponding
|
2020-04-11 23:10:18 +00:00
|
|
|
// to the pgp public key embedded in this Signifier.
|
2020-04-26 20:23:03 +00:00
|
|
|
func (s SignifierPGP) Sign(fs fs.FS, data []byte) (CredentialUnion, error) {
|
2020-02-15 22:13:50 +00:00
|
|
|
sigPGP, err := s.load(fs)
|
|
|
|
if err != nil {
|
2020-04-26 20:23:03 +00:00
|
|
|
return CredentialUnion{}, err
|
2020-02-15 22:13:50 +00:00
|
|
|
}
|
|
|
|
|
2020-04-12 17:02:05 +00:00
|
|
|
keyID := sigPGP.entity.PrimaryKey.KeyIdString()
|
|
|
|
sig, err := cmdGPG(data, "--detach-sign", "--local-user", keyID)
|
2020-04-11 23:10:18 +00:00
|
|
|
if err != nil {
|
2020-04-26 20:23:03 +00:00
|
|
|
return CredentialUnion{}, fmt.Errorf("signing with pgp key: %w", err)
|
2020-02-15 22:13:50 +00:00
|
|
|
}
|
|
|
|
|
2020-04-26 20:23:03 +00:00
|
|
|
return CredentialUnion{
|
2020-04-11 23:10:18 +00:00
|
|
|
PGPSignature: &CredentialPGPSignature{
|
2020-04-12 17:02:05 +00:00
|
|
|
PubKeyID: keyID,
|
2020-04-11 23:10:18 +00:00
|
|
|
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.
|
2020-04-26 20:23:03 +00:00
|
|
|
func (s SignifierPGP) Signed(fs fs.FS, cred CredentialUnion) (bool, error) {
|
2020-02-15 22:13:50 +00:00
|
|
|
sigPGP, err := s.load(fs)
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
2020-04-11 23:10:18 +00:00
|
|
|
|
2020-02-15 22:13:50 +00:00
|
|
|
return sigPGP.Signed(fs, cred)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Verify asserts that the given signature was produced by this key signing the
|
|
|
|
// given piece of data.
|
2020-04-26 20:23:03 +00:00
|
|
|
func (s SignifierPGP) Verify(fs fs.FS, data []byte, cred CredentialUnion) error {
|
2020-02-15 22:13:50 +00:00
|
|
|
sigPGP, err := s.load(fs)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return sigPGP.Verify(fs, data, cred)
|
|
|
|
}
|
2020-04-11 23:10:18 +00:00
|
|
|
|
|
|
|
// SignifierPGPFile is deprecated and should not be used, use the Path field of
|
|
|
|
// SignifierPGP instead.
|
|
|
|
type SignifierPGPFile struct {
|
|
|
|
Path string `yaml:"path"`
|
|
|
|
}
|