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