Support adding source packages.
[0publish-gui.git] / signing.py
blobaca7d2f3df62238464b75ea7f8b522fcc3f286d3
1 from zeroinstall import SafeException
2 from zeroinstall.injector import gpg
3 import tempfile, os, base64, sys, shutil, signal
4 import subprocess
5 from rox import tasks, g
6 import gobject
8 class LineBuffer:
9 def __init__(self):
10 self.data = ''
12 def add(self, new):
13 assert new
14 self.data += new
16 def __iter__(self):
17 while '\n' in self.data:
18 command, self.data = self.data.split('\n', 1)
19 yield command
21 # (version in ROX-Lib < 2.0.4 is buggy; missing IO_HUP)
22 class InputBlocker(tasks.Blocker):
23 """Triggers when os.read(stream) would not block."""
24 _tag = None
25 _stream = None
26 def __init__(self, stream):
27 tasks.Blocker.__init__(self)
28 self._stream = stream
30 def add_task(self, task):
31 tasks.Blocker.add_task(self, task)
32 if self._tag is None:
33 self._tag = gobject.io_add_watch(self._stream, gobject.IO_IN | gobject.IO_HUP,
34 lambda src, cond: self.trigger())
36 def remove_task(self, task):
37 tasks.Blocker.remove_task(self, task)
38 if not self._rox_lib_tasks:
39 gobject.source_remove(self._tag)
40 self._tag = None
42 def get_secret_keys():
43 child = subprocess.Popen(('gpg', '--list-secret-keys', '--with-colons', '--with-fingerprint'),
44 stdout = subprocess.PIPE)
45 stdout, _ = child.communicate()
46 status = child.wait()
47 if status:
48 raise Exception("GPG failed with exit code %d" % status)
49 keys = []
50 for line in stdout.split('\n'):
51 line = line.split(':')
52 if line[0] == 'sec':
53 keys.append([None, line[9]])
54 elif line[0] == 'fpr':
55 keys[-1][0] = line[9]
56 return keys
58 def check_signature(path):
59 data = file(path).read()
60 xml_comment = data.rfind('\n<!-- Base64 Signature')
61 if xml_comment >= 0:
62 data_stream, sigs = gpg.check_stream(file(path))
63 sign_fn = sign_xml
64 data = data[:xml_comment + 1]
65 data_stream.close()
66 elif data.startswith('-----BEGIN'):
67 data_stream, sigs = gpg.check_stream(file(path))
68 sign_fn = sign_xml # Don't support saving as plain
69 data = data_stream.read()
70 else:
71 return data, sign_unsigned, None
72 for sig in sigs:
73 if isinstance(sig, gpg.ValidSig):
74 return data, sign_fn, sig.fingerprint
75 error = "ERROR: No valid signatures found!\n"
76 for sig in sigs:
77 error += "\nGot: %s" % sig
78 error += '\n\nTo edit it anyway, remove the signature using a text editor.'
79 raise Exception(error)
81 def write_tmp(path, data):
82 """Create a temporary file in the same directory as 'path' and write data to it."""
83 fd, tmp = tempfile.mkstemp(prefix = 'tmp-', dir = os.path.dirname(path))
84 stream = os.fdopen(fd, 'w')
85 stream.write(data)
86 stream.close()
87 return tmp
89 def run_gpg(default_key, *arguments):
90 arguments = list(arguments)
91 if default_key is not None:
92 arguments = ['--default-key', default_key] + arguments
93 arguments.insert(0, 'gpg')
94 if os.spawnvp(os.P_WAIT, 'gpg', arguments):
95 raise SafeException("Command '%s' failed" % arguments)
97 def sign_unsigned(path, data, key, callback):
98 os.rename(write_tmp(path, data), path)
99 if callback: callback()
101 def sign_xml(path, data, key, callback):
102 import main
103 wTree = g.glade.XML(main.gladefile, 'get_passphrase')
104 box = wTree.get_widget('get_passphrase')
105 box.set_default_response(g.RESPONSE_OK)
106 entry = wTree.get_widget('passphrase')
108 buffer = LineBuffer()
110 killed = False
111 error = False
112 tmp = None
113 r, w = os.pipe()
114 try:
115 def setup_child():
116 os.close(r)
118 tmp = write_tmp(path, data)
120 child = subprocess.Popen(('gpg', '--default-key', key,
121 '--detach-sign', '--status-fd', str(w),
122 '--command-fd', '0',
123 '-q',
124 tmp),
125 preexec_fn = setup_child,
126 stdin = subprocess.PIPE)
128 os.close(w)
129 w = None
130 while True:
131 input = InputBlocker(r)
132 yield input
133 msg = os.read(r, 100)
134 if not msg: break
135 buffer.add(msg)
136 for command in buffer:
137 if command.startswith('[GNUPG:] NEED_PASSPHRASE '):
138 entry.set_text('')
139 box.present()
140 resp = box.run()
141 box.hide()
142 if resp == g.RESPONSE_OK:
143 child.stdin.write(entry.get_text() + '\n')
144 child.stdin.flush()
145 else:
146 os.kill(child.pid, signal.SIGTERM)
147 killed = True
149 status = child.wait()
150 if status:
151 raise Exception("GPG failed with exit code %d" % status)
152 except:
153 # No generator finally blocks in Python 2.4...
154 error = True
156 if r is not None: os.close(r)
157 if w is not None: os.close(w)
158 if tmp is not None: os.unlink(tmp)
160 if killed: return
161 if error: raise
163 tmp += '.sig'
164 encoded = base64.encodestring(file(tmp).read())
165 os.unlink(tmp)
166 sig = "<!-- Base64 Signature\n" + encoded + "\n-->\n"
167 os.rename(write_tmp(path, data + sig), path)
169 if callback: callback()
171 def export_key(dir, fingerprint):
172 assert fingerprint is not None
173 # Convert fingerprint to key ID
174 stream = os.popen('gpg --with-colons --list-keys %s' % fingerprint)
175 try:
176 keyID = None
177 for line in stream:
178 parts = line.split(':')
179 if parts[0] == 'pub':
180 if keyID:
181 raise Exception('Two key IDs returned from GPG!')
182 keyID = parts[4]
183 finally:
184 stream.close()
185 key_file = os.path.join(dir, keyID + '.gpg')
186 if os.path.isfile(key_file):
187 return None
188 key_stream = file(key_file, 'w')
189 stream = os.popen("gpg -a --export '%s'" % fingerprint)
190 shutil.copyfileobj(stream, key_stream)
191 stream.close()
192 key_stream.close()
193 return key_file