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