comparison hgext/gpg.py @ 1592:347c44611348

gpg signing extension for hg the principle is almost the same as how tags work: .hgsigs stores signatures, localsigs stores local signatures the format of the signatures is: nodeid sigversion base64_detached_sig sigversion 0 signs simply the nodeid (maybe we would like to sign other things in the future). you can associate fingerprints with roles in hgrc like: [gpg] fingerprint_of_a_key_without_spaces = release fingerprint_of_a_key_without_spaces = contributor, testing the key used for signing can be specified on the command line or via hgrc (key =) thanks to Eric Hopper for testing and bugfixing
author Benoit Boissinot <benoit.boissinot@ens-lyon.org>
date Fri, 16 Dec 2005 11:12:08 -0600
parents
children ff339dd21976
comparison
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