##// END OF EJS Templates
Merge pull request #6094 from minrk/sign-profile...
Thomas Kluyver -
r17169:ca79fd46 merge
parent child Browse files
Show More
@@ -1,312 +1,319 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 100 secret_file = Unicode(config=True,
101 101 help="""The file where the secret key is stored."""
102 102 )
103 103 def _secret_file_default(self):
104 104 if self.profile_dir is None:
105 105 return ''
106 106 return os.path.join(self.profile_dir.security_dir, 'notebook_secret')
107 107
108 108 secret = Bytes(config=True,
109 109 help="""The secret key with which notebooks are signed."""
110 110 )
111 111 def _secret_default(self):
112 112 # note : this assumes an Application is running
113 113 if os.path.exists(self.secret_file):
114 114 with io.open(self.secret_file, 'rb') as f:
115 115 return f.read()
116 116 else:
117 117 secret = base64.encodestring(os.urandom(1024))
118 118 self._write_secret_file(secret)
119 119 return secret
120 120
121 121 def _write_secret_file(self, secret):
122 122 """write my secret to my secret_file"""
123 123 self.log.info("Writing notebook-signing key to %s", self.secret_file)
124 124 with io.open(self.secret_file, 'wb') as f:
125 125 f.write(secret)
126 126 try:
127 127 os.chmod(self.secret_file, 0o600)
128 128 except OSError:
129 129 self.log.warn(
130 130 "Could not set permissions on %s",
131 131 self.secret_file
132 132 )
133 133 return secret
134 134
135 135 def compute_signature(self, nb):
136 136 """Compute a notebook's signature
137 137
138 138 by hashing the entire contents of the notebook via HMAC digest.
139 139 """
140 140 hmac = HMAC(self.secret, digestmod=self.digestmod)
141 141 # don't include the previous hash in the content to hash
142 142 with signature_removed(nb):
143 143 # sign the whole thing
144 144 for b in yield_everything(nb):
145 145 hmac.update(b)
146 146
147 147 return hmac.hexdigest()
148 148
149 149 def check_signature(self, nb):
150 150 """Check a notebook's stored signature
151 151
152 152 If a signature is stored in the notebook's metadata,
153 153 a new signature is computed and compared with the stored value.
154 154
155 155 Returns True if the signature is found and matches, False otherwise.
156 156
157 157 The following conditions must all be met for a notebook to be trusted:
158 158 - a signature is stored in the form 'scheme:hexdigest'
159 159 - the stored scheme matches the requested scheme
160 160 - the requested scheme is available from hashlib
161 161 - the computed hash from notebook_signature matches the stored hash
162 162 """
163 163 stored_signature = nb['metadata'].get('signature', None)
164 164 if not stored_signature \
165 165 or not isinstance(stored_signature, string_types) \
166 166 or ':' not in stored_signature:
167 167 return False
168 168 stored_algo, sig = stored_signature.split(':', 1)
169 169 if self.algorithm != stored_algo:
170 170 return False
171 171 my_signature = self.compute_signature(nb)
172 172 return my_signature == sig
173 173
174 174 def sign(self, nb):
175 175 """Sign a notebook, indicating that its output is trusted
176 176
177 177 stores 'algo:hmac-hexdigest' in notebook.metadata.signature
178 178
179 179 e.g. 'sha256:deadbeef123...'
180 180 """
181 181 signature = self.compute_signature(nb)
182 182 nb['metadata']['signature'] = "%s:%s" % (self.algorithm, signature)
183 183
184 184 def mark_cells(self, nb, trusted):
185 185 """Mark cells as trusted if the notebook's signature can be verified
186 186
187 187 Sets ``cell.trusted = True | False`` on all code cells,
188 188 depending on whether the stored signature can be verified.
189 189
190 190 This function is the inverse of check_cells
191 191 """
192 192 if not nb['worksheets']:
193 193 # nothing to mark if there are no cells
194 194 return
195 195 for cell in nb['worksheets'][0]['cells']:
196 196 if cell['cell_type'] == 'code':
197 197 cell['trusted'] = trusted
198 198
199 199 def _check_cell(self, cell):
200 200 """Do we trust an individual cell?
201 201
202 202 Return True if:
203 203
204 204 - cell is explicitly trusted
205 205 - cell has no potentially unsafe rich output
206 206
207 207 If a cell has no output, or only simple print statements,
208 208 it will always be trusted.
209 209 """
210 210 # explicitly trusted
211 211 if cell.pop("trusted", False):
212 212 return True
213 213
214 214 # explicitly safe output
215 215 safe = {
216 216 'text/plain', 'image/png', 'image/jpeg',
217 217 'text', 'png', 'jpg', # v3-style short keys
218 218 }
219 219
220 220 for output in cell['outputs']:
221 221 output_type = output['output_type']
222 222 if output_type in ('pyout', 'display_data'):
223 223 # if there are any data keys not in the safe whitelist
224 224 output_keys = set(output).difference({"output_type", "prompt_number", "metadata"})
225 225 if output_keys.difference(safe):
226 226 return False
227 227
228 228 return True
229 229
230 230 def check_cells(self, nb):
231 231 """Return whether all code cells are trusted
232 232
233 233 If there are no code cells, return True.
234 234
235 235 This function is the inverse of mark_cells.
236 236 """
237 237 if not nb['worksheets']:
238 238 return True
239 239 trusted = True
240 240 for cell in nb['worksheets'][0]['cells']:
241 241 if cell['cell_type'] != 'code':
242 242 continue
243 243 # only distrust a cell if it actually has some output to distrust
244 244 if not self._check_cell(cell):
245 245 trusted = False
246 246 return trusted
247 247
248 248
249 249 trust_flags = {
250 250 'reset' : (
251 251 {'TrustNotebookApp' : { 'reset' : True}},
252 252 """Generate a new key for notebook signature.
253 253 All previously signed notebooks will become untrusted.
254 254 """
255 255 ),
256 256 }
257 257 trust_flags.update(base_flags)
258 258 trust_flags.pop('init')
259 259
260 260
261 261 class TrustNotebookApp(BaseIPythonApplication):
262 262
263 263 description="""Sign one or more IPython notebooks with your key,
264 264 to trust their dynamic (HTML, Javascript) output.
265 265
266 Trusting a notebook only applies to the current IPython profile.
267 To trust a notebook for use with a profile other than default,
268 add `--profile [profile name]`.
269
266 270 Otherwise, you will have to re-execute the notebook to see output.
267 271 """
268 272
269 examples = """ipython trust mynotebook.ipynb and_this_one.ipynb"""
273 examples = """
274 ipython trust mynotebook.ipynb and_this_one.ipynb
275 ipython trust --profile myprofile mynotebook.ipynb
276 """
270 277
271 278 flags = trust_flags
272 279
273 280 reset = Bool(False, config=True,
274 281 help="""If True, generate a new key for notebook signature.
275 282 After reset, all previously signed notebooks will become untrusted.
276 283 """
277 284 )
278 285
279 286 notary = Instance(NotebookNotary)
280 287 def _notary_default(self):
281 288 return NotebookNotary(parent=self, profile_dir=self.profile_dir)
282 289
283 290 def sign_notebook(self, notebook_path):
284 291 if not os.path.exists(notebook_path):
285 292 self.log.error("Notebook missing: %s" % notebook_path)
286 293 self.exit(1)
287 294 with io.open(notebook_path, encoding='utf8') as f:
288 295 nb = read(f, 'json')
289 296 if self.notary.check_signature(nb):
290 297 print("Notebook already signed: %s" % notebook_path)
291 298 else:
292 299 print("Signing notebook: %s" % notebook_path)
293 300 self.notary.sign(nb)
294 301 with io.open(notebook_path, 'w', encoding='utf8') as f:
295 302 write(nb, f, 'json')
296 303
297 304 def generate_new_key(self):
298 305 """Generate a new notebook signature key"""
299 306 print("Generating new notebook key: %s" % self.notary.secret_file)
300 307 self.notary._write_secret_file(os.urandom(1024))
301 308
302 309 def start(self):
303 310 if self.reset:
304 311 self.generate_new_key()
305 312 return
306 313 if not self.extra_args:
307 314 self.log.critical("Specify at least one notebook to sign.")
308 315 self.exit(1)
309 316
310 317 for notebook_path in self.extra_args:
311 318 self.sign_notebook(notebook_path)
312 319
@@ -1,148 +1,154 b''
1 1 .. _notebook_security:
2 2
3 3 Security in IPython notebooks
4 4 =============================
5 5
6 6 As IPython notebooks become more popular for sharing and collaboration,
7 7 the potential for malicious people to attempt to exploit the notebook
8 8 for their nefarious purposes increases. IPython 2.0 introduces a
9 9 security model to prevent execution of untrusted code without explicit
10 10 user input.
11 11
12 12 The problem
13 13 -----------
14 14
15 15 The whole point of IPython is arbitrary code execution. We have no
16 16 desire to limit what can be done with a notebook, which would negatively
17 17 impact its utility.
18 18
19 19 Unlike other programs, an IPython notebook document includes output.
20 20 Unlike other documents, that output exists in a context that can execute
21 21 code (via Javascript).
22 22
23 23 The security problem we need to solve is that no code should execute
24 24 just because a user has **opened** a notebook that **they did not
25 25 write**. Like any other program, once a user decides to execute code in
26 26 a notebook, it is considered trusted, and should be allowed to do
27 27 anything.
28 28
29 29 Our security model
30 30 ------------------
31 31
32 32 - Untrusted HTML is always sanitized
33 33 - Untrusted Javascript is never executed
34 34 - HTML and Javascript in Markdown cells are never trusted
35 35 - **Outputs** generated by the user are trusted
36 36 - Any other HTML or Javascript (in Markdown cells, output generated by
37 37 others) is never trusted
38 38 - The central question of trust is "Did the current user do this?"
39 39
40 40 The details of trust
41 41 --------------------
42 42
43 43 IPython notebooks store a signature in metadata, which is used to answer
44 44 the question "Did the current user do this?"
45 45
46 46 This signature is a digest of the notebooks contents plus a secret key,
47 47 known only to the user. The secret key is a user-only readable file in
48 48 the IPython profile's security directory. By default, this is::
49 49
50 50 ~/.ipython/profile_default/security/notebook_secret
51 51
52 .. note::
53
54 The notebook secret being stored in the profile means that
55 loading a notebook in another profile results in it being untrusted,
56 unless you copy or symlink the notebook secret to share it across profiles.
57
52 58 When a notebook is opened by a user, the server computes a signature
53 59 with the user's key, and compares it with the signature stored in the
54 60 notebook's metadata. If the signature matches, HTML and Javascript
55 61 output in the notebook will be trusted at load, otherwise it will be
56 62 untrusted.
57 63
58 64 Any output generated during an interactive session is trusted.
59 65
60 66 Updating trust
61 67 **************
62 68
63 69 A notebook's trust is updated when the notebook is saved. If there are
64 70 any untrusted outputs still in the notebook, the notebook will not be
65 71 trusted, and no signature will be stored. If all untrusted outputs have
66 72 been removed (either via ``Clear Output`` or re-execution), then the
67 73 notebook will become trusted.
68 74
69 75 While trust is updated per output, this is only for the duration of a
70 76 single session. A notebook file on disk is either trusted or not in its
71 77 entirety.
72 78
73 79 Explicit trust
74 80 **************
75 81
76 82 Sometimes re-executing a notebook to generate trusted output is not an
77 83 option, either because dependencies are unavailable, or it would take a
78 84 long time. Users can explicitly trust a notebook in two ways:
79 85
80 86 - At the command-line, with::
81 87
82 88 ipython trust /path/to/notebook.ipynb
83 89
84 90 - After loading the untrusted notebook, with ``File / Trust Notebook``
85 91
86 92 These two methods simply load the notebook, compute a new signature with
87 93 the user's key, and then store the newly signed notebook.
88 94
89 95 Reporting security issues
90 96 -------------------------
91 97
92 98 If you find a security vulnerability in IPython, either a failure of the
93 99 code to properly implement the model described here, or a failure of the
94 100 model itself, please report it to security@ipython.org.
95 101
96 102 If you prefer to encrypt your security reports,
97 103 you can use :download:`this PGP public key <ipython_security.asc>`.
98 104
99 105 Affected use cases
100 106 ------------------
101 107
102 108 Some use cases that work in IPython 1.0 will become less convenient in
103 109 2.0 as a result of the security changes. We do our best to minimize
104 110 these annoyance, but security is always at odds with convenience.
105 111
106 112 Javascript and CSS in Markdown cells
107 113 ************************************
108 114
109 115 While never officially supported, it had become common practice to put
110 116 hidden Javascript or CSS styling in Markdown cells, so that they would
111 117 not be visible on the page. Since Markdown cells are now sanitized (by
112 118 `Google Caja <https://developers.google.com/caja>`__), all Javascript
113 119 (including click event handlers, etc.) and CSS will be stripped.
114 120
115 121 We plan to provide a mechanism for notebook themes, but in the meantime
116 122 styling the notebook can only be done via either ``custom.css`` or CSS
117 123 in HTML output. The latter only have an effect if the notebook is
118 124 trusted, because otherwise the output will be sanitized just like
119 125 Markdown.
120 126
121 127 Collaboration
122 128 *************
123 129
124 130 When collaborating on a notebook, people probably want to see the
125 131 outputs produced by their colleagues' most recent executions. Since each
126 132 collaborator's key will differ, this will result in each share starting
127 133 in an untrusted state. There are three basic approaches to this:
128 134
129 135 - re-run notebooks when you get them (not always viable)
130 136 - explicitly trust notebooks via ``ipython trust`` or the notebook menu
131 137 (annoying, but easy)
132 138 - share a notebook secret, and use an IPython profile dedicated to the
133 139 collaboration while working on the project.
134 140
135 141 Multiple profiles or machines
136 142 *****************************
137 143
138 144 Since the notebook secret is stored in a profile directory by default,
139 145 opening a notebook with a different profile or on a different machine
140 146 will result in a different key, and thus be untrusted. The only current
141 147 way to address this is by sharing the notebook secret. This can be
142 148 facilitated by setting the configurable:
143 149
144 150 .. sourcecode:: python
145 151
146 152 c.NotebookApp.secret_file = "/path/to/notebook_secret"
147 153
148 154 in each profile, and only sharing the secret once per machine.
General Comments 0
You need to be logged in to leave comments. Login now