Show More
@@ -262,7 +262,7 b' class NotebookTrustApp(BaseIPythonApplication):' | |||
|
262 | 262 | self.exit(1) |
|
263 | 263 | with io.open(notebook_path, encoding='utf8') as f: |
|
264 | 264 | nb = current.read(f, 'json') |
|
265 | sign.trust_notebook(nb, self.notary.secret, self.notary.signature_scheme) | |
|
265 | self.notary.sign(nb) | |
|
266 | 266 | with io.open(notebook_path, 'w', encoding='utf8') as f: |
|
267 | 267 | current.write(nb, f, 'json') |
|
268 | 268 |
@@ -214,7 +214,10 b' class FileNotebookManager(NotebookManager):' | |||
|
214 | 214 | except Exception as e: |
|
215 | 215 | raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e)) |
|
216 | 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 | 221 | return model |
|
219 | 222 | |
|
220 | 223 | def save_notebook_model(self, model, name='', path=''): |
@@ -238,8 +241,11 b' class FileNotebookManager(NotebookManager):' | |||
|
238 | 241 | os_path = self.get_os_path(new_name, new_path) |
|
239 | 242 | nb = current.to_notebook_json(model['content']) |
|
240 | 243 | |
|
241 |
if s |
|
|
242 | sign.trust_notebook(nb, self.notary.secret, self.notary.scheme) | |
|
244 | if self.notary.check_cells(nb): | |
|
245 | self.notary.sign(nb) | |
|
246 | else: | |
|
247 | self.log.warn("Saving untrusted notebook %s/%s", new_path, new_name) | |
|
248 | ||
|
243 | 249 | |
|
244 | 250 | if 'name' in nb['metadata']: |
|
245 | 251 | nb['metadata']['name'] = u'' |
@@ -19,7 +19,7 b' import os' | |||
|
19 | 19 | |
|
20 | 20 | from IPython.utils.py3compat import string_types, unicode_type, cast_bytes |
|
21 | 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 | 25 | # Code |
@@ -62,104 +62,8 b' def signature_removed(nb):' | |||
|
62 | 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 | 65 | class NotebookNotary(LoggingConfigurable): |
|
154 |
"""A class for co |
|
|
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 | ) | |
|
66 | """A class for computing and verifying notebook signatures.""" | |
|
163 | 67 | |
|
164 | 68 | profile_dir = Instance("IPython.core.profiledir.ProfileDir") |
|
165 | 69 | def _profile_dir_default(self): |
@@ -172,28 +76,128 b' class NotebookNotary(LoggingConfigurable):' | |||
|
172 | 76 | app.initialize() |
|
173 | 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 | 95 | secret = Bytes(config=True, |
|
176 | 96 | help="""The secret key with which notebooks are signed.""" |
|
177 | 97 | ) |
|
178 | 98 | def _secret_default(self): |
|
179 | 99 | # 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: | |
|
100 | if os.path.exists(self.secret_file): | |
|
101 | with io.open(self.secret_file, 'rb') as f: | |
|
184 | 102 | return f.read() |
|
185 | 103 | else: |
|
186 | 104 | 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 | ) | |
|
105 | self._write_secret_file(secret) | |
|
197 | 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 | 203 | No newline at end of file |
General Comments 0
You need to be logged in to leave comments.
Login now