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