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