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