gpg.py
328 lines
| 10.2 KiB
| text/x-python
|
PythonLexer
/ hgext / gpg.py
Benoit Boissinot
|
r1681 | # Copyright 2005, 2006 Benoit Boissinot <benoit.boissinot@ens-lyon.org> | ||
# | ||||
Martin Geisler
|
r8225 | # This software may be used and distributed according to the terms of the | ||
Matt Mackall
|
r10263 | # GNU General Public License version 2 or any later version. | ||
Benoit Boissinot
|
r1681 | |||
Dirkjan Ochtman
|
r8934 | '''commands to sign and verify changesets''' | ||
Dirkjan Ochtman
|
r8873 | |||
Pulkit Goyal
|
r29124 | from __future__ import absolute_import | ||
import binascii | ||||
import os | ||||
import tempfile | ||||
Yuya Nishihara
|
r29205 | |||
from mercurial.i18n import _ | ||||
Pulkit Goyal
|
r29124 | from mercurial import ( | ||
cmdutil, | ||||
error, | ||||
match, | ||||
node as hgnode, | ||||
Pulkit Goyal
|
r30925 | pycompat, | ||
Yuya Nishihara
|
r32337 | registrar, | ||
Pulkit Goyal
|
r29124 | util, | ||
) | ||||
Boris Feld
|
r36625 | from mercurial.utils import dateutil | ||
Benoit Boissinot
|
r1592 | |||
Martin Geisler
|
r14299 | cmdtable = {} | ||
Yuya Nishihara
|
r32337 | command = registrar.command(cmdtable) | ||
Augie Fackler
|
r29841 | # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for | ||
Augie Fackler
|
r25186 | # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should | ||
# be specifying the version(s) of Mercurial they are tested with, or | ||||
# leave the attribute unspecified. | ||||
Augie Fackler
|
r29841 | testedwith = 'ships-with-hg-core' | ||
Martin Geisler
|
r14299 | |||
Boris Feld
|
r34502 | configtable = {} | ||
configitem = registrar.configitem(configtable) | ||||
configitem('gpg', 'cmd', | ||||
default='gpg', | ||||
) | ||||
Boris Feld
|
r34503 | configitem('gpg', 'key', | ||
default=None, | ||||
) | ||||
Boris Feld
|
r34771 | configitem('gpg', '.*', | ||
default=None, | ||||
generic=True, | ||||
) | ||||
Boris Feld
|
r34502 | |||
Benoit Boissinot
|
r8778 | class gpg(object): | ||
Benoit Boissinot
|
r1592 | def __init__(self, path, key=None): | ||
self.path = path | ||||
self.key = (key and " --local-user \"%s\"" % key) or "" | ||||
def sign(self, data): | ||||
gpgcmd = "%s --sign --detach-sign%s" % (self.path, self.key) | ||||
return util.filter(data, gpgcmd) | ||||
def verify(self, data, sig): | ||||
""" returns of the good and bad signatures""" | ||||
Thomas Arendsen Hein
|
r2231 | sigfile = datafile = None | ||
Benoit Boissinot
|
r1592 | try: | ||
Benoit Boissinot
|
r1681 | # create temporary files | ||
Thomas Arendsen Hein
|
r2165 | fd, sigfile = tempfile.mkstemp(prefix="hg-gpg-", suffix=".sig") | ||
Pulkit Goyal
|
r30925 | fp = os.fdopen(fd, pycompat.sysstr('wb')) | ||
Benoit Boissinot
|
r1592 | fp.write(sig) | ||
fp.close() | ||||
Thomas Arendsen Hein
|
r2165 | fd, datafile = tempfile.mkstemp(prefix="hg-gpg-", suffix=".txt") | ||
Pulkit Goyal
|
r30925 | fp = os.fdopen(fd, pycompat.sysstr('wb')) | ||
Benoit Boissinot
|
r1592 | fp.write(data) | ||
fp.close() | ||||
Benoit Boissinot
|
r1681 | gpgcmd = ("%s --logger-fd 1 --status-fd 1 --verify " | ||
"\"%s\" \"%s\"" % (self.path, sigfile, datafile)) | ||||
Benoit Boissinot
|
r1592 | ret = util.filter("", gpgcmd) | ||
Thomas Arendsen Hein
|
r2231 | finally: | ||
Benoit Boissinot
|
r1592 | for f in (sigfile, datafile): | ||
try: | ||||
Matt Mackall
|
r10282 | if f: | ||
os.unlink(f) | ||||
Brodie Rao
|
r16688 | except OSError: | ||
Matt Mackall
|
r10282 | pass | ||
Benoit Boissinot
|
r1592 | keys = [] | ||
key, fingerprint = None, None | ||||
for l in ret.splitlines(): | ||||
# see DETAILS in the gnupg documentation | ||||
# filter the logger output | ||||
if not l.startswith("[GNUPG:]"): | ||||
continue | ||||
l = l[9:] | ||||
Wei, Elson
|
r19441 | if l.startswith("VALIDSIG"): | ||
Benoit Boissinot
|
r1592 | # fingerprint of the primary key | ||
fingerprint = l.split()[10] | ||||
Wei, Elson
|
r19441 | elif l.startswith("ERRSIG"): | ||
key = l.split(" ", 3)[:2] | ||||
key.append("") | ||||
fingerprint = None | ||||
Benoit Boissinot
|
r1592 | elif (l.startswith("GOODSIG") or | ||
l.startswith("EXPSIG") or | ||||
l.startswith("EXPKEYSIG") or | ||||
l.startswith("BADSIG")): | ||||
if key is not None: | ||||
keys.append(key + [fingerprint]) | ||||
key = l.split(" ", 2) | ||||
fingerprint = None | ||||
if key is not None: | ||||
keys.append(key + [fingerprint]) | ||||
Wei, Elson
|
r19442 | return keys | ||
Benoit Boissinot
|
r1592 | |||
def newgpg(ui, **opts): | ||||
Benoit Boissinot
|
r1681 | """create a new gpg instance""" | ||
Boris Feld
|
r34502 | gpgpath = ui.config("gpg", "cmd") | ||
Pulkit Goyal
|
r34979 | gpgkey = opts.get(r'key') | ||
Benoit Boissinot
|
r1592 | if not gpgkey: | ||
Boris Feld
|
r34503 | gpgkey = ui.config("gpg", "key") | ||
Benoit Boissinot
|
r1592 | return gpg(gpgpath, gpgkey) | ||
Benoit Boissinot
|
r1681 | def sigwalk(repo): | ||
""" | ||||
walk over every sigs, yields a couple | ||||
((node, version, sig), (filename, linenumber)) | ||||
""" | ||||
def parsefile(fileiter, context): | ||||
ln = 1 | ||||
for l in fileiter: | ||||
if not l: | ||||
continue | ||||
yield (l.split(" ", 2), (context, ln)) | ||||
Benoit Boissinot
|
r10394 | ln += 1 | ||
Benoit Boissinot
|
r1681 | |||
Matt Mackall
|
r8210 | # read the heads | ||
Benoit Boissinot
|
r1681 | fl = repo.file(".hgsigs") | ||
Matt Mackall
|
r8210 | for r in reversed(fl.heads()): | ||
Benoit Boissinot
|
r1681 | fn = ".hgsigs|%s" % hgnode.short(r) | ||
for item in parsefile(fl.read(r).splitlines(), fn): | ||||
yield item | ||||
try: | ||||
# read local signatures | ||||
fn = "localsigs" | ||||
Angel Ezquerra
|
r23877 | for item in parsefile(repo.vfs(fn), fn): | ||
Benoit Boissinot
|
r1681 | yield item | ||
except IOError: | ||||
pass | ||||
def getkeys(ui, repo, mygpg, sigdata, context): | ||||
"""get the keys who signed a data""" | ||||
fn, ln = context | ||||
node, version, sig = sigdata | ||||
prefix = "%s:%d" % (fn, ln) | ||||
node = hgnode.bin(node) | ||||
data = node2txt(repo, node, version) | ||||
sig = binascii.a2b_base64(sig) | ||||
Wei, Elson
|
r19442 | keys = mygpg.verify(data, sig) | ||
Benoit Boissinot
|
r1681 | |||
validkeys = [] | ||||
# warn for expired key and/or sigs | ||||
for key in keys: | ||||
Wei, Elson
|
r19444 | if key[0] == "ERRSIG": | ||
Josef 'Jeff' Sipek
|
r36051 | ui.write(_("%s Unknown key ID \"%s\"\n") % (prefix, key[1])) | ||
Wei, Elson
|
r19444 | continue | ||
Benoit Boissinot
|
r1681 | if key[0] == "BADSIG": | ||
ui.write(_("%s Bad signature from \"%s\"\n") % (prefix, key[2])) | ||||
continue | ||||
if key[0] == "EXPSIG": | ||||
ui.write(_("%s Note: Signature has expired" | ||||
" (signed by: \"%s\")\n") % (prefix, key[2])) | ||||
elif key[0] == "EXPKEYSIG": | ||||
ui.write(_("%s Note: This key has expired" | ||||
" (signed by: \"%s\")\n") % (prefix, key[2])) | ||||
validkeys.append((key[1], key[2], key[3])) | ||||
return validkeys | ||||
Martin Geisler
|
r14299 | @command("sigs", [], _('hg sigs')) | ||
Benoit Boissinot
|
r1681 | def sigs(ui, repo): | ||
"""list signed changesets""" | ||||
mygpg = newgpg(ui) | ||||
revs = {} | ||||
for data, context in sigwalk(repo): | ||||
node, version, sig = data | ||||
fn, ln = context | ||||
try: | ||||
n = repo.lookup(node) | ||||
except KeyError: | ||||
ui.warn(_("%s:%d node does not exist\n") % (fn, ln)) | ||||
continue | ||||
r = repo.changelog.rev(n) | ||||
keys = getkeys(ui, repo, mygpg, data, context) | ||||
if not keys: | ||||
continue | ||||
revs.setdefault(r, []) | ||||
revs[r].extend(keys) | ||||
Martin Geisler
|
r8303 | for rev in sorted(revs, reverse=True): | ||
Benoit Boissinot
|
r1682 | for k in revs[rev]: | ||
r = "%5d:%s" % (rev, hgnode.hex(repo.changelog.node(rev))) | ||||
Benoit Boissinot
|
r1681 | ui.write("%-30s %s\n" % (keystr(ui, k), r)) | ||
Thomas Arendsen Hein
|
r16991 | @command("sigcheck", [], _('hg sigcheck REV')) | ||
timeless
|
r27117 | def sigcheck(ui, repo, rev): | ||
Benoit Boissinot
|
r1592 | """verify all the signatures there may be for a particular revision""" | ||
mygpg = newgpg(ui) | ||||
rev = repo.lookup(rev) | ||||
hexrev = hgnode.hex(rev) | ||||
keys = [] | ||||
Benoit Boissinot
|
r1681 | for data, context in sigwalk(repo): | ||
node, version, sig = data | ||||
if node == hexrev: | ||||
k = getkeys(ui, repo, mygpg, data, context) | ||||
if k: | ||||
keys.extend(k) | ||||
Benoit Boissinot
|
r1592 | |||
if not keys: | ||||
Martin Geisler
|
r16927 | ui.write(_("no valid signature for %s\n") % hgnode.short(rev)) | ||
Benoit Boissinot
|
r1592 | return | ||
Benoit Boissinot
|
r1681 | |||
Benoit Boissinot
|
r1592 | # print summary | ||
FUJIWARA Katsunori
|
r29239 | ui.write(_("%s is signed by:\n") % hgnode.short(rev)) | ||
Benoit Boissinot
|
r1681 | for key in keys: | ||
ui.write(" %s\n" % keystr(ui, key)) | ||||
Benoit Boissinot
|
r1592 | |||
Benoit Boissinot
|
r1681 | def keystr(ui, key): | ||
"""associate a string to a key (username, comment)""" | ||||
keyid, user, fingerprint = key | ||||
Boris Feld
|
r34771 | comment = ui.config("gpg", fingerprint) | ||
Benoit Boissinot
|
r1681 | if comment: | ||
return "%s (%s)" % (user, comment) | ||||
else: | ||||
return user | ||||
Benoit Boissinot
|
r1592 | |||
Martin Geisler
|
r14299 | @command("sign", | ||
[('l', 'local', None, _('make the signature local')), | ||||
('f', 'force', None, _('sign even if the sigfile is modified')), | ||||
('', 'no-commit', None, _('do not commit the sigfile after signing')), | ||||
('k', 'key', '', | ||||
_('the key id to sign with'), _('ID')), | ||||
('m', 'message', '', | ||||
FUJIWARA Katsunori
|
r21951 | _('use text as commit message'), _('TEXT')), | ||
FUJIWARA Katsunori
|
r21711 | ('e', 'edit', False, _('invoke editor on commit messages')), | ||
Yuya Nishihara
|
r32375 | ] + cmdutil.commitopts2, | ||
Thomas Arendsen Hein
|
r16991 | _('hg sign [OPTION]... [REV]...')) | ||
Benoit Boissinot
|
r1592 | def sign(ui, repo, *revs, **opts): | ||
Thomas Arendsen Hein
|
r3916 | """add a signature for the current or given revision | ||
If no revision is given, the parent of the working directory is used, | ||||
or tip if no revision is checked out. | ||||
Thomas Arendsen Hein
|
r6163 | |||
Matt Mackall
|
r25791 | The ``gpg.cmd`` config setting can be used to specify the command | ||
to run. A default key can be specified with ``gpg.key``. | ||||
Martin Geisler
|
r11193 | See :hg:`help dates` for a list of formats valid for -d/--date. | ||
Thomas Arendsen Hein
|
r3916 | """ | ||
Bryan O'Sullivan
|
r27814 | with repo.wlock(): | ||
FUJIWARA Katsunori
|
r27196 | return _dosign(ui, repo, *revs, **opts) | ||
Thomas Arendsen Hein
|
r3916 | |||
FUJIWARA Katsunori
|
r27196 | def _dosign(ui, repo, *revs, **opts): | ||
Benoit Boissinot
|
r1592 | mygpg = newgpg(ui, **opts) | ||
Pulkit Goyal
|
r34979 | opts = pycompat.byteskwargs(opts) | ||
Benoit Boissinot
|
r1592 | sigver = "0" | ||
sigmessage = "" | ||||
Thomas Arendsen Hein
|
r6139 | |||
date = opts.get('date') | ||||
if date: | ||||
Boris Feld
|
r36625 | opts['date'] = dateutil.parsedate(date) | ||
Thomas Arendsen Hein
|
r6139 | |||
Benoit Boissinot
|
r1592 | if revs: | ||
nodes = [repo.lookup(n) for n in revs] | ||||
else: | ||||
Thomas Arendsen Hein
|
r3916 | nodes = [node for node in repo.dirstate.parents() | ||
if node != hgnode.nullid] | ||||
if len(nodes) > 1: | ||||
Pierre-Yves David
|
r26587 | raise error.Abort(_('uncommitted merge - please provide a ' | ||
Thomas Arendsen Hein
|
r3916 | 'specific revision')) | ||
if not nodes: | ||||
nodes = [repo.changelog.tip()] | ||||
Benoit Boissinot
|
r1592 | |||
for n in nodes: | ||||
hexnode = hgnode.hex(n) | ||||
Martin Geisler
|
r16927 | ui.write(_("signing %d:%s\n") % (repo.changelog.rev(n), | ||
Benoit Boissinot
|
r10510 | hgnode.short(n))) | ||
Benoit Boissinot
|
r1592 | # build data | ||
data = node2txt(repo, n, sigver) | ||||
sig = mygpg.sign(data) | ||||
if not sig: | ||||
Pierre-Yves David
|
r26587 | raise error.Abort(_("error while signing")) | ||
Benoit Boissinot
|
r1592 | sig = binascii.b2a_base64(sig) | ||
sig = sig.replace("\n", "") | ||||
sigmessage += "%s %s %s\n" % (hexnode, sigver, sig) | ||||
# write it | ||||
if opts['local']: | ||||
Angel Ezquerra
|
r23877 | repo.vfs.append("localsigs", sigmessage) | ||
Benoit Boissinot
|
r1592 | return | ||
Matt Mackall
|
r22682 | if not opts["force"]: | ||
msigs = match.exact(repo.root, '', ['.hgsigs']) | ||||
Augie Fackler
|
r25149 | if any(repo.status(match=msigs, unknown=True, ignored=True)): | ||
Pierre-Yves David
|
r26587 | raise error.Abort(_("working copy of .hgsigs is changed "), | ||
Matt Mackall
|
r22683 | hint=_("please commit .hgsigs manually")) | ||
Benoit Boissinot
|
r1592 | |||
Pierre-Yves David
|
r31414 | sigsfile = repo.wvfs(".hgsigs", "ab") | ||
Dan Villiom Podlaski Christiansen
|
r13400 | sigsfile.write(sigmessage) | ||
sigsfile.close() | ||||
Benoit Boissinot
|
r1592 | |||
Matt Mackall
|
r4906 | if '.hgsigs' not in repo.dirstate: | ||
Dirkjan Ochtman
|
r11303 | repo[None].add([".hgsigs"]) | ||
Benoit Boissinot
|
r1592 | |||
if opts["no_commit"]: | ||||
return | ||||
message = opts['message'] | ||||
if not message: | ||||
Martin Geisler
|
r9183 | # we don't translate commit messages | ||
message = "\n".join(["Added signature for changeset %s" | ||||
Benoit Boissinot
|
r5475 | % hgnode.short(n) | ||
Benoit Boissinot
|
r1592 | for n in nodes]) | ||
try: | ||||
Pulkit Goyal
|
r34979 | editor = cmdutil.getcommiteditor(editform='gpg.sign', | ||
**pycompat.strkwargs(opts)) | ||||
FUJIWARA Katsunori
|
r21711 | repo.commit(message, opts['user'], opts['date'], match=msigs, | ||
FUJIWARA Katsunori
|
r22001 | editor=editor) | ||
Gregory Szorc
|
r25660 | except ValueError as inst: | ||
Pierre-Yves David
|
r26587 | raise error.Abort(str(inst)) | ||
Benoit Boissinot
|
r1592 | |||
def node2txt(repo, node, ver): | ||||
"""map a manifest into some text""" | ||||
if ver == "0": | ||||
return "%s\n" % hgnode.hex(node) | ||||
else: | ||||
Pierre-Yves David
|
r26587 | raise error.Abort(_("unknown signature version")) | ||