hgext/gpg.py
changeset 1681 98eef041f9c7
parent 1676 0690d0f202e1
child 1682 ca1cda9220d5
equal deleted inserted replaced
1680:c21b54f7f7b8 1681:98eef041f9c7
     1 import os, tempfile, binascii, errno
     1 # GnuPG signing extension for Mercurial
       
     2 #
       
     3 # Copyright 2005, 2006 Benoit Boissinot <benoit.boissinot@ens-lyon.org>
       
     4 #
       
     5 # This software may be used and distributed according to the terms
       
     6 # of the GNU General Public License, incorporated herein by reference.
       
     7 
       
     8 import os, tempfile, binascii
     2 from mercurial import util
     9 from mercurial import util
     3 from mercurial import node as hgnode
    10 from mercurial import node as hgnode
       
    11 from mercurial.i18n import gettext as _
     4 
    12 
     5 class gpg:
    13 class gpg:
     6     def __init__(self, path, key=None):
    14     def __init__(self, path, key=None):
     7         self.path = path
    15         self.path = path
     8         self.key = (key and " --local-user \"%s\"" % key) or ""
    16         self.key = (key and " --local-user \"%s\"" % key) or ""
    12         return util.filter(data, gpgcmd)
    20         return util.filter(data, gpgcmd)
    13 
    21 
    14     def verify(self, data, sig):
    22     def verify(self, data, sig):
    15         """ returns of the good and bad signatures"""
    23         """ returns of the good and bad signatures"""
    16         try:
    24         try:
       
    25             # create temporary files
    17             fd, sigfile = tempfile.mkstemp(prefix="hggpgsig")
    26             fd, sigfile = tempfile.mkstemp(prefix="hggpgsig")
    18             fp = os.fdopen(fd, 'wb')
    27             fp = os.fdopen(fd, 'wb')
    19             fp.write(sig)
    28             fp.write(sig)
    20             fp.close()
    29             fp.close()
    21             fd, datafile = tempfile.mkstemp(prefix="hggpgdata")
    30             fd, datafile = tempfile.mkstemp(prefix="hggpgdata")
    22             fp = os.fdopen(fd, 'wb')
    31             fp = os.fdopen(fd, 'wb')
    23             fp.write(data)
    32             fp.write(data)
    24             fp.close()
    33             fp.close()
    25             gpgcmd = "%s --logger-fd 1 --status-fd 1 --verify \"%s\" \"%s\"" % (self.path, sigfile, datafile)
    34             gpgcmd = ("%s --logger-fd 1 --status-fd 1 --verify "
    26             #gpgcmd = "%s --status-fd 1 --verify \"%s\" \"%s\"" % (self.path, sigfile, datafile)
    35                       "\"%s\" \"%s\"" % (self.path, sigfile, datafile))
    27             ret = util.filter("", gpgcmd)
    36             ret = util.filter("", gpgcmd)
    28         except:
    37         except:
    29             for f in (sigfile, datafile):
    38             for f in (sigfile, datafile):
    30                 try:
    39                 try:
    31                     if f: os.unlink(f)
    40                     if f: os.unlink(f)
    39             # filter the logger output
    48             # filter the logger output
    40             if not l.startswith("[GNUPG:]"):
    49             if not l.startswith("[GNUPG:]"):
    41                 continue
    50                 continue
    42             l = l[9:]
    51             l = l[9:]
    43             if l.startswith("ERRSIG"):
    52             if l.startswith("ERRSIG"):
    44                 err = "error while verifying signature"
    53                 err = _("error while verifying signature")
    45                 break
    54                 break
    46             elif l.startswith("VALIDSIG"):
    55             elif l.startswith("VALIDSIG"):
    47                 # fingerprint of the primary key
    56                 # fingerprint of the primary key
    48                 fingerprint = l.split()[10]
    57                 fingerprint = l.split()[10]
    49             elif (l.startswith("GOODSIG") or
    58             elif (l.startswith("GOODSIG") or
    59         if key is not None:
    68         if key is not None:
    60             keys.append(key + [fingerprint])
    69             keys.append(key + [fingerprint])
    61         return err, keys
    70         return err, keys
    62 
    71 
    63 def newgpg(ui, **opts):
    72 def newgpg(ui, **opts):
       
    73     """create a new gpg instance"""
    64     gpgpath = ui.config("gpg", "cmd", "gpg")
    74     gpgpath = ui.config("gpg", "cmd", "gpg")
    65     gpgkey = opts.get('key')
    75     gpgkey = opts.get('key')
    66     if not gpgkey:
    76     if not gpgkey:
    67         gpgkey = ui.config("gpg", "key", None)
    77         gpgkey = ui.config("gpg", "key", None)
    68     return gpg(gpgpath, gpgkey)
    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     fl = repo.file(".hgsigs")
       
    94     h = fl.heads()
       
    95     h.reverse()
       
    96     # read the heads
       
    97     for r in h:
       
    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     nodes = list(revs)
       
   158     nodes.reverse()
       
   159     for r in nodes:
       
   160         for k in revs[r]:
       
   161             r = "%5d:%s" % (r, hgnode.hex(repo.changelog.node(r)))
       
   162             ui.write("%-30s %s\n" % (keystr(ui, k), r))
    69 
   163 
    70 def check(ui, repo, rev):
   164 def check(ui, repo, rev):
    71     """verify all the signatures there may be for a particular revision"""
   165     """verify all the signatures there may be for a particular revision"""
    72     mygpg = newgpg(ui)
   166     mygpg = newgpg(ui)
    73     rev = repo.lookup(rev)
   167     rev = repo.lookup(rev)
    74     hexrev = hgnode.hex(rev)
   168     hexrev = hgnode.hex(rev)
    75     keys = []
   169     keys = []
    76 
   170 
    77     def addsig(fn, ln, l):
   171     for data, context in sigwalk(repo):
    78         if not l: return
   172         node, version, sig = data
    79         n, v, sig = l.split(" ", 2)
   173         if node == hexrev:
    80         if n == hexrev:
   174             k = getkeys(ui, repo, mygpg, data, context)
    81             data = node2txt(repo, rev, v)
   175             if k:
    82             sig = binascii.a2b_base64(sig)
   176                 keys.extend(k)
    83             err, k = mygpg.verify(data, sig)
       
    84             if not err:
       
    85                 keys.append((k, fn, ln))
       
    86             else:
       
    87                 ui.warn("%s:%d %s\n" % (fn, ln , err))
       
    88 
       
    89     fl = repo.file(".hgsigs")
       
    90     h = fl.heads()
       
    91     h.reverse()
       
    92     # read the heads
       
    93     for r in h:
       
    94         ln = 1
       
    95         for l in fl.read(r).splitlines():
       
    96             addsig(".hgsigs|%s" % hgnode.short(r), ln, l)
       
    97             ln +=1
       
    98     try:
       
    99         # read local signatures
       
   100         ln = 1
       
   101         f = repo.opener("localsigs")
       
   102         for l in f:
       
   103             addsig("localsigs", ln, l)
       
   104             ln +=1
       
   105     except IOError:
       
   106         pass
       
   107 
   177 
   108     if not keys:
   178     if not keys:
   109         ui.write("%s not signed\n" % hgnode.short(rev))
   179         ui.write(_("No valid signature for %s\n") % hgnode.short(rev))
   110         return
   180         return
   111     valid = []
   181 
   112     # warn for expired key and/or sigs
       
   113     for k, fn, ln in keys:
       
   114         prefix = "%s:%d" % (fn, ln)
       
   115         for key in k:
       
   116             if key[0] == "BADSIG":
       
   117                 ui.write("%s Bad signature from \"%s\"\n" % (prefix, key[2]))
       
   118                 continue
       
   119             if key[0] == "EXPSIG":
       
   120                 ui.write("%s Note: Signature has expired"
       
   121                          " (signed by: \"%s\")\n" % (prefix, key[2]))
       
   122             elif key[0] == "EXPKEYSIG":
       
   123                 ui.write("%s Note: This key has expired"
       
   124                          " (signed by: \"%s\")\n" % (prefix, key[2]))
       
   125             valid.append((key[1], key[2], key[3]))
       
   126     # print summary
   182     # print summary
   127     ui.write("%s is signed by:\n" % hgnode.short(rev))
   183     ui.write("%s is signed by:\n" % hgnode.short(rev))
   128     for keyid, user, fingerprint in valid:
   184     for key in keys:
   129         role = getrole(ui, fingerprint)
   185         ui.write(" %s\n" % keystr(ui, key))
   130         ui.write("  %s (%s)\n" % (user, role))
   186 
   131 
   187 def keystr(ui, key):
   132 def getrole(ui, fingerprint):
   188     """associate a string to a key (username, comment)"""
   133     return ui.config("gpg", fingerprint, "no role defined")
   189     keyid, user, fingerprint = key
       
   190     comment = ui.config("gpg", fingerprint, None)
       
   191     if comment:
       
   192         return "%s (%s)" % (user, comment)
       
   193     else:
       
   194         return user
   134 
   195 
   135 def sign(ui, repo, *revs, **opts):
   196 def sign(ui, repo, *revs, **opts):
   136     """add a signature for the current tip or a given revision"""
   197     """add a signature for the current tip or a given revision"""
   137     mygpg = newgpg(ui, **opts)
   198     mygpg = newgpg(ui, **opts)
   138     sigver = "0"
   199     sigver = "0"
   148                                       hgnode.short(n)))
   209                                       hgnode.short(n)))
   149         # build data
   210         # build data
   150         data = node2txt(repo, n, sigver)
   211         data = node2txt(repo, n, sigver)
   151         sig = mygpg.sign(data)
   212         sig = mygpg.sign(data)
   152         if not sig:
   213         if not sig:
   153             raise util.Abort("Error while signing")
   214             raise util.Abort(_("Error while signing"))
   154         sig = binascii.b2a_base64(sig)
   215         sig = binascii.b2a_base64(sig)
   155         sig = sig.replace("\n", "")
   216         sig = sig.replace("\n", "")
   156         sigmessage += "%s %s %s\n" % (hexnode, sigver, sig)
   217         sigmessage += "%s %s %s\n" % (hexnode, sigver, sig)
   157 
   218 
   158     # write it
   219     # write it
   160         repo.opener("localsigs", "ab").write(sigmessage)
   221         repo.opener("localsigs", "ab").write(sigmessage)
   161         return
   222         return
   162 
   223 
   163     for x in repo.changes():
   224     for x in repo.changes():
   164         if ".hgsigs" in x and not opts["force"]:
   225         if ".hgsigs" in x and not opts["force"]:
   165             raise util.Abort("working copy of .hgsigs is changed "
   226             raise util.Abort(_("working copy of .hgsigs is changed "
   166                              "(please commit .hgsigs manually "
   227                                "(please commit .hgsigs manually "
   167                              "or use --force)")
   228                                "or use --force)"))
   168 
   229 
   169     repo.wfile(".hgsigs", "ab").write(sigmessage)
   230     repo.wfile(".hgsigs", "ab").write(sigmessage)
   170 
   231 
   171     if repo.dirstate.state(".hgsigs") == '?':
   232     if repo.dirstate.state(".hgsigs") == '?':
   172         repo.add([".hgsigs"])
   233         repo.add([".hgsigs"])
   174     if opts["no_commit"]:
   235     if opts["no_commit"]:
   175         return
   236         return
   176 
   237 
   177     message = opts['message']
   238     message = opts['message']
   178     if not message:
   239     if not message:
   179         message = "\n".join(["Added signature for changeset %s" % hgnode.hex(n)
   240         message = "\n".join([_("Added signature for changeset %s")
       
   241                              % hgnode.hex(n)
   180                              for n in nodes])
   242                              for n in nodes])
   181     try:
   243     try:
   182         repo.commit([".hgsigs"], message, opts['user'], opts['date'])
   244         repo.commit([".hgsigs"], message, opts['user'], opts['date'])
   183     except ValueError, inst:
   245     except ValueError, inst:
   184         raise util.Abort(str(inst))
   246         raise util.Abort(str(inst))
   186 def node2txt(repo, node, ver):
   248 def node2txt(repo, node, ver):
   187     """map a manifest into some text"""
   249     """map a manifest into some text"""
   188     if ver == "0":
   250     if ver == "0":
   189         return "%s\n" % hgnode.hex(node)
   251         return "%s\n" % hgnode.hex(node)
   190     else:
   252     else:
   191         util.Abort("unknown signature version")
   253         util.Abort(_("unknown signature version"))
   192 
   254 
   193 cmdtable = {
   255 cmdtable = {
   194     "sign":
   256     "sign":
   195         (sign,
   257         (sign,
   196          [('l', 'local', None, "make the signature local"),
   258          [('l', 'local', None, _("make the signature local")),
   197           ('f', 'force', None, "sign even if the sigfile is modified"),
   259           ('f', 'force', None, _("sign even if the sigfile is modified")),
   198           ('', 'no-commit', None, "do not commit the sigfile after signing"),
   260           ('', 'no-commit', None, _("do not commit the sigfile after signing")),
   199           ('m', 'message', "", "commit message"),
   261           ('m', 'message', "", _("commit message")),
   200           ('d', 'date', "", "date code"),
   262           ('d', 'date', "", _("date code")),
   201           ('u', 'user', "", "user"),
   263           ('u', 'user', "", _("user")),
   202           ('k', 'key', "", "the key id to sign with")],
   264           ('k', 'key', "", _("the key id to sign with"))],
   203          "hg sign [OPTION]... REVISIONS"),
   265          _("hg sign [OPTION]... REVISIONS")),
   204     "sigcheck": (check, [], 'hg sigcheck REVISION')
   266     "sigcheck": (check, [], _('hg sigcheck REVISION')),
       
   267     "sigs": (sigs, [], _('hg sigs')),
   205 }
   268 }
   206 
   269