##// END OF EJS Templates
rename "new-file" to "new > text file"...
rename "new-file" to "new > text file" Because it is not obvious to everyone... Closes #7571

File last commit:

r19629:6075fc10
r20143:8cf9971b
Show More
sign.py
427 lines | 14.2 KiB | text/x-python | PythonLexer
Min RK
don't store signatures in notebooks...
r19625 """Utilities for signing notebooks"""
MinRK
add notebook signing to nbformat
r14855
MinRK
trust is stored in code_cell.metadata...
r18255 # Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.
MinRK
add notebook signing to nbformat
r14855
MinRK
add nbformat.sign.NotebookNotary
r14857 import base64
MinRK
add notebook signing to nbformat
r14855 from contextlib import contextmanager
Min RK
don't store signatures in notebooks...
r19625 from datetime import datetime
MinRK
add notebook signing to nbformat
r14855 import hashlib
from hmac import HMAC
MinRK
add nbformat.sign.NotebookNotary
r14857 import io
import os
MinRK
add notebook signing to nbformat
r14855
Min RK
don't store signatures in notebooks...
r19625 try:
import sqlite3
except ImportError:
try:
from pysqlite2 import dbapi2 as sqlite3
except ImportError:
sqlite3 = None
MinRK
Add top-level IPython.nbformat API...
r18603 from IPython.utils.io import atomic_writing
Min RK
don't store signatures in notebooks...
r19625 from IPython.utils.py3compat import unicode_type, cast_bytes
from IPython.utils.traitlets import Instance, Bytes, Enum, Any, Unicode, Bool, Integer
MinRK
tweak default profile_dir...
r14862 from IPython.config import LoggingConfigurable, MultipleInstanceError
MinRK
add `ipython trust --reset
r14909 from IPython.core.application import BaseIPythonApplication, base_flags
MinRK
move `ipython notebook trust` to `ipython trust`...
r14861
MinRK
Add top-level IPython.nbformat API...
r18603 from . import read, write, NO_CONVERT
MinRK
add notebook signing to nbformat
r14855
MinRK
Python 3 renamed hashlib.algorithms to algorithms_guaranteed
r14908 try:
# Python 3
algorithms = hashlib.algorithms_guaranteed
except AttributeError:
algorithms = hashlib.algorithms
MinRK
add notebook signing to nbformat
r14855
MinRK
trust is stored in code_cell.metadata...
r18255
MinRK
add notebook signing to nbformat
r14855 def yield_everything(obj):
"""Yield every item in a container as bytes
Allows any JSONable object to be passed to an HMAC digester
without having to serialize the whole thing.
"""
if isinstance(obj, dict):
for key in sorted(obj):
value = obj[key]
yield cast_bytes(key)
for b in yield_everything(value):
yield b
elif isinstance(obj, (list, tuple)):
for element in obj:
for b in yield_everything(element):
yield b
elif isinstance(obj, unicode_type):
yield obj.encode('utf8')
else:
yield unicode_type(obj).encode('utf8')
MinRK
restore ability to sign v3 notebooks
r18612 def yield_code_cells(nb):
"""Iterator that yields all cells in a notebook
nbformat version independent
"""
if nb.nbformat >= 4:
for cell in nb['cells']:
if cell['cell_type'] == 'code':
yield cell
elif nb.nbformat == 3:
for ws in nb['worksheets']:
for cell in ws['cells']:
if cell['cell_type'] == 'code':
yield cell
MinRK
add notebook signing to nbformat
r14855
@contextmanager
def signature_removed(nb):
"""Context manager for operating on a notebook with its signature removed
Used for excluding the previous signature when computing a notebook's signature.
"""
save_signature = nb['metadata'].pop('signature', None)
try:
yield
finally:
if save_signature is not None:
nb['metadata']['signature'] = save_signature
MinRK
add nbformat.sign.NotebookNotary
r14857 class NotebookNotary(LoggingConfigurable):
MinRK
Notaries sign notebooks now
r14860 """A class for computing and verifying notebook signatures."""
MinRK
add nbformat.sign.NotebookNotary
r14857
profile_dir = Instance("IPython.core.profiledir.ProfileDir")
def _profile_dir_default(self):
from IPython.core.application import BaseIPythonApplication
MinRK
tweak default profile_dir...
r14862 app = None
try:
if BaseIPythonApplication.initialized():
app = BaseIPythonApplication.instance()
except MultipleInstanceError:
pass
if app is None:
MinRK
add nbformat.sign.NotebookNotary
r14857 # create an app, without the global instance
app = BaseIPythonApplication()
Thomas Kluyver
Ignore sys.argv for NotebookNotary in tests
r14954 app.initialize(argv=[])
MinRK
add nbformat.sign.NotebookNotary
r14857 return app.profile_dir
Min RK
automatically cull oldest 25% of signatures...
r19626 db_file = Unicode(config=True,
help="""The sqlite file in which to store notebook signatures.
By default, this will be in your IPython profile.
You can set it to ':memory:' to disable sqlite writing to the filesystem.
""")
Min RK
don't store signatures in notebooks...
r19625 def _db_file_default(self):
if self.profile_dir is None:
return ':memory:'
return os.path.join(self.profile_dir.security_dir, u'nbsignatures.db')
# 64k entries ~ 12MB
Min RK
automatically cull oldest 25% of signatures...
r19626 cache_size = Integer(65535, config=True,
help="""The number of notebook signatures to cache.
When the number of signatures exceeds this value,
the oldest 25% of signatures will be culled.
"""
)
Min RK
don't store signatures in notebooks...
r19625 db = Any()
def _db_default(self):
if sqlite3 is None:
self.log.warn("Missing SQLite3, all notebooks will be untrusted!")
return
kwargs = dict(detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES)
db = sqlite3.connect(self.db_file, **kwargs)
self.init_db(db)
return db
def init_db(self, db):
db.execute("""
CREATE TABLE IF NOT EXISTS nbsignatures
(
id integer PRIMARY KEY AUTOINCREMENT,
algorithm text,
signature text,
path text,
last_seen timestamp
)""")
db.execute("""
CREATE INDEX IF NOT EXISTS algosig ON nbsignatures(algorithm, signature)
""")
db.commit()
MinRK
Python 3 renamed hashlib.algorithms to algorithms_guaranteed
r14908 algorithm = Enum(algorithms, default_value='sha256', config=True,
MinRK
Notaries sign notebooks now
r14860 help="""The hashing algorithm used to sign notebooks."""
)
def _algorithm_changed(self, name, old, new):
self.digestmod = getattr(hashlib, self.algorithm)
digestmod = Any()
def _digestmod_default(self):
return getattr(hashlib, self.algorithm)
MinRK
Make Notary.secret_file configurable
r15792 secret_file = Unicode(config=True,
help="""The file where the secret key is stored."""
)
MinRK
Notaries sign notebooks now
r14860 def _secret_file_default(self):
if self.profile_dir is None:
return ''
return os.path.join(self.profile_dir.security_dir, 'notebook_secret')
MinRK
add nbformat.sign.NotebookNotary
r14857 secret = Bytes(config=True,
help="""The secret key with which notebooks are signed."""
)
def _secret_default(self):
# note : this assumes an Application is running
MinRK
Notaries sign notebooks now
r14860 if os.path.exists(self.secret_file):
with io.open(self.secret_file, 'rb') as f:
MinRK
add nbformat.sign.NotebookNotary
r14857 return f.read()
else:
secret = base64.encodestring(os.urandom(1024))
MinRK
Notaries sign notebooks now
r14860 self._write_secret_file(secret)
MinRK
add nbformat.sign.NotebookNotary
r14857 return secret
MinRK
Notaries sign notebooks now
r14860
def _write_secret_file(self, secret):
"""write my secret to my secret_file"""
MinRK
add `ipython trust --reset
r14909 self.log.info("Writing notebook-signing key to %s", self.secret_file)
MinRK
Notaries sign notebooks now
r14860 with io.open(self.secret_file, 'wb') as f:
f.write(secret)
try:
os.chmod(self.secret_file, 0o600)
except OSError:
self.log.warn(
"Could not set permissions on %s",
self.secret_file
)
return secret
def compute_signature(self, nb):
"""Compute a notebook's signature
by hashing the entire contents of the notebook via HMAC digest.
"""
hmac = HMAC(self.secret, digestmod=self.digestmod)
# don't include the previous hash in the content to hash
with signature_removed(nb):
# sign the whole thing
for b in yield_everything(nb):
hmac.update(b)
return hmac.hexdigest()
def check_signature(self, nb):
"""Check a notebook's stored signature
If a signature is stored in the notebook's metadata,
a new signature is computed and compared with the stored value.
Returns True if the signature is found and matches, False otherwise.
The following conditions must all be met for a notebook to be trusted:
- a signature is stored in the form 'scheme:hexdigest'
- the stored scheme matches the requested scheme
- the requested scheme is available from hashlib
- the computed hash from notebook_signature matches the stored hash
"""
MinRK
restore ability to sign v3 notebooks
r18612 if nb.nbformat < 3:
return False
Min RK
don't store signatures in notebooks...
r19625 if self.db is None:
MinRK
Notaries sign notebooks now
r14860 return False
Min RK
don't store signatures in notebooks...
r19625 signature = self.compute_signature(nb)
r = self.db.execute("""SELECT id FROM nbsignatures WHERE
algorithm = ? AND
signature = ?;
""", (self.algorithm, signature)).fetchone()
if r is None:
MinRK
Notaries sign notebooks now
r14860 return False
Min RK
don't store signatures in notebooks...
r19625 self.db.execute("""UPDATE nbsignatures SET last_seen = ? WHERE
algorithm = ? AND
signature = ?;
""",
(datetime.utcnow(), self.algorithm, signature),
)
self.db.commit()
return True
MinRK
Notaries sign notebooks now
r14860
def sign(self, nb):
Min RK
don't store signatures in notebooks...
r19625 """Sign a notebook, indicating that its output is trusted on this machine
MinRK
Notaries sign notebooks now
r14860
Min RK
don't store signatures in notebooks...
r19625 Stores hash algorithm and hmac digest in a local database of trusted notebooks.
MinRK
Notaries sign notebooks now
r14860 """
MinRK
restore ability to sign v3 notebooks
r18612 if nb.nbformat < 3:
return
MinRK
Notaries sign notebooks now
r14860 signature = self.compute_signature(nb)
Min RK
don't store signatures in notebooks...
r19625 self.store_signature(signature, nb)
def store_signature(self, signature, nb):
if self.db is None:
return
self.db.execute("""INSERT OR IGNORE INTO nbsignatures
(algorithm, signature, last_seen) VALUES (?, ?, ?)""",
(self.algorithm, signature, datetime.utcnow())
)
self.db.execute("""UPDATE nbsignatures SET last_seen = ? WHERE
algorithm = ? AND
signature = ?;
""",
(datetime.utcnow(), self.algorithm, signature),
)
self.db.commit()
Min RK
automatically cull oldest 25% of signatures...
r19626 n, = self.db.execute("SELECT Count(*) FROM nbsignatures").fetchone()
if n > self.cache_size:
self.cull_db()
Min RK
don't store signatures in notebooks...
r19625
def unsign(self, nb):
"""Ensure that a notebook is untrusted
by removing its signature from the trusted database, if present.
"""
signature = self.compute_signature(nb)
self.db.execute("""DELETE FROM nbsignatures WHERE
algorithm = ? AND
signature = ?;
""",
(self.algorithm, signature)
)
self.db.commit()
def cull_db(self):
Min RK
automatically cull oldest 25% of signatures...
r19626 """Cull oldest 25% of the trusted signatures when the size limit is reached"""
Min RK
don't store signatures in notebooks...
r19625 self.db.execute("""DELETE FROM nbsignatures WHERE id IN (
SELECT id FROM nbsignatures ORDER BY last_seen DESC LIMIT -1 OFFSET ?
);
Min RK
automatically cull oldest 25% of signatures...
r19626 """, (max(int(0.75 * self.cache_size), 1),))
MinRK
Notaries sign notebooks now
r14860
def mark_cells(self, nb, trusted):
"""Mark cells as trusted if the notebook's signature can be verified
MinRK
trust is stored in code_cell.metadata...
r18255 Sets ``cell.metadata.trusted = True | False`` on all code cells,
MinRK
Notaries sign notebooks now
r14860 depending on whether the stored signature can be verified.
This function is the inverse of check_cells
"""
MinRK
restore ability to sign v3 notebooks
r18612 if nb.nbformat < 3:
return
for cell in yield_code_cells(nb):
cell['metadata']['trusted'] = trusted
MinRK
Notaries sign notebooks now
r14860
MinRK
restore ability to sign v3 notebooks
r18612 def _check_cell(self, cell, nbformat_version):
MinRK
trust cells with no unsafe output...
r15271 """Do we trust an individual cell?
Return True if:
- cell is explicitly trusted
- cell has no potentially unsafe rich output
If a cell has no output, or only simple print statements,
it will always be trusted.
"""
# explicitly trusted
MinRK
trust is stored in code_cell.metadata...
r18255 if cell['metadata'].pop("trusted", False):
MinRK
trust cells with no unsafe output...
r15271 return True
# explicitly safe output
MinRK
restore ability to sign v3 notebooks
r18612 if nbformat_version >= 4:
unsafe_output_types = ['execute_result', 'display_data']
safe_keys = {"output_type", "execution_count", "metadata"}
else: # v3
unsafe_output_types = ['pyout', 'display_data']
safe_keys = {"output_type", "prompt_number", "metadata"}
MinRK
trust cells with no unsafe output...
r15271
for output in cell['outputs']:
output_type = output['output_type']
MinRK
restore ability to sign v3 notebooks
r18612 if output_type in unsafe_output_types:
MinRK
trust cells with no unsafe output...
r15271 # if there are any data keys not in the safe whitelist
MinRK
restore ability to sign v3 notebooks
r18612 output_keys = set(output)
if output_keys.difference(safe_keys):
MinRK
trust cells with no unsafe output...
r15271 return False
return True
MinRK
Notaries sign notebooks now
r14860 def check_cells(self, nb):
"""Return whether all code cells are trusted
If there are no code cells, return True.
This function is the inverse of mark_cells.
"""
MinRK
restore ability to sign v3 notebooks
r18612 if nb.nbformat < 3:
return False
MinRK
don't write cell.trusted to disk...
r15023 trusted = True
MinRK
restore ability to sign v3 notebooks
r18612 for cell in yield_code_cells(nb):
MinRK
trust cells with no unsafe output...
r15271 # only distrust a cell if it actually has some output to distrust
MinRK
restore ability to sign v3 notebooks
r18612 if not self._check_cell(cell, nb.nbformat):
MinRK
don't write cell.trusted to disk...
r15023 trusted = False
MinRK
trust is stored in code_cell.metadata...
r18255
MinRK
don't write cell.trusted to disk...
r15023 return trusted
MinRK
move `ipython notebook trust` to `ipython trust`...
r14861
MinRK
add `ipython trust --reset
r14909 trust_flags = {
'reset' : (
{'TrustNotebookApp' : { 'reset' : True}},
Min RK
clear signature db on reset
r19629 """Delete the trusted notebook cache.
MinRK
add `ipython trust --reset
r14909 All previously signed notebooks will become untrusted.
"""
),
}
trust_flags.update(base_flags)
trust_flags.pop('init')
MinRK
move `ipython notebook trust` to `ipython trust`...
r14861 class TrustNotebookApp(BaseIPythonApplication):
description="""Sign one or more IPython notebooks with your key,
to trust their dynamic (HTML, Javascript) output.
MinRK
Notaries sign notebooks now
r14860
MinRK
add some documentation notes about trust being per-profile
r17168 Trusting a notebook only applies to the current IPython profile.
To trust a notebook for use with a profile other than default,
add `--profile [profile name]`.
MinRK
move `ipython notebook trust` to `ipython trust`...
r14861 Otherwise, you will have to re-execute the notebook to see output.
"""
MinRK
add some documentation notes about trust being per-profile
r17168 examples = """
ipython trust mynotebook.ipynb and_this_one.ipynb
ipython trust --profile myprofile mynotebook.ipynb
"""
MinRK
add `ipython trust --reset
r14909
flags = trust_flags
reset = Bool(False, config=True,
Min RK
clear signature db on reset
r19629 help="""If True, delete the trusted signature cache.
MinRK
add `ipython trust --reset
r14909 After reset, all previously signed notebooks will become untrusted.
"""
)
MinRK
move `ipython notebook trust` to `ipython trust`...
r14861
notary = Instance(NotebookNotary)
def _notary_default(self):
return NotebookNotary(parent=self, profile_dir=self.profile_dir)
def sign_notebook(self, notebook_path):
if not os.path.exists(notebook_path):
self.log.error("Notebook missing: %s" % notebook_path)
self.exit(1)
with io.open(notebook_path, encoding='utf8') as f:
MinRK
Add top-level IPython.nbformat API...
r18603 nb = read(f, NO_CONVERT)
MinRK
move `ipython notebook trust` to `ipython trust`...
r14861 if self.notary.check_signature(nb):
print("Notebook already signed: %s" % notebook_path)
else:
print("Signing notebook: %s" % notebook_path)
self.notary.sign(nb)
MinRK
Add top-level IPython.nbformat API...
r18603 with atomic_writing(notebook_path) as f:
Min RK
fix backward `f, nb` args for nbformat.write
r18613 write(nb, f, NO_CONVERT)
MinRK
move `ipython notebook trust` to `ipython trust`...
r14861
MinRK
add `ipython trust --reset
r14909 def generate_new_key(self):
"""Generate a new notebook signature key"""
print("Generating new notebook key: %s" % self.notary.secret_file)
self.notary._write_secret_file(os.urandom(1024))
MinRK
move `ipython notebook trust` to `ipython trust`...
r14861 def start(self):
MinRK
add `ipython trust --reset
r14909 if self.reset:
Min RK
clear signature db on reset
r19629 if os.path.exists(self.notary.db_file):
print("Removing trusted signature cache: %s" % self.notary.db_file)
os.remove(self.notary.db_file)
MinRK
add `ipython trust --reset
r14909 self.generate_new_key()
return
MinRK
move `ipython notebook trust` to `ipython trust`...
r14861 if not self.extra_args:
self.log.critical("Specify at least one notebook to sign.")
self.exit(1)
for notebook_path in self.extra_args:
self.sign_notebook(notebook_path)
MinRK
add nbformat.sign.NotebookNotary
r14857