##// END OF EJS Templates
gpg: fix an UnboundLocalError whenever using --force...
Matt Harbison -
r51715:181936ad stable
parent child Browse files
Show More
@@ -1,391 +1,392 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
9 9 import binascii
10 10 import os
11 11
12 12 from mercurial.i18n import _
13 13 from mercurial.node import (
14 14 bin,
15 15 hex,
16 16 short,
17 17 )
18 18 from mercurial import (
19 19 cmdutil,
20 20 error,
21 21 help,
22 22 match,
23 23 pycompat,
24 24 registrar,
25 25 )
26 26 from mercurial.utils import (
27 27 dateutil,
28 28 procutil,
29 29 )
30 30
31 31 cmdtable = {}
32 32 command = registrar.command(cmdtable)
33 33 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
34 34 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
35 35 # be specifying the version(s) of Mercurial they are tested with, or
36 36 # leave the attribute unspecified.
37 37 testedwith = b'ships-with-hg-core'
38 38
39 39 configtable = {}
40 40 configitem = registrar.configitem(configtable)
41 41
42 42 configitem(
43 43 b'gpg',
44 44 b'cmd',
45 45 default=b'gpg',
46 46 )
47 47 configitem(
48 48 b'gpg',
49 49 b'key',
50 50 default=None,
51 51 )
52 52 configitem(
53 53 b'gpg',
54 54 b'.*',
55 55 default=None,
56 56 generic=True,
57 57 )
58 58
59 59 # Custom help category
60 60 _HELP_CATEGORY = b'gpg'
61 61 help.CATEGORY_ORDER.insert(
62 62 help.CATEGORY_ORDER.index(registrar.command.CATEGORY_HELP), _HELP_CATEGORY
63 63 )
64 64 help.CATEGORY_NAMES[_HELP_CATEGORY] = b'Signing changes (GPG)'
65 65
66 66
67 67 class gpg:
68 68 def __init__(self, path, key=None):
69 69 self.path = path
70 70 self.key = (key and b" --local-user \"%s\"" % key) or b""
71 71
72 72 def sign(self, data):
73 73 gpgcmd = b"%s --sign --detach-sign%s" % (self.path, self.key)
74 74 return procutil.filter(data, gpgcmd)
75 75
76 76 def verify(self, data, sig):
77 77 """returns of the good and bad signatures"""
78 78 sigfile = datafile = None
79 79 try:
80 80 # create temporary files
81 81 fd, sigfile = pycompat.mkstemp(prefix=b"hg-gpg-", suffix=b".sig")
82 82 fp = os.fdopen(fd, 'wb')
83 83 fp.write(sig)
84 84 fp.close()
85 85 fd, datafile = pycompat.mkstemp(prefix=b"hg-gpg-", suffix=b".txt")
86 86 fp = os.fdopen(fd, 'wb')
87 87 fp.write(data)
88 88 fp.close()
89 89 gpgcmd = (
90 90 b"%s --logger-fd 1 --status-fd 1 --verify \"%s\" \"%s\""
91 91 % (
92 92 self.path,
93 93 sigfile,
94 94 datafile,
95 95 )
96 96 )
97 97 ret = procutil.filter(b"", gpgcmd)
98 98 finally:
99 99 for f in (sigfile, datafile):
100 100 try:
101 101 if f:
102 102 os.unlink(f)
103 103 except OSError:
104 104 pass
105 105 keys = []
106 106 key, fingerprint = None, None
107 107 for l in ret.splitlines():
108 108 # see DETAILS in the gnupg documentation
109 109 # filter the logger output
110 110 if not l.startswith(b"[GNUPG:]"):
111 111 continue
112 112 l = l[9:]
113 113 if l.startswith(b"VALIDSIG"):
114 114 # fingerprint of the primary key
115 115 fingerprint = l.split()[10]
116 116 elif l.startswith(b"ERRSIG"):
117 117 key = l.split(b" ", 3)[:2]
118 118 key.append(b"")
119 119 fingerprint = None
120 120 elif (
121 121 l.startswith(b"GOODSIG")
122 122 or l.startswith(b"EXPSIG")
123 123 or l.startswith(b"EXPKEYSIG")
124 124 or l.startswith(b"BADSIG")
125 125 ):
126 126 if key is not None:
127 127 keys.append(key + [fingerprint])
128 128 key = l.split(b" ", 2)
129 129 fingerprint = None
130 130 if key is not None:
131 131 keys.append(key + [fingerprint])
132 132 return keys
133 133
134 134
135 135 def newgpg(ui, **opts):
136 136 """create a new gpg instance"""
137 137 gpgpath = ui.config(b"gpg", b"cmd")
138 138 gpgkey = opts.get('key')
139 139 if not gpgkey:
140 140 gpgkey = ui.config(b"gpg", b"key")
141 141 return gpg(gpgpath, gpgkey)
142 142
143 143
144 144 def sigwalk(repo):
145 145 """
146 146 walk over every sigs, yields a couple
147 147 ((node, version, sig), (filename, linenumber))
148 148 """
149 149
150 150 def parsefile(fileiter, context):
151 151 ln = 1
152 152 for l in fileiter:
153 153 if not l:
154 154 continue
155 155 yield (l.split(b" ", 2), (context, ln))
156 156 ln += 1
157 157
158 158 # read the heads
159 159 fl = repo.file(b".hgsigs")
160 160 for r in reversed(fl.heads()):
161 161 fn = b".hgsigs|%s" % short(r)
162 162 for item in parsefile(fl.read(r).splitlines(), fn):
163 163 yield item
164 164 try:
165 165 # read local signatures
166 166 fn = b"localsigs"
167 167 for item in parsefile(repo.vfs(fn), fn):
168 168 yield item
169 169 except IOError:
170 170 pass
171 171
172 172
173 173 def getkeys(ui, repo, mygpg, sigdata, context):
174 174 """get the keys who signed a data"""
175 175 fn, ln = context
176 176 node, version, sig = sigdata
177 177 prefix = b"%s:%d" % (fn, ln)
178 178 node = bin(node)
179 179
180 180 data = node2txt(repo, node, version)
181 181 sig = binascii.a2b_base64(sig)
182 182 keys = mygpg.verify(data, sig)
183 183
184 184 validkeys = []
185 185 # warn for expired key and/or sigs
186 186 for key in keys:
187 187 if key[0] == b"ERRSIG":
188 188 ui.write(_(b"%s Unknown key ID \"%s\"\n") % (prefix, key[1]))
189 189 continue
190 190 if key[0] == b"BADSIG":
191 191 ui.write(_(b"%s Bad signature from \"%s\"\n") % (prefix, key[2]))
192 192 continue
193 193 if key[0] == b"EXPSIG":
194 194 ui.write(
195 195 _(b"%s Note: Signature has expired (signed by: \"%s\")\n")
196 196 % (prefix, key[2])
197 197 )
198 198 elif key[0] == b"EXPKEYSIG":
199 199 ui.write(
200 200 _(b"%s Note: This key has expired (signed by: \"%s\")\n")
201 201 % (prefix, key[2])
202 202 )
203 203 validkeys.append((key[1], key[2], key[3]))
204 204 return validkeys
205 205
206 206
207 207 @command(b"sigs", [], _(b'hg sigs'), helpcategory=_HELP_CATEGORY)
208 208 def sigs(ui, repo):
209 209 """list signed changesets"""
210 210 mygpg = newgpg(ui)
211 211 revs = {}
212 212
213 213 for data, context in sigwalk(repo):
214 214 node, version, sig = data
215 215 fn, ln = context
216 216 try:
217 217 n = repo.lookup(node)
218 218 except KeyError:
219 219 ui.warn(_(b"%s:%d node does not exist\n") % (fn, ln))
220 220 continue
221 221 r = repo.changelog.rev(n)
222 222 keys = getkeys(ui, repo, mygpg, data, context)
223 223 if not keys:
224 224 continue
225 225 revs.setdefault(r, [])
226 226 revs[r].extend(keys)
227 227 for rev in sorted(revs, reverse=True):
228 228 for k in revs[rev]:
229 229 r = b"%5d:%s" % (rev, hex(repo.changelog.node(rev)))
230 230 ui.write(b"%-30s %s\n" % (keystr(ui, k), r))
231 231
232 232
233 233 @command(b"sigcheck", [], _(b'hg sigcheck REV'), helpcategory=_HELP_CATEGORY)
234 234 def sigcheck(ui, repo, rev):
235 235 """verify all the signatures there may be for a particular revision"""
236 236 mygpg = newgpg(ui)
237 237 rev = repo.lookup(rev)
238 238 hexrev = hex(rev)
239 239 keys = []
240 240
241 241 for data, context in sigwalk(repo):
242 242 node, version, sig = data
243 243 if node == hexrev:
244 244 k = getkeys(ui, repo, mygpg, data, context)
245 245 if k:
246 246 keys.extend(k)
247 247
248 248 if not keys:
249 249 ui.write(_(b"no valid signature for %s\n") % short(rev))
250 250 return
251 251
252 252 # print summary
253 253 ui.write(_(b"%s is signed by:\n") % short(rev))
254 254 for key in keys:
255 255 ui.write(b" %s\n" % keystr(ui, key))
256 256
257 257
258 258 def keystr(ui, key):
259 259 """associate a string to a key (username, comment)"""
260 260 keyid, user, fingerprint = key
261 261 comment = ui.config(b"gpg", fingerprint)
262 262 if comment:
263 263 return b"%s (%s)" % (user, comment)
264 264 else:
265 265 return user
266 266
267 267
268 268 @command(
269 269 b"sign",
270 270 [
271 271 (b'l', b'local', None, _(b'make the signature local')),
272 272 (b'f', b'force', None, _(b'sign even if the sigfile is modified')),
273 273 (
274 274 b'',
275 275 b'no-commit',
276 276 None,
277 277 _(b'do not commit the sigfile after signing'),
278 278 ),
279 279 (b'k', b'key', b'', _(b'the key id to sign with'), _(b'ID')),
280 280 (b'm', b'message', b'', _(b'use text as commit message'), _(b'TEXT')),
281 281 (b'e', b'edit', False, _(b'invoke editor on commit messages')),
282 282 ]
283 283 + cmdutil.commitopts2,
284 284 _(b'hg sign [OPTION]... [REV]...'),
285 285 helpcategory=_HELP_CATEGORY,
286 286 )
287 287 def sign(ui, repo, *revs, **opts):
288 288 """add a signature for the current or given revision
289 289
290 290 If no revision is given, the parent of the working directory is used,
291 291 or tip if no revision is checked out.
292 292
293 293 The ``gpg.cmd`` config setting can be used to specify the command
294 294 to run. A default key can be specified with ``gpg.key``.
295 295
296 296 See :hg:`help dates` for a list of formats valid for -d/--date.
297 297 """
298 298 with repo.wlock():
299 299 return _dosign(ui, repo, *revs, **opts)
300 300
301 301
302 302 def _dosign(ui, repo, *revs, **opts):
303 303 mygpg = newgpg(ui, **opts)
304 304 opts = pycompat.byteskwargs(opts)
305 305 sigver = b"0"
306 306 sigmessage = b""
307 307
308 308 date = opts.get(b'date')
309 309 if date:
310 310 opts[b'date'] = dateutil.parsedate(date)
311 311
312 312 if revs:
313 313 nodes = [repo.lookup(n) for n in revs]
314 314 else:
315 315 nodes = [
316 316 node for node in repo.dirstate.parents() if node != repo.nullid
317 317 ]
318 318 if len(nodes) > 1:
319 319 raise error.Abort(
320 320 _(b'uncommitted merge - please provide a specific revision')
321 321 )
322 322 if not nodes:
323 323 nodes = [repo.changelog.tip()]
324 324
325 325 for n in nodes:
326 326 hexnode = hex(n)
327 327 ui.write(_(b"signing %d:%s\n") % (repo.changelog.rev(n), short(n)))
328 328 # build data
329 329 data = node2txt(repo, n, sigver)
330 330 sig = mygpg.sign(data)
331 331 if not sig:
332 332 raise error.Abort(_(b"error while signing"))
333 333 sig = binascii.b2a_base64(sig)
334 334 sig = sig.replace(b"\n", b"")
335 335 sigmessage += b"%s %s %s\n" % (hexnode, sigver, sig)
336 336
337 337 # write it
338 338 if opts[b'local']:
339 339 repo.vfs.append(b"localsigs", sigmessage)
340 340 return
341 341
342 msigs = match.exact([b'.hgsigs'])
343
342 344 if not opts[b"force"]:
343 msigs = match.exact([b'.hgsigs'])
344 345 if any(repo.status(match=msigs, unknown=True, ignored=True)):
345 346 raise error.Abort(
346 347 _(b"working copy of .hgsigs is changed "),
347 348 hint=_(b"please commit .hgsigs manually"),
348 349 )
349 350
350 351 sigsfile = repo.wvfs(b".hgsigs", b"ab")
351 352 sigsfile.write(sigmessage)
352 353 sigsfile.close()
353 354
354 355 if b'.hgsigs' not in repo.dirstate:
355 356 with repo.dirstate.changing_files(repo):
356 357 repo[None].add([b".hgsigs"])
357 358
358 359 if opts[b"no_commit"]:
359 360 return
360 361
361 362 message = opts[b'message']
362 363 if not message:
363 364 # we don't translate commit messages
364 365 message = b"\n".join(
365 366 [b"Added signature for changeset %s" % short(n) for n in nodes]
366 367 )
367 368 try:
368 369 editor = cmdutil.getcommiteditor(
369 370 editform=b'gpg.sign', **pycompat.strkwargs(opts)
370 371 )
371 372 repo.commit(
372 373 message, opts[b'user'], opts[b'date'], match=msigs, editor=editor
373 374 )
374 375 except ValueError as inst:
375 376 raise error.Abort(pycompat.bytestr(inst))
376 377
377 378
378 379 def node2txt(repo, node, ver):
379 380 """map a manifest into some text"""
380 381 if ver == b"0":
381 382 return b"%s\n" % hex(node)
382 383 else:
383 384 raise error.Abort(_(b"unknown signature version"))
384 385
385 386
386 387 def extsetup(ui):
387 388 # Add our category before "Repository maintenance".
388 389 help.CATEGORY_ORDER.insert(
389 390 help.CATEGORY_ORDER.index(command.CATEGORY_MAINTENANCE), _HELP_CATEGORY
390 391 )
391 392 help.CATEGORY_NAMES[_HELP_CATEGORY] = b'GPG signing'
@@ -1,57 +1,74 b''
1 1 #require gpg
2 2
3 3 Test the GPG extension
4 4
5 5 $ cat <<EOF >> $HGRCPATH
6 6 > [extensions]
7 7 > gpg=
8 8 >
9 9 > [gpg]
10 10 > cmd=gpg --no-permission-warning --no-secmem-warning --no-auto-check-trustdb
11 11 > EOF
12 12 $ GNUPGHOME="$TESTTMP/gpg"; export GNUPGHOME
13 13 $ cp -R "$TESTDIR/gpg" "$GNUPGHOME"
14 14
15 15 Start gpg-agent, which is required by GnuPG v2
16 16
17 17 #if gpg21
18 18 $ gpg-connect-agent -q --subst /serverpid '/echo ${get serverpid}' /bye \
19 19 > >> $DAEMON_PIDS
20 20 #endif
21 21
22 22 and migrate secret keys
23 23
24 24 #if gpg2
25 25 $ gpg --no-permission-warning --no-secmem-warning --list-secret-keys \
26 26 > > /dev/null 2>&1
27 27 #endif
28 28
29 29 $ hg init r
30 30 $ cd r
31 31 $ echo foo > foo
32 32 $ hg ci -Amfoo
33 33 adding foo
34 34
35 35 $ hg sigs
36 36
37 37 $ HGEDITOR=cat hg sign -e 0
38 38 gpg: error retrieving key fingerprint from card: Invalid name (?)
39 39 signing 0:e63c23eaa88a
40 40 Added signature for changeset e63c23eaa88a
41 41
42 42
43 43 HG: Enter commit message. Lines beginning with 'HG:' are removed.
44 44 HG: Leave message empty to abort commit.
45 45 HG: --
46 46 HG: user: test
47 47 HG: branch 'default'
48 48 HG: added .hgsigs
49 49
50 50 $ hg sigs
51 51 hgtest 0:e63c23eaa88ae77967edcf4ea194d31167c478b0
52 52
53 53 $ hg sigcheck 0
54 54 e63c23eaa88a is signed by:
55 55 hgtest
56 56
57 The signature is different each time, so avoid signing the previous signature so
58 that the cset hashes are unchanging.
59 $ hg up -q '.^'
60
61 $ HGEDITOR=cat hg sign -f -e .
62 gpg: error retrieving key fingerprint from card: Invalid name (?)
63 signing 0:e63c23eaa88a
64 Added signature for changeset e63c23eaa88a
65
66
67 HG: Enter commit message. Lines beginning with 'HG:' are removed.
68 HG: Leave message empty to abort commit.
69 HG: --
70 HG: user: test
71 HG: branch 'default'
72 HG: added .hgsigs
73
57 74 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now