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