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