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