package sigcred import ( "bytes" "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/sha256" "fmt" "io" "io/ioutil" "os/exec" "path/filepath" "strings" "time" "dehub.dev/src/dehub.git/fs" "dehub.dev/src/dehub.git/yamlutil" "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"` 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} return sig.Verify(nil, data, Credential{PGPSignature: c}) } 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, 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. 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. type SignifierPGP struct { // An armored string encoding of the public key, as exported via // `gpg -a --export ` Body string `yaml:"body,omitempty"` // Path, relative to the root of the repo, of the armored public key file. Path string `yaml:"path,omitempty"` } var _ SignifierInterface = SignifierPGP{} func cmdGPG(stdin []byte, args ...string) ([]byte, error) { args = append([]string{"--openpgp"}, args...) stderr := new(bytes.Buffer) cmd := exec.Command("gpg", args...) cmd.Stdin = bytes.NewBuffer(stdin) cmd.Stderr = stderr out, err := cmd.Output() if err != nil { return nil, fmt.Errorf("calling gpg command (%v): %s", err, stderr.String()) } return out, nil } // SignifierPGPFromKeyID loads a pgp key using the given identifier. The key is // assumed to be stored. in the client's keyring already. // // If setPubKeyBody is true, then CredentialPGPSignature instances produced by // the returned Signifier will have their PubKeyBody field set. func SignifierPGPFromKeyID(keyID string, setPubKeyBody bool) (SignifierInterface, error) { pubKey, err := cmdGPG(nil, "-a", "--export", keyID) if err != nil { return nil, fmt.Errorf("loading public key: %w", err) } var sigInt SignifierInterface = &SignifierPGP{Body: string(pubKey)} if setPubKeyBody { sigInt = signifierMiddleware{ SignifierInterface: sigInt, signCallback: func(cred *Credential) { cred.PGPSignature.PubKeyBody = string(pubKey) }, } } return sigInt, nil } func (s SignifierPGP) load(fs fs.FS) (pgpPubKey, error) { if s.Body != "" { return newPGPPubKey(strings.NewReader(s.Body)) } path := filepath.Clean(s.Path) fr, err := fs.Open(path) if err != nil { return pgpPubKey{}, fmt.Errorf("opening PGP public key file at %q: %w", path, err) } defer fr.Close() pubKeyB, err := ioutil.ReadAll(fr) if err != nil { return pgpPubKey{}, fmt.Errorf("reading PGP public key from file at %q: %w", s.Path, err) } return SignifierPGP{Body: string(pubKeyB)}.load(fs) } // 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(fs) if err != nil { return Credential{}, err } sig, err := cmdGPG(data, "--detach-sign", "--local-user", sigPGP.pubKey.KeyIdString()) if err != nil { return Credential{}, fmt.Errorf("signing with pgp key: %w", err) } 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(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 SignifierPGP) 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) } // SignifierPGPFile is deprecated and should not be used, use the Path field of // SignifierPGP instead. type SignifierPGPFile struct { Path string `yaml:"path"` }