Mercurial > hg
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 |