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