##// END OF EJS Templates
Notaries sign notebooks now
MinRK -
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 sign.check_trusted_cells(nb):
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 configuring notebook signatures
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