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

Commit 7b6c126

Browse files
authored
Merge pull request #920 from vancluever/f-add-commit-signkey
git: Add ability to PGP sign commits
2 parents ba0f659 + 39954f2 commit 7b6c126

File tree

4 files changed

+178
-7
lines changed

4 files changed

+178
-7
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.

plumbing/object/commit.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -263,18 +263,18 @@ func (b *Commit) encode(o plumbing.EncodedObject, includeSig bool) (err error) {
263263
}
264264

265265
if b.PGPSignature != "" && includeSig {
266-
if _, err = fmt.Fprint(w, "\n"+headerpgp); err != nil {
266+
if _, err = fmt.Fprint(w, "\n"+headerpgp+" "); err != nil {
267267
return err
268268
}
269269

270-
// Split all the signature lines and write with a left padding and
271-
// newline at the end.
270+
// Split all the signature lines and re-write with a left padding and
271+
// newline. Use join for this so it's clear that a newline should not be
272+
// added after this section, as it will be added when the message is
273+
// printed.
272274
signature := strings.TrimSuffix(b.PGPSignature, "\n")
273275
lines := strings.Split(signature, "\n")
274-
for _, line := range lines {
275-
if _, err = fmt.Fprintf(w, " %s\n", line); err != nil {
276-
return err
277-
}
276+
if _, err = fmt.Fprint(w, strings.Join(lines, "\n ")); err != nil {
277+
return err
278278
}
279279
}
280280

worktree_commit.go

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

33
import (
4+
"bytes"
45
"path"
56
"strings"
67

8+
"golang.org/x/crypto/openpgp"
79
"gopkg.in/src-d/go-git.v4/plumbing"
810
"gopkg.in/src-d/go-git.v4/plumbing/filemode"
911
"gopkg.in/src-d/go-git.v4/plumbing/format/index"
@@ -92,13 +94,37 @@ func (w *Worktree) buildCommitObject(msg string, opts *CommitOptions, tree plumb
9294
ParentHashes: opts.Parents,
9395
}
9496

97+
if opts.SignKey != nil {
98+
sig, err := w.buildCommitSignature(commit, opts.SignKey)
99+
if err != nil {
100+
return plumbing.ZeroHash, err
101+
}
102+
commit.PGPSignature = sig
103+
}
104+
95105
obj := w.r.Storer.NewEncodedObject()
96106
if err := commit.Encode(obj); err != nil {
97107
return plumbing.ZeroHash, err
98108
}
99109
return w.r.Storer.SetEncodedObject(obj)
100110
}
101111

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

worktree_commit_test.go

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

33
import (
4+
"bytes"
5+
"strings"
46
"time"
57

8+
"golang.org/x/crypto/openpgp"
9+
"golang.org/x/crypto/openpgp/armor"
10+
"golang.org/x/crypto/openpgp/errors"
611
"gopkg.in/src-d/go-git.v4/plumbing"
712
"gopkg.in/src-d/go-git.v4/plumbing/object"
813
"gopkg.in/src-d/go-git.v4/plumbing/storer"
@@ -135,6 +140,62 @@ func (s *WorktreeSuite) TestRemoveAndCommitAll(c *C) {
135140
assertStorageStatus(c, s.Repository, 13, 11, 11, expected)
136141
}
137142

143+
func (s *WorktreeSuite) TestCommitSign(c *C) {
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, true)
159+
hash, err := w.Commit("foo\n", &CommitOptions{Author: defaultSignature(), SignKey: key})
160+
c.Assert(err, IsNil)
161+
162+
// Verify the commit.
163+
pks := new(bytes.Buffer)
164+
pkw, err := armor.Encode(pks, openpgp.PublicKeyType, nil)
165+
c.Assert(err, IsNil)
166+
167+
err = key.Serialize(pkw)
168+
c.Assert(err, IsNil)
169+
err = pkw.Close()
170+
c.Assert(err, IsNil)
171+
172+
expectedCommit, err := r.CommitObject(hash)
173+
c.Assert(err, IsNil)
174+
actual, err := expectedCommit.Verify(pks.String())
175+
c.Assert(err, IsNil)
176+
c.Assert(actual.PrimaryKey, DeepEquals, key.PrimaryKey)
177+
}
178+
179+
func (s *WorktreeSuite) TestCommitSignBadKey(c *C) {
180+
fs := memfs.New()
181+
storage := memory.NewStorage()
182+
183+
r, err := Init(storage, fs)
184+
c.Assert(err, IsNil)
185+
186+
w, err := r.Worktree()
187+
c.Assert(err, IsNil)
188+
189+
util.WriteFile(fs, "foo", []byte("foo"), 0644)
190+
191+
_, err = w.Add("foo")
192+
c.Assert(err, IsNil)
193+
194+
key := commitSignKey(c, false)
195+
_, err = w.Commit("foo\n", &CommitOptions{Author: defaultSignature(), SignKey: key})
196+
c.Assert(err, Equals, errors.InvalidArgumentError("signing key is encrypted"))
197+
}
198+
138199
func assertStorageStatus(
139200
c *C, r *Repository,
140201
treesCount, blobCount, commitCount int, head plumbing.Hash,
@@ -173,3 +234,83 @@ func defaultSignature() *object.Signature {
173234
When: when,
174235
}
175236
}
237+
238+
func commitSignKey(c *C, decrypt bool) *openpgp.Entity {
239+
s := strings.NewReader(armoredKeyRing)
240+
es, err := openpgp.ReadArmoredKeyRing(s)
241+
c.Assert(err, IsNil)
242+
243+
c.Assert(es, HasLen, 1)
244+
c.Assert(es[0].Identities, HasLen, 1)
245+
_, ok := es[0].Identities["foo bar <[email protected]>"]
246+
c.Assert(ok, Equals, true)
247+
248+
key := es[0]
249+
if decrypt {
250+
err = key.PrivateKey.Decrypt([]byte(keyPassphrase))
251+
c.Assert(err, IsNil)
252+
}
253+
254+
return key
255+
}
256+
257+
const armoredKeyRing = `
258+
-----BEGIN PGP PRIVATE KEY BLOCK-----
259+
260+
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
312+
=8g3t
313+
-----END PGP PRIVATE KEY BLOCK-----
314+
`
315+
316+
const keyPassphrase = "abcdef0123456789"

0 commit comments

Comments
 (0)