A read-only clone of the dehub project, for until dehub.dev can be brought back online.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
dehub/sigcred/pgp.go

283 lines
7.4 KiB

package sigcred
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/sha256"
"dehub.dev/src/dehub.git/fs"
"dehub.dev/src/dehub.git/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)
}