##// END OF EJS Templates
tweak default profile_dir...
MinRK -
Show More
@@ -1,241 +1,245 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
21 from IPython.utils.traitlets import Instance, Bytes, Enum, Any, Unicode
22 from IPython.config import LoggingConfigurable
22 from IPython.config import LoggingConfigurable, MultipleInstanceError
23 from IPython.core.application import BaseIPythonApplication
23 from IPython.core.application import BaseIPythonApplication
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
30
31
31
32 def yield_everything(obj):
32 def yield_everything(obj):
33 """Yield every item in a container as bytes
33 """Yield every item in a container as bytes
34
34
35 Allows any JSONable object to be passed to an HMAC digester
35 Allows any JSONable object to be passed to an HMAC digester
36 without having to serialize the whole thing.
36 without having to serialize the whole thing.
37 """
37 """
38 if isinstance(obj, dict):
38 if isinstance(obj, dict):
39 for key in sorted(obj):
39 for key in sorted(obj):
40 value = obj[key]
40 value = obj[key]
41 yield cast_bytes(key)
41 yield cast_bytes(key)
42 for b in yield_everything(value):
42 for b in yield_everything(value):
43 yield b
43 yield b
44 elif isinstance(obj, (list, tuple)):
44 elif isinstance(obj, (list, tuple)):
45 for element in obj:
45 for element in obj:
46 for b in yield_everything(element):
46 for b in yield_everything(element):
47 yield b
47 yield b
48 elif isinstance(obj, unicode_type):
48 elif isinstance(obj, unicode_type):
49 yield obj.encode('utf8')
49 yield obj.encode('utf8')
50 else:
50 else:
51 yield unicode_type(obj).encode('utf8')
51 yield unicode_type(obj).encode('utf8')
52
52
53
53
54 @contextmanager
54 @contextmanager
55 def signature_removed(nb):
55 def signature_removed(nb):
56 """Context manager for operating on a notebook with its signature removed
56 """Context manager for operating on a notebook with its signature removed
57
57
58 Used for excluding the previous signature when computing a notebook's signature.
58 Used for excluding the previous signature when computing a notebook's signature.
59 """
59 """
60 save_signature = nb['metadata'].pop('signature', None)
60 save_signature = nb['metadata'].pop('signature', None)
61 try:
61 try:
62 yield
62 yield
63 finally:
63 finally:
64 if save_signature is not None:
64 if save_signature is not None:
65 nb['metadata']['signature'] = save_signature
65 nb['metadata']['signature'] = save_signature
66
66
67
67
68 class NotebookNotary(LoggingConfigurable):
68 class NotebookNotary(LoggingConfigurable):
69 """A class for computing and verifying notebook signatures."""
69 """A class for computing and verifying notebook signatures."""
70
70
71 profile_dir = Instance("IPython.core.profiledir.ProfileDir")
71 profile_dir = Instance("IPython.core.profiledir.ProfileDir")
72 def _profile_dir_default(self):
72 def _profile_dir_default(self):
73 from IPython.core.application import BaseIPythonApplication
73 from IPython.core.application import BaseIPythonApplication
74 if BaseIPythonApplication.initialized():
74 app = None
75 app = BaseIPythonApplication.instance()
75 try:
76 else:
76 if BaseIPythonApplication.initialized():
77 app = BaseIPythonApplication.instance()
78 except MultipleInstanceError:
79 pass
80 if app is None:
77 # create an app, without the global instance
81 # create an app, without the global instance
78 app = BaseIPythonApplication()
82 app = BaseIPythonApplication()
79 app.initialize()
83 app.initialize()
80 return app.profile_dir
84 return app.profile_dir
81
85
82 algorithm = Enum(hashlib.algorithms, default_value='sha256', config=True,
86 algorithm = Enum(hashlib.algorithms, default_value='sha256', config=True,
83 help="""The hashing algorithm used to sign notebooks."""
87 help="""The hashing algorithm used to sign notebooks."""
84 )
88 )
85 def _algorithm_changed(self, name, old, new):
89 def _algorithm_changed(self, name, old, new):
86 self.digestmod = getattr(hashlib, self.algorithm)
90 self.digestmod = getattr(hashlib, self.algorithm)
87
91
88 digestmod = Any()
92 digestmod = Any()
89 def _digestmod_default(self):
93 def _digestmod_default(self):
90 return getattr(hashlib, self.algorithm)
94 return getattr(hashlib, self.algorithm)
91
95
92 secret_file = Unicode()
96 secret_file = Unicode()
93 def _secret_file_default(self):
97 def _secret_file_default(self):
94 if self.profile_dir is None:
98 if self.profile_dir is None:
95 return ''
99 return ''
96 return os.path.join(self.profile_dir.security_dir, 'notebook_secret')
100 return os.path.join(self.profile_dir.security_dir, 'notebook_secret')
97
101
98 secret = Bytes(config=True,
102 secret = Bytes(config=True,
99 help="""The secret key with which notebooks are signed."""
103 help="""The secret key with which notebooks are signed."""
100 )
104 )
101 def _secret_default(self):
105 def _secret_default(self):
102 # note : this assumes an Application is running
106 # note : this assumes an Application is running
103 if os.path.exists(self.secret_file):
107 if os.path.exists(self.secret_file):
104 with io.open(self.secret_file, 'rb') as f:
108 with io.open(self.secret_file, 'rb') as f:
105 return f.read()
109 return f.read()
106 else:
110 else:
107 secret = base64.encodestring(os.urandom(1024))
111 secret = base64.encodestring(os.urandom(1024))
108 self._write_secret_file(secret)
112 self._write_secret_file(secret)
109 return secret
113 return secret
110
114
111 def _write_secret_file(self, secret):
115 def _write_secret_file(self, secret):
112 """write my secret to my secret_file"""
116 """write my secret to my secret_file"""
113 self.log.info("Writing output secret to %s", self.secret_file)
117 self.log.info("Writing output secret to %s", self.secret_file)
114 with io.open(self.secret_file, 'wb') as f:
118 with io.open(self.secret_file, 'wb') as f:
115 f.write(secret)
119 f.write(secret)
116 try:
120 try:
117 os.chmod(self.secret_file, 0o600)
121 os.chmod(self.secret_file, 0o600)
118 except OSError:
122 except OSError:
119 self.log.warn(
123 self.log.warn(
120 "Could not set permissions on %s",
124 "Could not set permissions on %s",
121 self.secret_file
125 self.secret_file
122 )
126 )
123 return secret
127 return secret
124
128
125 def compute_signature(self, nb):
129 def compute_signature(self, nb):
126 """Compute a notebook's signature
130 """Compute a notebook's signature
127
131
128 by hashing the entire contents of the notebook via HMAC digest.
132 by hashing the entire contents of the notebook via HMAC digest.
129 """
133 """
130 hmac = HMAC(self.secret, digestmod=self.digestmod)
134 hmac = HMAC(self.secret, digestmod=self.digestmod)
131 # don't include the previous hash in the content to hash
135 # don't include the previous hash in the content to hash
132 with signature_removed(nb):
136 with signature_removed(nb):
133 # sign the whole thing
137 # sign the whole thing
134 for b in yield_everything(nb):
138 for b in yield_everything(nb):
135 hmac.update(b)
139 hmac.update(b)
136
140
137 return hmac.hexdigest()
141 return hmac.hexdigest()
138
142
139 def check_signature(self, nb):
143 def check_signature(self, nb):
140 """Check a notebook's stored signature
144 """Check a notebook's stored signature
141
145
142 If a signature is stored in the notebook's metadata,
146 If a signature is stored in the notebook's metadata,
143 a new signature is computed and compared with the stored value.
147 a new signature is computed and compared with the stored value.
144
148
145 Returns True if the signature is found and matches, False otherwise.
149 Returns True if the signature is found and matches, False otherwise.
146
150
147 The following conditions must all be met for a notebook to be trusted:
151 The following conditions must all be met for a notebook to be trusted:
148 - a signature is stored in the form 'scheme:hexdigest'
152 - a signature is stored in the form 'scheme:hexdigest'
149 - the stored scheme matches the requested scheme
153 - the stored scheme matches the requested scheme
150 - the requested scheme is available from hashlib
154 - the requested scheme is available from hashlib
151 - the computed hash from notebook_signature matches the stored hash
155 - the computed hash from notebook_signature matches the stored hash
152 """
156 """
153 stored_signature = nb['metadata'].get('signature', None)
157 stored_signature = nb['metadata'].get('signature', None)
154 if not stored_signature \
158 if not stored_signature \
155 or not isinstance(stored_signature, string_types) \
159 or not isinstance(stored_signature, string_types) \
156 or ':' not in stored_signature:
160 or ':' not in stored_signature:
157 return False
161 return False
158 stored_algo, sig = stored_signature.split(':', 1)
162 stored_algo, sig = stored_signature.split(':', 1)
159 if self.algorithm != stored_algo:
163 if self.algorithm != stored_algo:
160 return False
164 return False
161 my_signature = self.compute_signature(nb)
165 my_signature = self.compute_signature(nb)
162 return my_signature == sig
166 return my_signature == sig
163
167
164 def sign(self, nb):
168 def sign(self, nb):
165 """Sign a notebook, indicating that its output is trusted
169 """Sign a notebook, indicating that its output is trusted
166
170
167 stores 'algo:hmac-hexdigest' in notebook.metadata.signature
171 stores 'algo:hmac-hexdigest' in notebook.metadata.signature
168
172
169 e.g. 'sha256:deadbeef123...'
173 e.g. 'sha256:deadbeef123...'
170 """
174 """
171 signature = self.compute_signature(nb)
175 signature = self.compute_signature(nb)
172 nb['metadata']['signature'] = "%s:%s" % (self.algorithm, signature)
176 nb['metadata']['signature'] = "%s:%s" % (self.algorithm, signature)
173
177
174 def mark_cells(self, nb, trusted):
178 def mark_cells(self, nb, trusted):
175 """Mark cells as trusted if the notebook's signature can be verified
179 """Mark cells as trusted if the notebook's signature can be verified
176
180
177 Sets ``cell.trusted = True | False`` on all code cells,
181 Sets ``cell.trusted = True | False`` on all code cells,
178 depending on whether the stored signature can be verified.
182 depending on whether the stored signature can be verified.
179
183
180 This function is the inverse of check_cells
184 This function is the inverse of check_cells
181 """
185 """
182 if not nb['worksheets']:
186 if not nb['worksheets']:
183 # nothing to mark if there are no cells
187 # nothing to mark if there are no cells
184 return
188 return
185 for cell in nb['worksheets'][0]['cells']:
189 for cell in nb['worksheets'][0]['cells']:
186 if cell['cell_type'] == 'code':
190 if cell['cell_type'] == 'code':
187 cell['trusted'] = trusted
191 cell['trusted'] = trusted
188
192
189 def check_cells(self, nb):
193 def check_cells(self, nb):
190 """Return whether all code cells are trusted
194 """Return whether all code cells are trusted
191
195
192 If there are no code cells, return True.
196 If there are no code cells, return True.
193
197
194 This function is the inverse of mark_cells.
198 This function is the inverse of mark_cells.
195 """
199 """
196 if not nb['worksheets']:
200 if not nb['worksheets']:
197 return True
201 return True
198 for cell in nb['worksheets'][0]['cells']:
202 for cell in nb['worksheets'][0]['cells']:
199 if cell['cell_type'] != 'code':
203 if cell['cell_type'] != 'code':
200 continue
204 continue
201 if not cell.get('trusted', False):
205 if not cell.get('trusted', False):
202 return False
206 return False
203 return True
207 return True
204
208
205
209
206 class TrustNotebookApp(BaseIPythonApplication):
210 class TrustNotebookApp(BaseIPythonApplication):
207
211
208 description="""Sign one or more IPython notebooks with your key,
212 description="""Sign one or more IPython notebooks with your key,
209 to trust their dynamic (HTML, Javascript) output.
213 to trust their dynamic (HTML, Javascript) output.
210
214
211 Otherwise, you will have to re-execute the notebook to see output.
215 Otherwise, you will have to re-execute the notebook to see output.
212 """
216 """
213
217
214 examples="""ipython trust mynotebook.ipynb and_this_one.ipynb"""
218 examples="""ipython trust mynotebook.ipynb and_this_one.ipynb"""
215
219
216 notary = Instance(NotebookNotary)
220 notary = Instance(NotebookNotary)
217 def _notary_default(self):
221 def _notary_default(self):
218 return NotebookNotary(parent=self, profile_dir=self.profile_dir)
222 return NotebookNotary(parent=self, profile_dir=self.profile_dir)
219
223
220 def sign_notebook(self, notebook_path):
224 def sign_notebook(self, notebook_path):
221 if not os.path.exists(notebook_path):
225 if not os.path.exists(notebook_path):
222 self.log.error("Notebook missing: %s" % notebook_path)
226 self.log.error("Notebook missing: %s" % notebook_path)
223 self.exit(1)
227 self.exit(1)
224 with io.open(notebook_path, encoding='utf8') as f:
228 with io.open(notebook_path, encoding='utf8') as f:
225 nb = read(f, 'json')
229 nb = read(f, 'json')
226 if self.notary.check_signature(nb):
230 if self.notary.check_signature(nb):
227 print("Notebook already signed: %s" % notebook_path)
231 print("Notebook already signed: %s" % notebook_path)
228 else:
232 else:
229 print("Signing notebook: %s" % notebook_path)
233 print("Signing notebook: %s" % notebook_path)
230 self.notary.sign(nb)
234 self.notary.sign(nb)
231 with io.open(notebook_path, 'w', encoding='utf8') as f:
235 with io.open(notebook_path, 'w', encoding='utf8') as f:
232 write(nb, f, 'json')
236 write(nb, f, 'json')
233
237
234 def start(self):
238 def start(self):
235 if not self.extra_args:
239 if not self.extra_args:
236 self.log.critical("Specify at least one notebook to sign.")
240 self.log.critical("Specify at least one notebook to sign.")
237 self.exit(1)
241 self.exit(1)
238
242
239 for notebook_path in self.extra_args:
243 for notebook_path in self.extra_args:
240 self.sign_notebook(notebook_path)
244 self.sign_notebook(notebook_path)
241
245
General Comments 0
You need to be logged in to leave comments. Login now