From 307fef9114e89b1eefc4e734c9da293ba5103260 Mon Sep 17 00:00:00 2001 From: Anand Patel Date: Tue, 2 Apr 2019 00:20:01 -0400 Subject: [PATCH] add SignKey gpg entity param to allow easier pgp signing of commits --- github/git_commits.go | 53 +++++++++ github/git_commits_test.go | 229 +++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 + 4 files changed, 285 insertions(+) diff --git a/github/git_commits.go b/github/git_commits.go index 9dfd3afb3b5..ef0bcabd7c0 100644 --- a/github/git_commits.go +++ b/github/git_commits.go @@ -6,9 +6,12 @@ package github import ( + "bytes" "context" "fmt" "time" + + "golang.org/x/crypto/openpgp" ) // SignatureVerification represents GPG signature verification. @@ -37,6 +40,11 @@ type Commit struct { // is only populated for requests that fetch GitHub data like // Pulls.ListCommits, Repositories.ListCommits, etc. CommentCount *int `json:"comment_count,omitempty"` + + // SignKey denotes a key to sign the commit with. If not nil this key will + // be used to sign the commit. The private key must be present and already + // decrypted. Ignored if Verification.Signature is defined. + SignKey *openpgp.Entity } func (c Commit) String() string { @@ -116,6 +124,13 @@ func (s *GitService) CreateCommit(ctx context.Context, owner string, repo string if commit.Tree != nil { body.Tree = commit.Tree.SHA } + if commit.SignKey != nil { + signature, err := createSignature(commit.SignKey, body) + if err != nil { + return nil, nil, err + } + body.Signature = &signature + } if commit.Verification != nil { body.Signature = commit.Verification.Signature } @@ -133,3 +148,41 @@ func (s *GitService) CreateCommit(ctx context.Context, owner string, repo string return c, resp, nil } + +func createSignature(signKey *openpgp.Entity, commit *createCommit) (string, error) { + if commit.Author == nil { + return "", fmt.Errorf("Commit Author is required to sign a commit") + } + message := createSignatureMessage(commit) + + writer := new(bytes.Buffer) + reader := bytes.NewReader([]byte(message)) + err := openpgp.ArmoredDetachSign(writer, signKey, reader, nil) + if err != nil { + return "", err + } + + return writer.String(), nil +} + +func createSignatureMessage(commit *createCommit) string { + message := "" + + if commit.Tree != nil { + message = fmt.Sprintf("tree %s\n", *commit.Tree) + } + + for _, parent := range commit.Parents { + message += fmt.Sprintf("parent %s\n", parent) + } + + message += fmt.Sprintf("author %s <%s> %d %s\n", *commit.Author.Name, *commit.Author.Email, commit.Author.Date.Unix(), commit.Author.Date.Format("-0700")) + commiter := commit.Committer + if commiter == nil { + commiter = commit.Author + } + // There needs to be a double newline after committer + message += fmt.Sprintf("committer %s <%s> %d %s\n\n", *commiter.Name, *commiter.Email, commiter.Date.Unix(), commiter.Date.Format("-0700")) + message += fmt.Sprintf("%s", *commit.Message) + return message +} diff --git a/github/git_commits_test.go b/github/git_commits_test.go index 83f56407295..e92a8af8d34 100644 --- a/github/git_commits_test.go +++ b/github/git_commits_test.go @@ -11,7 +11,11 @@ import ( "fmt" "net/http" "reflect" + "strings" "testing" + "time" + + "golang.org/x/crypto/openpgp" ) func TestGitService_GetCommit(t *testing.T) { @@ -123,6 +127,172 @@ func TestGitService_CreateSignedCommit(t *testing.T) { t.Errorf("Git.CreateCommit returned %+v, want %+v", commit, want) } } +func TestGitService_CreateSignedCommitWithInvalidParams(t *testing.T) { + client, _, _, teardown := setup() + defer teardown() + + input := &Commit{ + SignKey: &openpgp.Entity{}, + } + + _, _, err := client.Git.CreateCommit(context.Background(), "o", "r", input) + if err == nil { + t.Errorf("Expected error to be returned because invalid params was passed") + } +} + +func TestGitService_CreateSignedCommitWithKey(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + s := strings.NewReader(testKey) + keyring, err := openpgp.ReadArmoredKeyRing(s) + + date, _ := time.Parse("Mon Jan 02 15:04:05 2006 -0700", "Thu May 04 00:03:43 2017 +0200") + author := CommitAuthor{ + Name: String("go-github"), + Email: String("go-github@github.com"), + Date: &date, + } + input := &Commit{ + Message: String("m"), + Tree: &Tree{SHA: String("t")}, + Parents: []Commit{{SHA: String("p")}}, + SignKey: keyring[0], + Author: &author, + } + + messageReader := strings.NewReader(`tree t +parent p +author go-github 1493849023 +0200 +committer go-github 1493849023 +0200 + +m`) + + mux.HandleFunc("/repos/o/r/git/commits", func(w http.ResponseWriter, r *http.Request) { + v := new(createCommit) + json.NewDecoder(r.Body).Decode(v) + + testMethod(t, r, "POST") + + want := &createCommit{ + Message: input.Message, + Tree: String("t"), + Parents: []string{"p"}, + Author: &author, + } + + sigReader := strings.NewReader(*v.Signature) + signer, err := openpgp.CheckArmoredDetachedSignature(keyring, messageReader, sigReader) + if err != nil { + t.Errorf("Error verifying signature: %+v", err) + } + if signer.Identities["go-github "].Name != "go-github " { + t.Errorf("Signer is incorrect. got: %+v, want %+v", signer.Identities["go-github "].Name, "go-github ") + } + // Nullify Signature since we checked it above + v.Signature = nil + if !reflect.DeepEqual(v, want) { + t.Errorf("Request body = %+v, want %+v", v, want) + } + fmt.Fprint(w, `{"sha":"s"}`) + }) + + commit, _, err := client.Git.CreateCommit(context.Background(), "o", "r", input) + if err != nil { + t.Errorf("Git.CreateCommit returned error: %v", err) + } + + want := &Commit{SHA: String("s")} + if !reflect.DeepEqual(commit, want) { + t.Errorf("Git.CreateCommit returned %+v, want %+v", commit, want) + } +} + +func TestGitService_createSignature_noAuthor(t *testing.T) { + a := &createCommit{ + Message: String("m"), + Tree: String("t"), + Parents: []string{"p"}, + } + + _, err := createSignature(nil, a) + + if err == nil { + t.Errorf("Expected error to be returned because no author was passed") + } + expectedString := "Commit Author is required to sign a commit" + if !strings.Contains(err.Error(), expectedString) { + t.Errorf("Returned incorrect error. returned %s, want %s", err.Error(), expectedString) + } +} + +func TestGitService_createSignature_invalidKey(t *testing.T) { + date, _ := time.Parse("Mon Jan 02 15:04:05 2006 -0700", "Thu May 04 00:03:43 2017 +0200") + + _, err := createSignature(&openpgp.Entity{}, &createCommit{ + Message: String("m"), + Tree: String("t"), + Parents: []string{"p"}, + Author: &CommitAuthor{ + Name: String("go-github"), + Email: String("go-github@github.com"), + Date: &date, + }, + }) + + if err == nil { + t.Errorf("Expected error to be returned due to invalid key") + } +} + +func TestGitService_createSignatureMessage_withoutTree(t *testing.T) { + date, _ := time.Parse("Mon Jan 02 15:04:05 2006 -0700", "Thu May 04 00:03:43 2017 +0200") + + msg := createSignatureMessage(&createCommit{ + Message: String("m"), + Parents: []string{"p"}, + Author: &CommitAuthor{ + Name: String("go-github"), + Email: String("go-github@github.com"), + Date: &date, + }, + }) + expected := `parent p +author go-github 1493849023 +0200 +committer go-github 1493849023 +0200 + +m` + if msg != expected { + t.Errorf("Returned message incorrect. returned %s, want %s", msg, expected) + } +} + +func TestGitService_createSignatureMessage_withoutCommitter(t *testing.T) { + date, _ := time.Parse("Mon Jan 02 15:04:05 2006 -0700", "Thu May 04 00:03:43 2017 +0200") + + msg := createSignatureMessage(&createCommit{ + Message: String("m"), + Parents: []string{"p"}, + Author: &CommitAuthor{ + Name: String("go-github"), + Email: String("go-github@github.com"), + Date: &date, + }, + Committer: &CommitAuthor{ + Name: String("foo"), + Email: String("foo@bar.com"), + Date: &date, + }, + }) + expected := `parent p +author go-github 1493849023 +0200 +committer foo 1493849023 +0200 + +m` + if msg != expected { + t.Errorf("Returned message incorrect. returned %s, want %s", msg, expected) + } +} func TestGitService_CreateCommit_invalidOwner(t *testing.T) { client, _, _, teardown := setup() @@ -131,3 +301,62 @@ func TestGitService_CreateCommit_invalidOwner(t *testing.T) { _, _, err := client.Git.CreateCommit(context.Background(), "%", "%", &Commit{}) testURLParseError(t, err) } + +const testKey = ` +-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +=VFSn +-----END PGP PRIVATE KEY BLOCK-----` diff --git a/go.mod b/go.mod index eb127af8e6c..71742415302 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ require ( github.com/golang/protobuf v1.2.0 // indirect github.com/google/go-github v17.0.0+incompatible github.com/google/go-querystring v1.0.0 + github.com/jchavannes/go-pgp v0.0.0-20170517064339-cb4b0b0ac8cf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 golang.org/x/net v0.0.0-20190311183353-d8887717615a // indirect golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be diff --git a/go.sum b/go.sum index 317188b42fa..d01ed448627 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4r github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/jchavannes/go-pgp v0.0.0-20170517064339-cb4b0b0ac8cf h1:y8QyJtluNQtCj0K70OUtGUxlpGiKDB74iJArA9h4eaE= +github.com/jchavannes/go-pgp v0.0.0-20170517064339-cb4b0b0ac8cf/go.mod h1:dtFptCZ3M/9AWU38htm1xFvWqaJr5ZvkiOiozne99Ps= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=