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) }