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