python-tuf/tuf/ed25519_keys.py
2015-06-02 16:42:41 -04:00

389 lines
14 KiB
Python
Executable file

"""
<Program Name>
ed25519_keys.py
<Author>
Vladimir Diaz <vladimir.v.diaz@gmail.com>
<Started>
September 24, 2013.
<Copyright>
See LICENSE for licensing information.
<Purpose>
The goal of this module is to support ed25519 signatures. ed25519 is an
elliptic-curve public key signature scheme, its main strength being small
signatures (64 bytes) and small public keys (32 bytes).
http://ed25519.cr.yp.to/
'tuf/ed25519_keys.py' calls 'ed25519.py', which is the pure Python
implementation of ed25519 optimized for a faster runtime. The Python
reference implementation is concise, but very slow (verifying signatures
takes ~9 seconds on an Intel core 2 duo @ 2.2 ghz x 2). The optimized
version can verify signatures in ~2 seconds.
http://ed25519.cr.yp.to/software.html
https://github.com/pyca/ed25519
Optionally, ed25519 cryptographic operations may be executed by PyNaCl, which
is a Python binding to the NaCl library and is faster than the pure python
implementation. Verifying signatures can take approximately 0.0009 seconds.
PyNaCl relies on the libsodium C library. PyNaCl is required for key and
signature generation. Verifying signatures may be done in pure Python.
https://github.com/pyca/pynacl
https://github.com/jedisct1/libsodium
http://nacl.cr.yp.to/
https://github.com/pyca/ed25519
The ed25519-related functions included here are generate(), create_signature()
and verify_signature(). The 'ed25519' and PyNaCl (i.e., 'nacl') modules used
by ed25519_keys.py perform the actual ed25519 computations and the functions
listed above can be viewed as an easy-to-use public interface.
"""
# Help with Python 3 compatibility, where the print statement is a function, an
# implicit relative import is invalid, and the '/' operator performs true
# division. Example: print 'hello world' raises a 'SyntaxError' exception.
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
# 'binascii' required for hexadecimal conversions. Signatures and
# public/private keys are hexlified.
import binascii
# TODO: The 'warnings' module needed to temporarily suppress user warnings
# raised by 'pynacl' (as of version 0.2.3). Warnings temporarily suppressed
# here to avoid confusing users with an unexpected error message that gives
# no indication of its source. These warnings are printed when using
# the repository tools, including for clients that request an update.
# http://docs.python.org/2/library/warnings.html#temporarily-suppressing-warnings
import warnings
# 'os' required to generate OS-specific randomness (os.urandom) suitable for
# cryptographic use.
# http://docs.python.org/2/library/os.html#miscellaneous-functions
import os
# Import the python implementation of the ed25519 algorithm provided by pyca,
# which is an optimized version of the one provided by ed25519's authors.
# Note: The pure Python version does not include protection against side-channel
# attacks. Verifying signatures can take approximately 2 seconds on an intel
# core 2 duo @ 2.2 ghz x 2). Optionally, the PyNaCl module may be used to
# speed up ed25519 cryptographic operations.
# http://ed25519.cr.yp.to/software.html
# https://github.com/pyca/ed25519
# https://github.com/pyca/pynacl
#
# Import the PyNaCl library, if available. It is recommended this library be
# used over the pure python implementation of ed25519, due to its speedier
# routines and side-channel protections available in the libsodium library.
#
# TODO: Version 0.2.3 of 'pynacl' prints: "UserWarning: reimporting '...' might
# overwrite older definitions." when importing 'nacl.signing'. Suppress user
# warnings temporarily (at least until this issue is fixed by PyNaCl).
#
# Note: A 'pragma: no cover' comment is intended for test 'coverage'. Lines
# or code blocks with this comment should not be flagged as uncovered.
# pynacl will always be install prior to running the unit tests.
with warnings.catch_warnings():
warnings.simplefilter('ignore')
try:
import nacl.signing
import nacl.encoding
# PyNaCl's 'cffi' dependency may raise an 'IOError' exception when importing
# 'nacl.signing'.
except (ImportError, IOError): # pragma: no cover
pass
# The optimized pure Python implementation of ed25519 provided by TUF. If
# PyNaCl cannot be imported and an attempt to use is made in this module, a
# 'tuf.UnsupportedLibraryError' exception is raised.
import tuf._vendor.ed25519.ed25519
import tuf
# Digest objects needed to generate hashes.
import tuf.hash
# Perform object format-checking.
import tuf.formats
# Supported ed25519 signing method: 'ed25519'. The pure Python implementation
# (i.e., ed25519') and PyNaCl (i.e., 'nacl', libsodium+Python bindings) modules
# are currently supported in the creationg of 'ed25519' signatures.
# Previously, a distinction was made between signatures made by the pure Python
# implementation and PyNaCl.
_SUPPORTED_ED25519_SIGNING_METHODS = ['ed25519']
def generate_public_and_private():
"""
<Purpose>
Generate a pair of ed25519 public and private keys with PyNaCl. The public
and private keys returned conform to 'tuf.formats.ED25519PULIC_SCHEMA' and
'tuf.formats.ED25519SEED_SCHEMA', respectively, and have the form:
'\xa2F\x99\xe0\x86\x80%\xc8\xee\x11\xb95T\xd9\...'
An ed25519 seed key is a random 32-byte string. Public keys are also 32
bytes.
>>> public, private = generate_public_and_private()
>>> tuf.formats.ED25519PUBLIC_SCHEMA.matches(public)
True
>>> tuf.formats.ED25519SEED_SCHEMA.matches(private)
True
<Arguments>
None.
<Exceptions>
tuf.UnsupportedLibraryError, if the PyNaCl ('nacl') module is unavailable.
NotImplementedError, if a randomness source is not found by 'os.urandom'.
<Side Effects>
The ed25519 keys are generated by first creating a random 32-byte seed
with os.urandom() and then calling PyNaCl's nacl.signing.SigningKey().
<Returns>
A (public, private) tuple that conform to 'tuf.formats.ED25519PUBLIC_SCHEMA'
and 'tuf.formats.ED25519SEED_SCHEMA', respectively.
"""
# Generate ed25519's seed key by calling os.urandom(). The random bytes
# returned should be suitable for cryptographic use and is OS-specific.
# Raise 'NotImplementedError' if a randomness source is not found.
# ed25519 seed keys are fixed at 32 bytes (256-bit keys).
# http://blog.mozilla.org/warner/2011/11/29/ed25519-keys/
seed = os.urandom(32)
public = None
# Generate the public key. PyNaCl (i.e., 'nacl' module) performs the actual
# key generation.
try:
nacl_key = nacl.signing.SigningKey(seed)
public = nacl_key.verify_key.encode(encoder=nacl.encoding.RawEncoder())
except NameError: # pragma: no cover
message = 'The PyNaCl library and/or its dependencies unavailable.'
raise tuf.UnsupportedLibraryError(message)
return public, seed
def create_signature(public_key, private_key, data):
"""
<Purpose>
Return a (signature, method) tuple, where the method is 'ed25519' and is
always generated by PyNaCl (i.e., 'nacl'). The signature returned conforms
to 'tuf.formats.ED25519SIGNATURE_SCHEMA', and has the form:
'\xae\xd7\x9f\xaf\x95{bP\x9e\xa8YO Z\x86\x9d...'
A signature is a 64-byte string.
>>> public, private = generate_public_and_private()
>>> data = b'The quick brown fox jumps over the lazy dog'
>>> signature, method = \
create_signature(public, private, data)
>>> tuf.formats.ED25519SIGNATURE_SCHEMA.matches(signature)
True
>>> method == 'ed25519'
True
>>> signature, method = \
create_signature(public, private, data)
>>> tuf.formats.ED25519SIGNATURE_SCHEMA.matches(signature)
True
>>> method == 'ed25519'
True
<Arguments>
public:
The ed25519 public key, which is a 32-byte string.
private:
The ed25519 private key, which is a 32-byte string.
data:
Data object used by create_signature() to generate the signature.
<Exceptions>
tuf.FormatError, if the arguments are improperly formatted.
tuf.CryptoError, if a signature cannot be created.
<Side Effects>
nacl.signing.SigningKey.sign() called to generate the actual signature.
<Returns>
A signature dictionary conformat to 'tuf.format.SIGNATURE_SCHEMA'.
ed25519 signatures are 64 bytes, however, the hexlified signature is
stored in the dictionary returned.
"""
# Does 'public_key' have the correct format?
# This check will ensure 'public_key' conforms to
# 'tuf.formats.ED25519PUBLIC_SCHEMA', which must have length 32 bytes.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.ED25519PUBLIC_SCHEMA.check_match(public_key)
# Is 'private_key' properly formatted?
tuf.formats.ED25519SEED_SCHEMA.check_match(private_key)
# Signing the 'data' object requires a seed and public key.
# nacl.signing.SigningKey.sign() generates the signature.
public = public_key
private = private_key
method = None
signature = None
# The private and public keys have been validated above by 'tuf.formats' and
# should be 32-byte strings.
method = 'ed25519'
try:
nacl_key = nacl.signing.SigningKey(private)
nacl_sig = nacl_key.sign(data)
signature = nacl_sig.signature
except NameError: # pragma: no cover
message = 'The PyNaCl library and/or its dependencies unavailable.'
raise tuf.UnsupportedLibraryError(message)
except (ValueError, TypeError, nacl.exceptions.CryptoError) as e:
message = 'An "ed25519" signature could not be created with PyNaCl.'
raise tuf.CryptoError(message + str(e))
return signature, method
def verify_signature(public_key, method, signature, data, use_pynacl=False):
"""
<Purpose>
Determine whether the private key corresponding to 'public_key' produced
'signature'. verify_signature() will use the public key, the 'method' and
'sig', and 'data' arguments to complete the verification.
>>> public, private = generate_public_and_private()
>>> data = b'The quick brown fox jumps over the lazy dog'
>>> signature, method = \
create_signature(public, private, data)
>>> verify_signature(public, method, signature, data, use_pynacl=False)
True
>>> verify_signature(public, method, signature, data, use_pynacl=True)
True
>>> bad_data = b'The sly brown fox jumps over the lazy dog'
>>> bad_signature, method = \
create_signature(public, private, bad_data)
>>> verify_signature(public, method, bad_signature, data, use_pynacl=False)
False
<Arguments>
public_key:
The public key is a 32-byte string.
method:
'ed25519' signature method generated by either the pure python
implementation (i.e., ed25519.py) or PyNacl (i.e., 'nacl').
signature:
The signature is a 64-byte string.
data:
Data object used by tuf.ed25519_keys.create_signature() to generate
'signature'. 'data' is needed here to verify the signature.
use_pynacl:
True, if the ed25519 signature should be verified by PyNaCl. False,
if the signature should be verified with the pure Python implementation
of ed25519 (slower).
<Exceptions>
tuf.UnknownMethodError. Raised if the signing method used by
'signature' is not one supported by tuf.ed25519_keys.create_signature().
tuf.FormatError. Raised if the arguments are improperly formatted.
<Side Effects>
tuf._vendor.ed25519.ed25519.checkvalid() called to do the actual
verification. nacl.signing.VerifyKey.verify() called if 'use_pynacl' is
True.
<Returns>
Boolean. True if the signature is valid, False otherwise.
"""
# Does 'public_key' have the correct format?
# This check will ensure 'public_key' conforms to
# 'tuf.formats.ED25519PUBLIC_SCHEMA', which must have length 32 bytes.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.ED25519PUBLIC_SCHEMA.check_match(public_key)
# Is 'method' properly formatted?
tuf.formats.NAME_SCHEMA.check_match(method)
# Is 'signature' properly formatted?
tuf.formats.ED25519SIGNATURE_SCHEMA.check_match(signature)
# Is 'use_pynacl' properly formatted?
tuf.formats.BOOLEAN_SCHEMA.check_match(use_pynacl)
# Verify 'signature'. Before returning the Boolean result,
# ensure 'ed25519' was used as the signing method.
# Raise 'tuf.UnsupportedLibraryError' if 'use_pynacl' is True but 'nacl' is
# unavailable.
public = public_key
valid_signature = False
if method in _SUPPORTED_ED25519_SIGNING_METHODS:
if use_pynacl:
try:
nacl_verify_key = nacl.signing.VerifyKey(public)
nacl_message = nacl_verify_key.verify(data, signature)
valid_signature = True
except NameError: # pragma: no cover
message = 'The PyNaCl library and/or its dependencies unavailable.'
raise tuf.UnsupportedLibraryError(message)
except nacl.exceptions.BadSignatureError:
pass
# Verify 'ed25519' signature with the pure Python implementation.
else:
try:
tuf._vendor.ed25519.ed25519.checkvalid(signature, data, public)
valid_signature = True
# The pure Python implementation raises 'Exception' if 'signature' is
# invalid.
except Exception as e:
pass
else:
message = 'Unsupported ed25519 signing method: '+repr(method)+'.\n'+ \
'Supported methods: '+repr(_SUPPORTED_ED25519_SIGNING_METHODS)+'.'
raise tuf.UnknownMethodError(message)
return valid_signature
if __name__ == '__main__':
# The interactive sessions of the documentation strings can
# be tested by running 'ed25519_keys.py' as a standalone module.
# python -B ed25519_keys.py
import doctest
doctest.testmod()