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