Show More
@@ -262,7 +262,7 class NotebookTrustApp(BaseIPythonApplication): | |||||
262 | self.exit(1) |
|
262 | self.exit(1) | |
263 | with io.open(notebook_path, encoding='utf8') as f: |
|
263 | with io.open(notebook_path, encoding='utf8') as f: | |
264 | nb = current.read(f, 'json') |
|
264 | nb = current.read(f, 'json') | |
265 | sign.trust_notebook(nb, self.notary.secret, self.notary.signature_scheme) |
|
265 | self.notary.sign(nb) | |
266 | with io.open(notebook_path, 'w', encoding='utf8') as f: |
|
266 | with io.open(notebook_path, 'w', encoding='utf8') as f: | |
267 | current.write(nb, f, 'json') |
|
267 | current.write(nb, f, 'json') | |
268 |
|
268 |
@@ -214,7 +214,10 class FileNotebookManager(NotebookManager): | |||||
214 | except Exception as e: |
|
214 | except Exception as e: | |
215 | raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e)) |
|
215 | raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e)) | |
216 | model['content'] = nb |
|
216 | model['content'] = nb | |
217 | sign.mark_trusted_cells(nb, self.notary.secret, self.notary.scheme) |
|
217 | trusted = self.notary.check_signature(nb) | |
|
218 | if not trusted: | |||
|
219 | self.log.warn("Notebook %s/%s is not trusted", model['path'], model['name']) | |||
|
220 | self.notary.mark_cells(nb, trusted) | |||
218 | return model |
|
221 | return model | |
219 |
|
222 | |||
220 | def save_notebook_model(self, model, name='', path=''): |
|
223 | def save_notebook_model(self, model, name='', path=''): | |
@@ -238,8 +241,11 class FileNotebookManager(NotebookManager): | |||||
238 | os_path = self.get_os_path(new_name, new_path) |
|
241 | os_path = self.get_os_path(new_name, new_path) | |
239 | nb = current.to_notebook_json(model['content']) |
|
242 | nb = current.to_notebook_json(model['content']) | |
240 |
|
243 | |||
241 |
if s |
|
244 | if self.notary.check_cells(nb): | |
242 | sign.trust_notebook(nb, self.notary.secret, self.notary.scheme) |
|
245 | self.notary.sign(nb) | |
|
246 | else: | |||
|
247 | self.log.warn("Saving untrusted notebook %s/%s", new_path, new_name) | |||
|
248 | ||||
243 |
|
249 | |||
244 | if 'name' in nb['metadata']: |
|
250 | if 'name' in nb['metadata']: | |
245 | nb['metadata']['name'] = u'' |
|
251 | nb['metadata']['name'] = u'' |
@@ -19,7 +19,7 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.config import LoggingConfigurable |
|
21 | from IPython.config import LoggingConfigurable | |
22 | from IPython.utils.traitlets import Instance, Bytes, Enum |
|
22 | from IPython.utils.traitlets import Instance, Bytes, Enum, Any, Unicode | |
23 |
|
23 | |||
24 | #----------------------------------------------------------------------------- |
|
24 | #----------------------------------------------------------------------------- | |
25 | # Code |
|
25 | # Code | |
@@ -62,14 +62,69 def signature_removed(nb): | |||||
62 | nb['metadata']['signature'] = save_signature |
|
62 | nb['metadata']['signature'] = save_signature | |
63 |
|
63 | |||
64 |
|
64 | |||
65 | def notebook_signature(nb, secret, scheme): |
|
65 | class NotebookNotary(LoggingConfigurable): | |
|
66 | """A class for computing and verifying notebook signatures.""" | |||
|
67 | ||||
|
68 | profile_dir = Instance("IPython.core.profiledir.ProfileDir") | |||
|
69 | def _profile_dir_default(self): | |||
|
70 | from IPython.core.application import BaseIPythonApplication | |||
|
71 | if BaseIPythonApplication.initialized(): | |||
|
72 | app = BaseIPythonApplication.instance() | |||
|
73 | else: | |||
|
74 | # create an app, without the global instance | |||
|
75 | app = BaseIPythonApplication() | |||
|
76 | app.initialize() | |||
|
77 | return app.profile_dir | |||
|
78 | ||||
|
79 | algorithm = Enum(hashlib.algorithms, default_value='sha256', config=True, | |||
|
80 | help="""The hashing algorithm used to sign notebooks.""" | |||
|
81 | ) | |||
|
82 | def _algorithm_changed(self, name, old, new): | |||
|
83 | self.digestmod = getattr(hashlib, self.algorithm) | |||
|
84 | ||||
|
85 | digestmod = Any() | |||
|
86 | def _digestmod_default(self): | |||
|
87 | return getattr(hashlib, self.algorithm) | |||
|
88 | ||||
|
89 | secret_file = Unicode() | |||
|
90 | def _secret_file_default(self): | |||
|
91 | if self.profile_dir is None: | |||
|
92 | return '' | |||
|
93 | return os.path.join(self.profile_dir.security_dir, 'notebook_secret') | |||
|
94 | ||||
|
95 | secret = Bytes(config=True, | |||
|
96 | help="""The secret key with which notebooks are signed.""" | |||
|
97 | ) | |||
|
98 | def _secret_default(self): | |||
|
99 | # note : this assumes an Application is running | |||
|
100 | if os.path.exists(self.secret_file): | |||
|
101 | with io.open(self.secret_file, 'rb') as f: | |||
|
102 | return f.read() | |||
|
103 | else: | |||
|
104 | secret = base64.encodestring(os.urandom(1024)) | |||
|
105 | self._write_secret_file(secret) | |||
|
106 | return secret | |||
|
107 | ||||
|
108 | def _write_secret_file(self, secret): | |||
|
109 | """write my secret to my secret_file""" | |||
|
110 | self.log.info("Writing output secret to %s", self.secret_file) | |||
|
111 | with io.open(self.secret_file, 'wb') as f: | |||
|
112 | f.write(secret) | |||
|
113 | try: | |||
|
114 | os.chmod(self.secret_file, 0o600) | |||
|
115 | except OSError: | |||
|
116 | self.log.warn( | |||
|
117 | "Could not set permissions on %s", | |||
|
118 | self.secret_file | |||
|
119 | ) | |||
|
120 | return secret | |||
|
121 | ||||
|
122 | def compute_signature(self, nb): | |||
66 | """Compute a notebook's signature |
|
123 | """Compute a notebook's signature | |
67 |
|
124 | |||
68 | by hashing the entire contents of the notebook via HMAC digest. |
|
125 | by hashing the entire contents of the notebook via HMAC digest. | |
69 | scheme is the hashing scheme, which must be an attribute of the hashlib module, |
|
|||
70 | as listed in hashlib.algorithms. |
|
|||
71 | """ |
|
126 | """ | |
72 |
hmac = HMAC(secret, digestmod= |
|
127 | hmac = HMAC(self.secret, digestmod=self.digestmod) | |
73 | # don't include the previous hash in the content to hash |
|
128 | # don't include the previous hash in the content to hash | |
74 | with signature_removed(nb): |
|
129 | with signature_removed(nb): | |
75 | # sign the whole thing |
|
130 | # sign the whole thing | |
@@ -78,8 +133,7 def notebook_signature(nb, secret, scheme): | |||||
78 |
|
133 | |||
79 | return hmac.hexdigest() |
|
134 | return hmac.hexdigest() | |
80 |
|
135 | |||
81 |
|
136 | def check_signature(self, nb): | ||
82 | def check_notebook_signature(nb, secret, scheme): |
|
|||
83 | """Check a notebook's stored signature |
|
137 | """Check a notebook's stored signature | |
84 |
|
138 | |||
85 | If a signature is stored in the notebook's metadata, |
|
139 | If a signature is stored in the notebook's metadata, | |
@@ -98,47 +152,43 def check_notebook_signature(nb, secret, scheme): | |||||
98 | or not isinstance(stored_signature, string_types) \ |
|
152 | or not isinstance(stored_signature, string_types) \ | |
99 | or ':' not in stored_signature: |
|
153 | or ':' not in stored_signature: | |
100 | return False |
|
154 | return False | |
101 |
stored_ |
|
155 | stored_algo, sig = stored_signature.split(':', 1) | |
102 |
if s |
|
156 | if self.algorithm != stored_algo: | |
103 | return False |
|
|||
104 | try: |
|
|||
105 | my_signature = notebook_signature(nb, secret, scheme) |
|
|||
106 | except AttributeError: |
|
|||
107 | return False |
|
157 | return False | |
|
158 | my_signature = self.compute_signature(nb) | |||
108 | return my_signature == sig |
|
159 | return my_signature == sig | |
109 |
|
160 | |||
|
161 | def sign(self, nb): | |||
|
162 | """Sign a notebook, indicating that its output is trusted | |||
110 |
|
163 | |||
111 | def trust_notebook(nb, secret, scheme): |
|
164 | stores 'algo:hmac-hexdigest' in notebook.metadata.signature | |
112 | """Re-sign a notebook, indicating that its output is trusted |
|
|||
113 |
|
||||
114 | stores 'scheme:hmac-hexdigest' in notebook.metadata.signature |
|
|||
115 |
|
165 | |||
116 | e.g. 'sha256:deadbeef123...' |
|
166 | e.g. 'sha256:deadbeef123...' | |
117 | """ |
|
167 | """ | |
118 |
signature = |
|
168 | signature = self.compute_signature(nb) | |
119 |
nb['metadata']['signature'] = "%s:%s" % (s |
|
169 | nb['metadata']['signature'] = "%s:%s" % (self.algorithm, signature) | |
120 |
|
170 | |||
121 |
|
171 | def mark_cells(self, nb, trusted): | ||
122 | def mark_trusted_cells(nb, secret, scheme): |
|
|||
123 | """Mark cells as trusted if the notebook's signature can be verified |
|
172 | """Mark cells as trusted if the notebook's signature can be verified | |
124 |
|
173 | |||
125 | Sets ``cell.trusted = True | False`` on all code cells, |
|
174 | Sets ``cell.trusted = True | False`` on all code cells, | |
126 | depending on whether the stored signature can be verified. |
|
175 | depending on whether the stored signature can be verified. | |
|
176 | ||||
|
177 | This function is the inverse of check_cells | |||
127 | """ |
|
178 | """ | |
128 | if not nb['worksheets']: |
|
179 | if not nb['worksheets']: | |
129 | # nothing to mark if there are no cells |
|
180 | # nothing to mark if there are no cells | |
130 |
return |
|
181 | return | |
131 | trusted = check_notebook_signature(nb, secret, scheme) |
|
|||
132 | for cell in nb['worksheets'][0]['cells']: |
|
182 | for cell in nb['worksheets'][0]['cells']: | |
133 | if cell['cell_type'] == 'code': |
|
183 | if cell['cell_type'] == 'code': | |
134 | cell['trusted'] = trusted |
|
184 | cell['trusted'] = trusted | |
135 | return trusted |
|
|||
136 |
|
185 | |||
137 |
|
186 | def check_cells(self, nb): | ||
138 | def check_trusted_cells(nb): |
|
|||
139 | """Return whether all code cells are trusted |
|
187 | """Return whether all code cells are trusted | |
140 |
|
188 | |||
141 | If there are no code cells, return True. |
|
189 | If there are no code cells, return True. | |
|
190 | ||||
|
191 | This function is the inverse of mark_cells. | |||
142 | """ |
|
192 | """ | |
143 | if not nb['worksheets']: |
|
193 | if not nb['worksheets']: | |
144 | return True |
|
194 | return True | |
@@ -150,50 +200,4 def check_trusted_cells(nb): | |||||
150 | return True |
|
200 | return True | |
151 |
|
201 | |||
152 |
|
202 | |||
153 | class NotebookNotary(LoggingConfigurable): |
|
|||
154 | """A class for configuring notebook signatures |
|
|||
155 |
|
||||
156 | It stores the secret with which to sign notebooks, |
|
|||
157 | and the hashing scheme to use for notebook signatures. |
|
|||
158 | """ |
|
|||
159 |
|
||||
160 | scheme = Enum(hashlib.algorithms, default_value='sha256', config=True, |
|
|||
161 | help="""The hashing algorithm used to sign notebooks.""" |
|
|||
162 | ) |
|
|||
163 |
|
||||
164 | profile_dir = Instance("IPython.core.profiledir.ProfileDir") |
|
|||
165 | def _profile_dir_default(self): |
|
|||
166 | from IPython.core.application import BaseIPythonApplication |
|
|||
167 | if BaseIPythonApplication.initialized(): |
|
|||
168 | app = BaseIPythonApplication.instance() |
|
|||
169 | else: |
|
|||
170 | # create an app, without the global instance |
|
|||
171 | app = BaseIPythonApplication() |
|
|||
172 | app.initialize() |
|
|||
173 | return app.profile_dir |
|
|||
174 |
|
||||
175 | secret = Bytes(config=True, |
|
|||
176 | help="""The secret key with which notebooks are signed.""" |
|
|||
177 | ) |
|
|||
178 | def _secret_default(self): |
|
|||
179 | # note : this assumes an Application is running |
|
|||
180 | profile_dir = self.profile_dir |
|
|||
181 | secret_file = os.path.join(profile_dir.security_dir, 'notebook_secret') |
|
|||
182 | if os.path.exists(secret_file): |
|
|||
183 | with io.open(secret_file, 'rb') as f: |
|
|||
184 | return f.read() |
|
|||
185 | else: |
|
|||
186 | secret = base64.encodestring(os.urandom(1024)) |
|
|||
187 | self.log.info("Writing output secret to %s", secret_file) |
|
|||
188 | with io.open(secret_file, 'wb') as f: |
|
|||
189 | f.write(secret) |
|
|||
190 | try: |
|
|||
191 | os.chmod(secret_file, 0o600) |
|
|||
192 | except OSError: |
|
|||
193 | self.log.warn( |
|
|||
194 | "Could not set permissions on %s", |
|
|||
195 | secret_file |
|
|||
196 | ) |
|
|||
197 | return secret |
|
|||
198 |
|
||||
199 | No newline at end of file |
|
203 |
General Comments 0
You need to be logged in to leave comments.
Login now