##// END OF EJS Templates
Merge pull request #5087 from minrk/master...
Paul Ivanov -
r15312:f7d6d5f4 merge
parent child Browse files
Show More
@@ -1,278 +1,310 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()
101 def _secret_file_default(self):
101 def _secret_file_default(self):
102 if self.profile_dir is None:
102 if self.profile_dir is None:
103 return ''
103 return ''
104 return os.path.join(self.profile_dir.security_dir, 'notebook_secret')
104 return os.path.join(self.profile_dir.security_dir, 'notebook_secret')
105
105
106 secret = Bytes(config=True,
106 secret = Bytes(config=True,
107 help="""The secret key with which notebooks are signed."""
107 help="""The secret key with which notebooks are signed."""
108 )
108 )
109 def _secret_default(self):
109 def _secret_default(self):
110 # note : this assumes an Application is running
110 # note : this assumes an Application is running
111 if os.path.exists(self.secret_file):
111 if os.path.exists(self.secret_file):
112 with io.open(self.secret_file, 'rb') as f:
112 with io.open(self.secret_file, 'rb') as f:
113 return f.read()
113 return f.read()
114 else:
114 else:
115 secret = base64.encodestring(os.urandom(1024))
115 secret = base64.encodestring(os.urandom(1024))
116 self._write_secret_file(secret)
116 self._write_secret_file(secret)
117 return secret
117 return secret
118
118
119 def _write_secret_file(self, secret):
119 def _write_secret_file(self, secret):
120 """write my secret to my secret_file"""
120 """write my secret to my secret_file"""
121 self.log.info("Writing notebook-signing key to %s", self.secret_file)
121 self.log.info("Writing notebook-signing key to %s", self.secret_file)
122 with io.open(self.secret_file, 'wb') as f:
122 with io.open(self.secret_file, 'wb') as f:
123 f.write(secret)
123 f.write(secret)
124 try:
124 try:
125 os.chmod(self.secret_file, 0o600)
125 os.chmod(self.secret_file, 0o600)
126 except OSError:
126 except OSError:
127 self.log.warn(
127 self.log.warn(
128 "Could not set permissions on %s",
128 "Could not set permissions on %s",
129 self.secret_file
129 self.secret_file
130 )
130 )
131 return secret
131 return secret
132
132
133 def compute_signature(self, nb):
133 def compute_signature(self, nb):
134 """Compute a notebook's signature
134 """Compute a notebook's signature
135
135
136 by hashing the entire contents of the notebook via HMAC digest.
136 by hashing the entire contents of the notebook via HMAC digest.
137 """
137 """
138 hmac = HMAC(self.secret, digestmod=self.digestmod)
138 hmac = HMAC(self.secret, digestmod=self.digestmod)
139 # don't include the previous hash in the content to hash
139 # don't include the previous hash in the content to hash
140 with signature_removed(nb):
140 with signature_removed(nb):
141 # sign the whole thing
141 # sign the whole thing
142 for b in yield_everything(nb):
142 for b in yield_everything(nb):
143 hmac.update(b)
143 hmac.update(b)
144
144
145 return hmac.hexdigest()
145 return hmac.hexdigest()
146
146
147 def check_signature(self, nb):
147 def check_signature(self, nb):
148 """Check a notebook's stored signature
148 """Check a notebook's stored signature
149
149
150 If a signature is stored in the notebook's metadata,
150 If a signature is stored in the notebook's metadata,
151 a new signature is computed and compared with the stored value.
151 a new signature is computed and compared with the stored value.
152
152
153 Returns True if the signature is found and matches, False otherwise.
153 Returns True if the signature is found and matches, False otherwise.
154
154
155 The following conditions must all be met for a notebook to be trusted:
155 The following conditions must all be met for a notebook to be trusted:
156 - a signature is stored in the form 'scheme:hexdigest'
156 - a signature is stored in the form 'scheme:hexdigest'
157 - the stored scheme matches the requested scheme
157 - the stored scheme matches the requested scheme
158 - the requested scheme is available from hashlib
158 - the requested scheme is available from hashlib
159 - the computed hash from notebook_signature matches the stored hash
159 - the computed hash from notebook_signature matches the stored hash
160 """
160 """
161 stored_signature = nb['metadata'].get('signature', None)
161 stored_signature = nb['metadata'].get('signature', None)
162 if not stored_signature \
162 if not stored_signature \
163 or not isinstance(stored_signature, string_types) \
163 or not isinstance(stored_signature, string_types) \
164 or ':' not in stored_signature:
164 or ':' not in stored_signature:
165 return False
165 return False
166 stored_algo, sig = stored_signature.split(':', 1)
166 stored_algo, sig = stored_signature.split(':', 1)
167 if self.algorithm != stored_algo:
167 if self.algorithm != stored_algo:
168 return False
168 return False
169 my_signature = self.compute_signature(nb)
169 my_signature = self.compute_signature(nb)
170 return my_signature == sig
170 return my_signature == sig
171
171
172 def sign(self, nb):
172 def sign(self, nb):
173 """Sign a notebook, indicating that its output is trusted
173 """Sign a notebook, indicating that its output is trusted
174
174
175 stores 'algo:hmac-hexdigest' in notebook.metadata.signature
175 stores 'algo:hmac-hexdigest' in notebook.metadata.signature
176
176
177 e.g. 'sha256:deadbeef123...'
177 e.g. 'sha256:deadbeef123...'
178 """
178 """
179 signature = self.compute_signature(nb)
179 signature = self.compute_signature(nb)
180 nb['metadata']['signature'] = "%s:%s" % (self.algorithm, signature)
180 nb['metadata']['signature'] = "%s:%s" % (self.algorithm, signature)
181
181
182 def mark_cells(self, nb, trusted):
182 def mark_cells(self, nb, trusted):
183 """Mark cells as trusted if the notebook's signature can be verified
183 """Mark cells as trusted if the notebook's signature can be verified
184
184
185 Sets ``cell.trusted = True | False`` on all code cells,
185 Sets ``cell.trusted = True | False`` on all code cells,
186 depending on whether the stored signature can be verified.
186 depending on whether the stored signature can be verified.
187
187
188 This function is the inverse of check_cells
188 This function is the inverse of check_cells
189 """
189 """
190 if not nb['worksheets']:
190 if not nb['worksheets']:
191 # nothing to mark if there are no cells
191 # nothing to mark if there are no cells
192 return
192 return
193 for cell in nb['worksheets'][0]['cells']:
193 for cell in nb['worksheets'][0]['cells']:
194 if cell['cell_type'] == 'code':
194 if cell['cell_type'] == 'code':
195 cell['trusted'] = trusted
195 cell['trusted'] = trusted
196
196
197 def _check_cell(self, cell):
198 """Do we trust an individual cell?
199
200 Return True if:
201
202 - cell is explicitly trusted
203 - cell has no potentially unsafe rich output
204
205 If a cell has no output, or only simple print statements,
206 it will always be trusted.
207 """
208 # explicitly trusted
209 if cell.pop("trusted", False):
210 return True
211
212 # explicitly safe output
213 safe = {
214 'text/plain', 'image/png', 'image/jpeg',
215 'text', 'png', 'jpg', # v3-style short keys
216 }
217
218 for output in cell['outputs']:
219 output_type = output['output_type']
220 if output_type in ('pyout', 'display_data'):
221 # if there are any data keys not in the safe whitelist
222 output_keys = set(output).difference({"output_type", "prompt_number", "metadata"})
223 if output_keys.difference(safe):
224 return False
225
226 return True
227
197 def check_cells(self, nb):
228 def check_cells(self, nb):
198 """Return whether all code cells are trusted
229 """Return whether all code cells are trusted
199
230
200 If there are no code cells, return True.
231 If there are no code cells, return True.
201
232
202 This function is the inverse of mark_cells.
233 This function is the inverse of mark_cells.
203 """
234 """
204 if not nb['worksheets']:
235 if not nb['worksheets']:
205 return True
236 return True
206 trusted = True
237 trusted = True
207 for cell in nb['worksheets'][0]['cells']:
238 for cell in nb['worksheets'][0]['cells']:
208 if cell['cell_type'] != 'code':
239 if cell['cell_type'] != 'code':
209 continue
240 continue
210 if not cell.pop('trusted', False):
241 # only distrust a cell if it actually has some output to distrust
242 if not self._check_cell(cell):
211 trusted = False
243 trusted = False
212 return trusted
244 return trusted
213
245
214
246
215 trust_flags = {
247 trust_flags = {
216 'reset' : (
248 'reset' : (
217 {'TrustNotebookApp' : { 'reset' : True}},
249 {'TrustNotebookApp' : { 'reset' : True}},
218 """Generate a new key for notebook signature.
250 """Generate a new key for notebook signature.
219 All previously signed notebooks will become untrusted.
251 All previously signed notebooks will become untrusted.
220 """
252 """
221 ),
253 ),
222 }
254 }
223 trust_flags.update(base_flags)
255 trust_flags.update(base_flags)
224 trust_flags.pop('init')
256 trust_flags.pop('init')
225
257
226
258
227 class TrustNotebookApp(BaseIPythonApplication):
259 class TrustNotebookApp(BaseIPythonApplication):
228
260
229 description="""Sign one or more IPython notebooks with your key,
261 description="""Sign one or more IPython notebooks with your key,
230 to trust their dynamic (HTML, Javascript) output.
262 to trust their dynamic (HTML, Javascript) output.
231
263
232 Otherwise, you will have to re-execute the notebook to see output.
264 Otherwise, you will have to re-execute the notebook to see output.
233 """
265 """
234
266
235 examples = """ipython trust mynotebook.ipynb and_this_one.ipynb"""
267 examples = """ipython trust mynotebook.ipynb and_this_one.ipynb"""
236
268
237 flags = trust_flags
269 flags = trust_flags
238
270
239 reset = Bool(False, config=True,
271 reset = Bool(False, config=True,
240 help="""If True, generate a new key for notebook signature.
272 help="""If True, generate a new key for notebook signature.
241 After reset, all previously signed notebooks will become untrusted.
273 After reset, all previously signed notebooks will become untrusted.
242 """
274 """
243 )
275 )
244
276
245 notary = Instance(NotebookNotary)
277 notary = Instance(NotebookNotary)
246 def _notary_default(self):
278 def _notary_default(self):
247 return NotebookNotary(parent=self, profile_dir=self.profile_dir)
279 return NotebookNotary(parent=self, profile_dir=self.profile_dir)
248
280
249 def sign_notebook(self, notebook_path):
281 def sign_notebook(self, notebook_path):
250 if not os.path.exists(notebook_path):
282 if not os.path.exists(notebook_path):
251 self.log.error("Notebook missing: %s" % notebook_path)
283 self.log.error("Notebook missing: %s" % notebook_path)
252 self.exit(1)
284 self.exit(1)
253 with io.open(notebook_path, encoding='utf8') as f:
285 with io.open(notebook_path, encoding='utf8') as f:
254 nb = read(f, 'json')
286 nb = read(f, 'json')
255 if self.notary.check_signature(nb):
287 if self.notary.check_signature(nb):
256 print("Notebook already signed: %s" % notebook_path)
288 print("Notebook already signed: %s" % notebook_path)
257 else:
289 else:
258 print("Signing notebook: %s" % notebook_path)
290 print("Signing notebook: %s" % notebook_path)
259 self.notary.sign(nb)
291 self.notary.sign(nb)
260 with io.open(notebook_path, 'w', encoding='utf8') as f:
292 with io.open(notebook_path, 'w', encoding='utf8') as f:
261 write(nb, f, 'json')
293 write(nb, f, 'json')
262
294
263 def generate_new_key(self):
295 def generate_new_key(self):
264 """Generate a new notebook signature key"""
296 """Generate a new notebook signature key"""
265 print("Generating new notebook key: %s" % self.notary.secret_file)
297 print("Generating new notebook key: %s" % self.notary.secret_file)
266 self.notary._write_secret_file(os.urandom(1024))
298 self.notary._write_secret_file(os.urandom(1024))
267
299
268 def start(self):
300 def start(self):
269 if self.reset:
301 if self.reset:
270 self.generate_new_key()
302 self.generate_new_key()
271 return
303 return
272 if not self.extra_args:
304 if not self.extra_args:
273 self.log.critical("Specify at least one notebook to sign.")
305 self.log.critical("Specify at least one notebook to sign.")
274 self.exit(1)
306 self.exit(1)
275
307
276 for notebook_path in self.extra_args:
308 for notebook_path in self.extra_args:
277 self.sign_notebook(notebook_path)
309 self.sign_notebook(notebook_path)
278
310
@@ -1,143 +1,156 b''
1 {
1 {
2 "metadata": {
2 "metadata": {
3 "cell_tags": [
3 "cell_tags": [
4 [
4 [
5 "<None>",
5 "<None>",
6 null
6 null
7 ]
7 ]
8 ],
8 ],
9 "name": ""
9 "name": ""
10 },
10 },
11 "nbformat": 3,
11 "nbformat": 3,
12 "nbformat_minor": 0,
12 "nbformat_minor": 0,
13 "worksheets": [
13 "worksheets": [
14 {
14 {
15 "cells": [
15 "cells": [
16 {
16 {
17 "cell_type": "heading",
17 "cell_type": "heading",
18 "level": 1,
18 "level": 1,
19 "metadata": {},
19 "metadata": {},
20 "source": [
20 "source": [
21 "nbconvert latex test"
21 "nbconvert latex test"
22 ]
22 ]
23 },
23 },
24 {
24 {
25 "cell_type": "markdown",
25 "cell_type": "markdown",
26 "metadata": {},
26 "metadata": {},
27 "source": [
27 "source": [
28 "**Lorem ipsum** dolor sit amet, consectetur adipiscing elit. Nunc luctus bibendum felis dictum sodales. Ut suscipit, orci ut interdum imperdiet, purus ligula mollis *justo*, non malesuada nisl augue eget lorem. Donec bibendum, erat sit amet porttitor aliquam, urna lorem ornare libero, in vehicula diam diam ut ante. Nam non urna rhoncus, accumsan elit sit amet, mollis tellus. Vestibulum nec tellus metus. Vestibulum tempor, ligula et vehicula rhoncus, sapien turpis faucibus lorem, id dapibus turpis mauris ac orci. Sed volutpat vestibulum venenatis."
28 "**Lorem ipsum** dolor sit amet, consectetur adipiscing elit. Nunc luctus bibendum felis dictum sodales. Ut suscipit, orci ut interdum imperdiet, purus ligula mollis *justo*, non malesuada nisl augue eget lorem. Donec bibendum, erat sit amet porttitor aliquam, urna lorem ornare libero, in vehicula diam diam ut ante. Nam non urna rhoncus, accumsan elit sit amet, mollis tellus. Vestibulum nec tellus metus. Vestibulum tempor, ligula et vehicula rhoncus, sapien turpis faucibus lorem, id dapibus turpis mauris ac orci. Sed volutpat vestibulum venenatis."
29 ]
29 ]
30 },
30 },
31 {
31 {
32 "cell_type": "heading",
32 "cell_type": "heading",
33 "level": 2,
33 "level": 2,
34 "metadata": {},
34 "metadata": {},
35 "source": [
35 "source": [
36 "Printed Using Python"
36 "Printed Using Python"
37 ]
37 ]
38 },
38 },
39 {
39 {
40 "cell_type": "code",
40 "cell_type": "code",
41 "collapsed": false,
41 "collapsed": false,
42 "input": [
42 "input": [
43 "next_paragraph = \"\"\"\n",
43 "print(\"hello\")"
44 "Aenean vitae diam consectetur, tempus arcu quis, ultricies urna. Vivamus venenatis sem \n",
45 "quis orci condimentum, sed feugiat dui porta.\n",
46 "\"\"\"\n",
47 "\n",
48 "def nifty_print(text):\n",
49 " \"\"\"Used to test syntax highlighting\"\"\"\n",
50 " \n",
51 " print(text * 2)\n",
52 "\n",
53 "nifty_print(next_paragraph)"
54 ],
44 ],
55 "language": "python",
45 "language": "python",
56 "metadata": {},
46 "metadata": {},
57 "outputs": [
47 "outputs": [
58 {
48 {
59 "output_type": "stream",
49 "output_type": "stream",
60 "stream": "stdout",
50 "stream": "stdout",
61 "text": [
51 "text": [
62 "\n",
52 "hello\n"
63 "Aenean vitae diam consectetur, tempus arcu quis, ultricies urna. Vivamus venenatis sem \n",
64 "quis orci condimentum, sed feugiat dui porta.\n",
65 "\n",
66 "Aenean vitae diam consectetur, tempus arcu quis, ultricies urna. Vivamus venenatis sem \n",
67 "quis orci condimentum, sed feugiat dui porta.\n",
68 "\n"
69 ]
53 ]
70 }
54 }
71 ],
55 ],
72 "prompt_number": 3
56 "prompt_number": 1
73 },
57 },
74 {
58 {
75 "cell_type": "heading",
59 "cell_type": "heading",
76 "level": 2,
60 "level": 2,
77 "metadata": {},
61 "metadata": {},
78 "source": [
62 "source": [
79 "Pyout"
63 "Pyout"
80 ]
64 ]
81 },
65 },
82 {
66 {
83 "cell_type": "code",
67 "cell_type": "code",
84 "collapsed": false,
68 "collapsed": false,
85 "input": [
69 "input": [
86 "Text = \"\"\"\n",
70 "from IPython.display import HTML\n",
87 "Aliquam blandit aliquet enim, eget scelerisque eros adipiscing quis. Nunc sed metus \n",
71 "HTML(\"\"\"\n",
88 "ut lorem condimentum condimentum nec id enim. Sed malesuada cursus hendrerit. Praesent \n",
72 "<script>\n",
89 "et commodo justo. Interdum et malesuada fames ac ante ipsum primis in faucibus. \n",
73 "console.log(\"hello\");\n",
90 "Curabitur et magna ante. Proin luctus tellus sit amet egestas laoreet. Sed dapibus \n",
74 "</script>\n",
91 "neque ac nulla mollis cursus. Fusce mollis egestas libero mattis facilisis.\n",
75 "<b>HTML</b>\n",
92 "\"\"\"\n",
76 "\"\"\")"
93 "Text"
94 ],
77 ],
95 "language": "python",
78 "language": "python",
96 "metadata": {},
79 "metadata": {},
97 "outputs": [
80 "outputs": [
98 {
81 {
82 "html": [
83 "\n",
84 "<script>\n",
85 "console.log(\"hello\");\n",
86 "</script>\n",
87 "<b>HTML</b>\n"
88 ],
99 "metadata": {},
89 "metadata": {},
100 "output_type": "pyout",
90 "output_type": "pyout",
101 "prompt_number": 2,
91 "prompt_number": 3,
92 "text": [
93 "<IPython.core.display.HTML at 0x1112757d0>"
94 ]
95 }
96 ],
97 "prompt_number": 3
98 },
99 {
100 "cell_type": "code",
101 "collapsed": false,
102 "input": [
103 "%%javascript\n",
104 "console.log(\"hi\");"
105 ],
106 "language": "python",
107 "metadata": {},
108 "outputs": [
109 {
110 "javascript": [
111 "console.log(\"hi\");"
112 ],
113 "metadata": {},
114 "output_type": "display_data",
102 "text": [
115 "text": [
103 "'\\nAliquam blandit aliquet enim, eget scelerisque eros adipiscing quis. Nunc sed metus \\nut lorem condimentum condimentum nec id enim. Sed malesuada cursus hendrerit. Praesent \\net commodo justo. Interdum et malesuada fames ac ante ipsum primis in faucibus. \\nCurabitur et magna ante. Proin luctus tellus sit amet egestas laoreet. Sed dapibus \\nneque ac nulla mollis cursus. Fusce mollis egestas libero mattis facilisis.\\n'"
116 "<IPython.core.display.Javascript at 0x1112b4b50>"
104 ]
117 ]
105 }
118 }
106 ],
119 ],
107 "prompt_number": 2
120 "prompt_number": 7
108 },
121 },
109 {
122 {
110 "cell_type": "heading",
123 "cell_type": "heading",
111 "level": 3,
124 "level": 3,
112 "metadata": {},
125 "metadata": {},
113 "source": [
126 "source": [
114 "Image"
127 "Image"
115 ]
128 ]
116 },
129 },
117 {
130 {
118 "cell_type": "code",
131 "cell_type": "code",
119 "collapsed": false,
132 "collapsed": false,
120 "input": [
133 "input": [
121 "from IPython.core.display import Image\n",
134 "from IPython.display import Image\n",
122 "Image(data=\"http://ipython.org/_static/IPy_header.png\")"
135 "Image(\"http://ipython.org/_static/IPy_header.png\")"
123 ],
136 ],
124 "language": "python",
137 "language": "python",
125 "metadata": {},
138 "metadata": {},
126 "outputs": [
139 "outputs": [
127 {
140 {
128 "metadata": {},
141 "metadata": {},
129 "output_type": "pyout",
142 "output_type": "pyout",
130 "png": "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\n",
143 "png": "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\n",
131 "prompt_number": 6,
144 "prompt_number": 6,
132 "text": [
145 "text": [
133 "<IPython.core.display.Image at 0x2100f90>"
146 "<IPython.core.display.Image at 0x111275490>"
134 ]
147 ]
135 }
148 }
136 ],
149 ],
137 "prompt_number": 6
150 "prompt_number": 6
138 }
151 }
139 ],
152 ],
140 "metadata": {}
153 "metadata": {}
141 }
154 }
142 ]
155 ]
143 } No newline at end of file
156 }
@@ -1,112 +1,120 b''
1 """Test Notebook signing"""
1 """Test Notebook signing"""
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 from .. import sign
13 from .. import sign
14 from .base import TestsBase
14 from .base import TestsBase
15
15
16 from ..current import read
16 from ..current import read
17 from IPython.core.getipython import get_ipython
17 from IPython.core.getipython import get_ipython
18
18
19 #-----------------------------------------------------------------------------
19 #-----------------------------------------------------------------------------
20 # Classes and functions
20 # Classes and functions
21 #-----------------------------------------------------------------------------
21 #-----------------------------------------------------------------------------
22
22
23 class TestNotary(TestsBase):
23 class TestNotary(TestsBase):
24
24
25 def setUp(self):
25 def setUp(self):
26 self.notary = sign.NotebookNotary(
26 self.notary = sign.NotebookNotary(
27 secret=b'secret',
27 secret=b'secret',
28 profile_dir=get_ipython().profile_dir
28 profile_dir=get_ipython().profile_dir
29 )
29 )
30 with self.fopen(u'test3.ipynb', u'r') as f:
30 with self.fopen(u'test3.ipynb', u'r') as f:
31 self.nb = read(f, u'json')
31 self.nb = read(f, u'json')
32
32
33 def test_algorithms(self):
33 def test_algorithms(self):
34 last_sig = ''
34 last_sig = ''
35 for algo in sign.algorithms:
35 for algo in sign.algorithms:
36 self.notary.algorithm = algo
36 self.notary.algorithm = algo
37 self.notary.sign(self.nb)
37 self.notary.sign(self.nb)
38 sig = self.nb.metadata.signature
38 sig = self.nb.metadata.signature
39 print(sig)
39 print(sig)
40 self.assertEqual(sig[:len(self.notary.algorithm)+1], '%s:' % self.notary.algorithm)
40 self.assertEqual(sig[:len(self.notary.algorithm)+1], '%s:' % self.notary.algorithm)
41 self.assertNotEqual(last_sig, sig)
41 self.assertNotEqual(last_sig, sig)
42 last_sig = sig
42 last_sig = sig
43
43
44 def test_sign_same(self):
44 def test_sign_same(self):
45 """Multiple signatures of the same notebook are the same"""
45 """Multiple signatures of the same notebook are the same"""
46 sig1 = self.notary.compute_signature(self.nb)
46 sig1 = self.notary.compute_signature(self.nb)
47 sig2 = self.notary.compute_signature(self.nb)
47 sig2 = self.notary.compute_signature(self.nb)
48 self.assertEqual(sig1, sig2)
48 self.assertEqual(sig1, sig2)
49
49
50 def test_change_secret(self):
50 def test_change_secret(self):
51 """Changing the secret changes the signature"""
51 """Changing the secret changes the signature"""
52 sig1 = self.notary.compute_signature(self.nb)
52 sig1 = self.notary.compute_signature(self.nb)
53 self.notary.secret = b'different'
53 self.notary.secret = b'different'
54 sig2 = self.notary.compute_signature(self.nb)
54 sig2 = self.notary.compute_signature(self.nb)
55 self.assertNotEqual(sig1, sig2)
55 self.assertNotEqual(sig1, sig2)
56
56
57 def test_sign(self):
57 def test_sign(self):
58 self.notary.sign(self.nb)
58 self.notary.sign(self.nb)
59 sig = self.nb.metadata.signature
59 sig = self.nb.metadata.signature
60 self.assertEqual(sig[:len(self.notary.algorithm)+1], '%s:' % self.notary.algorithm)
60 self.assertEqual(sig[:len(self.notary.algorithm)+1], '%s:' % self.notary.algorithm)
61
61
62 def test_check_signature(self):
62 def test_check_signature(self):
63 nb = self.nb
63 nb = self.nb
64 md = nb.metadata
64 md = nb.metadata
65 notary = self.notary
65 notary = self.notary
66 check_signature = notary.check_signature
66 check_signature = notary.check_signature
67 # no signature:
67 # no signature:
68 md.pop('signature', None)
68 md.pop('signature', None)
69 self.assertFalse(check_signature(nb))
69 self.assertFalse(check_signature(nb))
70 # hash only, no algo
70 # hash only, no algo
71 md.signature = notary.compute_signature(nb)
71 md.signature = notary.compute_signature(nb)
72 self.assertFalse(check_signature(nb))
72 self.assertFalse(check_signature(nb))
73 # proper signature, algo mismatch
73 # proper signature, algo mismatch
74 notary.algorithm = 'sha224'
74 notary.algorithm = 'sha224'
75 notary.sign(nb)
75 notary.sign(nb)
76 notary.algorithm = 'sha256'
76 notary.algorithm = 'sha256'
77 self.assertFalse(check_signature(nb))
77 self.assertFalse(check_signature(nb))
78 # check correctly signed notebook
78 # check correctly signed notebook
79 notary.sign(nb)
79 notary.sign(nb)
80 self.assertTrue(check_signature(nb))
80 self.assertTrue(check_signature(nb))
81
81
82 def test_mark_cells_untrusted(self):
82 def test_mark_cells_untrusted(self):
83 cells = self.nb.worksheets[0].cells
83 cells = self.nb.worksheets[0].cells
84 self.notary.mark_cells(self.nb, False)
84 self.notary.mark_cells(self.nb, False)
85 for cell in cells:
85 for cell in cells:
86 if cell.cell_type == 'code':
86 if cell.cell_type == 'code':
87 self.assertIn('trusted', cell)
87 self.assertIn('trusted', cell)
88 self.assertFalse(cell.trusted)
88 self.assertFalse(cell.trusted)
89 else:
89 else:
90 self.assertNotIn('trusted', cell)
90 self.assertNotIn('trusted', cell)
91
91
92 def test_mark_cells_trusted(self):
92 def test_mark_cells_trusted(self):
93 cells = self.nb.worksheets[0].cells
93 cells = self.nb.worksheets[0].cells
94 self.notary.mark_cells(self.nb, True)
94 self.notary.mark_cells(self.nb, True)
95 for cell in cells:
95 for cell in cells:
96 if cell.cell_type == 'code':
96 if cell.cell_type == 'code':
97 self.assertIn('trusted', cell)
97 self.assertIn('trusted', cell)
98 self.assertTrue(cell.trusted)
98 self.assertTrue(cell.trusted)
99 else:
99 else:
100 self.assertNotIn('trusted', cell)
100 self.assertNotIn('trusted', cell)
101
101
102 def test_check_cells(self):
102 def test_check_cells(self):
103 nb = self.nb
103 nb = self.nb
104 self.notary.mark_cells(nb, True)
104 self.notary.mark_cells(nb, True)
105 self.assertTrue(self.notary.check_cells(nb))
105 self.assertTrue(self.notary.check_cells(nb))
106 for cell in nb.worksheets[0].cells:
106 for cell in nb.worksheets[0].cells:
107 self.assertNotIn('trusted', cell)
107 self.assertNotIn('trusted', cell)
108 self.notary.mark_cells(nb, False)
108 self.notary.mark_cells(nb, False)
109 self.assertFalse(self.notary.check_cells(nb))
109 self.assertFalse(self.notary.check_cells(nb))
110 for cell in nb.worksheets[0].cells:
110 for cell in nb.worksheets[0].cells:
111 self.assertNotIn('trusted', cell)
111 self.assertNotIn('trusted', cell)
112
113 def test_trust_no_output(self):
114 nb = self.nb
115 self.notary.mark_cells(nb, False)
116 for cell in nb.worksheets[0].cells:
117 if cell.cell_type == 'code':
118 cell.outputs = []
119 self.assertTrue(self.notary.check_cells(nb))
112
120
General Comments 0
You need to be logged in to leave comments. Login now