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