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