Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add SignKey gpg entity param to allow easier pgp signing of commits #1198

Merged
merged 12 commits into from
Jun 22, 2019
53 changes: 53 additions & 0 deletions github/git_commits.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
package github

import (
"bytes"
"context"
"fmt"
"time"

"golang.org/x/crypto/openpgp"
)

// SignatureVerification represents GPG signature verification.
Expand Down Expand Up @@ -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"`

// SigningKey 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.
SigningKey *openpgp.Entity `json:"-"`
}

func (c Commit) String() string {
Expand Down Expand Up @@ -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.SigningKey != nil {
signature, err := createSignature(commit.SigningKey, body)
if err != nil {
return nil, nil, err
}
body.Signature = &signature
}
if commit.Verification != nil {
body.Signature = commit.Verification.Signature
}
Expand All @@ -133,3 +148,41 @@ func (s *GitService) CreateCommit(ctx context.Context, owner string, repo string

return c, resp, nil
}

func createSignature(SigningKey *openpgp.Entity, commit *createCommit) (string, error) {
if commit.Author == nil {
anandkumarpatel marked this conversation as resolved.
Show resolved Hide resolved
return "", fmt.Errorf("Commit Author is required to sign a commit")
anandkumarpatel marked this conversation as resolved.
Show resolved Hide resolved
}
message := createSignatureMessage(commit)

writer := new(bytes.Buffer)
reader := bytes.NewReader([]byte(message))
err := openpgp.ArmoredDetachSign(writer, SigningKey, reader, nil)
anandkumarpatel marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return "", err
}

return writer.String(), nil
}

func createSignatureMessage(commit *createCommit) string {
message := ""
anandkumarpatel marked this conversation as resolved.
Show resolved Hide resolved

if commit.Tree != nil {
message = fmt.Sprintf("tree %s\n", *commit.Tree)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the getters... commit.GetTree().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are no getters defined for createCommit object

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, whups, my bad. Thanks, @anandkumarpatel.

}

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"))
anandkumarpatel marked this conversation as resolved.
Show resolved Hide resolved
commiter := commit.Committer
anandkumarpatel marked this conversation as resolved.
Show resolved Hide resolved
if commiter == nil {
commiter = commit.Author
}
anandkumarpatel marked this conversation as resolved.
Show resolved Hide resolved
// 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"))
anandkumarpatel marked this conversation as resolved.
Show resolved Hide resolved
message += fmt.Sprintf("%s", *commit.Message)
anandkumarpatel marked this conversation as resolved.
Show resolved Hide resolved
return message
}
229 changes: 229 additions & 0 deletions github/git_commits_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import (
"fmt"
"net/http"
"reflect"
"strings"
"testing"
"time"

"golang.org/x/crypto/openpgp"
)

func TestGitService_GetCommit(t *testing.T) {
Expand Down Expand Up @@ -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{
SigningKey: &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("[email protected]"),
Date: &date,
}
input := &Commit{
Message: String("m"),
anandkumarpatel marked this conversation as resolved.
Show resolved Hide resolved
Tree: &Tree{SHA: String("t")},
Parents: []Commit{{SHA: String("p")}},
SigningKey: keyring[0],
Author: &author,
}

messageReader := strings.NewReader(`tree t
parent p
author go-github <[email protected]> 1493849023 +0200
committer go-github <[email protected]> 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 <[email protected]>"].Name != "go-github <[email protected]>" {
t.Errorf("Signer is incorrect. got: %+v, want %+v", signer.Identities["go-github <[email protected]>"].Name, "go-github <[email protected]>")
}
// 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"
anandkumarpatel marked this conversation as resolved.
Show resolved Hide resolved
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("[email protected]"),
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"),
anandkumarpatel marked this conversation as resolved.
Show resolved Hide resolved
Parents: []string{"p"},
Author: &CommitAuthor{
Name: String("go-github"),
Email: String("[email protected]"),
Date: &date,
},
})
expected := `parent p
author go-github <[email protected]> 1493849023 +0200
committer go-github <[email protected]> 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"),
anandkumarpatel marked this conversation as resolved.
Show resolved Hide resolved
Parents: []string{"p"},
Author: &CommitAuthor{
Name: String("go-github"),
Email: String("[email protected]"),
Date: &date,
},
Committer: &CommitAuthor{
Name: String("foo"),
Email: String("[email protected]"),
Date: &date,
},
})
expected := `parent p
author go-github <[email protected]> 1493849023 +0200
committer foo <[email protected]> 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()
Expand All @@ -131,3 +301,62 @@ func TestGitService_CreateCommit_invalidOwner(t *testing.T) {
_, _, err := client.Git.CreateCommit(context.Background(), "%", "%", &Commit{})
testURLParseError(t, err)
}

const testKey = `
anandkumarpatel marked this conversation as resolved.
Show resolved Hide resolved
-----BEGIN PGP PRIVATE KEY BLOCK-----

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
=VFSn
-----END PGP PRIVATE KEY BLOCK-----`
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/google/go-github/v26
require (
github.com/golang/protobuf v1.2.0 // indirect
github.com/google/go-querystring v1.0.0
github.com/jchavannes/go-pgp v0.0.0-20170517064339-cb4b0b0ac8cf
anandkumarpatel marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
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=
Expand Down