##// END OF EJS Templates
Merge pull request #7244 from minrk/rm-signature...
Min RK -
r19680:d4983156 merge
parent child Browse files
Show More
@@ -3,6 +3,7 b''
3 3 from __future__ import print_function
4 4
5 5 import os
6 import time
6 7
7 8 from tornado.web import HTTPError
8 9 from unittest import TestCase
@@ -156,6 +157,7 b' class TestContentsManager(TestCase):'
156 157
157 158 full_model = cm.get(path)
158 159 nb = full_model['content']
160 nb['metadata']['counter'] = int(1e6 * time.time())
159 161 self.add_code_cell(nb)
160 162
161 163 cm.save(full_model, path)
@@ -1,18 +1,27 b''
1 """Functions for signing notebooks"""
1 """Utilities for signing notebooks"""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import base64
7 7 from contextlib import contextmanager
8 from datetime import datetime
8 9 import hashlib
9 10 from hmac import HMAC
10 11 import io
11 12 import os
12 13
14 try:
15 import sqlite3
16 except ImportError:
17 try:
18 from pysqlite2 import dbapi2 as sqlite3
19 except ImportError:
20 sqlite3 = None
21
13 22 from IPython.utils.io import atomic_writing
14 from IPython.utils.py3compat import string_types, unicode_type, cast_bytes
15 from IPython.utils.traitlets import Instance, Bytes, Enum, Any, Unicode, Bool
23 from IPython.utils.py3compat import unicode_type, cast_bytes
24 from IPython.utils.traitlets import Instance, Bytes, Enum, Any, Unicode, Bool, Integer
16 25 from IPython.config import LoggingConfigurable, MultipleInstanceError
17 26 from IPython.core.application import BaseIPythonApplication, base_flags
18 27
@@ -93,6 +102,48 b' class NotebookNotary(LoggingConfigurable):'
93 102 app.initialize(argv=[])
94 103 return app.profile_dir
95 104
105 db_file = Unicode(config=True,
106 help="""The sqlite file in which to store notebook signatures.
107 By default, this will be in your IPython profile.
108 You can set it to ':memory:' to disable sqlite writing to the filesystem.
109 """)
110 def _db_file_default(self):
111 if self.profile_dir is None:
112 return ':memory:'
113 return os.path.join(self.profile_dir.security_dir, u'nbsignatures.db')
114
115 # 64k entries ~ 12MB
116 cache_size = Integer(65535, config=True,
117 help="""The number of notebook signatures to cache.
118 When the number of signatures exceeds this value,
119 the oldest 25% of signatures will be culled.
120 """
121 )
122 db = Any()
123 def _db_default(self):
124 if sqlite3 is None:
125 self.log.warn("Missing SQLite3, all notebooks will be untrusted!")
126 return
127 kwargs = dict(detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES)
128 db = sqlite3.connect(self.db_file, **kwargs)
129 self.init_db(db)
130 return db
131
132 def init_db(self, db):
133 db.execute("""
134 CREATE TABLE IF NOT EXISTS nbsignatures
135 (
136 id integer PRIMARY KEY AUTOINCREMENT,
137 algorithm text,
138 signature text,
139 path text,
140 last_seen timestamp
141 )""")
142 db.execute("""
143 CREATE INDEX IF NOT EXISTS algosig ON nbsignatures(algorithm, signature)
144 """)
145 db.commit()
146
96 147 algorithm = Enum(algorithms, default_value='sha256', config=True,
97 148 help="""The hashing algorithm used to sign notebooks."""
98 149 )
@@ -168,28 +219,72 b' class NotebookNotary(LoggingConfigurable):'
168 219 """
169 220 if nb.nbformat < 3:
170 221 return False
171 stored_signature = nb['metadata'].get('signature', None)
172 if not stored_signature \
173 or not isinstance(stored_signature, string_types) \
174 or ':' not in stored_signature:
222 if self.db is None:
175 223 return False
176 stored_algo, sig = stored_signature.split(':', 1)
177 if self.algorithm != stored_algo:
224 signature = self.compute_signature(nb)
225 r = self.db.execute("""SELECT id FROM nbsignatures WHERE
226 algorithm = ? AND
227 signature = ?;
228 """, (self.algorithm, signature)).fetchone()
229 if r is None:
178 230 return False
179 my_signature = self.compute_signature(nb)
180 return my_signature == sig
231 self.db.execute("""UPDATE nbsignatures SET last_seen = ? WHERE
232 algorithm = ? AND
233 signature = ?;
234 """,
235 (datetime.utcnow(), self.algorithm, signature),
236 )
237 self.db.commit()
238 return True
181 239
182 240 def sign(self, nb):
183 """Sign a notebook, indicating that its output is trusted
184
185 stores 'algo:hmac-hexdigest' in notebook.metadata.signature
241 """Sign a notebook, indicating that its output is trusted on this machine
186 242
187 e.g. 'sha256:deadbeef123...'
243 Stores hash algorithm and hmac digest in a local database of trusted notebooks.
188 244 """
189 245 if nb.nbformat < 3:
190 246 return
191 247 signature = self.compute_signature(nb)
192 nb['metadata']['signature'] = "%s:%s" % (self.algorithm, signature)
248 self.store_signature(signature, nb)
249
250 def store_signature(self, signature, nb):
251 if self.db is None:
252 return
253 self.db.execute("""INSERT OR IGNORE INTO nbsignatures
254 (algorithm, signature, last_seen) VALUES (?, ?, ?)""",
255 (self.algorithm, signature, datetime.utcnow())
256 )
257 self.db.execute("""UPDATE nbsignatures SET last_seen = ? WHERE
258 algorithm = ? AND
259 signature = ?;
260 """,
261 (datetime.utcnow(), self.algorithm, signature),
262 )
263 self.db.commit()
264 n, = self.db.execute("SELECT Count(*) FROM nbsignatures").fetchone()
265 if n > self.cache_size:
266 self.cull_db()
267
268 def unsign(self, nb):
269 """Ensure that a notebook is untrusted
270
271 by removing its signature from the trusted database, if present.
272 """
273 signature = self.compute_signature(nb)
274 self.db.execute("""DELETE FROM nbsignatures WHERE
275 algorithm = ? AND
276 signature = ?;
277 """,
278 (self.algorithm, signature)
279 )
280 self.db.commit()
281
282 def cull_db(self):
283 """Cull oldest 25% of the trusted signatures when the size limit is reached"""
284 self.db.execute("""DELETE FROM nbsignatures WHERE id IN (
285 SELECT id FROM nbsignatures ORDER BY last_seen DESC LIMIT -1 OFFSET ?
286 );
287 """, (max(int(0.75 * self.cache_size), 1),))
193 288
194 289 def mark_cells(self, nb, trusted):
195 290 """Mark cells as trusted if the notebook's signature can be verified
@@ -222,11 +317,9 b' class NotebookNotary(LoggingConfigurable):'
222 317
223 318 # explicitly safe output
224 319 if nbformat_version >= 4:
225 safe = {'text/plain', 'image/png', 'image/jpeg'}
226 320 unsafe_output_types = ['execute_result', 'display_data']
227 321 safe_keys = {"output_type", "execution_count", "metadata"}
228 322 else: # v3
229 safe = {'text', 'png', 'jpeg'}
230 323 unsafe_output_types = ['pyout', 'display_data']
231 324 safe_keys = {"output_type", "prompt_number", "metadata"}
232 325
@@ -261,7 +354,7 b' class NotebookNotary(LoggingConfigurable):'
261 354 trust_flags = {
262 355 'reset' : (
263 356 {'TrustNotebookApp' : { 'reset' : True}},
264 """Generate a new key for notebook signature.
357 """Delete the trusted notebook cache.
265 358 All previously signed notebooks will become untrusted.
266 359 """
267 360 ),
@@ -290,7 +383,7 b' class TrustNotebookApp(BaseIPythonApplication):'
290 383 flags = trust_flags
291 384
292 385 reset = Bool(False, config=True,
293 help="""If True, generate a new key for notebook signature.
386 help="""If True, delete the trusted signature cache.
294 387 After reset, all previously signed notebooks will become untrusted.
295 388 """
296 389 )
@@ -320,6 +413,9 b' class TrustNotebookApp(BaseIPythonApplication):'
320 413
321 414 def start(self):
322 415 if self.reset:
416 if os.path.exists(self.notary.db_file):
417 print("Removing trusted signature cache: %s" % self.notary.db_file)
418 os.remove(self.notary.db_file)
323 419 self.generate_new_key()
324 420 return
325 421 if not self.extra_args:
@@ -3,6 +3,9 b''
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 import copy
7 import time
8
6 9 from .base import TestsBase
7 10
8 11 from IPython.nbformat import read, sign
@@ -13,8 +16,9 b' class TestNotary(TestsBase):'
13 16
14 17 def setUp(self):
15 18 self.notary = sign.NotebookNotary(
19 db_file=':memory:',
16 20 secret=b'secret',
17 profile_dir=get_ipython().profile_dir
21 profile_dir=get_ipython().profile_dir,
18 22 )
19 23 with self.fopen(u'test3.ipynb', u'r') as f:
20 24 self.nb = read(f, as_version=4)
@@ -25,10 +29,7 b' class TestNotary(TestsBase):'
25 29 last_sig = ''
26 30 for algo in sign.algorithms:
27 31 self.notary.algorithm = algo
28 self.notary.sign(self.nb)
29 sig = self.nb.metadata.signature
30 print(sig)
31 self.assertEqual(sig[:len(self.notary.algorithm)+1], '%s:' % self.notary.algorithm)
32 sig = self.notary.compute_signature(self.nb)
32 33 self.assertNotEqual(last_sig, sig)
33 34 last_sig = sig
34 35
@@ -46,9 +47,49 b' class TestNotary(TestsBase):'
46 47 self.assertNotEqual(sig1, sig2)
47 48
48 49 def test_sign(self):
50 self.assertFalse(self.notary.check_signature(self.nb))
51 self.notary.sign(self.nb)
52 self.assertTrue(self.notary.check_signature(self.nb))
53
54 def test_unsign(self):
49 55 self.notary.sign(self.nb)
50 sig = self.nb.metadata.signature
51 self.assertEqual(sig[:len(self.notary.algorithm)+1], '%s:' % self.notary.algorithm)
56 self.assertTrue(self.notary.check_signature(self.nb))
57 self.notary.unsign(self.nb)
58 self.assertFalse(self.notary.check_signature(self.nb))
59 self.notary.unsign(self.nb)
60 self.assertFalse(self.notary.check_signature(self.nb))
61
62 def test_cull_db(self):
63 # this test has various sleeps of 2ms
64 # to ensure low resolution timestamps compare as expected
65 dt = 2e-3
66 nbs = [
67 copy.deepcopy(self.nb) for i in range(10)
68 ]
69 for row in self.notary.db.execute("SELECT * FROM nbsignatures"):
70 print(row)
71 self.notary.cache_size = 8
72 for i, nb in enumerate(nbs[:8]):
73 nb.metadata.dirty = i
74 self.notary.sign(nb)
75
76 for i, nb in enumerate(nbs[:8]):
77 time.sleep(dt)
78 self.assertTrue(self.notary.check_signature(nb), 'nb %i is trusted' % i)
79
80 # signing the 9th triggers culling of first 3
81 # (75% of 8 = 6, 9 - 6 = 3 culled)
82 self.notary.sign(nbs[8])
83 self.assertFalse(self.notary.check_signature(nbs[0]))
84 self.assertFalse(self.notary.check_signature(nbs[1]))
85 self.assertFalse(self.notary.check_signature(nbs[2]))
86 self.assertTrue(self.notary.check_signature(nbs[3]))
87 # checking nb3 should keep it from being culled:
88 self.notary.sign(nbs[0])
89 self.notary.sign(nbs[1])
90 self.notary.sign(nbs[2])
91 self.assertTrue(self.notary.check_signature(nbs[3]))
92 self.assertFalse(self.notary.check_signature(nbs[4]))
52 93
53 94 def test_check_signature(self):
54 95 nb = self.nb
@@ -56,6 +56,7 b' def upgrade(nb, from_version=3, from_minor=0):'
56 56 cells.append(upgrade_cell(cell))
57 57 # upgrade metadata
58 58 nb.metadata.pop('name', '')
59 nb.metadata.pop('signature', '')
59 60 # Validate the converted notebook before returning it
60 61 _warn_if_invalid(nb, nbformat)
61 62 return nb
@@ -55,10 +55,6 b''
55 55 }
56 56 }
57 57 },
58 "signature": {
59 "description": "Hash of the notebook.",
60 "type": "string"
61 },
62 58 "orig_nbformat": {
63 59 "description": "Original notebook format (major number) before converting the notebook between versions. This should never be written to a file.",
64 60 "type": "integer",
@@ -64,6 +64,7 b' def strip_transient(nb):'
64 64 """
65 65 nb.metadata.pop('orig_nbformat', None)
66 66 nb.metadata.pop('orig_nbformat_minor', None)
67 nb.metadata.pop('signature', None)
67 68 for cell in nb.cells:
68 69 cell.metadata.pop('trusted', None)
69 70 return nb
@@ -95,9 +95,7 b''
95 95 ]
96 96 }
97 97 ],
98 "metadata": {
99 "signature": "sha256:de8cb1aff3da9097ba3fc7afab4327fc589f2bb1c154feeeb5ed97c87e37486f"
100 },
98 "metadata": {},
101 99 "nbformat": 4,
102 100 "nbformat_minor": 0
103 101 } No newline at end of file
@@ -186,9 +186,7 b''
186 186 ]
187 187 }
188 188 ],
189 "metadata": {
190 "signature": "sha256:627cdf03b8de558c9344f9d1e8f0beeb2448e37e492d676e6db7b07d33251a2b"
191 },
189 "metadata": {},
192 190 "nbformat": 4,
193 191 "nbformat_minor": 0
194 192 } No newline at end of file
@@ -1799,9 +1799,7 b''
1799 1799 ]
1800 1800 }
1801 1801 ],
1802 "metadata": {
1803 "signature": "sha256:31071a05d0ecd75ed72fe3f0de0ad447a6f85cffe382c26efa5e68db1fee54ee"
1804 },
1802 "metadata": {},
1805 1803 "nbformat": 4,
1806 1804 "nbformat_minor": 0
1807 1805 } No newline at end of file
@@ -481,9 +481,7 b''
481 481 ]
482 482 }
483 483 ],
484 "metadata": {
485 "signature": "sha256:df6354daf203e842bc040989d149760382d8ceec769160e4efe8cde9dfcb9107"
486 },
484 "metadata": {},
487 485 "nbformat": 4,
488 486 "nbformat_minor": 0
489 487 } No newline at end of file
@@ -1317,9 +1317,7 b''
1317 1317 "source": []
1318 1318 }
1319 1319 ],
1320 "metadata": {
1321 "signature": "sha256:86c779d5798c4a68bda7e71c8ef320cb7ba9d7e3d0f1bc4b828ee65f617a5ae3"
1322 },
1320 "metadata": {},
1323 1321 "nbformat": 4,
1324 1322 "nbformat_minor": 0
1325 1323 } No newline at end of file
@@ -160,9 +160,7 b''
160 160 ]
161 161 }
162 162 ],
163 "metadata": {
164 "signature": "sha256:ee769d05a7e195e4b8546ef9a866ef03e59bff2f0fcba499d168c06b516aa79a"
165 },
163 "metadata": {},
166 164 "nbformat": 4,
167 165 "nbformat_minor": 0
168 166 } No newline at end of file
@@ -732,9 +732,7 b''
732 732 ]
733 733 }
734 734 ],
735 "metadata": {
736 "signature": "sha256:74dbf5caa25c937be70dfe2ab509783a01f4a2044850d7044e729300a8c3644d"
737 },
735 "metadata": {},
738 736 "nbformat": 4,
739 737 "nbformat_minor": 0
740 738 } No newline at end of file
@@ -150,9 +150,7 b''
150 150 ]
151 151 }
152 152 ],
153 "metadata": {
154 "signature": "sha256:ac5c21534f3dd013c78d4d201527f3ed4dea5b6fad4116b8d23c67ba107e48c3"
155 },
153 "metadata": {},
156 154 "nbformat": 4,
157 155 "nbformat_minor": 0
158 156 } No newline at end of file
@@ -3051,9 +3051,7 b''
3051 3051 ]
3052 3052 }
3053 3053 ],
3054 "metadata": {
3055 "signature": "sha256:cf83dc9e6288480ac94c44a5983b4ee421f0ade792a9fac64bc00719263386c0"
3056 },
3054 "metadata": {},
3057 3055 "nbformat": 4,
3058 3056 "nbformat_minor": 0
3059 3057 } No newline at end of file
@@ -1647,9 +1647,7 b''
1647 1647 ]
1648 1648 }
1649 1649 ],
1650 "metadata": {
1651 "signature": "sha256:a0f4cda82587c5b8e7a4502192d1210abedac9084077604eb60aea15f262a344"
1652 },
1650 "metadata": {},
1653 1651 "nbformat": 4,
1654 1652 "nbformat_minor": 0
1655 1653 } No newline at end of file
@@ -261,9 +261,7 b''
261 261 ]
262 262 }
263 263 ],
264 "metadata": {
265 "signature": "sha256:993106eecfd7abe1920e1dbe670c4518189c26e7b29dcc541835f7dcf6fffbb2"
266 },
264 "metadata": {},
267 265 "nbformat": 4,
268 266 "nbformat_minor": 0
269 267 } No newline at end of file
@@ -517,9 +517,7 b''
517 517 ]
518 518 }
519 519 ],
520 "metadata": {
521 "signature": "sha256:123d82ef0551f78e5dca94db6e00f1e10ae07d930467cf44709ccc6a9216776a"
522 },
520 "metadata": {},
523 521 "nbformat": 4,
524 522 "nbformat_minor": 0
525 523 } No newline at end of file
@@ -374,9 +374,7 b''
374 374 ]
375 375 }
376 376 ],
377 "metadata": {
378 "signature": "sha256:cbf49a9ceac0258773ae0a652e9ac7c13e0d042debbf0115cdc081862b5f7308"
379 },
377 "metadata": {},
380 378 "nbformat": 4,
381 379 "nbformat_minor": 0
382 380 } No newline at end of file
@@ -333,9 +333,7 b''
333 333 ]
334 334 }
335 335 ],
336 "metadata": {
337 "signature": "sha256:4352d4e1c693d919ce40b29ecf5a536917160df68b19a85caccedb1ea7ad06e1"
338 },
336 "metadata": {},
339 337 "nbformat": 4,
340 338 "nbformat_minor": 0
341 339 } No newline at end of file
@@ -41,9 +41,7 b''
41 41 ]
42 42 }
43 43 ],
44 "metadata": {
45 "signature": "sha256:0b4b631419772e40e0f4893f5a0f0fe089a39e46c8862af0256164628394302d"
46 },
44 "metadata": {},
47 45 "nbformat": 4,
48 46 "nbformat_minor": 0
49 47 } No newline at end of file
@@ -591,9 +591,7 b''
591 591 ]
592 592 }
593 593 ],
594 "metadata": {
595 "signature": "sha256:da6a3d73881e321e5ef406aea3e7d706c9dd405c94675318afde957d292f9cb9"
596 },
594 "metadata": {},
597 595 "nbformat": 4,
598 596 "nbformat_minor": 0
599 597 } No newline at end of file
@@ -830,9 +830,7 b''
830 830 ]
831 831 }
832 832 ],
833 "metadata": {
834 "signature": "sha256:61f24f38b76cb12d46c178eadc5d85d61bd0fc27f959236ea756ef8e1adc2693"
835 },
833 "metadata": {},
836 834 "nbformat": 4,
837 835 "nbformat_minor": 0
838 836 } No newline at end of file
@@ -131,9 +131,7 b''
131 131 "source": []
132 132 }
133 133 ],
134 "metadata": {
135 "signature": "sha256:5f38c57d9570e4b6f6edf98c38a7bff81cd16365baa11fd40265f1504bfc008c"
136 },
134 "metadata": {},
137 135 "nbformat": 4,
138 136 "nbformat_minor": 0
139 137 } No newline at end of file
@@ -297,9 +297,7 b''
297 297 ]
298 298 }
299 299 ],
300 "metadata": {
301 "signature": "sha256:4e9f4ce8fa9be2e33ffdae399daec11bf3ef63a6f0065b1d59739310ebfe8008"
302 },
300 "metadata": {},
303 301 "nbformat": 4,
304 302 "nbformat_minor": 0
305 303 } No newline at end of file
@@ -26762,9 +26762,7 b''
26762 26762 ]
26763 26763 }
26764 26764 ],
26765 "metadata": {
26766 "signature": "sha256:d75ab1c53fa3389eeac78ecf8e89beb52871950f296aad25776699b6d6125037"
26767 },
26765 "metadata": {},
26768 26766 "nbformat": 4,
26769 26767 "nbformat_minor": 0
26770 26768 } No newline at end of file
@@ -13428,9 +13428,7 b''
13428 13428 ]
13429 13429 }
13430 13430 ],
13431 "metadata": {
13432 "signature": "sha256:c6ccfb14927d633933da8b5c0f776a8da756d833654b5a3f8b6121b390ca6424"
13433 },
13431 "metadata": {},
13434 13432 "nbformat": 4,
13435 13433 "nbformat_minor": 0
13436 13434 } No newline at end of file
@@ -131,9 +131,7 b''
131 131 ]
132 132 }
133 133 ],
134 "metadata": {
135 "signature": "sha256:9a1dd30de04270174c09ef33ca214e5b15ca3721547420087c1563ad557d78e3"
136 },
134 "metadata": {},
137 135 "nbformat": 4,
138 136 "nbformat_minor": 0
139 137 } No newline at end of file
@@ -68,9 +68,7 b''
68 68 ]
69 69 }
70 70 ],
71 "metadata": {
72 "signature": "sha256:5a3c5ebae9154e13957e7c9d28bd3b7697c4b14f965482feb288d2cdf078983e"
73 },
71 "metadata": {},
74 72 "nbformat": 4,
75 73 "nbformat_minor": 0
76 74 } No newline at end of file
@@ -1,8 +1,6 b''
1 1 {
2 2 "cells": [],
3 "metadata": {
4 "signature": "sha256:0abf067a20ebda26a671db997ac954770350d292dff7b7d6a4ace8808f70aca1"
5 },
3 "metadata": {},
6 4 "nbformat": 4,
7 5 "nbformat_minor": 0
8 6 } No newline at end of file
@@ -355,9 +355,7 b''
355 355 ]
356 356 }
357 357 ],
358 "metadata": {
359 "signature": "sha256:ee4b22b4c949fe21b3e5cda24f0916ba59d8c09443f4a897d98b96d4a73ac335"
360 },
358 "metadata": {},
361 359 "nbformat": 4,
362 360 "nbformat_minor": 0
363 361 } No newline at end of file
@@ -296,9 +296,7 b''
296 296 ]
297 297 }
298 298 ],
299 "metadata": {
300 "signature": "sha256:3b7cae0c0936f25e6ccb7acafe310c08a4162a1a7fd66fa9874a52cffa0f64f9"
301 },
299 "metadata": {},
302 300 "nbformat": 4,
303 301 "nbformat_minor": 0
304 302 } No newline at end of file
@@ -346,9 +346,7 b''
346 346 ]
347 347 }
348 348 ],
349 "metadata": {
350 "signature": "sha256:1e9336d35cc07875300c5b876df6ce1f1971c2ee94870788c6ea32bbb789c42b"
351 },
349 "metadata": {},
352 350 "nbformat": 4,
353 351 "nbformat_minor": 0
354 352 } No newline at end of file
@@ -2533,9 +2533,7 b''
2533 2533 ]
2534 2534 }
2535 2535 ],
2536 "metadata": {
2537 "signature": "sha256:1b19dedc6473d4e886e549020c6710f2d14c17296168a02e7e7fa9673912b893"
2538 },
2536 "metadata": {},
2539 2537 "nbformat": 4,
2540 2538 "nbformat_minor": 0
2541 2539 } No newline at end of file
@@ -107,9 +107,7 b''
107 107 "source": []
108 108 }
109 109 ],
110 "metadata": {
111 "signature": "sha256:8781781d8835d77bf0f71b535b72ee2718405543c48e87ad0408242119cdb3cc"
112 },
110 "metadata": {},
113 111 "nbformat": 4,
114 112 "nbformat_minor": 0
115 113 } No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now