Skip to content
This repository was archived by the owner on Sep 11, 2020. It is now read-only.

Commit 0ef699d

Browse files
committed
git: Add ability to PGP sign commits
This adds the ability to sign commits by adding the SignKey field to CommitOptions. If present, the commit will be signed during the WorkTree.Commit call. The supplied SignKey must already be decrypted by the caller. Signed-off-by: Chris Marchesi <chrism@vancluevertech.com>
1 parent ec3d2a8 commit 0ef699d

File tree

3 files changed

+144
-0
lines changed

3 files changed

+144
-0
lines changed

options.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55
"regexp"
66

7+
"golang.org/x/crypto/openpgp"
78
"gopkg.in/src-d/go-git.v4/config"
89
"gopkg.in/src-d/go-git.v4/plumbing"
910
"gopkg.in/src-d/go-git.v4/plumbing/object"
@@ -347,6 +348,9 @@ type CommitOptions struct {
347348
// Parents are the parents commits for the new commit, by default when
348349
// len(Parents) is zero, the hash of HEAD reference is used.
349350
Parents []plumbing.Hash
351+
// A key to sign the commit with. A nil value here means the commit will not
352+
// be signed. The private key must be present and already decrypted.
353+
SignKey *openpgp.Entity
350354
}
351355

352356
// Validate validates the fields and sets the default values.

worktree_commit.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"path"
55
"strings"
66

7+
"golang.org/x/crypto/openpgp"
78
"gopkg.in/src-d/go-git.v4/plumbing"
89
"gopkg.in/src-d/go-git.v4/plumbing/filemode"
910
"gopkg.in/src-d/go-git.v4/plumbing/format/index"
@@ -92,13 +93,37 @@ func (w *Worktree) buildCommitObject(msg string, opts *CommitOptions, tree plumb
9293
ParentHashes: opts.Parents,
9394
}
9495

96+
if opts.SignKey != nil {
97+
sig, err := w.buildCommitSignature(commit, opts.SignKey)
98+
if err != nil {
99+
return plumbing.ZeroHash, err
100+
}
101+
commit.PGPSignature = sig
102+
}
103+
95104
obj := w.r.Storer.NewEncodedObject()
96105
if err := commit.Encode(obj); err != nil {
97106
return plumbing.ZeroHash, err
98107
}
99108
return w.r.Storer.SetEncodedObject(obj)
100109
}
101110

111+
func (w *Worktree) buildCommitSignature(commit *object.Commit, signKey *openpgp.Entity) (string, error) {
112+
encoded := &plumbing.MemoryObject{}
113+
if err := commit.Encode(encoded); err != nil {
114+
return "", err
115+
}
116+
r, err := encoded.Reader()
117+
if err != nil {
118+
return "", err
119+
}
120+
var b strings.Builder
121+
if err := openpgp.ArmoredDetachSign(&b, signKey, r, nil); err != nil {
122+
return "", err
123+
}
124+
return b.String(), nil
125+
}
126+
102127
// buildTreeHelper converts a given index.Index file into multiple git objects
103128
// reading the blobs from the given filesystem and creating the trees from the
104129
// index structure. The created objects are pushed to a given Storer.

worktree_commit_test.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package git
22

33
import (
4+
"strings"
45
"time"
56

7+
"golang.org/x/crypto/openpgp"
8+
"golang.org/x/crypto/openpgp/armor"
69
"gopkg.in/src-d/go-git.v4/plumbing"
710
"gopkg.in/src-d/go-git.v4/plumbing/object"
811
"gopkg.in/src-d/go-git.v4/plumbing/storer"
@@ -135,6 +138,47 @@ func (s *WorktreeSuite) TestRemoveAndCommitAll(c *C) {
135138
assertStorageStatus(c, s.Repository, 13, 11, 11, expected)
136139
}
137140

141+
func (s *WorktreeSuite) TestCommitSign(c *C) {
142+
// expectedHash := plumbing.NewHash("98c4ac7c29c913f7461eae06e024dc18e80d23a4")
143+
144+
fs := memfs.New()
145+
storage := memory.NewStorage()
146+
147+
r, err := Init(storage, fs)
148+
c.Assert(err, IsNil)
149+
150+
w, err := r.Worktree()
151+
c.Assert(err, IsNil)
152+
153+
util.WriteFile(fs, "foo", []byte("foo"), 0644)
154+
155+
_, err = w.Add("foo")
156+
c.Assert(err, IsNil)
157+
158+
key := commitSignKey(c)
159+
hash, err := w.Commit("foo\n", &CommitOptions{Author: defaultSignature(), SignKey: key})
160+
// c.Assert(hash, Equals, expectedHash)
161+
c.Assert(err, IsNil)
162+
163+
// assertStorageStatus(c, r, 1, 1, 1, expectedHash)
164+
165+
// Verify the commit.
166+
pks := new(strings.Builder)
167+
pkw, err := armor.Encode(pks, openpgp.PublicKeyType, nil)
168+
c.Assert(err, IsNil)
169+
170+
err = key.Serialize(pkw)
171+
c.Assert(err, IsNil)
172+
err = pkw.Close()
173+
c.Assert(err, IsNil)
174+
175+
expectedCommit, err := r.CommitObject(hash)
176+
c.Assert(err, IsNil)
177+
actual, err := expectedCommit.Verify(pks.String())
178+
c.Assert(err, IsNil)
179+
c.Assert(actual.PrimaryKey, DeepEquals, key.PrimaryKey)
180+
}
181+
138182
func assertStorageStatus(
139183
c *C, r *Repository,
140184
treesCount, blobCount, commitCount int, head plumbing.Hash,
@@ -173,3 +217,74 @@ func defaultSignature() *object.Signature {
173217
When: when,
174218
}
175219
}
220+
221+
func commitSignKey(c *C) *openpgp.Entity {
222+
s := strings.NewReader(armoredKeyRing)
223+
es, err := openpgp.ReadArmoredKeyRing(s)
224+
c.Assert(err, IsNil)
225+
226+
c.Assert(es, HasLen, 1)
227+
c.Assert(es[0].Identities, HasLen, 1)
228+
_, ok := es[0].Identities["foo bar <foo@foo.foo>"]
229+
c.Assert(ok, Equals, true)
230+
231+
return es[0]
232+
}
233+
234+
const armoredKeyRing = `
235+
-----BEGIN PGP PRIVATE KEY BLOCK-----
236+
237+
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
288+
=bnr6
289+
-----END PGP PRIVATE KEY BLOCK-----
290+
`

0 commit comments

Comments
 (0)