PageRenderTime 47ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/hgext/gpg.py

https://bitbucket.org/mirror/mercurial/
Python | 297 lines | 288 code | 4 blank | 5 comment | 5 complexity | a59ed8d80d3c84f49cb0349208c5dcc9 MD5 | raw file
Possible License(s): GPL-2.0
  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. '''commands to sign and verify changesets'''
  6. import os, tempfile, binascii
  7. from mercurial import util, commands, match, cmdutil
  8. from mercurial import node as hgnode
  9. from mercurial.i18n import _
  10. cmdtable = {}
  11. command = cmdutil.command(cmdtable)
  12. testedwith = 'internal'
  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. def sign(self, data):
  18. gpgcmd = "%s --sign --detach-sign%s" % (self.path, self.key)
  19. return util.filter(data, gpgcmd)
  20. def verify(self, data, sig):
  21. """ returns of the good and bad signatures"""
  22. sigfile = datafile = None
  23. try:
  24. # create temporary files
  25. fd, sigfile = tempfile.mkstemp(prefix="hg-gpg-", suffix=".sig")
  26. fp = os.fdopen(fd, 'wb')
  27. fp.write(sig)
  28. fp.close()
  29. fd, datafile = tempfile.mkstemp(prefix="hg-gpg-", suffix=".txt")
  30. fp = os.fdopen(fd, 'wb')
  31. fp.write(data)
  32. fp.close()
  33. gpgcmd = ("%s --logger-fd 1 --status-fd 1 --verify "
  34. "\"%s\" \"%s\"" % (self.path, sigfile, datafile))
  35. ret = util.filter("", gpgcmd)
  36. finally:
  37. for f in (sigfile, datafile):
  38. try:
  39. if f:
  40. os.unlink(f)
  41. except OSError:
  42. pass
  43. keys = []
  44. key, fingerprint = None, None
  45. for l in ret.splitlines():
  46. # see DETAILS in the gnupg documentation
  47. # filter the logger output
  48. if not l.startswith("[GNUPG:]"):
  49. continue
  50. l = l[9:]
  51. if l.startswith("VALIDSIG"):
  52. # fingerprint of the primary key
  53. fingerprint = l.split()[10]
  54. elif l.startswith("ERRSIG"):
  55. key = l.split(" ", 3)[:2]
  56. key.append("")
  57. fingerprint = None
  58. elif (l.startswith("GOODSIG") or
  59. l.startswith("EXPSIG") or
  60. l.startswith("EXPKEYSIG") or
  61. l.startswith("BADSIG")):
  62. if key is not None:
  63. keys.append(key + [fingerprint])
  64. key = l.split(" ", 2)
  65. fingerprint = None
  66. if key is not None:
  67. keys.append(key + [fingerprint])
  68. return keys
  69. def newgpg(ui, **opts):
  70. """create a new gpg instance"""
  71. gpgpath = ui.config("gpg", "cmd", "gpg")
  72. gpgkey = opts.get('key')
  73. if not gpgkey:
  74. gpgkey = ui.config("gpg", "key", None)
  75. return gpg(gpgpath, gpgkey)
  76. def sigwalk(repo):
  77. """
  78. walk over every sigs, yields a couple
  79. ((node, version, sig), (filename, linenumber))
  80. """
  81. def parsefile(fileiter, context):
  82. ln = 1
  83. for l in fileiter:
  84. if not l:
  85. continue
  86. yield (l.split(" ", 2), (context, ln))
  87. ln += 1
  88. # read the heads
  89. fl = repo.file(".hgsigs")
  90. for r in reversed(fl.heads()):
  91. fn = ".hgsigs|%s" % hgnode.short(r)
  92. for item in parsefile(fl.read(r).splitlines(), fn):
  93. yield item
  94. try:
  95. # read local signatures
  96. fn = "localsigs"
  97. for item in parsefile(repo.opener(fn), fn):
  98. yield item
  99. except IOError:
  100. pass
  101. def getkeys(ui, repo, mygpg, sigdata, context):
  102. """get the keys who signed a data"""
  103. fn, ln = context
  104. node, version, sig = sigdata
  105. prefix = "%s:%d" % (fn, ln)
  106. node = hgnode.bin(node)
  107. data = node2txt(repo, node, version)
  108. sig = binascii.a2b_base64(sig)
  109. keys = mygpg.verify(data, sig)
  110. validkeys = []
  111. # warn for expired key and/or sigs
  112. for key in keys:
  113. if key[0] == "ERRSIG":
  114. ui.write(_("%s Unknown key ID \"%s\"\n")
  115. % (prefix, shortkey(ui, key[1][:15])))
  116. continue
  117. if key[0] == "BADSIG":
  118. ui.write(_("%s Bad signature from \"%s\"\n") % (prefix, key[2]))
  119. continue
  120. if key[0] == "EXPSIG":
  121. ui.write(_("%s Note: Signature has expired"
  122. " (signed by: \"%s\")\n") % (prefix, key[2]))
  123. elif key[0] == "EXPKEYSIG":
  124. ui.write(_("%s Note: This key has expired"
  125. " (signed by: \"%s\")\n") % (prefix, key[2]))
  126. validkeys.append((key[1], key[2], key[3]))
  127. return validkeys
  128. @command("sigs", [], _('hg sigs'))
  129. def sigs(ui, repo):
  130. """list signed changesets"""
  131. mygpg = newgpg(ui)
  132. revs = {}
  133. for data, context in sigwalk(repo):
  134. node, version, sig = data
  135. fn, ln = context
  136. try:
  137. n = repo.lookup(node)
  138. except KeyError:
  139. ui.warn(_("%s:%d node does not exist\n") % (fn, ln))
  140. continue
  141. r = repo.changelog.rev(n)
  142. keys = getkeys(ui, repo, mygpg, data, context)
  143. if not keys:
  144. continue
  145. revs.setdefault(r, [])
  146. revs[r].extend(keys)
  147. for rev in sorted(revs, reverse=True):
  148. for k in revs[rev]:
  149. r = "%5d:%s" % (rev, hgnode.hex(repo.changelog.node(rev)))
  150. ui.write("%-30s %s\n" % (keystr(ui, k), r))
  151. @command("sigcheck", [], _('hg sigcheck REV'))
  152. def check(ui, repo, rev):
  153. """verify all the signatures there may be for a particular revision"""
  154. mygpg = newgpg(ui)
  155. rev = repo.lookup(rev)
  156. hexrev = hgnode.hex(rev)
  157. keys = []
  158. for data, context in sigwalk(repo):
  159. node, version, sig = data
  160. if node == hexrev:
  161. k = getkeys(ui, repo, mygpg, data, context)
  162. if k:
  163. keys.extend(k)
  164. if not keys:
  165. ui.write(_("no valid signature for %s\n") % hgnode.short(rev))
  166. return
  167. # print summary
  168. ui.write("%s is signed by:\n" % hgnode.short(rev))
  169. for key in keys:
  170. ui.write(" %s\n" % keystr(ui, key))
  171. def keystr(ui, key):
  172. """associate a string to a key (username, comment)"""
  173. keyid, user, fingerprint = key
  174. comment = ui.config("gpg", fingerprint, None)
  175. if comment:
  176. return "%s (%s)" % (user, comment)
  177. else:
  178. return user
  179. @command("sign",
  180. [('l', 'local', None, _('make the signature local')),
  181. ('f', 'force', None, _('sign even if the sigfile is modified')),
  182. ('', 'no-commit', None, _('do not commit the sigfile after signing')),
  183. ('k', 'key', '',
  184. _('the key id to sign with'), _('ID')),
  185. ('m', 'message', '',
  186. _('commit message'), _('TEXT')),
  187. ('e', 'edit', False, _('invoke editor on commit messages')),
  188. ] + commands.commitopts2,
  189. _('hg sign [OPTION]... [REV]...'))
  190. def sign(ui, repo, *revs, **opts):
  191. """add a signature for the current or given revision
  192. If no revision is given, the parent of the working directory is used,
  193. or tip if no revision is checked out.
  194. See :hg:`help dates` for a list of formats valid for -d/--date.
  195. """
  196. mygpg = newgpg(ui, **opts)
  197. sigver = "0"
  198. sigmessage = ""
  199. date = opts.get('date')
  200. if date:
  201. opts['date'] = util.parsedate(date)
  202. if revs:
  203. nodes = [repo.lookup(n) for n in revs]
  204. else:
  205. nodes = [node for node in repo.dirstate.parents()
  206. if node != hgnode.nullid]
  207. if len(nodes) > 1:
  208. raise util.Abort(_('uncommitted merge - please provide a '
  209. 'specific revision'))
  210. if not nodes:
  211. nodes = [repo.changelog.tip()]
  212. for n in nodes:
  213. hexnode = hgnode.hex(n)
  214. ui.write(_("signing %d:%s\n") % (repo.changelog.rev(n),
  215. hgnode.short(n)))
  216. # build data
  217. data = node2txt(repo, n, sigver)
  218. sig = mygpg.sign(data)
  219. if not sig:
  220. raise util.Abort(_("error while signing"))
  221. sig = binascii.b2a_base64(sig)
  222. sig = sig.replace("\n", "")
  223. sigmessage += "%s %s %s\n" % (hexnode, sigver, sig)
  224. # write it
  225. if opts['local']:
  226. repo.opener.append("localsigs", sigmessage)
  227. return
  228. msigs = match.exact(repo.root, '', ['.hgsigs'])
  229. s = repo.status(match=msigs, unknown=True, ignored=True)[:6]
  230. if util.any(s) and not opts["force"]:
  231. raise util.Abort(_("working copy of .hgsigs is changed "
  232. "(please commit .hgsigs manually "
  233. "or use --force)"))
  234. sigsfile = repo.wfile(".hgsigs", "ab")
  235. sigsfile.write(sigmessage)
  236. sigsfile.close()
  237. if '.hgsigs' not in repo.dirstate:
  238. repo[None].add([".hgsigs"])
  239. if opts["no_commit"]:
  240. return
  241. message = opts['message']
  242. if not message:
  243. # we don't translate commit messages
  244. message = "\n".join(["Added signature for changeset %s"
  245. % hgnode.short(n)
  246. for n in nodes])
  247. try:
  248. repo.commit(message, opts['user'], opts['date'], match=msigs,
  249. editor=cmdutil.getcommiteditor(**opts))
  250. except ValueError, inst:
  251. raise util.Abort(str(inst))
  252. def shortkey(ui, key):
  253. if len(key) != 16:
  254. ui.debug("key ID \"%s\" format error\n" % key)
  255. return key
  256. return key[-8:]
  257. def node2txt(repo, node, ver):
  258. """map a manifest into some text"""
  259. if ver == "0":
  260. return "%s\n" % hgnode.hex(node)
  261. else:
  262. raise util.Abort(_("unknown signature version"))