##// END OF EJS Templates
gpg: lowercase messages
Martin Geisler -
r16927:7922004a default
parent child Browse files
Show More
@@ -1,289 +1,289
1 # Copyright 2005, 2006 Benoit Boissinot <benoit.boissinot@ens-lyon.org>
1 # Copyright 2005, 2006 Benoit Boissinot <benoit.boissinot@ens-lyon.org>
2 #
2 #
3 # This software may be used and distributed according to the terms of the
3 # This software may be used and distributed according to the terms of the
4 # GNU General Public License version 2 or any later version.
4 # GNU General Public License version 2 or any later version.
5
5
6 '''commands to sign and verify changesets'''
6 '''commands to sign and verify changesets'''
7
7
8 import os, tempfile, binascii
8 import os, tempfile, binascii
9 from mercurial import util, commands, match, cmdutil
9 from mercurial import util, commands, match, cmdutil
10 from mercurial import node as hgnode
10 from mercurial import node as hgnode
11 from mercurial.i18n import _
11 from mercurial.i18n import _
12
12
13 cmdtable = {}
13 cmdtable = {}
14 command = cmdutil.command(cmdtable)
14 command = cmdutil.command(cmdtable)
15 testedwith = 'internal'
15 testedwith = 'internal'
16
16
17 class gpg(object):
17 class gpg(object):
18 def __init__(self, path, key=None):
18 def __init__(self, path, key=None):
19 self.path = path
19 self.path = path
20 self.key = (key and " --local-user \"%s\"" % key) or ""
20 self.key = (key and " --local-user \"%s\"" % key) or ""
21
21
22 def sign(self, data):
22 def sign(self, data):
23 gpgcmd = "%s --sign --detach-sign%s" % (self.path, self.key)
23 gpgcmd = "%s --sign --detach-sign%s" % (self.path, self.key)
24 return util.filter(data, gpgcmd)
24 return util.filter(data, gpgcmd)
25
25
26 def verify(self, data, sig):
26 def verify(self, data, sig):
27 """ returns of the good and bad signatures"""
27 """ returns of the good and bad signatures"""
28 sigfile = datafile = None
28 sigfile = datafile = None
29 try:
29 try:
30 # create temporary files
30 # create temporary files
31 fd, sigfile = tempfile.mkstemp(prefix="hg-gpg-", suffix=".sig")
31 fd, sigfile = tempfile.mkstemp(prefix="hg-gpg-", suffix=".sig")
32 fp = os.fdopen(fd, 'wb')
32 fp = os.fdopen(fd, 'wb')
33 fp.write(sig)
33 fp.write(sig)
34 fp.close()
34 fp.close()
35 fd, datafile = tempfile.mkstemp(prefix="hg-gpg-", suffix=".txt")
35 fd, datafile = tempfile.mkstemp(prefix="hg-gpg-", suffix=".txt")
36 fp = os.fdopen(fd, 'wb')
36 fp = os.fdopen(fd, 'wb')
37 fp.write(data)
37 fp.write(data)
38 fp.close()
38 fp.close()
39 gpgcmd = ("%s --logger-fd 1 --status-fd 1 --verify "
39 gpgcmd = ("%s --logger-fd 1 --status-fd 1 --verify "
40 "\"%s\" \"%s\"" % (self.path, sigfile, datafile))
40 "\"%s\" \"%s\"" % (self.path, sigfile, datafile))
41 ret = util.filter("", gpgcmd)
41 ret = util.filter("", gpgcmd)
42 finally:
42 finally:
43 for f in (sigfile, datafile):
43 for f in (sigfile, datafile):
44 try:
44 try:
45 if f:
45 if f:
46 os.unlink(f)
46 os.unlink(f)
47 except OSError:
47 except OSError:
48 pass
48 pass
49 keys = []
49 keys = []
50 key, fingerprint = None, None
50 key, fingerprint = None, None
51 err = ""
51 err = ""
52 for l in ret.splitlines():
52 for l in ret.splitlines():
53 # see DETAILS in the gnupg documentation
53 # see DETAILS in the gnupg documentation
54 # filter the logger output
54 # filter the logger output
55 if not l.startswith("[GNUPG:]"):
55 if not l.startswith("[GNUPG:]"):
56 continue
56 continue
57 l = l[9:]
57 l = l[9:]
58 if l.startswith("ERRSIG"):
58 if l.startswith("ERRSIG"):
59 err = _("error while verifying signature")
59 err = _("error while verifying signature")
60 break
60 break
61 elif l.startswith("VALIDSIG"):
61 elif l.startswith("VALIDSIG"):
62 # fingerprint of the primary key
62 # fingerprint of the primary key
63 fingerprint = l.split()[10]
63 fingerprint = l.split()[10]
64 elif (l.startswith("GOODSIG") or
64 elif (l.startswith("GOODSIG") or
65 l.startswith("EXPSIG") or
65 l.startswith("EXPSIG") or
66 l.startswith("EXPKEYSIG") or
66 l.startswith("EXPKEYSIG") or
67 l.startswith("BADSIG")):
67 l.startswith("BADSIG")):
68 if key is not None:
68 if key is not None:
69 keys.append(key + [fingerprint])
69 keys.append(key + [fingerprint])
70 key = l.split(" ", 2)
70 key = l.split(" ", 2)
71 fingerprint = None
71 fingerprint = None
72 if err:
72 if err:
73 return err, []
73 return err, []
74 if key is not None:
74 if key is not None:
75 keys.append(key + [fingerprint])
75 keys.append(key + [fingerprint])
76 return err, keys
76 return err, keys
77
77
78 def newgpg(ui, **opts):
78 def newgpg(ui, **opts):
79 """create a new gpg instance"""
79 """create a new gpg instance"""
80 gpgpath = ui.config("gpg", "cmd", "gpg")
80 gpgpath = ui.config("gpg", "cmd", "gpg")
81 gpgkey = opts.get('key')
81 gpgkey = opts.get('key')
82 if not gpgkey:
82 if not gpgkey:
83 gpgkey = ui.config("gpg", "key", None)
83 gpgkey = ui.config("gpg", "key", None)
84 return gpg(gpgpath, gpgkey)
84 return gpg(gpgpath, gpgkey)
85
85
86 def sigwalk(repo):
86 def sigwalk(repo):
87 """
87 """
88 walk over every sigs, yields a couple
88 walk over every sigs, yields a couple
89 ((node, version, sig), (filename, linenumber))
89 ((node, version, sig), (filename, linenumber))
90 """
90 """
91 def parsefile(fileiter, context):
91 def parsefile(fileiter, context):
92 ln = 1
92 ln = 1
93 for l in fileiter:
93 for l in fileiter:
94 if not l:
94 if not l:
95 continue
95 continue
96 yield (l.split(" ", 2), (context, ln))
96 yield (l.split(" ", 2), (context, ln))
97 ln += 1
97 ln += 1
98
98
99 # read the heads
99 # read the heads
100 fl = repo.file(".hgsigs")
100 fl = repo.file(".hgsigs")
101 for r in reversed(fl.heads()):
101 for r in reversed(fl.heads()):
102 fn = ".hgsigs|%s" % hgnode.short(r)
102 fn = ".hgsigs|%s" % hgnode.short(r)
103 for item in parsefile(fl.read(r).splitlines(), fn):
103 for item in parsefile(fl.read(r).splitlines(), fn):
104 yield item
104 yield item
105 try:
105 try:
106 # read local signatures
106 # read local signatures
107 fn = "localsigs"
107 fn = "localsigs"
108 for item in parsefile(repo.opener(fn), fn):
108 for item in parsefile(repo.opener(fn), fn):
109 yield item
109 yield item
110 except IOError:
110 except IOError:
111 pass
111 pass
112
112
113 def getkeys(ui, repo, mygpg, sigdata, context):
113 def getkeys(ui, repo, mygpg, sigdata, context):
114 """get the keys who signed a data"""
114 """get the keys who signed a data"""
115 fn, ln = context
115 fn, ln = context
116 node, version, sig = sigdata
116 node, version, sig = sigdata
117 prefix = "%s:%d" % (fn, ln)
117 prefix = "%s:%d" % (fn, ln)
118 node = hgnode.bin(node)
118 node = hgnode.bin(node)
119
119
120 data = node2txt(repo, node, version)
120 data = node2txt(repo, node, version)
121 sig = binascii.a2b_base64(sig)
121 sig = binascii.a2b_base64(sig)
122 err, keys = mygpg.verify(data, sig)
122 err, keys = mygpg.verify(data, sig)
123 if err:
123 if err:
124 ui.warn("%s:%d %s\n" % (fn, ln , err))
124 ui.warn("%s:%d %s\n" % (fn, ln , err))
125 return None
125 return None
126
126
127 validkeys = []
127 validkeys = []
128 # warn for expired key and/or sigs
128 # warn for expired key and/or sigs
129 for key in keys:
129 for key in keys:
130 if key[0] == "BADSIG":
130 if key[0] == "BADSIG":
131 ui.write(_("%s Bad signature from \"%s\"\n") % (prefix, key[2]))
131 ui.write(_("%s Bad signature from \"%s\"\n") % (prefix, key[2]))
132 continue
132 continue
133 if key[0] == "EXPSIG":
133 if key[0] == "EXPSIG":
134 ui.write(_("%s Note: Signature has expired"
134 ui.write(_("%s Note: Signature has expired"
135 " (signed by: \"%s\")\n") % (prefix, key[2]))
135 " (signed by: \"%s\")\n") % (prefix, key[2]))
136 elif key[0] == "EXPKEYSIG":
136 elif key[0] == "EXPKEYSIG":
137 ui.write(_("%s Note: This key has expired"
137 ui.write(_("%s Note: This key has expired"
138 " (signed by: \"%s\")\n") % (prefix, key[2]))
138 " (signed by: \"%s\")\n") % (prefix, key[2]))
139 validkeys.append((key[1], key[2], key[3]))
139 validkeys.append((key[1], key[2], key[3]))
140 return validkeys
140 return validkeys
141
141
142 @command("sigs", [], _('hg sigs'))
142 @command("sigs", [], _('hg sigs'))
143 def sigs(ui, repo):
143 def sigs(ui, repo):
144 """list signed changesets"""
144 """list signed changesets"""
145 mygpg = newgpg(ui)
145 mygpg = newgpg(ui)
146 revs = {}
146 revs = {}
147
147
148 for data, context in sigwalk(repo):
148 for data, context in sigwalk(repo):
149 node, version, sig = data
149 node, version, sig = data
150 fn, ln = context
150 fn, ln = context
151 try:
151 try:
152 n = repo.lookup(node)
152 n = repo.lookup(node)
153 except KeyError:
153 except KeyError:
154 ui.warn(_("%s:%d node does not exist\n") % (fn, ln))
154 ui.warn(_("%s:%d node does not exist\n") % (fn, ln))
155 continue
155 continue
156 r = repo.changelog.rev(n)
156 r = repo.changelog.rev(n)
157 keys = getkeys(ui, repo, mygpg, data, context)
157 keys = getkeys(ui, repo, mygpg, data, context)
158 if not keys:
158 if not keys:
159 continue
159 continue
160 revs.setdefault(r, [])
160 revs.setdefault(r, [])
161 revs[r].extend(keys)
161 revs[r].extend(keys)
162 for rev in sorted(revs, reverse=True):
162 for rev in sorted(revs, reverse=True):
163 for k in revs[rev]:
163 for k in revs[rev]:
164 r = "%5d:%s" % (rev, hgnode.hex(repo.changelog.node(rev)))
164 r = "%5d:%s" % (rev, hgnode.hex(repo.changelog.node(rev)))
165 ui.write("%-30s %s\n" % (keystr(ui, k), r))
165 ui.write("%-30s %s\n" % (keystr(ui, k), r))
166
166
167 @command("sigcheck", [], _('hg sigcheck REVISION'))
167 @command("sigcheck", [], _('hg sigcheck REVISION'))
168 def check(ui, repo, rev):
168 def check(ui, repo, rev):
169 """verify all the signatures there may be for a particular revision"""
169 """verify all the signatures there may be for a particular revision"""
170 mygpg = newgpg(ui)
170 mygpg = newgpg(ui)
171 rev = repo.lookup(rev)
171 rev = repo.lookup(rev)
172 hexrev = hgnode.hex(rev)
172 hexrev = hgnode.hex(rev)
173 keys = []
173 keys = []
174
174
175 for data, context in sigwalk(repo):
175 for data, context in sigwalk(repo):
176 node, version, sig = data
176 node, version, sig = data
177 if node == hexrev:
177 if node == hexrev:
178 k = getkeys(ui, repo, mygpg, data, context)
178 k = getkeys(ui, repo, mygpg, data, context)
179 if k:
179 if k:
180 keys.extend(k)
180 keys.extend(k)
181
181
182 if not keys:
182 if not keys:
183 ui.write(_("No valid signature for %s\n") % hgnode.short(rev))
183 ui.write(_("no valid signature for %s\n") % hgnode.short(rev))
184 return
184 return
185
185
186 # print summary
186 # print summary
187 ui.write("%s is signed by:\n" % hgnode.short(rev))
187 ui.write("%s is signed by:\n" % hgnode.short(rev))
188 for key in keys:
188 for key in keys:
189 ui.write(" %s\n" % keystr(ui, key))
189 ui.write(" %s\n" % keystr(ui, key))
190
190
191 def keystr(ui, key):
191 def keystr(ui, key):
192 """associate a string to a key (username, comment)"""
192 """associate a string to a key (username, comment)"""
193 keyid, user, fingerprint = key
193 keyid, user, fingerprint = key
194 comment = ui.config("gpg", fingerprint, None)
194 comment = ui.config("gpg", fingerprint, None)
195 if comment:
195 if comment:
196 return "%s (%s)" % (user, comment)
196 return "%s (%s)" % (user, comment)
197 else:
197 else:
198 return user
198 return user
199
199
200 @command("sign",
200 @command("sign",
201 [('l', 'local', None, _('make the signature local')),
201 [('l', 'local', None, _('make the signature local')),
202 ('f', 'force', None, _('sign even if the sigfile is modified')),
202 ('f', 'force', None, _('sign even if the sigfile is modified')),
203 ('', 'no-commit', None, _('do not commit the sigfile after signing')),
203 ('', 'no-commit', None, _('do not commit the sigfile after signing')),
204 ('k', 'key', '',
204 ('k', 'key', '',
205 _('the key id to sign with'), _('ID')),
205 _('the key id to sign with'), _('ID')),
206 ('m', 'message', '',
206 ('m', 'message', '',
207 _('commit message'), _('TEXT')),
207 _('commit message'), _('TEXT')),
208 ] + commands.commitopts2,
208 ] + commands.commitopts2,
209 _('hg sign [OPTION]... [REVISION]...'))
209 _('hg sign [OPTION]... [REVISION]...'))
210 def sign(ui, repo, *revs, **opts):
210 def sign(ui, repo, *revs, **opts):
211 """add a signature for the current or given revision
211 """add a signature for the current or given revision
212
212
213 If no revision is given, the parent of the working directory is used,
213 If no revision is given, the parent of the working directory is used,
214 or tip if no revision is checked out.
214 or tip if no revision is checked out.
215
215
216 See :hg:`help dates` for a list of formats valid for -d/--date.
216 See :hg:`help dates` for a list of formats valid for -d/--date.
217 """
217 """
218
218
219 mygpg = newgpg(ui, **opts)
219 mygpg = newgpg(ui, **opts)
220 sigver = "0"
220 sigver = "0"
221 sigmessage = ""
221 sigmessage = ""
222
222
223 date = opts.get('date')
223 date = opts.get('date')
224 if date:
224 if date:
225 opts['date'] = util.parsedate(date)
225 opts['date'] = util.parsedate(date)
226
226
227 if revs:
227 if revs:
228 nodes = [repo.lookup(n) for n in revs]
228 nodes = [repo.lookup(n) for n in revs]
229 else:
229 else:
230 nodes = [node for node in repo.dirstate.parents()
230 nodes = [node for node in repo.dirstate.parents()
231 if node != hgnode.nullid]
231 if node != hgnode.nullid]
232 if len(nodes) > 1:
232 if len(nodes) > 1:
233 raise util.Abort(_('uncommitted merge - please provide a '
233 raise util.Abort(_('uncommitted merge - please provide a '
234 'specific revision'))
234 'specific revision'))
235 if not nodes:
235 if not nodes:
236 nodes = [repo.changelog.tip()]
236 nodes = [repo.changelog.tip()]
237
237
238 for n in nodes:
238 for n in nodes:
239 hexnode = hgnode.hex(n)
239 hexnode = hgnode.hex(n)
240 ui.write(_("Signing %d:%s\n") % (repo.changelog.rev(n),
240 ui.write(_("signing %d:%s\n") % (repo.changelog.rev(n),
241 hgnode.short(n)))
241 hgnode.short(n)))
242 # build data
242 # build data
243 data = node2txt(repo, n, sigver)
243 data = node2txt(repo, n, sigver)
244 sig = mygpg.sign(data)
244 sig = mygpg.sign(data)
245 if not sig:
245 if not sig:
246 raise util.Abort(_("error while signing"))
246 raise util.Abort(_("error while signing"))
247 sig = binascii.b2a_base64(sig)
247 sig = binascii.b2a_base64(sig)
248 sig = sig.replace("\n", "")
248 sig = sig.replace("\n", "")
249 sigmessage += "%s %s %s\n" % (hexnode, sigver, sig)
249 sigmessage += "%s %s %s\n" % (hexnode, sigver, sig)
250
250
251 # write it
251 # write it
252 if opts['local']:
252 if opts['local']:
253 repo.opener.append("localsigs", sigmessage)
253 repo.opener.append("localsigs", sigmessage)
254 return
254 return
255
255
256 msigs = match.exact(repo.root, '', ['.hgsigs'])
256 msigs = match.exact(repo.root, '', ['.hgsigs'])
257 s = repo.status(match=msigs, unknown=True, ignored=True)[:6]
257 s = repo.status(match=msigs, unknown=True, ignored=True)[:6]
258 if util.any(s) and not opts["force"]:
258 if util.any(s) and not opts["force"]:
259 raise util.Abort(_("working copy of .hgsigs is changed "
259 raise util.Abort(_("working copy of .hgsigs is changed "
260 "(please commit .hgsigs manually "
260 "(please commit .hgsigs manually "
261 "or use --force)"))
261 "or use --force)"))
262
262
263 sigsfile = repo.wfile(".hgsigs", "ab")
263 sigsfile = repo.wfile(".hgsigs", "ab")
264 sigsfile.write(sigmessage)
264 sigsfile.write(sigmessage)
265 sigsfile.close()
265 sigsfile.close()
266
266
267 if '.hgsigs' not in repo.dirstate:
267 if '.hgsigs' not in repo.dirstate:
268 repo[None].add([".hgsigs"])
268 repo[None].add([".hgsigs"])
269
269
270 if opts["no_commit"]:
270 if opts["no_commit"]:
271 return
271 return
272
272
273 message = opts['message']
273 message = opts['message']
274 if not message:
274 if not message:
275 # we don't translate commit messages
275 # we don't translate commit messages
276 message = "\n".join(["Added signature for changeset %s"
276 message = "\n".join(["Added signature for changeset %s"
277 % hgnode.short(n)
277 % hgnode.short(n)
278 for n in nodes])
278 for n in nodes])
279 try:
279 try:
280 repo.commit(message, opts['user'], opts['date'], match=msigs)
280 repo.commit(message, opts['user'], opts['date'], match=msigs)
281 except ValueError, inst:
281 except ValueError, inst:
282 raise util.Abort(str(inst))
282 raise util.Abort(str(inst))
283
283
284 def node2txt(repo, node, ver):
284 def node2txt(repo, node, ver):
285 """map a manifest into some text"""
285 """map a manifest into some text"""
286 if ver == "0":
286 if ver == "0":
287 return "%s\n" % hgnode.hex(node)
287 return "%s\n" % hgnode.hex(node)
288 else:
288 else:
289 raise util.Abort(_("unknown signature version"))
289 raise util.Abort(_("unknown signature version"))
@@ -1,34 +1,34
1 Test the GPG extension
1 Test the GPG extension
2
2
3 $ "$TESTDIR/hghave" gpg || exit 80
3 $ "$TESTDIR/hghave" gpg || exit 80
4 $ cat <<EOF >> $HGRCPATH
4 $ cat <<EOF >> $HGRCPATH
5 > [extensions]
5 > [extensions]
6 > gpg=
6 > gpg=
7 >
7 >
8 > [gpg]
8 > [gpg]
9 > cmd=gpg --no-permission-warning --no-secmem-warning --no-auto-check-trustdb --homedir "$TESTDIR/gpg"
9 > cmd=gpg --no-permission-warning --no-secmem-warning --no-auto-check-trustdb --homedir "$TESTDIR/gpg"
10 > EOF
10 > EOF
11 $ hg init r
11 $ hg init r
12 $ cd r
12 $ cd r
13 $ echo foo > foo
13 $ echo foo > foo
14 $ hg ci -Amfoo
14 $ hg ci -Amfoo
15 adding foo
15 adding foo
16
16
17 $ hg sigs
17 $ hg sigs
18
18
19 $ hg sign 0
19 $ hg sign 0
20 Signing 0:e63c23eaa88a
20 signing 0:e63c23eaa88a
21
21
22 $ hg sigs
22 $ hg sigs
23 hgtest 0:e63c23eaa88ae77967edcf4ea194d31167c478b0
23 hgtest 0:e63c23eaa88ae77967edcf4ea194d31167c478b0
24
24
25 $ hg sigcheck 0
25 $ hg sigcheck 0
26 e63c23eaa88a is signed by:
26 e63c23eaa88a is signed by:
27 hgtest
27 hgtest
28
28
29 verify that this test has not modified the trustdb.gpg file back in
29 verify that this test has not modified the trustdb.gpg file back in
30 the main hg working dir
30 the main hg working dir
31 $ "$TESTDIR/md5sum.py" "$TESTDIR/gpg/trustdb.gpg"
31 $ "$TESTDIR/md5sum.py" "$TESTDIR/gpg/trustdb.gpg"
32 f6b9c78c65fa9536e7512bb2ceb338ae */gpg/trustdb.gpg (glob)
32 f6b9c78c65fa9536e7512bb2ceb338ae */gpg/trustdb.gpg (glob)
33
33
34 $ cd ..
34 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now