##// END OF EJS Templates
Make Notary.secret_file configurable
MinRK -
Show More
@@ -1,310 +1,312 b''
1 1 """Functions for signing notebooks"""
2 2 #-----------------------------------------------------------------------------
3 3 # Copyright (C) 2014, The IPython Development Team
4 4 #
5 5 # Distributed under the terms of the BSD License. The full license is in
6 6 # the file COPYING, distributed as part of this software.
7 7 #-----------------------------------------------------------------------------
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Imports
11 11 #-----------------------------------------------------------------------------
12 12
13 13 import base64
14 14 from contextlib import contextmanager
15 15 import hashlib
16 16 from hmac import HMAC
17 17 import io
18 18 import os
19 19
20 20 from IPython.utils.py3compat import string_types, unicode_type, cast_bytes
21 21 from IPython.utils.traitlets import Instance, Bytes, Enum, Any, Unicode, Bool
22 22 from IPython.config import LoggingConfigurable, MultipleInstanceError
23 23 from IPython.core.application import BaseIPythonApplication, base_flags
24 24
25 25 from .current import read, write
26 26
27 27 #-----------------------------------------------------------------------------
28 28 # Code
29 29 #-----------------------------------------------------------------------------
30 30 try:
31 31 # Python 3
32 32 algorithms = hashlib.algorithms_guaranteed
33 33 except AttributeError:
34 34 algorithms = hashlib.algorithms
35 35
36 36 def yield_everything(obj):
37 37 """Yield every item in a container as bytes
38 38
39 39 Allows any JSONable object to be passed to an HMAC digester
40 40 without having to serialize the whole thing.
41 41 """
42 42 if isinstance(obj, dict):
43 43 for key in sorted(obj):
44 44 value = obj[key]
45 45 yield cast_bytes(key)
46 46 for b in yield_everything(value):
47 47 yield b
48 48 elif isinstance(obj, (list, tuple)):
49 49 for element in obj:
50 50 for b in yield_everything(element):
51 51 yield b
52 52 elif isinstance(obj, unicode_type):
53 53 yield obj.encode('utf8')
54 54 else:
55 55 yield unicode_type(obj).encode('utf8')
56 56
57 57
58 58 @contextmanager
59 59 def signature_removed(nb):
60 60 """Context manager for operating on a notebook with its signature removed
61 61
62 62 Used for excluding the previous signature when computing a notebook's signature.
63 63 """
64 64 save_signature = nb['metadata'].pop('signature', None)
65 65 try:
66 66 yield
67 67 finally:
68 68 if save_signature is not None:
69 69 nb['metadata']['signature'] = save_signature
70 70
71 71
72 72 class NotebookNotary(LoggingConfigurable):
73 73 """A class for computing and verifying notebook signatures."""
74 74
75 75 profile_dir = Instance("IPython.core.profiledir.ProfileDir")
76 76 def _profile_dir_default(self):
77 77 from IPython.core.application import BaseIPythonApplication
78 78 app = None
79 79 try:
80 80 if BaseIPythonApplication.initialized():
81 81 app = BaseIPythonApplication.instance()
82 82 except MultipleInstanceError:
83 83 pass
84 84 if app is None:
85 85 # create an app, without the global instance
86 86 app = BaseIPythonApplication()
87 87 app.initialize(argv=[])
88 88 return app.profile_dir
89 89
90 90 algorithm = Enum(algorithms, default_value='sha256', config=True,
91 91 help="""The hashing algorithm used to sign notebooks."""
92 92 )
93 93 def _algorithm_changed(self, name, old, new):
94 94 self.digestmod = getattr(hashlib, self.algorithm)
95 95
96 96 digestmod = Any()
97 97 def _digestmod_default(self):
98 98 return getattr(hashlib, self.algorithm)
99 99
100 secret_file = Unicode()
100 secret_file = Unicode(config=True,
101 help="""The file where the secret key is stored."""
102 )
101 103 def _secret_file_default(self):
102 104 if self.profile_dir is None:
103 105 return ''
104 106 return os.path.join(self.profile_dir.security_dir, 'notebook_secret')
105 107
106 108 secret = Bytes(config=True,
107 109 help="""The secret key with which notebooks are signed."""
108 110 )
109 111 def _secret_default(self):
110 112 # note : this assumes an Application is running
111 113 if os.path.exists(self.secret_file):
112 114 with io.open(self.secret_file, 'rb') as f:
113 115 return f.read()
114 116 else:
115 117 secret = base64.encodestring(os.urandom(1024))
116 118 self._write_secret_file(secret)
117 119 return secret
118 120
119 121 def _write_secret_file(self, secret):
120 122 """write my secret to my secret_file"""
121 123 self.log.info("Writing notebook-signing key to %s", self.secret_file)
122 124 with io.open(self.secret_file, 'wb') as f:
123 125 f.write(secret)
124 126 try:
125 127 os.chmod(self.secret_file, 0o600)
126 128 except OSError:
127 129 self.log.warn(
128 130 "Could not set permissions on %s",
129 131 self.secret_file
130 132 )
131 133 return secret
132 134
133 135 def compute_signature(self, nb):
134 136 """Compute a notebook's signature
135 137
136 138 by hashing the entire contents of the notebook via HMAC digest.
137 139 """
138 140 hmac = HMAC(self.secret, digestmod=self.digestmod)
139 141 # don't include the previous hash in the content to hash
140 142 with signature_removed(nb):
141 143 # sign the whole thing
142 144 for b in yield_everything(nb):
143 145 hmac.update(b)
144 146
145 147 return hmac.hexdigest()
146 148
147 149 def check_signature(self, nb):
148 150 """Check a notebook's stored signature
149 151
150 152 If a signature is stored in the notebook's metadata,
151 153 a new signature is computed and compared with the stored value.
152 154
153 155 Returns True if the signature is found and matches, False otherwise.
154 156
155 157 The following conditions must all be met for a notebook to be trusted:
156 158 - a signature is stored in the form 'scheme:hexdigest'
157 159 - the stored scheme matches the requested scheme
158 160 - the requested scheme is available from hashlib
159 161 - the computed hash from notebook_signature matches the stored hash
160 162 """
161 163 stored_signature = nb['metadata'].get('signature', None)
162 164 if not stored_signature \
163 165 or not isinstance(stored_signature, string_types) \
164 166 or ':' not in stored_signature:
165 167 return False
166 168 stored_algo, sig = stored_signature.split(':', 1)
167 169 if self.algorithm != stored_algo:
168 170 return False
169 171 my_signature = self.compute_signature(nb)
170 172 return my_signature == sig
171 173
172 174 def sign(self, nb):
173 175 """Sign a notebook, indicating that its output is trusted
174 176
175 177 stores 'algo:hmac-hexdigest' in notebook.metadata.signature
176 178
177 179 e.g. 'sha256:deadbeef123...'
178 180 """
179 181 signature = self.compute_signature(nb)
180 182 nb['metadata']['signature'] = "%s:%s" % (self.algorithm, signature)
181 183
182 184 def mark_cells(self, nb, trusted):
183 185 """Mark cells as trusted if the notebook's signature can be verified
184 186
185 187 Sets ``cell.trusted = True | False`` on all code cells,
186 188 depending on whether the stored signature can be verified.
187 189
188 190 This function is the inverse of check_cells
189 191 """
190 192 if not nb['worksheets']:
191 193 # nothing to mark if there are no cells
192 194 return
193 195 for cell in nb['worksheets'][0]['cells']:
194 196 if cell['cell_type'] == 'code':
195 197 cell['trusted'] = trusted
196 198
197 199 def _check_cell(self, cell):
198 200 """Do we trust an individual cell?
199 201
200 202 Return True if:
201 203
202 204 - cell is explicitly trusted
203 205 - cell has no potentially unsafe rich output
204 206
205 207 If a cell has no output, or only simple print statements,
206 208 it will always be trusted.
207 209 """
208 210 # explicitly trusted
209 211 if cell.pop("trusted", False):
210 212 return True
211 213
212 214 # explicitly safe output
213 215 safe = {
214 216 'text/plain', 'image/png', 'image/jpeg',
215 217 'text', 'png', 'jpg', # v3-style short keys
216 218 }
217 219
218 220 for output in cell['outputs']:
219 221 output_type = output['output_type']
220 222 if output_type in ('pyout', 'display_data'):
221 223 # if there are any data keys not in the safe whitelist
222 224 output_keys = set(output).difference({"output_type", "prompt_number", "metadata"})
223 225 if output_keys.difference(safe):
224 226 return False
225 227
226 228 return True
227 229
228 230 def check_cells(self, nb):
229 231 """Return whether all code cells are trusted
230 232
231 233 If there are no code cells, return True.
232 234
233 235 This function is the inverse of mark_cells.
234 236 """
235 237 if not nb['worksheets']:
236 238 return True
237 239 trusted = True
238 240 for cell in nb['worksheets'][0]['cells']:
239 241 if cell['cell_type'] != 'code':
240 242 continue
241 243 # only distrust a cell if it actually has some output to distrust
242 244 if not self._check_cell(cell):
243 245 trusted = False
244 246 return trusted
245 247
246 248
247 249 trust_flags = {
248 250 'reset' : (
249 251 {'TrustNotebookApp' : { 'reset' : True}},
250 252 """Generate a new key for notebook signature.
251 253 All previously signed notebooks will become untrusted.
252 254 """
253 255 ),
254 256 }
255 257 trust_flags.update(base_flags)
256 258 trust_flags.pop('init')
257 259
258 260
259 261 class TrustNotebookApp(BaseIPythonApplication):
260 262
261 263 description="""Sign one or more IPython notebooks with your key,
262 264 to trust their dynamic (HTML, Javascript) output.
263 265
264 266 Otherwise, you will have to re-execute the notebook to see output.
265 267 """
266 268
267 269 examples = """ipython trust mynotebook.ipynb and_this_one.ipynb"""
268 270
269 271 flags = trust_flags
270 272
271 273 reset = Bool(False, config=True,
272 274 help="""If True, generate a new key for notebook signature.
273 275 After reset, all previously signed notebooks will become untrusted.
274 276 """
275 277 )
276 278
277 279 notary = Instance(NotebookNotary)
278 280 def _notary_default(self):
279 281 return NotebookNotary(parent=self, profile_dir=self.profile_dir)
280 282
281 283 def sign_notebook(self, notebook_path):
282 284 if not os.path.exists(notebook_path):
283 285 self.log.error("Notebook missing: %s" % notebook_path)
284 286 self.exit(1)
285 287 with io.open(notebook_path, encoding='utf8') as f:
286 288 nb = read(f, 'json')
287 289 if self.notary.check_signature(nb):
288 290 print("Notebook already signed: %s" % notebook_path)
289 291 else:
290 292 print("Signing notebook: %s" % notebook_path)
291 293 self.notary.sign(nb)
292 294 with io.open(notebook_path, 'w', encoding='utf8') as f:
293 295 write(nb, f, 'json')
294 296
295 297 def generate_new_key(self):
296 298 """Generate a new notebook signature key"""
297 299 print("Generating new notebook key: %s" % self.notary.secret_file)
298 300 self.notary._write_secret_file(os.urandom(1024))
299 301
300 302 def start(self):
301 303 if self.reset:
302 304 self.generate_new_key()
303 305 return
304 306 if not self.extra_args:
305 307 self.log.critical("Specify at least one notebook to sign.")
306 308 self.exit(1)
307 309
308 310 for notebook_path in self.extra_args:
309 311 self.sign_notebook(notebook_path)
310 312
General Comments 0
You need to be logged in to leave comments. Login now