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