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