##// END OF EJS Templates
Notaries sign notebooks now
Notaries sign notebooks now

File last commit:

r14860:98aa10c5
r14860:98aa10c5
Show More
sign.py
202 lines | 7.0 KiB | text/x-python | PythonLexer
"""Functions for signing notebooks"""
#-----------------------------------------------------------------------------
# Copyright (C) 2014, The IPython Development Team
#
# Distributed under the terms of the BSD License. The full license is in
# the file COPYING, distributed as part of this software.
#-----------------------------------------------------------------------------
#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------
import base64
from contextlib import contextmanager
import hashlib
from hmac import HMAC
import io
import os
from IPython.utils.py3compat import string_types, unicode_type, cast_bytes
from IPython.config import LoggingConfigurable
from IPython.utils.traitlets import Instance, Bytes, Enum, Any, Unicode
#-----------------------------------------------------------------------------
# Code
#-----------------------------------------------------------------------------
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')
@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
class NotebookNotary(LoggingConfigurable):
"""A class for computing and verifying notebook signatures."""
profile_dir = Instance("IPython.core.profiledir.ProfileDir")
def _profile_dir_default(self):
from IPython.core.application import BaseIPythonApplication
if BaseIPythonApplication.initialized():
app = BaseIPythonApplication.instance()
else:
# create an app, without the global instance
app = BaseIPythonApplication()
app.initialize()
return app.profile_dir
algorithm = Enum(hashlib.algorithms, default_value='sha256', config=True,
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)
secret_file = Unicode()
def _secret_file_default(self):
if self.profile_dir is None:
return ''
return os.path.join(self.profile_dir.security_dir, 'notebook_secret')
secret = Bytes(config=True,
help="""The secret key with which notebooks are signed."""
)
def _secret_default(self):
# note : this assumes an Application is running
if os.path.exists(self.secret_file):
with io.open(self.secret_file, 'rb') as f:
return f.read()
else:
secret = base64.encodestring(os.urandom(1024))
self._write_secret_file(secret)
return secret
def _write_secret_file(self, secret):
"""write my secret to my secret_file"""
self.log.info("Writing output secret to %s", self.secret_file)
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
"""
stored_signature = nb['metadata'].get('signature', None)
if not stored_signature \
or not isinstance(stored_signature, string_types) \
or ':' not in stored_signature:
return False
stored_algo, sig = stored_signature.split(':', 1)
if self.algorithm != stored_algo:
return False
my_signature = self.compute_signature(nb)
return my_signature == sig
def sign(self, nb):
"""Sign a notebook, indicating that its output is trusted
stores 'algo:hmac-hexdigest' in notebook.metadata.signature
e.g. 'sha256:deadbeef123...'
"""
signature = self.compute_signature(nb)
nb['metadata']['signature'] = "%s:%s" % (self.algorithm, signature)
def mark_cells(self, nb, trusted):
"""Mark cells as trusted if the notebook's signature can be verified
Sets ``cell.trusted = True | False`` on all code cells,
depending on whether the stored signature can be verified.
This function is the inverse of check_cells
"""
if not nb['worksheets']:
# nothing to mark if there are no cells
return
for cell in nb['worksheets'][0]['cells']:
if cell['cell_type'] == 'code':
cell['trusted'] = trusted
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.
"""
if not nb['worksheets']:
return True
for cell in nb['worksheets'][0]['cells']:
if cell['cell_type'] != 'code':
continue
if not cell.get('trusted', False):
return False
return True