##// END OF EJS Templates
Don't write notebooks to disk when signing them...
Min RK -
Show More
@@ -1,427 +1,424 b''
1 """Utilities for signing notebooks"""
1 """Utilities for signing notebooks"""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 import base64
6 import base64
7 from contextlib import contextmanager
7 from contextlib import contextmanager
8 from datetime import datetime
8 from datetime import datetime
9 import hashlib
9 import hashlib
10 from hmac import HMAC
10 from hmac import HMAC
11 import io
11 import io
12 import os
12 import os
13
13
14 try:
14 try:
15 import sqlite3
15 import sqlite3
16 except ImportError:
16 except ImportError:
17 try:
17 try:
18 from pysqlite2 import dbapi2 as sqlite3
18 from pysqlite2 import dbapi2 as sqlite3
19 except ImportError:
19 except ImportError:
20 sqlite3 = None
20 sqlite3 = None
21
21
22 from IPython.utils.io import atomic_writing
23 from IPython.utils.py3compat import unicode_type, cast_bytes
22 from IPython.utils.py3compat import unicode_type, cast_bytes
24 from IPython.utils.traitlets import Instance, Bytes, Enum, Any, Unicode, Bool, Integer
23 from IPython.utils.traitlets import Instance, Bytes, Enum, Any, Unicode, Bool, Integer
25 from IPython.config import LoggingConfigurable, MultipleInstanceError
24 from IPython.config import LoggingConfigurable, MultipleInstanceError
26 from IPython.core.application import BaseIPythonApplication, base_flags
25 from IPython.core.application import BaseIPythonApplication, base_flags
27
26
28 from . import read, write, NO_CONVERT
27 from . import read, write, NO_CONVERT
29
28
30 try:
29 try:
31 # Python 3
30 # Python 3
32 algorithms = hashlib.algorithms_guaranteed
31 algorithms = hashlib.algorithms_guaranteed
33 except AttributeError:
32 except AttributeError:
34 algorithms = hashlib.algorithms
33 algorithms = hashlib.algorithms
35
34
36
35
37 def yield_everything(obj):
36 def yield_everything(obj):
38 """Yield every item in a container as bytes
37 """Yield every item in a container as bytes
39
38
40 Allows any JSONable object to be passed to an HMAC digester
39 Allows any JSONable object to be passed to an HMAC digester
41 without having to serialize the whole thing.
40 without having to serialize the whole thing.
42 """
41 """
43 if isinstance(obj, dict):
42 if isinstance(obj, dict):
44 for key in sorted(obj):
43 for key in sorted(obj):
45 value = obj[key]
44 value = obj[key]
46 yield cast_bytes(key)
45 yield cast_bytes(key)
47 for b in yield_everything(value):
46 for b in yield_everything(value):
48 yield b
47 yield b
49 elif isinstance(obj, (list, tuple)):
48 elif isinstance(obj, (list, tuple)):
50 for element in obj:
49 for element in obj:
51 for b in yield_everything(element):
50 for b in yield_everything(element):
52 yield b
51 yield b
53 elif isinstance(obj, unicode_type):
52 elif isinstance(obj, unicode_type):
54 yield obj.encode('utf8')
53 yield obj.encode('utf8')
55 else:
54 else:
56 yield unicode_type(obj).encode('utf8')
55 yield unicode_type(obj).encode('utf8')
57
56
58 def yield_code_cells(nb):
57 def yield_code_cells(nb):
59 """Iterator that yields all cells in a notebook
58 """Iterator that yields all cells in a notebook
60
59
61 nbformat version independent
60 nbformat version independent
62 """
61 """
63 if nb.nbformat >= 4:
62 if nb.nbformat >= 4:
64 for cell in nb['cells']:
63 for cell in nb['cells']:
65 if cell['cell_type'] == 'code':
64 if cell['cell_type'] == 'code':
66 yield cell
65 yield cell
67 elif nb.nbformat == 3:
66 elif nb.nbformat == 3:
68 for ws in nb['worksheets']:
67 for ws in nb['worksheets']:
69 for cell in ws['cells']:
68 for cell in ws['cells']:
70 if cell['cell_type'] == 'code':
69 if cell['cell_type'] == 'code':
71 yield cell
70 yield cell
72
71
73 @contextmanager
72 @contextmanager
74 def signature_removed(nb):
73 def signature_removed(nb):
75 """Context manager for operating on a notebook with its signature removed
74 """Context manager for operating on a notebook with its signature removed
76
75
77 Used for excluding the previous signature when computing a notebook's signature.
76 Used for excluding the previous signature when computing a notebook's signature.
78 """
77 """
79 save_signature = nb['metadata'].pop('signature', None)
78 save_signature = nb['metadata'].pop('signature', None)
80 try:
79 try:
81 yield
80 yield
82 finally:
81 finally:
83 if save_signature is not None:
82 if save_signature is not None:
84 nb['metadata']['signature'] = save_signature
83 nb['metadata']['signature'] = save_signature
85
84
86
85
87 class NotebookNotary(LoggingConfigurable):
86 class NotebookNotary(LoggingConfigurable):
88 """A class for computing and verifying notebook signatures."""
87 """A class for computing and verifying notebook signatures."""
89
88
90 profile_dir = Instance("IPython.core.profiledir.ProfileDir", allow_none=True)
89 profile_dir = Instance("IPython.core.profiledir.ProfileDir", allow_none=True)
91 def _profile_dir_default(self):
90 def _profile_dir_default(self):
92 from IPython.core.application import BaseIPythonApplication
91 from IPython.core.application import BaseIPythonApplication
93 app = None
92 app = None
94 try:
93 try:
95 if BaseIPythonApplication.initialized():
94 if BaseIPythonApplication.initialized():
96 app = BaseIPythonApplication.instance()
95 app = BaseIPythonApplication.instance()
97 except MultipleInstanceError:
96 except MultipleInstanceError:
98 pass
97 pass
99 if app is None:
98 if app is None:
100 # create an app, without the global instance
99 # create an app, without the global instance
101 app = BaseIPythonApplication()
100 app = BaseIPythonApplication()
102 app.initialize(argv=[])
101 app.initialize(argv=[])
103 return app.profile_dir
102 return app.profile_dir
104
103
105 db_file = Unicode(config=True,
104 db_file = Unicode(config=True,
106 help="""The sqlite file in which to store notebook signatures.
105 help="""The sqlite file in which to store notebook signatures.
107 By default, this will be in your IPython profile.
106 By default, this will be in your IPython profile.
108 You can set it to ':memory:' to disable sqlite writing to the filesystem.
107 You can set it to ':memory:' to disable sqlite writing to the filesystem.
109 """)
108 """)
110 def _db_file_default(self):
109 def _db_file_default(self):
111 if self.profile_dir is None:
110 if self.profile_dir is None:
112 return ':memory:'
111 return ':memory:'
113 return os.path.join(self.profile_dir.security_dir, u'nbsignatures.db')
112 return os.path.join(self.profile_dir.security_dir, u'nbsignatures.db')
114
113
115 # 64k entries ~ 12MB
114 # 64k entries ~ 12MB
116 cache_size = Integer(65535, config=True,
115 cache_size = Integer(65535, config=True,
117 help="""The number of notebook signatures to cache.
116 help="""The number of notebook signatures to cache.
118 When the number of signatures exceeds this value,
117 When the number of signatures exceeds this value,
119 the oldest 25% of signatures will be culled.
118 the oldest 25% of signatures will be culled.
120 """
119 """
121 )
120 )
122 db = Any()
121 db = Any()
123 def _db_default(self):
122 def _db_default(self):
124 if sqlite3 is None:
123 if sqlite3 is None:
125 self.log.warn("Missing SQLite3, all notebooks will be untrusted!")
124 self.log.warn("Missing SQLite3, all notebooks will be untrusted!")
126 return
125 return
127 kwargs = dict(detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES)
126 kwargs = dict(detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES)
128 db = sqlite3.connect(self.db_file, **kwargs)
127 db = sqlite3.connect(self.db_file, **kwargs)
129 self.init_db(db)
128 self.init_db(db)
130 return db
129 return db
131
130
132 def init_db(self, db):
131 def init_db(self, db):
133 db.execute("""
132 db.execute("""
134 CREATE TABLE IF NOT EXISTS nbsignatures
133 CREATE TABLE IF NOT EXISTS nbsignatures
135 (
134 (
136 id integer PRIMARY KEY AUTOINCREMENT,
135 id integer PRIMARY KEY AUTOINCREMENT,
137 algorithm text,
136 algorithm text,
138 signature text,
137 signature text,
139 path text,
138 path text,
140 last_seen timestamp
139 last_seen timestamp
141 )""")
140 )""")
142 db.execute("""
141 db.execute("""
143 CREATE INDEX IF NOT EXISTS algosig ON nbsignatures(algorithm, signature)
142 CREATE INDEX IF NOT EXISTS algosig ON nbsignatures(algorithm, signature)
144 """)
143 """)
145 db.commit()
144 db.commit()
146
145
147 algorithm = Enum(algorithms, default_value='sha256', config=True,
146 algorithm = Enum(algorithms, default_value='sha256', config=True,
148 help="""The hashing algorithm used to sign notebooks."""
147 help="""The hashing algorithm used to sign notebooks."""
149 )
148 )
150 def _algorithm_changed(self, name, old, new):
149 def _algorithm_changed(self, name, old, new):
151 self.digestmod = getattr(hashlib, self.algorithm)
150 self.digestmod = getattr(hashlib, self.algorithm)
152
151
153 digestmod = Any()
152 digestmod = Any()
154 def _digestmod_default(self):
153 def _digestmod_default(self):
155 return getattr(hashlib, self.algorithm)
154 return getattr(hashlib, self.algorithm)
156
155
157 secret_file = Unicode(config=True,
156 secret_file = Unicode(config=True,
158 help="""The file where the secret key is stored."""
157 help="""The file where the secret key is stored."""
159 )
158 )
160 def _secret_file_default(self):
159 def _secret_file_default(self):
161 if self.profile_dir is None:
160 if self.profile_dir is None:
162 return ''
161 return ''
163 return os.path.join(self.profile_dir.security_dir, 'notebook_secret')
162 return os.path.join(self.profile_dir.security_dir, 'notebook_secret')
164
163
165 secret = Bytes(config=True,
164 secret = Bytes(config=True,
166 help="""The secret key with which notebooks are signed."""
165 help="""The secret key with which notebooks are signed."""
167 )
166 )
168 def _secret_default(self):
167 def _secret_default(self):
169 # note : this assumes an Application is running
168 # note : this assumes an Application is running
170 if os.path.exists(self.secret_file):
169 if os.path.exists(self.secret_file):
171 with io.open(self.secret_file, 'rb') as f:
170 with io.open(self.secret_file, 'rb') as f:
172 return f.read()
171 return f.read()
173 else:
172 else:
174 secret = base64.encodestring(os.urandom(1024))
173 secret = base64.encodestring(os.urandom(1024))
175 self._write_secret_file(secret)
174 self._write_secret_file(secret)
176 return secret
175 return secret
177
176
178 def _write_secret_file(self, secret):
177 def _write_secret_file(self, secret):
179 """write my secret to my secret_file"""
178 """write my secret to my secret_file"""
180 self.log.info("Writing notebook-signing key to %s", self.secret_file)
179 self.log.info("Writing notebook-signing key to %s", self.secret_file)
181 with io.open(self.secret_file, 'wb') as f:
180 with io.open(self.secret_file, 'wb') as f:
182 f.write(secret)
181 f.write(secret)
183 try:
182 try:
184 os.chmod(self.secret_file, 0o600)
183 os.chmod(self.secret_file, 0o600)
185 except OSError:
184 except OSError:
186 self.log.warn(
185 self.log.warn(
187 "Could not set permissions on %s",
186 "Could not set permissions on %s",
188 self.secret_file
187 self.secret_file
189 )
188 )
190 return secret
189 return secret
191
190
192 def compute_signature(self, nb):
191 def compute_signature(self, nb):
193 """Compute a notebook's signature
192 """Compute a notebook's signature
194
193
195 by hashing the entire contents of the notebook via HMAC digest.
194 by hashing the entire contents of the notebook via HMAC digest.
196 """
195 """
197 hmac = HMAC(self.secret, digestmod=self.digestmod)
196 hmac = HMAC(self.secret, digestmod=self.digestmod)
198 # don't include the previous hash in the content to hash
197 # don't include the previous hash in the content to hash
199 with signature_removed(nb):
198 with signature_removed(nb):
200 # sign the whole thing
199 # sign the whole thing
201 for b in yield_everything(nb):
200 for b in yield_everything(nb):
202 hmac.update(b)
201 hmac.update(b)
203
202
204 return hmac.hexdigest()
203 return hmac.hexdigest()
205
204
206 def check_signature(self, nb):
205 def check_signature(self, nb):
207 """Check a notebook's stored signature
206 """Check a notebook's stored signature
208
207
209 If a signature is stored in the notebook's metadata,
208 If a signature is stored in the notebook's metadata,
210 a new signature is computed and compared with the stored value.
209 a new signature is computed and compared with the stored value.
211
210
212 Returns True if the signature is found and matches, False otherwise.
211 Returns True if the signature is found and matches, False otherwise.
213
212
214 The following conditions must all be met for a notebook to be trusted:
213 The following conditions must all be met for a notebook to be trusted:
215 - a signature is stored in the form 'scheme:hexdigest'
214 - a signature is stored in the form 'scheme:hexdigest'
216 - the stored scheme matches the requested scheme
215 - the stored scheme matches the requested scheme
217 - the requested scheme is available from hashlib
216 - the requested scheme is available from hashlib
218 - the computed hash from notebook_signature matches the stored hash
217 - the computed hash from notebook_signature matches the stored hash
219 """
218 """
220 if nb.nbformat < 3:
219 if nb.nbformat < 3:
221 return False
220 return False
222 if self.db is None:
221 if self.db is None:
223 return False
222 return False
224 signature = self.compute_signature(nb)
223 signature = self.compute_signature(nb)
225 r = self.db.execute("""SELECT id FROM nbsignatures WHERE
224 r = self.db.execute("""SELECT id FROM nbsignatures WHERE
226 algorithm = ? AND
225 algorithm = ? AND
227 signature = ?;
226 signature = ?;
228 """, (self.algorithm, signature)).fetchone()
227 """, (self.algorithm, signature)).fetchone()
229 if r is None:
228 if r is None:
230 return False
229 return False
231 self.db.execute("""UPDATE nbsignatures SET last_seen = ? WHERE
230 self.db.execute("""UPDATE nbsignatures SET last_seen = ? WHERE
232 algorithm = ? AND
231 algorithm = ? AND
233 signature = ?;
232 signature = ?;
234 """,
233 """,
235 (datetime.utcnow(), self.algorithm, signature),
234 (datetime.utcnow(), self.algorithm, signature),
236 )
235 )
237 self.db.commit()
236 self.db.commit()
238 return True
237 return True
239
238
240 def sign(self, nb):
239 def sign(self, nb):
241 """Sign a notebook, indicating that its output is trusted on this machine
240 """Sign a notebook, indicating that its output is trusted on this machine
242
241
243 Stores hash algorithm and hmac digest in a local database of trusted notebooks.
242 Stores hash algorithm and hmac digest in a local database of trusted notebooks.
244 """
243 """
245 if nb.nbformat < 3:
244 if nb.nbformat < 3:
246 return
245 return
247 signature = self.compute_signature(nb)
246 signature = self.compute_signature(nb)
248 self.store_signature(signature, nb)
247 self.store_signature(signature, nb)
249
248
250 def store_signature(self, signature, nb):
249 def store_signature(self, signature, nb):
251 if self.db is None:
250 if self.db is None:
252 return
251 return
253 self.db.execute("""INSERT OR IGNORE INTO nbsignatures
252 self.db.execute("""INSERT OR IGNORE INTO nbsignatures
254 (algorithm, signature, last_seen) VALUES (?, ?, ?)""",
253 (algorithm, signature, last_seen) VALUES (?, ?, ?)""",
255 (self.algorithm, signature, datetime.utcnow())
254 (self.algorithm, signature, datetime.utcnow())
256 )
255 )
257 self.db.execute("""UPDATE nbsignatures SET last_seen = ? WHERE
256 self.db.execute("""UPDATE nbsignatures SET last_seen = ? WHERE
258 algorithm = ? AND
257 algorithm = ? AND
259 signature = ?;
258 signature = ?;
260 """,
259 """,
261 (datetime.utcnow(), self.algorithm, signature),
260 (datetime.utcnow(), self.algorithm, signature),
262 )
261 )
263 self.db.commit()
262 self.db.commit()
264 n, = self.db.execute("SELECT Count(*) FROM nbsignatures").fetchone()
263 n, = self.db.execute("SELECT Count(*) FROM nbsignatures").fetchone()
265 if n > self.cache_size:
264 if n > self.cache_size:
266 self.cull_db()
265 self.cull_db()
267
266
268 def unsign(self, nb):
267 def unsign(self, nb):
269 """Ensure that a notebook is untrusted
268 """Ensure that a notebook is untrusted
270
269
271 by removing its signature from the trusted database, if present.
270 by removing its signature from the trusted database, if present.
272 """
271 """
273 signature = self.compute_signature(nb)
272 signature = self.compute_signature(nb)
274 self.db.execute("""DELETE FROM nbsignatures WHERE
273 self.db.execute("""DELETE FROM nbsignatures WHERE
275 algorithm = ? AND
274 algorithm = ? AND
276 signature = ?;
275 signature = ?;
277 """,
276 """,
278 (self.algorithm, signature)
277 (self.algorithm, signature)
279 )
278 )
280 self.db.commit()
279 self.db.commit()
281
280
282 def cull_db(self):
281 def cull_db(self):
283 """Cull oldest 25% of the trusted signatures when the size limit is reached"""
282 """Cull oldest 25% of the trusted signatures when the size limit is reached"""
284 self.db.execute("""DELETE FROM nbsignatures WHERE id IN (
283 self.db.execute("""DELETE FROM nbsignatures WHERE id IN (
285 SELECT id FROM nbsignatures ORDER BY last_seen DESC LIMIT -1 OFFSET ?
284 SELECT id FROM nbsignatures ORDER BY last_seen DESC LIMIT -1 OFFSET ?
286 );
285 );
287 """, (max(int(0.75 * self.cache_size), 1),))
286 """, (max(int(0.75 * self.cache_size), 1),))
288
287
289 def mark_cells(self, nb, trusted):
288 def mark_cells(self, nb, trusted):
290 """Mark cells as trusted if the notebook's signature can be verified
289 """Mark cells as trusted if the notebook's signature can be verified
291
290
292 Sets ``cell.metadata.trusted = True | False`` on all code cells,
291 Sets ``cell.metadata.trusted = True | False`` on all code cells,
293 depending on whether the stored signature can be verified.
292 depending on whether the stored signature can be verified.
294
293
295 This function is the inverse of check_cells
294 This function is the inverse of check_cells
296 """
295 """
297 if nb.nbformat < 3:
296 if nb.nbformat < 3:
298 return
297 return
299
298
300 for cell in yield_code_cells(nb):
299 for cell in yield_code_cells(nb):
301 cell['metadata']['trusted'] = trusted
300 cell['metadata']['trusted'] = trusted
302
301
303 def _check_cell(self, cell, nbformat_version):
302 def _check_cell(self, cell, nbformat_version):
304 """Do we trust an individual cell?
303 """Do we trust an individual cell?
305
304
306 Return True if:
305 Return True if:
307
306
308 - cell is explicitly trusted
307 - cell is explicitly trusted
309 - cell has no potentially unsafe rich output
308 - cell has no potentially unsafe rich output
310
309
311 If a cell has no output, or only simple print statements,
310 If a cell has no output, or only simple print statements,
312 it will always be trusted.
311 it will always be trusted.
313 """
312 """
314 # explicitly trusted
313 # explicitly trusted
315 if cell['metadata'].pop("trusted", False):
314 if cell['metadata'].pop("trusted", False):
316 return True
315 return True
317
316
318 # explicitly safe output
317 # explicitly safe output
319 if nbformat_version >= 4:
318 if nbformat_version >= 4:
320 unsafe_output_types = ['execute_result', 'display_data']
319 unsafe_output_types = ['execute_result', 'display_data']
321 safe_keys = {"output_type", "execution_count", "metadata"}
320 safe_keys = {"output_type", "execution_count", "metadata"}
322 else: # v3
321 else: # v3
323 unsafe_output_types = ['pyout', 'display_data']
322 unsafe_output_types = ['pyout', 'display_data']
324 safe_keys = {"output_type", "prompt_number", "metadata"}
323 safe_keys = {"output_type", "prompt_number", "metadata"}
325
324
326 for output in cell['outputs']:
325 for output in cell['outputs']:
327 output_type = output['output_type']
326 output_type = output['output_type']
328 if output_type in unsafe_output_types:
327 if output_type in unsafe_output_types:
329 # if there are any data keys not in the safe whitelist
328 # if there are any data keys not in the safe whitelist
330 output_keys = set(output)
329 output_keys = set(output)
331 if output_keys.difference(safe_keys):
330 if output_keys.difference(safe_keys):
332 return False
331 return False
333
332
334 return True
333 return True
335
334
336 def check_cells(self, nb):
335 def check_cells(self, nb):
337 """Return whether all code cells are trusted
336 """Return whether all code cells are trusted
338
337
339 If there are no code cells, return True.
338 If there are no code cells, return True.
340
339
341 This function is the inverse of mark_cells.
340 This function is the inverse of mark_cells.
342 """
341 """
343 if nb.nbformat < 3:
342 if nb.nbformat < 3:
344 return False
343 return False
345 trusted = True
344 trusted = True
346 for cell in yield_code_cells(nb):
345 for cell in yield_code_cells(nb):
347 # only distrust a cell if it actually has some output to distrust
346 # only distrust a cell if it actually has some output to distrust
348 if not self._check_cell(cell, nb.nbformat):
347 if not self._check_cell(cell, nb.nbformat):
349 trusted = False
348 trusted = False
350
349
351 return trusted
350 return trusted
352
351
353
352
354 trust_flags = {
353 trust_flags = {
355 'reset' : (
354 'reset' : (
356 {'TrustNotebookApp' : { 'reset' : True}},
355 {'TrustNotebookApp' : { 'reset' : True}},
357 """Delete the trusted notebook cache.
356 """Delete the trusted notebook cache.
358 All previously signed notebooks will become untrusted.
357 All previously signed notebooks will become untrusted.
359 """
358 """
360 ),
359 ),
361 }
360 }
362 trust_flags.update(base_flags)
361 trust_flags.update(base_flags)
363 trust_flags.pop('init')
362 trust_flags.pop('init')
364
363
365
364
366 class TrustNotebookApp(BaseIPythonApplication):
365 class TrustNotebookApp(BaseIPythonApplication):
367
366
368 description="""Sign one or more IPython notebooks with your key,
367 description="""Sign one or more IPython notebooks with your key,
369 to trust their dynamic (HTML, Javascript) output.
368 to trust their dynamic (HTML, Javascript) output.
370
369
371 Trusting a notebook only applies to the current IPython profile.
370 Trusting a notebook only applies to the current IPython profile.
372 To trust a notebook for use with a profile other than default,
371 To trust a notebook for use with a profile other than default,
373 add `--profile [profile name]`.
372 add `--profile [profile name]`.
374
373
375 Otherwise, you will have to re-execute the notebook to see output.
374 Otherwise, you will have to re-execute the notebook to see output.
376 """
375 """
377
376
378 examples = """
377 examples = """
379 ipython trust mynotebook.ipynb and_this_one.ipynb
378 ipython trust mynotebook.ipynb and_this_one.ipynb
380 ipython trust --profile myprofile mynotebook.ipynb
379 ipython trust --profile myprofile mynotebook.ipynb
381 """
380 """
382
381
383 flags = trust_flags
382 flags = trust_flags
384
383
385 reset = Bool(False, config=True,
384 reset = Bool(False, config=True,
386 help="""If True, delete the trusted signature cache.
385 help="""If True, delete the trusted signature cache.
387 After reset, all previously signed notebooks will become untrusted.
386 After reset, all previously signed notebooks will become untrusted.
388 """
387 """
389 )
388 )
390
389
391 notary = Instance(NotebookNotary)
390 notary = Instance(NotebookNotary)
392 def _notary_default(self):
391 def _notary_default(self):
393 return NotebookNotary(parent=self, profile_dir=self.profile_dir)
392 return NotebookNotary(parent=self, profile_dir=self.profile_dir)
394
393
395 def sign_notebook(self, notebook_path):
394 def sign_notebook(self, notebook_path):
396 if not os.path.exists(notebook_path):
395 if not os.path.exists(notebook_path):
397 self.log.error("Notebook missing: %s" % notebook_path)
396 self.log.error("Notebook missing: %s" % notebook_path)
398 self.exit(1)
397 self.exit(1)
399 with io.open(notebook_path, encoding='utf8') as f:
398 with io.open(notebook_path, encoding='utf8') as f:
400 nb = read(f, NO_CONVERT)
399 nb = read(f, NO_CONVERT)
401 if self.notary.check_signature(nb):
400 if self.notary.check_signature(nb):
402 print("Notebook already signed: %s" % notebook_path)
401 print("Notebook already signed: %s" % notebook_path)
403 else:
402 else:
404 print("Signing notebook: %s" % notebook_path)
403 print("Signing notebook: %s" % notebook_path)
405 self.notary.sign(nb)
404 self.notary.sign(nb)
406 with atomic_writing(notebook_path) as f:
407 write(nb, f, NO_CONVERT)
408
405
409 def generate_new_key(self):
406 def generate_new_key(self):
410 """Generate a new notebook signature key"""
407 """Generate a new notebook signature key"""
411 print("Generating new notebook key: %s" % self.notary.secret_file)
408 print("Generating new notebook key: %s" % self.notary.secret_file)
412 self.notary._write_secret_file(os.urandom(1024))
409 self.notary._write_secret_file(os.urandom(1024))
413
410
414 def start(self):
411 def start(self):
415 if self.reset:
412 if self.reset:
416 if os.path.exists(self.notary.db_file):
413 if os.path.exists(self.notary.db_file):
417 print("Removing trusted signature cache: %s" % self.notary.db_file)
414 print("Removing trusted signature cache: %s" % self.notary.db_file)
418 os.remove(self.notary.db_file)
415 os.remove(self.notary.db_file)
419 self.generate_new_key()
416 self.generate_new_key()
420 return
417 return
421 if not self.extra_args:
418 if not self.extra_args:
422 self.log.critical("Specify at least one notebook to sign.")
419 self.log.critical("Specify at least one notebook to sign.")
423 self.exit(1)
420 self.exit(1)
424
421
425 for notebook_path in self.extra_args:
422 for notebook_path in self.extra_args:
426 self.sign_notebook(notebook_path)
423 self.sign_notebook(notebook_path)
427
424
General Comments 0
You need to be logged in to leave comments. Login now