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