Show More
@@ -262,7 +262,7 b' 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 b' 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 b' 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 b' 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,104 +62,8 b' 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): |
|
|||
66 | """Compute a notebook's signature |
|
|||
67 |
|
||||
68 | 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 | """ |
|
|||
72 | hmac = HMAC(secret, digestmod=getattr(hashlib, scheme)) |
|
|||
73 | # don't include the previous hash in the content to hash |
|
|||
74 | with signature_removed(nb): |
|
|||
75 | # sign the whole thing |
|
|||
76 | for b in yield_everything(nb): |
|
|||
77 | hmac.update(b) |
|
|||
78 |
|
||||
79 | return hmac.hexdigest() |
|
|||
80 |
|
||||
81 |
|
||||
82 | def check_notebook_signature(nb, secret, scheme): |
|
|||
83 | """Check a notebook's stored signature |
|
|||
84 |
|
||||
85 | If a signature is stored in the notebook's metadata, |
|
|||
86 | a new signature is computed and compared with the stored value. |
|
|||
87 |
|
||||
88 | Returns True if the signature is found and matches, False otherwise. |
|
|||
89 |
|
||||
90 | The following conditions must all be met for a notebook to be trusted: |
|
|||
91 | - a signature is stored in the form 'scheme:hexdigest' |
|
|||
92 | - the stored scheme matches the requested scheme |
|
|||
93 | - the requested scheme is available from hashlib |
|
|||
94 | - the computed hash from notebook_signature matches the stored hash |
|
|||
95 | """ |
|
|||
96 | stored_signature = nb['metadata'].get('signature', None) |
|
|||
97 | if not stored_signature \ |
|
|||
98 | or not isinstance(stored_signature, string_types) \ |
|
|||
99 | or ':' not in stored_signature: |
|
|||
100 | return False |
|
|||
101 | stored_scheme, sig = stored_signature.split(':', 1) |
|
|||
102 | if scheme != stored_scheme: |
|
|||
103 | return False |
|
|||
104 | try: |
|
|||
105 | my_signature = notebook_signature(nb, secret, scheme) |
|
|||
106 | except AttributeError: |
|
|||
107 | return False |
|
|||
108 | return my_signature == sig |
|
|||
109 |
|
||||
110 |
|
||||
111 | def trust_notebook(nb, secret, scheme): |
|
|||
112 | """Re-sign a notebook, indicating that its output is trusted |
|
|||
113 |
|
||||
114 | stores 'scheme:hmac-hexdigest' in notebook.metadata.signature |
|
|||
115 |
|
||||
116 | e.g. 'sha256:deadbeef123...' |
|
|||
117 | """ |
|
|||
118 | signature = notebook_signature(nb, secret, scheme) |
|
|||
119 | nb['metadata']['signature'] = "%s:%s" % (scheme, signature) |
|
|||
120 |
|
||||
121 |
|
||||
122 | def mark_trusted_cells(nb, secret, scheme): |
|
|||
123 | """Mark cells as trusted if the notebook's signature can be verified |
|
|||
124 |
|
||||
125 | Sets ``cell.trusted = True | False`` on all code cells, |
|
|||
126 | depending on whether the stored signature can be verified. |
|
|||
127 | """ |
|
|||
128 | if not nb['worksheets']: |
|
|||
129 | # nothing to mark if there are no cells |
|
|||
130 | return True |
|
|||
131 | trusted = check_notebook_signature(nb, secret, scheme) |
|
|||
132 | for cell in nb['worksheets'][0]['cells']: |
|
|||
133 | if cell['cell_type'] == 'code': |
|
|||
134 | cell['trusted'] = trusted |
|
|||
135 | return trusted |
|
|||
136 |
|
||||
137 |
|
||||
138 | def check_trusted_cells(nb): |
|
|||
139 | """Return whether all code cells are trusted |
|
|||
140 |
|
||||
141 | If there are no code cells, return True. |
|
|||
142 | """ |
|
|||
143 | if not nb['worksheets']: |
|
|||
144 | return True |
|
|||
145 | for cell in nb['worksheets'][0]['cells']: |
|
|||
146 | if cell['cell_type'] != 'code': |
|
|||
147 | continue |
|
|||
148 | if not cell.get('trusted', False): |
|
|||
149 | return False |
|
|||
150 | return True |
|
|||
151 |
|
||||
152 |
|
||||
153 | class NotebookNotary(LoggingConfigurable): |
|
65 | class NotebookNotary(LoggingConfigurable): | |
154 |
"""A class for co |
|
66 | """A class for computing and verifying 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 |
|
67 | |||
164 | profile_dir = Instance("IPython.core.profiledir.ProfileDir") |
|
68 | profile_dir = Instance("IPython.core.profiledir.ProfileDir") | |
165 | def _profile_dir_default(self): |
|
69 | def _profile_dir_default(self): | |
@@ -172,28 +76,128 b' class NotebookNotary(LoggingConfigurable):' | |||||
172 | app.initialize() |
|
76 | app.initialize() | |
173 | return app.profile_dir |
|
77 | return app.profile_dir | |
174 |
|
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 | ||||
175 | secret = Bytes(config=True, |
|
95 | secret = Bytes(config=True, | |
176 | help="""The secret key with which notebooks are signed.""" |
|
96 | help="""The secret key with which notebooks are signed.""" | |
177 | ) |
|
97 | ) | |
178 | def _secret_default(self): |
|
98 | def _secret_default(self): | |
179 | # note : this assumes an Application is running |
|
99 | # note : this assumes an Application is running | |
180 | profile_dir = self.profile_dir |
|
100 | if os.path.exists(self.secret_file): | |
181 | secret_file = os.path.join(profile_dir.security_dir, 'notebook_secret') |
|
101 | with io.open(self.secret_file, 'rb') as f: | |
182 | if os.path.exists(secret_file): |
|
|||
183 | with io.open(secret_file, 'rb') as f: |
|
|||
184 | return f.read() |
|
102 | return f.read() | |
185 | else: |
|
103 | else: | |
186 | secret = base64.encodestring(os.urandom(1024)) |
|
104 | secret = base64.encodestring(os.urandom(1024)) | |
187 | self.log.info("Writing output secret to %s", secret_file) |
|
105 | self._write_secret_file(secret) | |
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 |
|
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): | |||
|
123 | """Compute a notebook's signature | |||
|
124 | ||||
|
125 | by hashing the entire contents of the notebook via HMAC digest. | |||
|
126 | """ | |||
|
127 | hmac = HMAC(self.secret, digestmod=self.digestmod) | |||
|
128 | # don't include the previous hash in the content to hash | |||
|
129 | with signature_removed(nb): | |||
|
130 | # sign the whole thing | |||
|
131 | for b in yield_everything(nb): | |||
|
132 | hmac.update(b) | |||
|
133 | ||||
|
134 | return hmac.hexdigest() | |||
|
135 | ||||
|
136 | def check_signature(self, nb): | |||
|
137 | """Check a notebook's stored signature | |||
|
138 | ||||
|
139 | If a signature is stored in the notebook's metadata, | |||
|
140 | a new signature is computed and compared with the stored value. | |||
|
141 | ||||
|
142 | Returns True if the signature is found and matches, False otherwise. | |||
|
143 | ||||
|
144 | The following conditions must all be met for a notebook to be trusted: | |||
|
145 | - a signature is stored in the form 'scheme:hexdigest' | |||
|
146 | - the stored scheme matches the requested scheme | |||
|
147 | - the requested scheme is available from hashlib | |||
|
148 | - the computed hash from notebook_signature matches the stored hash | |||
|
149 | """ | |||
|
150 | stored_signature = nb['metadata'].get('signature', None) | |||
|
151 | if not stored_signature \ | |||
|
152 | or not isinstance(stored_signature, string_types) \ | |||
|
153 | or ':' not in stored_signature: | |||
|
154 | return False | |||
|
155 | stored_algo, sig = stored_signature.split(':', 1) | |||
|
156 | if self.algorithm != stored_algo: | |||
|
157 | return False | |||
|
158 | my_signature = self.compute_signature(nb) | |||
|
159 | return my_signature == sig | |||
|
160 | ||||
|
161 | def sign(self, nb): | |||
|
162 | """Sign a notebook, indicating that its output is trusted | |||
|
163 | ||||
|
164 | stores 'algo:hmac-hexdigest' in notebook.metadata.signature | |||
|
165 | ||||
|
166 | e.g. 'sha256:deadbeef123...' | |||
|
167 | """ | |||
|
168 | signature = self.compute_signature(nb) | |||
|
169 | nb['metadata']['signature'] = "%s:%s" % (self.algorithm, signature) | |||
|
170 | ||||
|
171 | def mark_cells(self, nb, trusted): | |||
|
172 | """Mark cells as trusted if the notebook's signature can be verified | |||
|
173 | ||||
|
174 | Sets ``cell.trusted = True | False`` on all code cells, | |||
|
175 | depending on whether the stored signature can be verified. | |||
|
176 | ||||
|
177 | This function is the inverse of check_cells | |||
|
178 | """ | |||
|
179 | if not nb['worksheets']: | |||
|
180 | # nothing to mark if there are no cells | |||
|
181 | return | |||
|
182 | for cell in nb['worksheets'][0]['cells']: | |||
|
183 | if cell['cell_type'] == 'code': | |||
|
184 | cell['trusted'] = trusted | |||
|
185 | ||||
|
186 | def check_cells(self, nb): | |||
|
187 | """Return whether all code cells are trusted | |||
|
188 | ||||
|
189 | If there are no code cells, return True. | |||
|
190 | ||||
|
191 | This function is the inverse of mark_cells. | |||
|
192 | """ | |||
|
193 | if not nb['worksheets']: | |||
|
194 | return True | |||
|
195 | for cell in nb['worksheets'][0]['cells']: | |||
|
196 | if cell['cell_type'] != 'code': | |||
|
197 | continue | |||
|
198 | if not cell.get('trusted', False): | |||
|
199 | return False | |||
|
200 | return True | |||
|
201 | ||||
198 |
|
202 | |||
199 | No newline at end of file |
|
203 |
General Comments 0
You need to be logged in to leave comments.
Login now