##// END OF EJS Templates
help: register the 'gpg' command category and give it a description...
Sietse Brouwer -
r42421:ade02721 default
parent child Browse files
Show More
@@ -1,341 +1,346 b''
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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import binascii
10 import binascii
11 import os
11 import os
12
12
13 from mercurial.i18n import _
13 from mercurial.i18n import _
14 from mercurial import (
14 from mercurial import (
15 cmdutil,
15 cmdutil,
16 error,
16 error,
17 help,
17 help,
18 match,
18 match,
19 node as hgnode,
19 node as hgnode,
20 pycompat,
20 pycompat,
21 registrar,
21 registrar,
22 )
22 )
23 from mercurial.utils import (
23 from mercurial.utils import (
24 dateutil,
24 dateutil,
25 procutil,
25 procutil,
26 )
26 )
27
27
28 cmdtable = {}
28 cmdtable = {}
29 command = registrar.command(cmdtable)
29 command = registrar.command(cmdtable)
30 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
30 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
31 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
31 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
32 # be specifying the version(s) of Mercurial they are tested with, or
32 # be specifying the version(s) of Mercurial they are tested with, or
33 # leave the attribute unspecified.
33 # leave the attribute unspecified.
34 testedwith = 'ships-with-hg-core'
34 testedwith = 'ships-with-hg-core'
35
35
36 configtable = {}
36 configtable = {}
37 configitem = registrar.configitem(configtable)
37 configitem = registrar.configitem(configtable)
38
38
39 configitem('gpg', 'cmd',
39 configitem('gpg', 'cmd',
40 default='gpg',
40 default='gpg',
41 )
41 )
42 configitem('gpg', 'key',
42 configitem('gpg', 'key',
43 default=None,
43 default=None,
44 )
44 )
45 configitem('gpg', '.*',
45 configitem('gpg', '.*',
46 default=None,
46 default=None,
47 generic=True,
47 generic=True,
48 )
48 )
49
49
50 # Custom help category
50 # Custom help category
51 _HELP_CATEGORY = 'gpg'
51 _HELP_CATEGORY = 'gpg'
52 help.CATEGORY_ORDER.insert(
53 help.CATEGORY_ORDER.index(registrar.command.CATEGORY_HELP),
54 _HELP_CATEGORY
55 )
56 help.CATEGORY_NAMES[_HELP_CATEGORY] = 'Signing changes (GPG)'
52
57
53 class gpg(object):
58 class gpg(object):
54 def __init__(self, path, key=None):
59 def __init__(self, path, key=None):
55 self.path = path
60 self.path = path
56 self.key = (key and " --local-user \"%s\"" % key) or ""
61 self.key = (key and " --local-user \"%s\"" % key) or ""
57
62
58 def sign(self, data):
63 def sign(self, data):
59 gpgcmd = "%s --sign --detach-sign%s" % (self.path, self.key)
64 gpgcmd = "%s --sign --detach-sign%s" % (self.path, self.key)
60 return procutil.filter(data, gpgcmd)
65 return procutil.filter(data, gpgcmd)
61
66
62 def verify(self, data, sig):
67 def verify(self, data, sig):
63 """ returns of the good and bad signatures"""
68 """ returns of the good and bad signatures"""
64 sigfile = datafile = None
69 sigfile = datafile = None
65 try:
70 try:
66 # create temporary files
71 # create temporary files
67 fd, sigfile = pycompat.mkstemp(prefix="hg-gpg-", suffix=".sig")
72 fd, sigfile = pycompat.mkstemp(prefix="hg-gpg-", suffix=".sig")
68 fp = os.fdopen(fd, r'wb')
73 fp = os.fdopen(fd, r'wb')
69 fp.write(sig)
74 fp.write(sig)
70 fp.close()
75 fp.close()
71 fd, datafile = pycompat.mkstemp(prefix="hg-gpg-", suffix=".txt")
76 fd, datafile = pycompat.mkstemp(prefix="hg-gpg-", suffix=".txt")
72 fp = os.fdopen(fd, r'wb')
77 fp = os.fdopen(fd, r'wb')
73 fp.write(data)
78 fp.write(data)
74 fp.close()
79 fp.close()
75 gpgcmd = ("%s --logger-fd 1 --status-fd 1 --verify "
80 gpgcmd = ("%s --logger-fd 1 --status-fd 1 --verify "
76 "\"%s\" \"%s\"" % (self.path, sigfile, datafile))
81 "\"%s\" \"%s\"" % (self.path, sigfile, datafile))
77 ret = procutil.filter("", gpgcmd)
82 ret = procutil.filter("", gpgcmd)
78 finally:
83 finally:
79 for f in (sigfile, datafile):
84 for f in (sigfile, datafile):
80 try:
85 try:
81 if f:
86 if f:
82 os.unlink(f)
87 os.unlink(f)
83 except OSError:
88 except OSError:
84 pass
89 pass
85 keys = []
90 keys = []
86 key, fingerprint = None, None
91 key, fingerprint = None, None
87 for l in ret.splitlines():
92 for l in ret.splitlines():
88 # see DETAILS in the gnupg documentation
93 # see DETAILS in the gnupg documentation
89 # filter the logger output
94 # filter the logger output
90 if not l.startswith("[GNUPG:]"):
95 if not l.startswith("[GNUPG:]"):
91 continue
96 continue
92 l = l[9:]
97 l = l[9:]
93 if l.startswith("VALIDSIG"):
98 if l.startswith("VALIDSIG"):
94 # fingerprint of the primary key
99 # fingerprint of the primary key
95 fingerprint = l.split()[10]
100 fingerprint = l.split()[10]
96 elif l.startswith("ERRSIG"):
101 elif l.startswith("ERRSIG"):
97 key = l.split(" ", 3)[:2]
102 key = l.split(" ", 3)[:2]
98 key.append("")
103 key.append("")
99 fingerprint = None
104 fingerprint = None
100 elif (l.startswith("GOODSIG") or
105 elif (l.startswith("GOODSIG") or
101 l.startswith("EXPSIG") or
106 l.startswith("EXPSIG") or
102 l.startswith("EXPKEYSIG") or
107 l.startswith("EXPKEYSIG") or
103 l.startswith("BADSIG")):
108 l.startswith("BADSIG")):
104 if key is not None:
109 if key is not None:
105 keys.append(key + [fingerprint])
110 keys.append(key + [fingerprint])
106 key = l.split(" ", 2)
111 key = l.split(" ", 2)
107 fingerprint = None
112 fingerprint = None
108 if key is not None:
113 if key is not None:
109 keys.append(key + [fingerprint])
114 keys.append(key + [fingerprint])
110 return keys
115 return keys
111
116
112 def newgpg(ui, **opts):
117 def newgpg(ui, **opts):
113 """create a new gpg instance"""
118 """create a new gpg instance"""
114 gpgpath = ui.config("gpg", "cmd")
119 gpgpath = ui.config("gpg", "cmd")
115 gpgkey = opts.get(r'key')
120 gpgkey = opts.get(r'key')
116 if not gpgkey:
121 if not gpgkey:
117 gpgkey = ui.config("gpg", "key")
122 gpgkey = ui.config("gpg", "key")
118 return gpg(gpgpath, gpgkey)
123 return gpg(gpgpath, gpgkey)
119
124
120 def sigwalk(repo):
125 def sigwalk(repo):
121 """
126 """
122 walk over every sigs, yields a couple
127 walk over every sigs, yields a couple
123 ((node, version, sig), (filename, linenumber))
128 ((node, version, sig), (filename, linenumber))
124 """
129 """
125 def parsefile(fileiter, context):
130 def parsefile(fileiter, context):
126 ln = 1
131 ln = 1
127 for l in fileiter:
132 for l in fileiter:
128 if not l:
133 if not l:
129 continue
134 continue
130 yield (l.split(" ", 2), (context, ln))
135 yield (l.split(" ", 2), (context, ln))
131 ln += 1
136 ln += 1
132
137
133 # read the heads
138 # read the heads
134 fl = repo.file(".hgsigs")
139 fl = repo.file(".hgsigs")
135 for r in reversed(fl.heads()):
140 for r in reversed(fl.heads()):
136 fn = ".hgsigs|%s" % hgnode.short(r)
141 fn = ".hgsigs|%s" % hgnode.short(r)
137 for item in parsefile(fl.read(r).splitlines(), fn):
142 for item in parsefile(fl.read(r).splitlines(), fn):
138 yield item
143 yield item
139 try:
144 try:
140 # read local signatures
145 # read local signatures
141 fn = "localsigs"
146 fn = "localsigs"
142 for item in parsefile(repo.vfs(fn), fn):
147 for item in parsefile(repo.vfs(fn), fn):
143 yield item
148 yield item
144 except IOError:
149 except IOError:
145 pass
150 pass
146
151
147 def getkeys(ui, repo, mygpg, sigdata, context):
152 def getkeys(ui, repo, mygpg, sigdata, context):
148 """get the keys who signed a data"""
153 """get the keys who signed a data"""
149 fn, ln = context
154 fn, ln = context
150 node, version, sig = sigdata
155 node, version, sig = sigdata
151 prefix = "%s:%d" % (fn, ln)
156 prefix = "%s:%d" % (fn, ln)
152 node = hgnode.bin(node)
157 node = hgnode.bin(node)
153
158
154 data = node2txt(repo, node, version)
159 data = node2txt(repo, node, version)
155 sig = binascii.a2b_base64(sig)
160 sig = binascii.a2b_base64(sig)
156 keys = mygpg.verify(data, sig)
161 keys = mygpg.verify(data, sig)
157
162
158 validkeys = []
163 validkeys = []
159 # warn for expired key and/or sigs
164 # warn for expired key and/or sigs
160 for key in keys:
165 for key in keys:
161 if key[0] == "ERRSIG":
166 if key[0] == "ERRSIG":
162 ui.write(_("%s Unknown key ID \"%s\"\n") % (prefix, key[1]))
167 ui.write(_("%s Unknown key ID \"%s\"\n") % (prefix, key[1]))
163 continue
168 continue
164 if key[0] == "BADSIG":
169 if key[0] == "BADSIG":
165 ui.write(_("%s Bad signature from \"%s\"\n") % (prefix, key[2]))
170 ui.write(_("%s Bad signature from \"%s\"\n") % (prefix, key[2]))
166 continue
171 continue
167 if key[0] == "EXPSIG":
172 if key[0] == "EXPSIG":
168 ui.write(_("%s Note: Signature has expired"
173 ui.write(_("%s Note: Signature has expired"
169 " (signed by: \"%s\")\n") % (prefix, key[2]))
174 " (signed by: \"%s\")\n") % (prefix, key[2]))
170 elif key[0] == "EXPKEYSIG":
175 elif key[0] == "EXPKEYSIG":
171 ui.write(_("%s Note: This key has expired"
176 ui.write(_("%s Note: This key has expired"
172 " (signed by: \"%s\")\n") % (prefix, key[2]))
177 " (signed by: \"%s\")\n") % (prefix, key[2]))
173 validkeys.append((key[1], key[2], key[3]))
178 validkeys.append((key[1], key[2], key[3]))
174 return validkeys
179 return validkeys
175
180
176 @command("sigs", [], _('hg sigs'), helpcategory=_HELP_CATEGORY)
181 @command("sigs", [], _('hg sigs'), helpcategory=_HELP_CATEGORY)
177 def sigs(ui, repo):
182 def sigs(ui, repo):
178 """list signed changesets"""
183 """list signed changesets"""
179 mygpg = newgpg(ui)
184 mygpg = newgpg(ui)
180 revs = {}
185 revs = {}
181
186
182 for data, context in sigwalk(repo):
187 for data, context in sigwalk(repo):
183 node, version, sig = data
188 node, version, sig = data
184 fn, ln = context
189 fn, ln = context
185 try:
190 try:
186 n = repo.lookup(node)
191 n = repo.lookup(node)
187 except KeyError:
192 except KeyError:
188 ui.warn(_("%s:%d node does not exist\n") % (fn, ln))
193 ui.warn(_("%s:%d node does not exist\n") % (fn, ln))
189 continue
194 continue
190 r = repo.changelog.rev(n)
195 r = repo.changelog.rev(n)
191 keys = getkeys(ui, repo, mygpg, data, context)
196 keys = getkeys(ui, repo, mygpg, data, context)
192 if not keys:
197 if not keys:
193 continue
198 continue
194 revs.setdefault(r, [])
199 revs.setdefault(r, [])
195 revs[r].extend(keys)
200 revs[r].extend(keys)
196 for rev in sorted(revs, reverse=True):
201 for rev in sorted(revs, reverse=True):
197 for k in revs[rev]:
202 for k in revs[rev]:
198 r = "%5d:%s" % (rev, hgnode.hex(repo.changelog.node(rev)))
203 r = "%5d:%s" % (rev, hgnode.hex(repo.changelog.node(rev)))
199 ui.write("%-30s %s\n" % (keystr(ui, k), r))
204 ui.write("%-30s %s\n" % (keystr(ui, k), r))
200
205
201 @command("sigcheck", [], _('hg sigcheck REV'), helpcategory=_HELP_CATEGORY)
206 @command("sigcheck", [], _('hg sigcheck REV'), helpcategory=_HELP_CATEGORY)
202 def sigcheck(ui, repo, rev):
207 def sigcheck(ui, repo, rev):
203 """verify all the signatures there may be for a particular revision"""
208 """verify all the signatures there may be for a particular revision"""
204 mygpg = newgpg(ui)
209 mygpg = newgpg(ui)
205 rev = repo.lookup(rev)
210 rev = repo.lookup(rev)
206 hexrev = hgnode.hex(rev)
211 hexrev = hgnode.hex(rev)
207 keys = []
212 keys = []
208
213
209 for data, context in sigwalk(repo):
214 for data, context in sigwalk(repo):
210 node, version, sig = data
215 node, version, sig = data
211 if node == hexrev:
216 if node == hexrev:
212 k = getkeys(ui, repo, mygpg, data, context)
217 k = getkeys(ui, repo, mygpg, data, context)
213 if k:
218 if k:
214 keys.extend(k)
219 keys.extend(k)
215
220
216 if not keys:
221 if not keys:
217 ui.write(_("no valid signature for %s\n") % hgnode.short(rev))
222 ui.write(_("no valid signature for %s\n") % hgnode.short(rev))
218 return
223 return
219
224
220 # print summary
225 # print summary
221 ui.write(_("%s is signed by:\n") % hgnode.short(rev))
226 ui.write(_("%s is signed by:\n") % hgnode.short(rev))
222 for key in keys:
227 for key in keys:
223 ui.write(" %s\n" % keystr(ui, key))
228 ui.write(" %s\n" % keystr(ui, key))
224
229
225 def keystr(ui, key):
230 def keystr(ui, key):
226 """associate a string to a key (username, comment)"""
231 """associate a string to a key (username, comment)"""
227 keyid, user, fingerprint = key
232 keyid, user, fingerprint = key
228 comment = ui.config("gpg", fingerprint)
233 comment = ui.config("gpg", fingerprint)
229 if comment:
234 if comment:
230 return "%s (%s)" % (user, comment)
235 return "%s (%s)" % (user, comment)
231 else:
236 else:
232 return user
237 return user
233
238
234 @command("sign",
239 @command("sign",
235 [('l', 'local', None, _('make the signature local')),
240 [('l', 'local', None, _('make the signature local')),
236 ('f', 'force', None, _('sign even if the sigfile is modified')),
241 ('f', 'force', None, _('sign even if the sigfile is modified')),
237 ('', 'no-commit', None, _('do not commit the sigfile after signing')),
242 ('', 'no-commit', None, _('do not commit the sigfile after signing')),
238 ('k', 'key', '',
243 ('k', 'key', '',
239 _('the key id to sign with'), _('ID')),
244 _('the key id to sign with'), _('ID')),
240 ('m', 'message', '',
245 ('m', 'message', '',
241 _('use text as commit message'), _('TEXT')),
246 _('use text as commit message'), _('TEXT')),
242 ('e', 'edit', False, _('invoke editor on commit messages')),
247 ('e', 'edit', False, _('invoke editor on commit messages')),
243 ] + cmdutil.commitopts2,
248 ] + cmdutil.commitopts2,
244 _('hg sign [OPTION]... [REV]...'),
249 _('hg sign [OPTION]... [REV]...'),
245 helpcategory=_HELP_CATEGORY)
250 helpcategory=_HELP_CATEGORY)
246 def sign(ui, repo, *revs, **opts):
251 def sign(ui, repo, *revs, **opts):
247 """add a signature for the current or given revision
252 """add a signature for the current or given revision
248
253
249 If no revision is given, the parent of the working directory is used,
254 If no revision is given, the parent of the working directory is used,
250 or tip if no revision is checked out.
255 or tip if no revision is checked out.
251
256
252 The ``gpg.cmd`` config setting can be used to specify the command
257 The ``gpg.cmd`` config setting can be used to specify the command
253 to run. A default key can be specified with ``gpg.key``.
258 to run. A default key can be specified with ``gpg.key``.
254
259
255 See :hg:`help dates` for a list of formats valid for -d/--date.
260 See :hg:`help dates` for a list of formats valid for -d/--date.
256 """
261 """
257 with repo.wlock():
262 with repo.wlock():
258 return _dosign(ui, repo, *revs, **opts)
263 return _dosign(ui, repo, *revs, **opts)
259
264
260 def _dosign(ui, repo, *revs, **opts):
265 def _dosign(ui, repo, *revs, **opts):
261 mygpg = newgpg(ui, **opts)
266 mygpg = newgpg(ui, **opts)
262 opts = pycompat.byteskwargs(opts)
267 opts = pycompat.byteskwargs(opts)
263 sigver = "0"
268 sigver = "0"
264 sigmessage = ""
269 sigmessage = ""
265
270
266 date = opts.get('date')
271 date = opts.get('date')
267 if date:
272 if date:
268 opts['date'] = dateutil.parsedate(date)
273 opts['date'] = dateutil.parsedate(date)
269
274
270 if revs:
275 if revs:
271 nodes = [repo.lookup(n) for n in revs]
276 nodes = [repo.lookup(n) for n in revs]
272 else:
277 else:
273 nodes = [node for node in repo.dirstate.parents()
278 nodes = [node for node in repo.dirstate.parents()
274 if node != hgnode.nullid]
279 if node != hgnode.nullid]
275 if len(nodes) > 1:
280 if len(nodes) > 1:
276 raise error.Abort(_('uncommitted merge - please provide a '
281 raise error.Abort(_('uncommitted merge - please provide a '
277 'specific revision'))
282 'specific revision'))
278 if not nodes:
283 if not nodes:
279 nodes = [repo.changelog.tip()]
284 nodes = [repo.changelog.tip()]
280
285
281 for n in nodes:
286 for n in nodes:
282 hexnode = hgnode.hex(n)
287 hexnode = hgnode.hex(n)
283 ui.write(_("signing %d:%s\n") % (repo.changelog.rev(n),
288 ui.write(_("signing %d:%s\n") % (repo.changelog.rev(n),
284 hgnode.short(n)))
289 hgnode.short(n)))
285 # build data
290 # build data
286 data = node2txt(repo, n, sigver)
291 data = node2txt(repo, n, sigver)
287 sig = mygpg.sign(data)
292 sig = mygpg.sign(data)
288 if not sig:
293 if not sig:
289 raise error.Abort(_("error while signing"))
294 raise error.Abort(_("error while signing"))
290 sig = binascii.b2a_base64(sig)
295 sig = binascii.b2a_base64(sig)
291 sig = sig.replace("\n", "")
296 sig = sig.replace("\n", "")
292 sigmessage += "%s %s %s\n" % (hexnode, sigver, sig)
297 sigmessage += "%s %s %s\n" % (hexnode, sigver, sig)
293
298
294 # write it
299 # write it
295 if opts['local']:
300 if opts['local']:
296 repo.vfs.append("localsigs", sigmessage)
301 repo.vfs.append("localsigs", sigmessage)
297 return
302 return
298
303
299 if not opts["force"]:
304 if not opts["force"]:
300 msigs = match.exact(['.hgsigs'])
305 msigs = match.exact(['.hgsigs'])
301 if any(repo.status(match=msigs, unknown=True, ignored=True)):
306 if any(repo.status(match=msigs, unknown=True, ignored=True)):
302 raise error.Abort(_("working copy of .hgsigs is changed "),
307 raise error.Abort(_("working copy of .hgsigs is changed "),
303 hint=_("please commit .hgsigs manually"))
308 hint=_("please commit .hgsigs manually"))
304
309
305 sigsfile = repo.wvfs(".hgsigs", "ab")
310 sigsfile = repo.wvfs(".hgsigs", "ab")
306 sigsfile.write(sigmessage)
311 sigsfile.write(sigmessage)
307 sigsfile.close()
312 sigsfile.close()
308
313
309 if '.hgsigs' not in repo.dirstate:
314 if '.hgsigs' not in repo.dirstate:
310 repo[None].add([".hgsigs"])
315 repo[None].add([".hgsigs"])
311
316
312 if opts["no_commit"]:
317 if opts["no_commit"]:
313 return
318 return
314
319
315 message = opts['message']
320 message = opts['message']
316 if not message:
321 if not message:
317 # we don't translate commit messages
322 # we don't translate commit messages
318 message = "\n".join(["Added signature for changeset %s"
323 message = "\n".join(["Added signature for changeset %s"
319 % hgnode.short(n)
324 % hgnode.short(n)
320 for n in nodes])
325 for n in nodes])
321 try:
326 try:
322 editor = cmdutil.getcommiteditor(editform='gpg.sign',
327 editor = cmdutil.getcommiteditor(editform='gpg.sign',
323 **pycompat.strkwargs(opts))
328 **pycompat.strkwargs(opts))
324 repo.commit(message, opts['user'], opts['date'], match=msigs,
329 repo.commit(message, opts['user'], opts['date'], match=msigs,
325 editor=editor)
330 editor=editor)
326 except ValueError as inst:
331 except ValueError as inst:
327 raise error.Abort(pycompat.bytestr(inst))
332 raise error.Abort(pycompat.bytestr(inst))
328
333
329 def node2txt(repo, node, ver):
334 def node2txt(repo, node, ver):
330 """map a manifest into some text"""
335 """map a manifest into some text"""
331 if ver == "0":
336 if ver == "0":
332 return "%s\n" % hgnode.hex(node)
337 return "%s\n" % hgnode.hex(node)
333 else:
338 else:
334 raise error.Abort(_("unknown signature version"))
339 raise error.Abort(_("unknown signature version"))
335
340
336 def extsetup(ui):
341 def extsetup(ui):
337 # Add our category before "Repository maintenance".
342 # Add our category before "Repository maintenance".
338 help.CATEGORY_ORDER.insert(
343 help.CATEGORY_ORDER.insert(
339 help.CATEGORY_ORDER.index(command.CATEGORY_MAINTENANCE),
344 help.CATEGORY_ORDER.index(command.CATEGORY_MAINTENANCE),
340 _HELP_CATEGORY)
345 _HELP_CATEGORY)
341 help.CATEGORY_NAMES[_HELP_CATEGORY] = 'GPG signing'
346 help.CATEGORY_NAMES[_HELP_CATEGORY] = 'GPG signing'
General Comments 0
You need to be logged in to leave comments. Login now