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