##// END OF EJS Templates
Merge pull request #4966 from takluyver/sign-blank-app-argv...
Min RK -
r14957:fbc198ba merge
parent child Browse files
Show More
@@ -1,277 +1,277 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()
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_cells(self, nb):
197 def check_cells(self, nb):
198 """Return whether all code cells are trusted
198 """Return whether all code cells are trusted
199
199
200 If there are no code cells, return True.
200 If there are no code cells, return True.
201
201
202 This function is the inverse of mark_cells.
202 This function is the inverse of mark_cells.
203 """
203 """
204 if not nb['worksheets']:
204 if not nb['worksheets']:
205 return True
205 return True
206 for cell in nb['worksheets'][0]['cells']:
206 for cell in nb['worksheets'][0]['cells']:
207 if cell['cell_type'] != 'code':
207 if cell['cell_type'] != 'code':
208 continue
208 continue
209 if not cell.get('trusted', False):
209 if not cell.get('trusted', False):
210 return False
210 return False
211 return True
211 return True
212
212
213
213
214 trust_flags = {
214 trust_flags = {
215 'reset' : (
215 'reset' : (
216 {'TrustNotebookApp' : { 'reset' : True}},
216 {'TrustNotebookApp' : { 'reset' : True}},
217 """Generate a new key for notebook signature.
217 """Generate a new key for notebook signature.
218 All previously signed notebooks will become untrusted.
218 All previously signed notebooks will become untrusted.
219 """
219 """
220 ),
220 ),
221 }
221 }
222 trust_flags.update(base_flags)
222 trust_flags.update(base_flags)
223 trust_flags.pop('init')
223 trust_flags.pop('init')
224
224
225
225
226 class TrustNotebookApp(BaseIPythonApplication):
226 class TrustNotebookApp(BaseIPythonApplication):
227
227
228 description="""Sign one or more IPython notebooks with your key,
228 description="""Sign one or more IPython notebooks with your key,
229 to trust their dynamic (HTML, Javascript) output.
229 to trust their dynamic (HTML, Javascript) output.
230
230
231 Otherwise, you will have to re-execute the notebook to see output.
231 Otherwise, you will have to re-execute the notebook to see output.
232 """
232 """
233
233
234 examples = """ipython trust mynotebook.ipynb and_this_one.ipynb"""
234 examples = """ipython trust mynotebook.ipynb and_this_one.ipynb"""
235
235
236 flags = trust_flags
236 flags = trust_flags
237
237
238 reset = Bool(False, config=True,
238 reset = Bool(False, config=True,
239 help="""If True, generate a new key for notebook signature.
239 help="""If True, generate a new key for notebook signature.
240 After reset, all previously signed notebooks will become untrusted.
240 After reset, all previously signed notebooks will become untrusted.
241 """
241 """
242 )
242 )
243
243
244 notary = Instance(NotebookNotary)
244 notary = Instance(NotebookNotary)
245 def _notary_default(self):
245 def _notary_default(self):
246 return NotebookNotary(parent=self, profile_dir=self.profile_dir)
246 return NotebookNotary(parent=self, profile_dir=self.profile_dir)
247
247
248 def sign_notebook(self, notebook_path):
248 def sign_notebook(self, notebook_path):
249 if not os.path.exists(notebook_path):
249 if not os.path.exists(notebook_path):
250 self.log.error("Notebook missing: %s" % notebook_path)
250 self.log.error("Notebook missing: %s" % notebook_path)
251 self.exit(1)
251 self.exit(1)
252 with io.open(notebook_path, encoding='utf8') as f:
252 with io.open(notebook_path, encoding='utf8') as f:
253 nb = read(f, 'json')
253 nb = read(f, 'json')
254 if self.notary.check_signature(nb):
254 if self.notary.check_signature(nb):
255 print("Notebook already signed: %s" % notebook_path)
255 print("Notebook already signed: %s" % notebook_path)
256 else:
256 else:
257 print("Signing notebook: %s" % notebook_path)
257 print("Signing notebook: %s" % notebook_path)
258 self.notary.sign(nb)
258 self.notary.sign(nb)
259 with io.open(notebook_path, 'w', encoding='utf8') as f:
259 with io.open(notebook_path, 'w', encoding='utf8') as f:
260 write(nb, f, 'json')
260 write(nb, f, 'json')
261
261
262 def generate_new_key(self):
262 def generate_new_key(self):
263 """Generate a new notebook signature key"""
263 """Generate a new notebook signature key"""
264 print("Generating new notebook key: %s" % self.notary.secret_file)
264 print("Generating new notebook key: %s" % self.notary.secret_file)
265 self.notary._write_secret_file(os.urandom(1024))
265 self.notary._write_secret_file(os.urandom(1024))
266
266
267 def start(self):
267 def start(self):
268 if self.reset:
268 if self.reset:
269 self.generate_new_key()
269 self.generate_new_key()
270 return
270 return
271 if not self.extra_args:
271 if not self.extra_args:
272 self.log.critical("Specify at least one notebook to sign.")
272 self.log.critical("Specify at least one notebook to sign.")
273 self.exit(1)
273 self.exit(1)
274
274
275 for notebook_path in self.extra_args:
275 for notebook_path in self.extra_args:
276 self.sign_notebook(notebook_path)
276 self.sign_notebook(notebook_path)
277
277
General Comments 0
You need to be logged in to leave comments. Login now