1 # Copyright 2005, 2006 Benoit Boissinot <benoit.boissinot@ens-lyon.org>
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.
6 '''commands to sign and verify changesets'''
8 import os, tempfile, binascii
9 from mercurial import util, commands, match
10 from mercurial import node as hgnode
11 from mercurial.i18n import _
14 def __init__(self, path, key=None):
16 self.key = (key and " --local-user \"%s\"" % key) or ""
19 gpgcmd = "%s --sign --detach-sign%s" % (self.path, self.key)
20 return util.filter(data, gpgcmd)
22 def verify(self, data, sig):
23 """ returns of the good and bad signatures"""
24 sigfile = datafile = None
26 # create temporary files
27 fd, sigfile = tempfile.mkstemp(prefix="hg-gpg-", suffix=".sig")
28 fp = os.fdopen(fd, 'wb')
31 fd, datafile = tempfile.mkstemp(prefix="hg-gpg-", suffix=".txt")
32 fp = os.fdopen(fd, 'wb')
35 gpgcmd = ("%s --logger-fd 1 --status-fd 1 --verify "
36 "\"%s\" \"%s\"" % (self.path, sigfile, datafile))
37 ret = util.filter("", gpgcmd)
39 for f in (sigfile, datafile):
44 key, fingerprint = None, None
46 for l in ret.splitlines():
47 # see DETAILS in the gnupg documentation
48 # filter the logger output
49 if not l.startswith("[GNUPG:]"):
52 if l.startswith("ERRSIG"):
53 err = _("error while verifying signature")
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")):
63 keys.append(key + [fingerprint])
69 keys.append(key + [fingerprint])
72 def newgpg(ui, **opts):
73 """create a new gpg instance"""
74 gpgpath = ui.config("gpg", "cmd", "gpg")
75 gpgkey = opts.get('key')
77 gpgkey = ui.config("gpg", "key", None)
78 return gpg(gpgpath, gpgkey)
82 walk over every sigs, yields a couple
83 ((node, version, sig), (filename, linenumber))
85 def parsefile(fileiter, context):
90 yield (l.split(" ", 2), (context, ln))
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):
100 # read local signatures
102 for item in parsefile(repo.opener(fn), fn):
107 def getkeys(ui, repo, mygpg, sigdata, context):
108 """get the keys who signed a data"""
110 node, version, sig = sigdata
111 prefix = "%s:%d" % (fn, ln)
112 node = hgnode.bin(node)
114 data = node2txt(repo, node, version)
115 sig = binascii.a2b_base64(sig)
116 err, keys = mygpg.verify(data, sig)
118 ui.warn("%s:%d %s\n" % (fn, ln , err))
122 # warn for expired key and/or sigs
124 if key[0] == "BADSIG":
125 ui.write(_("%s Bad signature from \"%s\"\n") % (prefix, key[2]))
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]))
137 """list signed changesets"""
141 for data, context in sigwalk(repo):
142 node, version, sig = data
145 n = repo.lookup(node)
147 ui.warn(_("%s:%d node does not exist\n") % (fn, ln))
149 r = repo.changelog.rev(n)
150 keys = getkeys(ui, repo, mygpg, data, context)
153 revs.setdefault(r, [])
155 for rev in sorted(revs, reverse=True):
157 r = "%5d:%s" % (rev, hgnode.hex(repo.changelog.node(rev)))
158 ui.write("%-30s %s\n" % (keystr(ui, k), r))
160 def check(ui, repo, rev):
161 """verify all the signatures there may be for a particular revision"""
163 rev = repo.lookup(rev)
164 hexrev = hgnode.hex(rev)
167 for data, context in sigwalk(repo):
168 node, version, sig = data
170 k = getkeys(ui, repo, mygpg, data, context)
175 ui.write(_("No valid signature for %s\n") % hgnode.short(rev))
179 ui.write("%s is signed by:\n" % hgnode.short(rev))
181 ui.write(" %s\n" % keystr(ui, key))
184 """associate a string to a key (username, comment)"""
185 keyid, user, fingerprint = key
186 comment = ui.config("gpg", fingerprint, None)
188 return "%s (%s)" % (user, comment)
192 def sign(ui, repo, *revs, **opts):
193 """add a signature for the current or given revision
195 If no revision is given, the parent of the working directory is used,
196 or tip if no revision is checked out.
198 See 'hg help dates' for a list of formats valid for -d/--date.
201 mygpg = newgpg(ui, **opts)
205 date = opts.get('date')
207 opts['date'] = util.parsedate(date)
210 nodes = [repo.lookup(n) for n in revs]
212 nodes = [node for node in repo.dirstate.parents()
213 if node != hgnode.nullid]
215 raise util.Abort(_('uncommitted merge - please provide a '
216 'specific revision'))
218 nodes = [repo.changelog.tip()]
221 hexnode = hgnode.hex(n)
222 ui.write("Signing %d:%s\n" % (repo.changelog.rev(n),
225 data = node2txt(repo, n, sigver)
226 sig = mygpg.sign(data)
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)
235 repo.opener("localsigs", "ab").write(sigmessage)
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 "
244 repo.wfile(".hgsigs", "ab").write(sigmessage)
246 if '.hgsigs' not in repo.dirstate:
247 repo.add([".hgsigs"])
249 if opts["no_commit"]:
252 message = opts['message']
254 # we don't translate commit messages
255 message = "\n".join(["Added signature for changeset %s"
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))
264 def node2txt(repo, node, ver):
265 """map a manifest into some text"""
267 return "%s\n" % hgnode.hex(node)
269 raise util.Abort(_("unknown signature version"))
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')),