hgext/gpg.py
changeset 1592 347c44611348
child 1618 ff339dd21976
equal deleted inserted replaced
1591:5a3229cf1492 1592:347c44611348
       
     1 import os, tempfile, binascii, errno
       
     2 from mercurial import util
       
     3 from mercurial import node as hgnode
       
     4 
       
     5 class gpg:
       
     6     def __init__(self, path, key=None):
       
     7         self.path = path
       
     8         self.key = (key and " --local-user \"%s\"" % key) or ""
       
     9 
       
    10     def sign(self, data):
       
    11         gpgcmd = "%s --sign --detach-sign%s" % (self.path, self.key)
       
    12         return util.filter(data, gpgcmd)
       
    13 
       
    14     def verify(self, data, sig):
       
    15         """ returns of the good and bad signatures"""
       
    16         try:
       
    17             fd, sigfile = tempfile.mkstemp(prefix="hggpgsig")
       
    18             fp = os.fdopen(fd, 'wb')
       
    19             fp.write(sig)
       
    20             fp.close()
       
    21             fd, datafile = tempfile.mkstemp(prefix="hggpgdata")
       
    22             fp = os.fdopen(fd, 'wb')
       
    23             fp.write(data)
       
    24             fp.close()
       
    25             gpgcmd = "%s --logger-fd 1 --status-fd 1 --verify \"%s\" \"%s\"" % (self.path, sigfile, datafile)
       
    26             #gpgcmd = "%s --status-fd 1 --verify \"%s\" \"%s\"" % (self.path, sigfile, datafile)
       
    27             ret = util.filter("", gpgcmd)
       
    28         except:
       
    29             for f in (sigfile, datafile):
       
    30                 try:
       
    31                     if f: os.unlink(f)
       
    32                 except: pass
       
    33             raise
       
    34         keys = []
       
    35         key, fingerprint = None, None
       
    36         err = ""
       
    37         for l in ret.splitlines():
       
    38             # see DETAILS in the gnupg documentation
       
    39             # filter the logger output
       
    40             if not l.startswith("[GNUPG:]"):
       
    41                 continue
       
    42             l = l[9:]
       
    43             if l.startswith("ERRSIG"):
       
    44                 err = "error while verifying signature"
       
    45                 break
       
    46             elif l.startswith("VALIDSIG"):
       
    47                 # fingerprint of the primary key
       
    48                 fingerprint = l.split()[10]
       
    49             elif (l.startswith("GOODSIG") or
       
    50                   l.startswith("EXPSIG") or
       
    51                   l.startswith("EXPKEYSIG") or
       
    52                   l.startswith("BADSIG")):
       
    53                 if key is not None:
       
    54                     keys.append(key + [fingerprint])
       
    55                 key = l.split(" ", 2)
       
    56                 fingerprint = None
       
    57         if err:
       
    58             return err, []
       
    59         if key is not None:
       
    60             keys.append(key + [fingerprint])
       
    61         return err, keys
       
    62 
       
    63 def newgpg(ui, **opts):
       
    64     gpgpath = ui.config("gpg", "cmd", "gpg")
       
    65     gpgkey = opts.get('key')
       
    66     if not gpgkey:
       
    67         gpgkey = ui.config("gpg", "key", None)
       
    68     return gpg(gpgpath, gpgkey)
       
    69 
       
    70 def check(ui, repo, rev):
       
    71     """verify all the signatures there may be for a particular revision"""
       
    72     mygpg = newgpg(ui)
       
    73     rev = repo.lookup(rev)
       
    74     hexrev = hgnode.hex(rev)
       
    75     keys = []
       
    76 
       
    77     def addsig(fn, ln, l):
       
    78         if not l: return
       
    79         n, v, sig = l.split(" ", 2)
       
    80         if n == hexrev:
       
    81             data = node2txt(repo, rev, v)
       
    82             sig = binascii.a2b_base64(sig)
       
    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 
       
   108     if not keys:
       
   109         ui.write("%s not signed\n" % hgnode.short(rev))
       
   110         return
       
   111     valid = []
       
   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
       
   127     ui.write("%s is signed by:\n" % hgnode.short(rev))
       
   128     for keyid, user, fingerprint in valid:
       
   129         role = getrole(ui, fingerprint)
       
   130         ui.write("  %s (%s)\n" % (user, role))
       
   131 
       
   132 def getrole(ui, fingerprint):
       
   133     return ui.config("gpg", fingerprint, "no role defined")
       
   134 
       
   135 def sign(ui, repo, *revs, **opts):
       
   136     """add a signature for the current tip or a given revision"""
       
   137     mygpg = newgpg(ui, **opts)
       
   138     sigver = "0"
       
   139     sigmessage = ""
       
   140     if revs:
       
   141         nodes = [repo.lookup(n) for n in revs]
       
   142     else:
       
   143         nodes = [repo.changelog.tip()]
       
   144 
       
   145     for n in nodes:
       
   146         hexnode = hgnode.hex(n)
       
   147         ui.write("Signing %d:%s\n" % (repo.changelog.rev(n),
       
   148                                       hgnode.short(n)))
       
   149         # build data
       
   150         data = node2txt(repo, n, sigver)
       
   151         sig = mygpg.sign(data)
       
   152         if not sig:
       
   153             raise util.Abort("Error while signing")
       
   154         sig = binascii.b2a_base64(sig)
       
   155         sig = sig.replace("\n", "")
       
   156         sigmessage += "%s %s %s\n" % (hexnode, sigver, sig)
       
   157 
       
   158     # write it
       
   159     if opts['local']:
       
   160         repo.opener("localsigs", "ab").write(sigmessage)
       
   161         return
       
   162 
       
   163     (c, a, d, u) = repo.changes()
       
   164     for x in (c, a, d, u):
       
   165         if ".hgsigs" in x and not opts["force"]:
       
   166             raise util.Abort("working copy of .hgsigs is changed "
       
   167                              "(please commit .hgsigs manually"
       
   168                              "or use --force)")
       
   169 
       
   170     repo.wfile(".hgsigs", "ab").write(sigmessage)
       
   171 
       
   172     if repo.dirstate.state(".hgsigs") == '?':
       
   173         repo.add([".hgsigs"])
       
   174 
       
   175     if opts["no_commit"]:
       
   176         return
       
   177 
       
   178     message = opts['message']
       
   179     if not message:
       
   180         message = "\n".join(["Added signature for changeset %s" % hgnode.hex(n)
       
   181                              for n in nodes])
       
   182     try:
       
   183         repo.commit([".hgsigs"], message, opts['user'], opts['date'])
       
   184     except ValueError, inst:
       
   185         raise util.Abort(str(inst))
       
   186 
       
   187 def node2txt(repo, node, ver):
       
   188     """map a manifest into some text"""
       
   189     if ver == "0":
       
   190         return "%s\n" % hgnode.hex(node)
       
   191     else:
       
   192         util.Abort("unknown signature version")
       
   193 
       
   194 cmdtable = {
       
   195     "sign":
       
   196         (sign,
       
   197          [('l', 'local', None, "make the signature local"),
       
   198           ('f', 'force', None, "sign even if the sigfile is modified"),
       
   199           ('', 'no-commit', None, "do not commit the sigfile after signing"),
       
   200           ('m', 'message', "", "commit message"),
       
   201           ('d', 'date', "", "date code"),
       
   202           ('u', 'user', "", "user"),
       
   203           ('k', 'key', "", "the key id to sign with")],
       
   204          "hg sign [OPTION]... REVISIONS"),
       
   205     "sigcheck": (check, [], 'hg sigcheck REVISION')
       
   206 }
       
   207