From 8a2966d777e7f6efffa7dfb02353058b952be5f7 Mon Sep 17 00:00:00 2001 From: dan pittman Date: Mon, 22 Oct 2018 15:40:38 -0700 Subject: [PATCH 1/6] adds gpg package --- review/gpg/signable.go | 124 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 review/gpg/signable.go diff --git a/review/gpg/signable.go b/review/gpg/signable.go new file mode 100644 index 00000000..a7eaf2f2 --- /dev/null +++ b/review/gpg/signable.go @@ -0,0 +1,124 @@ +// Package gpg provides an interface and an abstraction with which to sign and +// verify review requests and comments. +package gpg + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "os" + "os/exec" +) + +const placeholder = "gpgsig" + +// Sig provides an abstraction around shelling out to GPG to sign the +// content it's given. +type Sig struct { + // Sig holds an object's content's signature. + Sig string `json:"signature,omitempty"` +} + +// Signable is an interfaces which provides the pointer to the signable +// object's stringified signature. +// +// This pointer is used by `Sign` and `Verify` to replace its contents with +// `placeholder` or the signature itself for the purposes of signing or +// verifying. +type Signable interface { + Signature() *string +} + +// Signature is `Sig`'s implementation of `Signable`. Through this function, an +// object which needs to implement `Signable` need only embed `Sig` +// anonymously. See, e.g., review/equest.go. +func (s *Sig) Signature() *string { + return &s.Sig +} + +// Sign uses gpg to sign the contents of a request and deposit it into the +// signature key of the request. +func Sign(key string, s Signable) error { + // First we retrieve the pointer and write `placeholder` as its value. + sigPtr := s.Signature() + *sigPtr = placeholder + + // Marshal the content and sign it. + content, err := json.Marshal(s) + if err != nil { + return err + } + sig, err := signContent(key, content) + if err != nil { + return err + } + + // Write the signature as the new value at the pointer. + *sigPtr = sig.String() + return nil +} + +func signContent(key string, content []byte) (*bytes.Buffer, + error) { + var stdout, stderr bytes.Buffer + cmd := exec.Command("gpg", "-u", key, "--detach-sign", "--armor") + cmd.Stdin = bytes.NewReader(content) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + return &stdout, err +} + +// Verify verifies the signatures on the request and its comments with the +// given key. +func Verify(key string, s Signable) error { + // Retrieve the pointer. + sigPtr := s.Signature() + // Copy its contents. + sig := *sigPtr + // Overwrite the value with the placeholder. + *sigPtr = placeholder + + // 1. Marshal the content into JSON. + // 2. Write the signature and the content to temp files. + // 3. Use gpg to verify the signature. + content, err := json.Marshal(s) + if err != nil { + return err + } + sigFile, err := ioutil.TempFile("", "sig") + if err != nil { + return err + } + defer os.Remove(sigFile.Name()) + _, err = sigFile.Write([]byte(sig)) + if err != nil { + return err + } + err = sigFile.Close() + if err != nil { + return err + } + + contentFile, err := ioutil.TempFile("", "content") + if err != nil { + return err + } + defer os.Remove(contentFile.Name()) + _, err = contentFile.Write(content) + if err != nil { + return err + } + err = contentFile.Close() + if err != nil { + return err + } + + var stdout, stderr bytes.Buffer + cmd := exec.Command("gpg", "-u", key, "--verify", sigFile.Name(), + contentFile.Name()) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err = cmd.Run() + return err +} From 431ccab3e75b00103eaa92a437760ac29eb63259 Mon Sep 17 00:00:00 2001 From: dan pittman Date: Mon, 22 Oct 2018 15:43:00 -0700 Subject: [PATCH 2/6] implements signing for requests acceptances --- commands/accept.go | 14 ++++++++++++++ commands/request.go | 17 +++++++++++++++-- repository/git.go | 6 ++++++ repository/mock_repo.go | 6 ++++++ repository/repo.go | 4 ++++ review/comment/comment.go | 3 +++ review/request/request.go | 2 ++ 7 files changed, 50 insertions(+), 2 deletions(-) diff --git a/commands/accept.go b/commands/accept.go index 6207b7b9..b50f424c 100644 --- a/commands/accept.go +++ b/commands/accept.go @@ -24,6 +24,7 @@ import ( "github.com/google/git-appraise/repository" "github.com/google/git-appraise/review" "github.com/google/git-appraise/review/comment" + "github.com/google/git-appraise/review/gpg" ) var acceptFlagSet = flag.NewFlagSet("accept", flag.ExitOnError) @@ -31,6 +32,9 @@ var acceptFlagSet = flag.NewFlagSet("accept", flag.ExitOnError) var ( acceptMessageFile = acceptFlagSet.String("F", "", "Take the comment from the given file. Use - to read the message from the standard input") acceptMessage = acceptFlagSet.String("m", "", "Message to attach to the review") + + acceptSign = acceptFlagSet.Bool("S", false, + "sign the contents of the acceptance") ) // acceptReview adds an LGTM comment to the current code review. @@ -80,6 +84,16 @@ func acceptReview(repo repository.Repo, args []string) error { c := comment.New(userEmail, *acceptMessage) c.Location = &location c.Resolved = &resolved + if *acceptSign { + key, err := repo.GetUserSigningKey() + if err != nil { + return err + } + err = gpg.Sign(key, &c) + if err != nil { + return err + } + } return r.AddComment(c) } diff --git a/commands/request.go b/commands/request.go index ff3ff7fd..9a9854c3 100644 --- a/commands/request.go +++ b/commands/request.go @@ -20,10 +20,12 @@ import ( "errors" "flag" "fmt" + "strings" + "github.com/google/git-appraise/commands/input" "github.com/google/git-appraise/repository" + "github.com/google/git-appraise/review/gpg" "github.com/google/git-appraise/review/request" - "strings" ) // Template for the "request" subcommand's output. @@ -44,6 +46,8 @@ var ( requestTarget = requestFlagSet.String("target", "refs/heads/master", "Revision against which to review") requestQuiet = requestFlagSet.Bool("quiet", false, "Suppress review summary output") requestAllowUncommitted = requestFlagSet.Bool("allow-uncommitted", false, "Allow uncommitted local changes.") + requestSign = requestFlagSet.Bool("S", false, + "GPG sign the content of the request") ) // Build the template review request based solely on the parsed flag values. @@ -145,7 +149,16 @@ func requestReview(repo repository.Repo, args []string) error { } r.Description = description } - + if *requestSign { + key, err := repo.GetUserSigningKey() + if err != nil { + return err + } + err = gpg.Sign(key, &r) + if err != nil { + return err + } + } note, err := r.Write() if err != nil { return err diff --git a/repository/git.go b/repository/git.go index a7a6e1a1..3aea6435 100644 --- a/repository/git.go +++ b/repository/git.go @@ -102,6 +102,12 @@ func (repo *GitRepo) GetUserEmail() (string, error) { return repo.runGitCommand("config", "user.email") } +// GetUserSigningKey returns the key id the user has configured for +// sigining git artifacts. +func (repo *GitRepo) GetUserSigningKey() (string, error) { + return repo.runGitCommand("config", "user.signingKey") +} + // GetCoreEditor returns the name of the editor that the user has used to configure git. func (repo *GitRepo) GetCoreEditor() (string, error) { return repo.runGitCommand("var", "GIT_EDITOR") diff --git a/repository/mock_repo.go b/repository/mock_repo.go index c9fd105b..cc45f944 100644 --- a/repository/mock_repo.go +++ b/repository/mock_repo.go @@ -194,6 +194,12 @@ func (r *mockRepoForTest) GetRepoStateHash() (string, error) { // GetUserEmail returns the email address that the user has used to configure git. func (r *mockRepoForTest) GetUserEmail() (string, error) { return "user@example.com", nil } +// GetUserSigningKey returns the key id the user has configured for +// sigining git artifacts. +func (r *mockRepoForTest) GetUserSigningKey() (string, error) { + return "gpgsig", nil +} + // GetCoreEditor returns the name of the editor that the user has used to configure git. func (r *mockRepoForTest) GetCoreEditor() (string, error) { return "vi", nil } diff --git a/repository/repo.go b/repository/repo.go index 03413e30..744c7929 100644 --- a/repository/repo.go +++ b/repository/repo.go @@ -41,6 +41,10 @@ type Repo interface { // GetUserEmail returns the email address that the user has used to configure git. GetUserEmail() (string, error) + // GetUserSigningKey returns the key id the user has configured for + // sigining git artifacts. + GetUserSigningKey() (string, error) + // GetCoreEditor returns the name of the editor that the user has used to configure git. GetCoreEditor() (string, error) diff --git a/review/comment/comment.go b/review/comment/comment.go index c083fb1f..b1dea49c 100644 --- a/review/comment/comment.go +++ b/review/comment/comment.go @@ -27,6 +27,7 @@ import ( "time" "github.com/google/git-appraise/repository" + "github.com/google/git-appraise/review/gpg" ) // Ref defines the git-notes ref that we expect to contain review comments. @@ -118,6 +119,8 @@ type Comment struct { Resolved *bool `json:"resolved,omitempty"` // Version represents the version of the metadata format. Version int `json:"v,omitempty"` + + gpg.Sig } // New returns a new comment with the given description message. diff --git a/review/request/request.go b/review/request/request.go index f332e300..18477d08 100644 --- a/review/request/request.go +++ b/review/request/request.go @@ -53,6 +53,8 @@ type Request struct { // Alias stores a post-rebase commit ID for the review. This allows the tool // to track the history of a review even if the commit history changes. Alias string `json:"alias,omitempty"` + + gpg.Sig } // New returns a new request. From a632af3b751f954b23525a7ea6bfd82ac80a6e6d Mon Sep 17 00:00:00 2001 From: dan pittman Date: Mon, 22 Oct 2018 15:43:40 -0700 Subject: [PATCH 3/6] adds verification for pulling --- commands/pull.go | 60 +++++++++++++++++++++++++---- repository/git.go | 81 +++++++++++++++++++++++++++++++++++---- repository/mock_repo.go | 17 ++++++++ repository/repo.go | 14 +++++++ review/request/request.go | 4 +- review/review.go | 49 ++++++++++++++++++++--- 6 files changed, 204 insertions(+), 21 deletions(-) diff --git a/commands/pull.go b/commands/pull.go index 48dfe5ba..bbbad6a3 100644 --- a/commands/pull.go +++ b/commands/pull.go @@ -18,30 +18,74 @@ package commands import ( "errors" + "flag" "fmt" + "github.com/google/git-appraise/repository" + "github.com/google/git-appraise/review" +) + +var ( + pullFlagSet = flag.NewFlagSet("pull", flag.ExitOnError) + pullVerify = pullFlagSet.Bool("verify-signatures", false, + "verify the signatures of pulled reviews") ) -// pull updates the local git-notes used for reviews with those from a remote repo. +// pull updates the local git-notes used for reviews with those from a remote +// repo. func pull(repo repository.Repo, args []string) error { - if len(args) > 1 { - return errors.New("Only pulling from one remote at a time is supported.") + pullFlagSet.Parse(args) + pullArgs := pullFlagSet.Args() + + if len(pullArgs) > 1 { + return errors.New( + "Only pulling from one remote at a time is supported.") } remote := "origin" - if len(args) == 1 { - remote = args[0] + if len(pullArgs) == 1 { + remote = pullArgs[0] + } + // This is the easy case. We're not checking signatures so just go the + // normal route. + if !*pullVerify { + return repo.PullNotesAndArchive(remote, notesRefPattern, + archiveRefPattern) } - if err := repo.PullNotesAndArchive(remote, notesRefPattern, archiveRefPattern); err != nil { + // Otherwise, we collect the fetched reviewed revisions (their hashes), get + // their reviews, and then one by one, verify them. If we make it through + // the set, _then_ we merge the remote reference into the local branch. + revisions, err := repo.FetchAndReturnNewReviewHashes(remote, + notesRefPattern, archiveRefPattern) + if err != nil { return err } - return nil + key, err := repo.GetUserSigningKey() + if err != nil { + return err + } + for _, revision := range revisions { + rvw, err := review.GetSummaryViaRefs(repo, + "refs/notes/"+remote+"/devtools/reviews", + "refs/notes/"+remote+"/devtools/discuss", revision) + if err != nil { + return err + } + err = rvw.Verify(key) + if err != nil { + return err + } + fmt.Println("verified review:", revision) + } + + return repo.MergeNotesAndArchive(remote, notesRefPattern, + archiveRefPattern) } var pullCmd = &Command{ Usage: func(arg0 string) { - fmt.Printf("Usage: %s pull []\n", arg0) + fmt.Printf("Usage: %s pull [