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