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