test_sign.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. """Test Notebook signing"""
  2. # Copyright (c) IPython Development Team.
  3. # Distributed under the terms of the Modified BSD License.
  4. import codecs
  5. import copy
  6. import os
  7. import shutil
  8. from subprocess import Popen, PIPE
  9. import sys
  10. import time
  11. import tempfile
  12. import testpath
  13. import unittest
  14. from .base import TestsBase
  15. from traitlets.config import Config
  16. from nbformat import read, sign, write
  17. class TestNotary(TestsBase):
  18. def setUp(self):
  19. self.data_dir = tempfile.mkdtemp()
  20. self.notary = sign.NotebookNotary(
  21. db_file=':memory:',
  22. secret=b'secret',
  23. data_dir=self.data_dir,
  24. )
  25. with self.fopen(u'test3.ipynb', u'r') as f:
  26. self.nb = read(f, as_version=4)
  27. with self.fopen(u'test3.ipynb', u'r') as f:
  28. self.nb3 = read(f, as_version=3)
  29. def tearDown(self):
  30. self.notary.store.close()
  31. shutil.rmtree(self.data_dir)
  32. def test_invalid_db_file(self):
  33. invalid_sql_file = os.path.join(self.data_dir, 'invalid_db_file.db')
  34. with open(invalid_sql_file, 'w') as tempfile:
  35. tempfile.write(u'[invalid data]')
  36. invalid_notary = sign.NotebookNotary(
  37. db_file=invalid_sql_file,
  38. secret=b'secret',
  39. )
  40. invalid_notary.sign(self.nb)
  41. invalid_notary.store.close()
  42. testpath.assert_isfile(os.path.join(self.data_dir, invalid_sql_file))
  43. testpath.assert_isfile(os.path.join(self.data_dir, invalid_sql_file + '.bak'))
  44. def test_algorithms(self):
  45. last_sig = ''
  46. for algo in sign.algorithms:
  47. self.notary.algorithm = algo
  48. sig = self.notary.compute_signature(self.nb)
  49. self.assertNotEqual(last_sig, sig)
  50. last_sig = sig
  51. def test_sign_same(self):
  52. """Multiple signatures of the same notebook are the same"""
  53. sig1 = self.notary.compute_signature(self.nb)
  54. sig2 = self.notary.compute_signature(self.nb)
  55. self.assertEqual(sig1, sig2)
  56. def test_change_secret(self):
  57. """Changing the secret changes the signature"""
  58. sig1 = self.notary.compute_signature(self.nb)
  59. self.notary.secret = b'different'
  60. sig2 = self.notary.compute_signature(self.nb)
  61. self.assertNotEqual(sig1, sig2)
  62. def test_sign(self):
  63. self.assertFalse(self.notary.check_signature(self.nb))
  64. self.notary.sign(self.nb)
  65. self.assertTrue(self.notary.check_signature(self.nb))
  66. def test_unsign(self):
  67. self.notary.sign(self.nb)
  68. self.assertTrue(self.notary.check_signature(self.nb))
  69. self.notary.unsign(self.nb)
  70. self.assertFalse(self.notary.check_signature(self.nb))
  71. self.notary.unsign(self.nb)
  72. self.assertFalse(self.notary.check_signature(self.nb))
  73. def test_cull_db(self):
  74. # this test has various sleeps of 2ms
  75. # to ensure low resolution timestamps compare as expected
  76. dt = 2e-3
  77. nbs = [
  78. copy.deepcopy(self.nb) for i in range(10)
  79. ]
  80. for row in self.notary.store.db.execute("SELECT * FROM nbsignatures"):
  81. print(row)
  82. self.notary.store.cache_size = 8
  83. for i, nb in enumerate(nbs[:8]):
  84. nb.metadata.dirty = i
  85. self.notary.sign(nb)
  86. for i, nb in enumerate(nbs[:8]):
  87. time.sleep(dt)
  88. self.assertTrue(self.notary.check_signature(nb), 'nb %i is trusted' % i)
  89. # signing the 9th triggers culling of first 3
  90. # (75% of 8 = 6, 9 - 6 = 3 culled)
  91. self.notary.sign(nbs[8])
  92. self.assertFalse(self.notary.check_signature(nbs[0]))
  93. self.assertFalse(self.notary.check_signature(nbs[1]))
  94. self.assertFalse(self.notary.check_signature(nbs[2]))
  95. self.assertTrue(self.notary.check_signature(nbs[3]))
  96. # checking nb3 should keep it from being culled:
  97. self.notary.sign(nbs[0])
  98. self.notary.sign(nbs[1])
  99. self.notary.sign(nbs[2])
  100. self.assertTrue(self.notary.check_signature(nbs[3]))
  101. self.assertFalse(self.notary.check_signature(nbs[4]))
  102. def test_check_signature(self):
  103. nb = self.nb
  104. md = nb.metadata
  105. notary = self.notary
  106. check_signature = notary.check_signature
  107. # no signature:
  108. md.pop('signature', None)
  109. self.assertFalse(check_signature(nb))
  110. # hash only, no algo
  111. md.signature = notary.compute_signature(nb)
  112. self.assertFalse(check_signature(nb))
  113. # proper signature, algo mismatch
  114. notary.algorithm = 'sha224'
  115. notary.sign(nb)
  116. notary.algorithm = 'sha256'
  117. self.assertFalse(check_signature(nb))
  118. # check correctly signed notebook
  119. notary.sign(nb)
  120. self.assertTrue(check_signature(nb))
  121. def test_mark_cells_untrusted(self):
  122. cells = self.nb.cells
  123. self.notary.mark_cells(self.nb, False)
  124. for cell in cells:
  125. self.assertNotIn('trusted', cell)
  126. if cell.cell_type == 'code':
  127. self.assertIn('trusted', cell.metadata)
  128. self.assertFalse(cell.metadata.trusted)
  129. else:
  130. self.assertNotIn('trusted', cell.metadata)
  131. def test_mark_cells_trusted(self):
  132. cells = self.nb.cells
  133. self.notary.mark_cells(self.nb, True)
  134. for cell in cells:
  135. self.assertNotIn('trusted', cell)
  136. if cell.cell_type == 'code':
  137. self.assertIn('trusted', cell.metadata)
  138. self.assertTrue(cell.metadata.trusted)
  139. else:
  140. self.assertNotIn('trusted', cell.metadata)
  141. def test_check_cells(self):
  142. nb = self.nb
  143. self.notary.mark_cells(nb, True)
  144. self.assertTrue(self.notary.check_cells(nb))
  145. for cell in nb.cells:
  146. self.assertNotIn('trusted', cell)
  147. self.notary.mark_cells(nb, False)
  148. self.assertFalse(self.notary.check_cells(nb))
  149. for cell in nb.cells:
  150. self.assertNotIn('trusted', cell)
  151. def test_trust_no_output(self):
  152. nb = self.nb
  153. self.notary.mark_cells(nb, False)
  154. for cell in nb.cells:
  155. if cell.cell_type == 'code':
  156. cell.outputs = []
  157. self.assertTrue(self.notary.check_cells(nb))
  158. def test_mark_cells_untrusted_v3(self):
  159. nb = self.nb3
  160. cells = nb.worksheets[0].cells
  161. self.notary.mark_cells(nb, False)
  162. for cell in cells:
  163. self.assertNotIn('trusted', cell)
  164. if cell.cell_type == 'code':
  165. self.assertIn('trusted', cell.metadata)
  166. self.assertFalse(cell.metadata.trusted)
  167. else:
  168. self.assertNotIn('trusted', cell.metadata)
  169. def test_mark_cells_trusted_v3(self):
  170. nb = self.nb3
  171. cells = nb.worksheets[0].cells
  172. self.notary.mark_cells(nb, True)
  173. for cell in cells:
  174. self.assertNotIn('trusted', cell)
  175. if cell.cell_type == 'code':
  176. self.assertIn('trusted', cell.metadata)
  177. self.assertTrue(cell.metadata.trusted)
  178. else:
  179. self.assertNotIn('trusted', cell.metadata)
  180. def test_check_cells_v3(self):
  181. nb = self.nb3
  182. cells = nb.worksheets[0].cells
  183. self.notary.mark_cells(nb, True)
  184. self.assertTrue(self.notary.check_cells(nb))
  185. for cell in cells:
  186. self.assertNotIn('trusted', cell)
  187. self.notary.mark_cells(nb, False)
  188. self.assertFalse(self.notary.check_cells(nb))
  189. for cell in cells:
  190. self.assertNotIn('trusted', cell)
  191. def test_sign_stdin(self):
  192. def sign_stdin(nb):
  193. env = os.environ.copy()
  194. env["JUPYTER_DATA_DIR"] = self.data_dir
  195. p = Popen([sys.executable, '-m', 'nbformat.sign', '--log-level=0'], stdin=PIPE, stdout=PIPE,
  196. env=env,
  197. )
  198. write(nb, codecs.getwriter("utf8")(p.stdin))
  199. p.stdin.close()
  200. p.wait()
  201. self.assertEqual(p.returncode, 0)
  202. out = p.stdout.read().decode('utf8', 'replace')
  203. p.stdout.close()
  204. return out
  205. out = sign_stdin(self.nb3)
  206. self.assertIn('Signing notebook: <stdin>', out)
  207. out = sign_stdin(self.nb3)
  208. self.assertIn('already signed: <stdin>', out)
  209. def test_config_store():
  210. store = sign.MemorySignatureStore()
  211. c = Config()
  212. c.NotebookNotary.store_factory = lambda: store
  213. notary = sign.NotebookNotary(config=c)
  214. assert notary.store is store
  215. class SignatureStoreTests(unittest.TestCase):
  216. def setUp(self):
  217. self.store = sign.MemorySignatureStore()
  218. def test_basics(self):
  219. digest = '0123457689abcef'
  220. algo = 'fake_sha'
  221. assert not self.store.check_signature(digest, algo)
  222. self.store.store_signature(digest, algo)
  223. assert self.store.check_signature(digest, algo)
  224. self.store.remove_signature(digest, algo)
  225. assert not self.store.check_signature(digest, algo)
  226. class SQLiteSignatureStoreTests(SignatureStoreTests):
  227. def setUp(self):
  228. self.store = sign.SQLiteSignatureStore(':memory:')