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