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