diff options
author | Paul Kehrer <paul.l.kehrer@gmail.com> | 2014-01-11 21:59:08 -0600 |
---|---|---|
committer | Paul Kehrer <paul.l.kehrer@gmail.com> | 2014-01-11 21:59:08 -0600 |
commit | 74169660e47b760f82c0653b4210b3bc5d3bf46b (patch) | |
tree | 33835b3e733f6b918b56240200e29402092cc384 | |
parent | 9ad4d755bb3a2edfb8e46b60f6dfaff6365f0386 (diff) | |
parent | 089a860f2d3f0ac923fc3f78190055990a940e2a (diff) | |
download | cryptography-74169660e47b760f82c0653b4210b3bc5d3bf46b.tar.gz cryptography-74169660e47b760f82c0653b4210b3bc5d3bf46b.tar.bz2 cryptography-74169660e47b760f82c0653b4210b3bc5d3bf46b.zip |
Merge branch 'master' into urandom-engine
* master: (169 commits)
Make just one call to ffi.cdef for most of the definitions
Use pytest.fixture for backends
drop to >= 0.8 to make pypy happy
change to anonymous enum
require cffi >= 0.8.1
remove extraneous spaces
add hmac to commoncrypto binding
bytes byte back
add check to confirm we've loaded error strings
Bind all the PEM errors
Spelling!
oops, bytes plz
don't leak a context in the test
add tests to the openssl backend to verify that we've registered
Nonsense I think we need.
This is a dep
init the ssl library in the backend
Actuall install a thing
Try to run the spellchecker on travis
Use a normal quote here, not sure where the smart quote came from
...
Conflicts:
cryptography/hazmat/bindings/openssl/binding.py
tests/hazmat/backends/test_openssl.py
55 files changed, 1519 insertions, 360 deletions
@@ -5,8 +5,6 @@ _build/ .cache/ *.egg-info/ .coverage -cffi-*.egg/ -pycparser-*.egg/ -pytest-*.egg/ +*.egg dist/ htmlcov/ diff --git a/.travis.yml b/.travis.yml index 0a8771ef..babea99b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,35 +1,36 @@ -language: python -python: 2.7 +language: c +os: + - linux + - osx +compiler: + - clang + - gcc env: - - TOX_ENV=py26 - - TOX_ENV=py27 - - TOX_ENV=py32 - - TOX_ENV=py33 - - TOX_ENV=pypy - - TOX_ENV=py26 CC=clang - - TOX_ENV=py27 CC=clang - - TOX_ENV=py32 CC=clang - - TOX_ENV=py33 CC=clang - - TOX_ENV=pypy CC=clang - - TOX_ENV=py26 OPENSSL=0.9.8 - - TOX_ENV=py27 OPENSSL=0.9.8 - - TOX_ENV=py32 OPENSSL=0.9.8 - - TOX_ENV=py33 OPENSSL=0.9.8 - - TOX_ENV=pypy OPENSSL=0.9.8 - - TOX_ENV=py26 CC=clang OPENSSL=0.9.8 - - TOX_ENV=py27 CC=clang OPENSSL=0.9.8 - - TOX_ENV=py32 CC=clang OPENSSL=0.9.8 - - TOX_ENV=py33 CC=clang OPENSSL=0.9.8 - - TOX_ENV=pypy CC=clang OPENSSL=0.9.8 - - TOX_ENV=docs - - TOX_ENV=pep8 - - TOX_ENV=py3pep8 + # this global section can be removed when + # https://github.com/travis-ci/travis-ci/issues/1844 is fixed + global: + - CI=true + - TRAVIS=true + matrix: + - TOX_ENV=py26 + - TOX_ENV=py27 + - TOX_ENV=py32 + - TOX_ENV=py33 + - TOX_ENV=pypy + - TOX_ENV=py26 OPENSSL=0.9.8 + - TOX_ENV=py27 OPENSSL=0.9.8 + - TOX_ENV=py32 OPENSSL=0.9.8 + - TOX_ENV=py33 OPENSSL=0.9.8 + - TOX_ENV=pypy OPENSSL=0.9.8 + - TOX_ENV=docs + - TOX_ENV=pep8 + - TOX_ENV=py3pep8 install: - ./.travis/install.sh script: - - tox -e $TOX_ENV + - if [[ "$(uname -s)" == "Darwin" ]]; then eval "$(pyenv init -)"; fi && source ~/.venv/bin/activate && tox -e $TOX_ENV after_success: - coveralls @@ -40,3 +41,82 @@ notifications: - "irc.freenode.org#cryptography-dev" use_notice: true skip_join: true + +# When building an exclude matrix on Travis you must supply +# the exact variable combinations you want to exclude from +# your build matrix. There is no (current) way to make this +# less verbose. +matrix: + exclude: + - os: osx + env: TOX_ENV=py26 + compiler: gcc + - os: osx + env: TOX_ENV=py27 + compiler: gcc + - os: osx + env: TOX_ENV=py32 + compiler: gcc + - os: osx + env: TOX_ENV=py33 + compiler: gcc + - os: osx + env: TOX_ENV=pypy + compiler: gcc + - os: osx + env: TOX_ENV=py26 + compiler: clang + - os: osx + env: TOX_ENV=py27 + compiler: clang + - os: osx + env: TOX_ENV=py32 + compiler: clang + - os: osx + env: TOX_ENV=py33 + compiler: clang + - os: osx + env: TOX_ENV=pypy + compiler: clang + - os: osx + env: TOX_ENV=py26 OPENSSL=0.9.8 + compiler: gcc + - os: osx + env: TOX_ENV=py27 OPENSSL=0.9.8 + compiler: gcc + - os: osx + env: TOX_ENV=py32 OPENSSL=0.9.8 + compiler: gcc + - os: osx + env: TOX_ENV=py33 OPENSSL=0.9.8 + compiler: gcc + - os: osx + env: TOX_ENV=pypy OPENSSL=0.9.8 + compiler: gcc + - os: osx + env: TOX_ENV=docs + compiler: gcc + - os: osx + env: TOX_ENV=pep8 + compiler: gcc + - os: osx + env: TOX_ENV=py3pep8 + compiler: gcc + - os: osx + env: TOX_ENV=docs + compiler: clang + - os: osx + env: TOX_ENV=pep8 + compiler: clang + - os: osx + env: TOX_ENV=py3pep8 + compiler: clang + - os: linux + env: TOX_ENV=docs + compiler: clang + - os: linux + env: TOX_ENV=pep8 + compiler: clang + - os: linux + env: TOX_ENV=py3pep8 + compiler: clang diff --git a/.travis/install.sh b/.travis/install.sh index fdd71907..e6ea2537 100755 --- a/.travis/install.sh +++ b/.travis/install.sh @@ -3,26 +3,82 @@ set -e set -x -if [[ "${OPENSSL}" == "0.9.8" ]]; then +if [[ "${OPENSSL}" == "0.9.8" && "$(uname -s)" != "Darwin" ]]; then sudo add-apt-repository "deb http://archive.ubuntu.com/ubuntu/ lucid main" + sudo apt-get -y update + sudo apt-get install -y --force-yes libssl-dev/lucid fi -if [[ "${TOX_ENV}" == "pypy" ]]; then - sudo add-apt-repository -y ppa:pypy/ppa +if [[ "${TOX_ENV}" == "docs" && "$(name -s)" != "Darwin" ]]; then + sudo apt-get -y update + sudo apt-get install libenchant-dev fi -sudo apt-get -y update +if [[ "$(uname -s)" == "Darwin" ]]; then + brew update + brew install pyenv + if which pyenv > /dev/null; then eval "$(pyenv init -)"; fi + case "${TOX_ENV}" in + py26) + curl -O https://raw.github.com/pypa/pip/master/contrib/get-pip.py + sudo python get-pip.py + sudo pip install setuptools --no-use-wheel --upgrade + sudo pip install virtualenv + ;; + py27) + curl -O https://raw.github.com/pypa/pip/master/contrib/get-pip.py + sudo python get-pip.py + sudo pip install setuptools --no-use-wheel --upgrade + sudo pip install virtualenv + ;; + pypy) + pyenv install pypy-2.2.1 + pyenv global pypy-2.2.1 + pip install virtualenv + ;; + py32) + pyenv install 3.2.5 + pyenv global 3.2.5 + pip install virtualenv + ;; + py33) + pyenv install 3.3.2 + pyenv global 3.3.2 + pip install virtualenv + ;; + esac + pyenv rehash +else + # add mega-python ppa + sudo add-apt-repository -y ppa:fkrull/deadsnakes + sudo apt-get -y update -if [[ "${OPENSSL}" == "0.9.8" ]]; then - sudo apt-get install -y --force-yes libssl-dev/lucid + case "${TOX_ENV}" in + py26) + sudo apt-get install python2.6 python2.6-dev + ;; + py32) + sudo apt-get install python3.2 python3.2-dev + ;; + py33) + sudo apt-get install python3.3 python3.3-dev + ;; + py3pep8) + sudo apt-get install python3.3 python3.3-dev + ;; + pypy) + sudo add-apt-repository -y ppa:pypy/ppa + sudo apt-get -y update + sudo apt-get install -y --force-yes pypy pypy-dev + ;; + esac + sudo pip install virtualenv fi -if [[ "${TOX_ENV}" == "pypy" ]]; then - sudo apt-get install -y pypy +virtualenv ~/.venv +source ~/.venv/bin/activate +pip install tox coveralls - # This is required because we need to get rid of the Travis installed PyPy - # or it'll take precedence over the PPA installed one. - sudo rm -rf /usr/local/pypy/bin +if [[ "$(uname -s)" == "Darwin" ]]; then + pyenv rehash fi - -pip install tox coveralls diff --git a/AUTHORS.rst b/AUTHORS.rst index 953ca55b..ad27cec6 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -11,3 +11,4 @@ PGP key fingerprints are enclosed in parentheses. * Paul Kehrer <paul.l.kehrer@gmail.com> * Jarret Raim <jarito@gmail.com> * Alex Stapleton <alexs@prol.etari.at> (A1C7 E50B 66DE 39ED C847 9665 8E3C 20D1 9BD9 5C4C) +* David Reid <dreid@dreid.org> (0F83 CC87 B32F 482B C726 B58A 9FBF D8F4 DA89 6D74) diff --git a/cryptography/__about__.py b/cryptography/__about__.py index 46212bff..ee1d8a05 100644 --- a/cryptography/__about__.py +++ b/cryptography/__about__.py @@ -22,11 +22,9 @@ __summary__ = ("cryptography is a package designed to expose cryptographic " "recipes and primitives to Python developers.") __uri__ = "https://github.com/pyca/cryptography" -__version__ = "0.1.dev1" +__version__ = "0.2.dev1" -__author__ = ("Alex Gaynor, Hynek Schlawack, Donald Stufft, " - "Laurens Van Houtven, Jean-Paul Calderone, Christian Heimes, " - "and individual contributors.") +__author__ = "The cryptography developers" __email__ = "cryptography-dev@python.org" __license__ = "Apache License, Version 2.0" diff --git a/cryptography/fernet.py b/cryptography/fernet.py new file mode 100644 index 00000000..c19309d5 --- /dev/null +++ b/cryptography/fernet.py @@ -0,0 +1,131 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base64 +import binascii +import os +import struct +import time + +import six + +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import padding, hashes +from cryptography.hazmat.primitives.hmac import HMAC +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + + +class InvalidToken(Exception): + pass + + +_MAX_CLOCK_SKEW = 60 + + +class Fernet(object): + def __init__(self, key, backend=None): + if backend is None: + backend = default_backend() + + key = base64.urlsafe_b64decode(key) + if len(key) != 32: + raise ValueError( + "Fernet key must be 32 url-safe base64-encoded bytes" + ) + + self._signing_key = key[:16] + self._encryption_key = key[16:] + self._backend = backend + + @classmethod + def generate_key(cls): + return base64.urlsafe_b64encode(os.urandom(32)) + + def encrypt(self, data): + current_time = int(time.time()) + iv = os.urandom(16) + return self._encrypt_from_parts(data, current_time, iv) + + def _encrypt_from_parts(self, data, current_time, iv): + if isinstance(data, six.text_type): + raise TypeError( + "Unicode-objects must be encoded before encryption" + ) + + padder = padding.PKCS7(algorithms.AES.block_size).padder() + padded_data = padder.update(data) + padder.finalize() + encryptor = Cipher( + algorithms.AES(self._encryption_key), modes.CBC(iv), self._backend + ).encryptor() + ciphertext = encryptor.update(padded_data) + encryptor.finalize() + + basic_parts = ( + b"\x80" + struct.pack(">Q", current_time) + iv + ciphertext + ) + + h = HMAC(self._signing_key, hashes.SHA256(), backend=self._backend) + h.update(basic_parts) + hmac = h.finalize() + return base64.urlsafe_b64encode(basic_parts + hmac) + + def decrypt(self, token, ttl=None): + if isinstance(token, six.text_type): + raise TypeError( + "Unicode-objects must be encoded before decryption" + ) + + current_time = int(time.time()) + + try: + data = base64.urlsafe_b64decode(token) + except (TypeError, binascii.Error): + raise InvalidToken + + if six.indexbytes(data, 0) != 0x80: + raise InvalidToken + + try: + timestamp, = struct.unpack(">Q", data[1:9]) + except struct.error: + raise InvalidToken + if ttl is not None: + if timestamp + ttl < current_time: + raise InvalidToken + if current_time + _MAX_CLOCK_SKEW < timestamp: + raise InvalidToken + h = HMAC(self._signing_key, hashes.SHA256(), backend=self._backend) + h.update(data[:-32]) + try: + h.verify(data[-32:]) + except InvalidSignature: + raise InvalidToken + + iv = data[9:25] + ciphertext = data[25:-32] + decryptor = Cipher( + algorithms.AES(self._encryption_key), modes.CBC(iv), self._backend + ).decryptor() + plaintext_padded = decryptor.update(ciphertext) + try: + plaintext_padded += decryptor.finalize() + except ValueError: + raise InvalidToken + unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder() + + unpadded = unpadder.update(plaintext_padded) + try: + unpadded += unpadder.finalize() + except ValueError: + raise InvalidToken + return unpadded diff --git a/cryptography/hazmat/backends/openssl/backend.py b/cryptography/hazmat/backends/openssl/backend.py index 089684ad..ec2824d1 100644 --- a/cryptography/hazmat/backends/openssl/backend.py +++ b/cryptography/hazmat/backends/openssl/backend.py @@ -22,7 +22,7 @@ from cryptography.hazmat.backends.interfaces import ( ) from cryptography.hazmat.primitives import interfaces from cryptography.hazmat.primitives.ciphers.algorithms import ( - AES, Blowfish, Camellia, CAST5, TripleDES, ARC4, + AES, Blowfish, Camellia, TripleDES, ARC4, ) from cryptography.hazmat.primitives.ciphers.modes import ( CBC, CTR, ECB, OFB, CFB, GCM, @@ -43,7 +43,11 @@ class Backend(object): self._ffi = self._binding.ffi self._lib = self._binding.lib + # adds all ciphers/digests for EVP self._lib.OpenSSL_add_all_algorithms() + # registers available SSL/TLS ciphers and digests + self._lib.SSL_library_init() + # loads error strings for libcrypto and libssl functions self._lib.SSL_load_error_strings() self._cipher_registry = {} @@ -149,11 +153,6 @@ class Backend(object): GetCipherByName("bf-{mode.name}") ) self.register_cipher_adapter( - CAST5, - ECB, - GetCipherByName("cast5-ecb") - ) - self.register_cipher_adapter( ARC4, type(None), GetCipherByName("rc4") diff --git a/cryptography/hazmat/bindings/commoncrypto/__init__.py b/cryptography/hazmat/bindings/commoncrypto/__init__.py new file mode 100644 index 00000000..55c925c6 --- /dev/null +++ b/cryptography/hazmat/bindings/commoncrypto/__init__.py @@ -0,0 +1,12 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/cryptography/hazmat/bindings/commoncrypto/binding.py b/cryptography/hazmat/bindings/commoncrypto/binding.py new file mode 100644 index 00000000..9c1af40a --- /dev/null +++ b/cryptography/hazmat/bindings/commoncrypto/binding.py @@ -0,0 +1,47 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +import sys + +from cryptography.hazmat.bindings.utils import build_ffi + + +class Binding(object): + """ + CommonCrypto API wrapper. + """ + _module_prefix = "cryptography.hazmat.bindings.commoncrypto." + _modules = [ + "common_digest", + "common_hmac", + ] + + ffi = None + lib = None + + def __init__(self): + self._ensure_ffi_initialized() + + @classmethod + def _ensure_ffi_initialized(cls): + if cls.ffi is not None and cls.lib is not None: + return + + cls.ffi, cls.lib = build_ffi(cls._module_prefix, cls._modules, + "", "", []) + + @classmethod + def is_available(cls): + return sys.platform == "darwin" diff --git a/cryptography/hazmat/bindings/commoncrypto/common_digest.py b/cryptography/hazmat/bindings/commoncrypto/common_digest.py new file mode 100644 index 00000000..ec0fcc92 --- /dev/null +++ b/cryptography/hazmat/bindings/commoncrypto/common_digest.py @@ -0,0 +1,67 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +INCLUDES = """ +#include <CommonCrypto/CommonDigest.h> +""" + +TYPES = """ +typedef uint32_t CC_LONG; +typedef uint64_t CC_LONG64; +typedef struct CC_MD5state_st { + ...; +} CC_MD5_CTX; +typedef struct CC_SHA1state_st { + ...; +} CC_SHA1_CTX; +typedef struct CC_SHA256state_st { + ...; +} CC_SHA256_CTX; +typedef struct CC_SHA512state_st { + ...; +} CC_SHA512_CTX; +""" + +FUNCTIONS = """ +int CC_MD5_Init(CC_MD5_CTX *); +int CC_MD5_Update(CC_MD5_CTX *, const void *, CC_LONG); +int CC_MD5_Final(unsigned char *, CC_MD5_CTX *); + +int CC_SHA1_Init(CC_SHA1_CTX *); +int CC_SHA1_Update(CC_SHA1_CTX *, const void *, CC_LONG); +int CC_SHA1_Final(unsigned char *, CC_SHA1_CTX *); + +int CC_SHA224_Init(CC_SHA256_CTX *); +int CC_SHA224_Update(CC_SHA256_CTX *, const void *, CC_LONG); +int CC_SHA224_Final(unsigned char *, CC_SHA256_CTX *); + +int CC_SHA256_Init(CC_SHA256_CTX *); +int CC_SHA256_Update(CC_SHA256_CTX *, const void *, CC_LONG); +int CC_SHA256_Final(unsigned char *, CC_SHA256_CTX *); + +int CC_SHA384_Init(CC_SHA512_CTX *); +int CC_SHA384_Update(CC_SHA512_CTX *, const void *, CC_LONG); +int CC_SHA384_Final(unsigned char *, CC_SHA512_CTX *); + +int CC_SHA512_Init(CC_SHA512_CTX *); +int CC_SHA512_Update(CC_SHA512_CTX *, const void *, CC_LONG); +int CC_SHA512_Final(unsigned char *, CC_SHA512_CTX *); +""" + +MACROS = """ +""" + +CUSTOMIZATIONS = """ +""" + +CONDITIONAL_NAMES = {} diff --git a/cryptography/hazmat/bindings/commoncrypto/common_hmac.py b/cryptography/hazmat/bindings/commoncrypto/common_hmac.py new file mode 100644 index 00000000..a4bf9009 --- /dev/null +++ b/cryptography/hazmat/bindings/commoncrypto/common_hmac.py @@ -0,0 +1,46 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +INCLUDES = """ +#include <CommonCrypto/CommonHMAC.h> +""" + +TYPES = """ +typedef struct { + ...; +} CCHmacContext; +enum { + kCCHmacAlgSHA1, + kCCHmacAlgMD5, + kCCHmacAlgSHA256, + kCCHmacAlgSHA384, + kCCHmacAlgSHA512, + kCCHmacAlgSHA224 +}; +typedef uint32_t CCHmacAlgorithm; +""" + +FUNCTIONS = """ +void CCHmacInit(CCHmacContext *, CCHmacAlgorithm, const void *, size_t); +void CCHmacUpdate(CCHmacContext *, const void *, size_t); +void CCHmacFinal(CCHmacContext *, void *); + +""" + +MACROS = """ +""" + +CUSTOMIZATIONS = """ +""" + +CONDITIONAL_NAMES = {} diff --git a/cryptography/hazmat/bindings/openssl/binding.py b/cryptography/hazmat/bindings/openssl/binding.py index 3b874466..2419044f 100644 --- a/cryptography/hazmat/bindings/openssl/binding.py +++ b/cryptography/hazmat/bindings/openssl/binding.py @@ -13,9 +13,8 @@ from __future__ import absolute_import, division, print_function -import sys +from cryptography.hazmat.bindings.utils import build_ffi -import cffi _OSX_PRE_INCLUDE = """ #ifdef __APPLE__ @@ -39,20 +38,6 @@ _OSX_POST_INCLUDE = """ class Binding(object): """ OpenSSL API wrapper. - - Modules listed in the ``_modules`` listed should have the following - attributes: - - * ``INCLUDES``: A string containg C includes. - * ``TYPES``: A string containing C declarations for types. - * ``FUNCTIONS``: A string containing C declarations for functions. - * ``MACROS``: A string containing C declarations for any macros. - * ``CUSTOMIZATIONS``: A string containing arbitrary top-level C code, this - can be used to do things like test for a define and provide an - alternate implementation based on that. - * ``CONDITIONAL_NAMES``: A dict mapping strings of condition names from the - library to a list of names which will not be present without the - condition. """ _module_prefix = "cryptography.hazmat.bindings.openssl." _modules = [ @@ -93,61 +78,13 @@ class Binding(object): if cls.ffi is not None and cls.lib is not None: return - ffi = cffi.FFI() - includes = [] - functions = [] - macros = [] - customizations = [] - for name in cls._modules: - module_name = cls._module_prefix + name - __import__(module_name) - module = sys.modules[module_name] - - ffi.cdef(module.TYPES) - - macros.append(module.MACROS) - functions.append(module.FUNCTIONS) - includes.append(module.INCLUDES) - customizations.append(module.CUSTOMIZATIONS) - - # loop over the functions & macros after declaring all the types - # so we can set interdependent types in different files and still - # have them all defined before we parse the funcs & macros - for func in functions: - ffi.cdef(func) - for macro in macros: - ffi.cdef(macro) - - # We include functions here so that if we got any of their definitions - # wrong, the underlying C compiler will explode. In C you are allowed - # to re-declare a function if it has the same signature. That is: - # int foo(int); - # int foo(int); - # is legal, but the following will fail to compile: - # int foo(int); - # int foo(short); - - lib = ffi.verify( - source="\n".join( - [_OSX_PRE_INCLUDE] + - includes + - [_OSX_POST_INCLUDE] + - functions + - customizations - ), - libraries=["crypto", "ssl"], - ) - - for name in cls._modules: - module_name = cls._module_prefix + name - module = sys.modules[module_name] - for condition, names in module.CONDITIONAL_NAMES.items(): - if not getattr(lib, condition): - for name in names: - delattr(lib, name) - - res = lib.Cryptography_add_osrandom_engine() + cls.ffi, cls.lib = build_ffi(cls._module_prefix, cls._modules, + _OSX_PRE_INCLUDE, _OSX_POST_INCLUDE, + ["crypto", "ssl"]) + res = cls.lib.Cryptography_add_osrandom_engine() assert res == 1 - cls.ffi = ffi - cls.lib = lib + @classmethod + def is_available(cls): + # OpenSSL is the only binding so for now it must always be available + return True diff --git a/cryptography/hazmat/bindings/openssl/err.py b/cryptography/hazmat/bindings/openssl/err.py index 6b2a77b1..1b66bd2a 100644 --- a/cryptography/hazmat/bindings/openssl/err.py +++ b/cryptography/hazmat/bindings/openssl/err.py @@ -22,23 +22,67 @@ struct ERR_string_data_st { }; typedef struct ERR_string_data_st ERR_STRING_DATA; +static const int ASN1_R_BAD_PASSWORD_READ; + static const int ERR_LIB_EVP; static const int ERR_LIB_PEM; -static const int EVP_F_EVP_ENCRYPTFINAL_EX; static const int EVP_F_EVP_DECRYPTFINAL_EX; +static const int EVP_F_EVP_ENCRYPTFINAL_EX; static const int EVP_R_DATA_NOT_MULTIPLE_OF_BLOCK_LENGTH; -static const int PEM_F_PEM_READ_BIO_PRIVATEKEY; static const int PEM_F_D2I_PKCS8PRIVATEKEY_BIO; +static const int PEM_F_D2I_PKCS8PRIVATEKEY_BIO; +static const int PEM_F_D2I_PKCS8PRIVATEKEY_FP; +static const int PEM_F_DO_PK8PKEY; +static const int PEM_F_DO_PK8PKEY_FP; +static const int PEM_F_LOAD_IV; +static const int PEM_F_PEM_ASN1_READ; +static const int PEM_F_PEM_ASN1_READ_BIO; +static const int PEM_F_PEM_ASN1_WRITE; +static const int PEM_F_PEM_ASN1_WRITE_BIO; +static const int PEM_F_PEM_DEF_CALLBACK; +static const int PEM_F_PEM_DO_HEADER; +static const int PEM_F_PEM_F_PEM_WRITE_PKCS8PRIVATEKEY; +static const int PEM_F_PEM_GET_EVP_CIPHER_INFO; +static const int PEM_F_PEM_PK8PKEY; +static const int PEM_F_PEM_READ; +static const int PEM_F_PEM_READ_BIO; +static const int PEM_F_PEM_READ_BIO_PRIVATEKEY; +static const int PEM_F_PEM_READ_BIO_PRIVATEKEY; +static const int PEM_F_PEM_READ_PRIVATEKEY; +static const int PEM_F_PEM_SEALFINAL; +static const int PEM_F_PEM_SEALINIT; +static const int PEM_F_PEM_SIGNFINAL; +static const int PEM_F_PEM_WRITE; +static const int PEM_F_PEM_WRITE_BIO; +static const int PEM_F_PEM_X509_INFO_READ; +static const int PEM_F_PEM_X509_INFO_READ_BIO; +static const int PEM_F_PEM_X509_INFO_WRITE_BIO; +static const int PEM_R_BAD_BASE64_DECODE; +static const int PEM_R_BAD_DECRYPT; +static const int PEM_R_BAD_END_LINE; +static const int PEM_R_BAD_IV_CHARS; static const int PEM_R_BAD_PASSWORD_READ; -static const int ASN1_R_BAD_PASSWORD_READ; +static const int PEM_R_BAD_PASSWORD_READ; +static const int PEM_R_ERROR_CONVERTING_PRIVATE_KEY; +static const int PEM_R_NOT_DEK_INFO; +static const int PEM_R_NOT_ENCRYPTED; +static const int PEM_R_NOT_PROC_TYPE; +static const int PEM_R_NO_START_LINE; +static const int PEM_R_PROBLEMS_GETTING_PASSWORD; +static const int PEM_R_PUBLIC_KEY_NO_RSA; +static const int PEM_R_READ_KEY; +static const int PEM_R_SHORT_HEADER; +static const int PEM_R_UNSUPPORTED_CIPHER; +static const int PEM_R_UNSUPPORTED_ENCRYPTION; """ FUNCTIONS = """ void ERR_load_crypto_strings(void); +void ERR_load_SSL_strings(void); void ERR_free_strings(void); char* ERR_error_string(unsigned long, char *); void ERR_error_string_n(unsigned long, char *, size_t); diff --git a/cryptography/hazmat/bindings/utils.py b/cryptography/hazmat/bindings/utils.py new file mode 100644 index 00000000..9cc05506 --- /dev/null +++ b/cryptography/hazmat/bindings/utils.py @@ -0,0 +1,86 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +import sys + +import cffi + + +def build_ffi(module_prefix, modules, pre_include, post_include, libraries): + """ + Modules listed in ``modules`` should have the following attributes: + + * ``INCLUDES``: A string containing C includes. + * ``TYPES``: A string containing C declarations for types. + * ``FUNCTIONS``: A string containing C declarations for functions. + * ``MACROS``: A string containing C declarations for any macros. + * ``CUSTOMIZATIONS``: A string containing arbitrary top-level C code, this + can be used to do things like test for a define and provide an + alternate implementation based on that. + * ``CONDITIONAL_NAMES``: A dict mapping strings of condition names from the + library to a list of names which will not be present without the + condition. + """ + ffi = cffi.FFI() + types = [] + includes = [] + functions = [] + macros = [] + customizations = [] + for name in modules: + module_name = module_prefix + name + __import__(module_name) + module = sys.modules[module_name] + + types.append(module.TYPES) + macros.append(module.MACROS) + functions.append(module.FUNCTIONS) + includes.append(module.INCLUDES) + customizations.append(module.CUSTOMIZATIONS) + + # loop over the functions & macros after declaring all the types + # so we can set interdependent types in different files and still + # have them all defined before we parse the funcs & macros + ffi.cdef("\n".join(types + functions + macros)) + + # We include functions here so that if we got any of their definitions + # wrong, the underlying C compiler will explode. In C you are allowed + # to re-declare a function if it has the same signature. That is: + # int foo(int); + # int foo(int); + # is legal, but the following will fail to compile: + # int foo(int); + # int foo(short); + lib = ffi.verify( + source="\n".join( + [pre_include] + + includes + + [post_include] + + functions + + customizations + ), + libraries=libraries, + ext_package="cryptography", + ) + + for name in modules: + module_name = module_prefix + name + module = sys.modules[module_name] + for condition, names in module.CONDITIONAL_NAMES.items(): + if not getattr(lib, condition): + for name in names: + delattr(lib, name) + + return ffi, lib diff --git a/cryptography/hazmat/primitives/ciphers/algorithms.py b/cryptography/hazmat/primitives/ciphers/algorithms.py index a5cfce92..19cf1920 100644 --- a/cryptography/hazmat/primitives/ciphers/algorithms.py +++ b/cryptography/hazmat/primitives/ciphers/algorithms.py @@ -90,21 +90,6 @@ class Blowfish(object): return len(self.key) * 8 -@utils.register_interface(interfaces.BlockCipherAlgorithm) -@utils.register_interface(interfaces.CipherAlgorithm) -class CAST5(object): - name = "CAST5" - block_size = 64 - key_sizes = frozenset(range(40, 129, 8)) - - def __init__(self, key): - self.key = _verify_key_size(self, key) - - @property - def key_size(self): - return len(self.key) * 8 - - @utils.register_interface(interfaces.CipherAlgorithm) class ARC4(object): name = "RC4" diff --git a/cryptography/hazmat/primitives/ciphers/base.py b/cryptography/hazmat/primitives/ciphers/base.py index 1da0802c..d366e4cf 100644 --- a/cryptography/hazmat/primitives/ciphers/base.py +++ b/cryptography/hazmat/primitives/ciphers/base.py @@ -25,6 +25,9 @@ class Cipher(object): if not isinstance(algorithm, interfaces.CipherAlgorithm): raise TypeError("Expected interface of interfaces.CipherAlgorithm") + if mode is not None: + mode.validate_for_algorithm(algorithm) + self.algorithm = algorithm self.mode = mode self._backend = backend diff --git a/cryptography/hazmat/primitives/ciphers/modes.py b/cryptography/hazmat/primitives/ciphers/modes.py index ab8501c6..739f23dd 100644 --- a/cryptography/hazmat/primitives/ciphers/modes.py +++ b/cryptography/hazmat/primitives/ciphers/modes.py @@ -25,11 +25,20 @@ class CBC(object): def __init__(self, initialization_vector): self.initialization_vector = initialization_vector + def validate_for_algorithm(self, algorithm): + if len(self.initialization_vector) * 8 != algorithm.block_size: + raise ValueError("Invalid iv size ({0}) for {1}".format( + len(self.initialization_vector), self.name + )) + @utils.register_interface(interfaces.Mode) class ECB(object): name = "ECB" + def validate_for_algorithm(self, algorithm): + pass + @utils.register_interface(interfaces.Mode) @utils.register_interface(interfaces.ModeWithInitializationVector) @@ -39,6 +48,12 @@ class OFB(object): def __init__(self, initialization_vector): self.initialization_vector = initialization_vector + def validate_for_algorithm(self, algorithm): + if len(self.initialization_vector) * 8 != algorithm.block_size: + raise ValueError("Invalid iv size ({0}) for {1}".format( + len(self.initialization_vector), self.name + )) + @utils.register_interface(interfaces.Mode) @utils.register_interface(interfaces.ModeWithInitializationVector) @@ -48,6 +63,12 @@ class CFB(object): def __init__(self, initialization_vector): self.initialization_vector = initialization_vector + def validate_for_algorithm(self, algorithm): + if len(self.initialization_vector) * 8 != algorithm.block_size: + raise ValueError("Invalid iv size ({0}) for {1}".format( + len(self.initialization_vector), self.name + )) + @utils.register_interface(interfaces.Mode) @utils.register_interface(interfaces.ModeWithNonce) @@ -57,6 +78,12 @@ class CTR(object): def __init__(self, nonce): self.nonce = nonce + def validate_for_algorithm(self, algorithm): + if len(self.nonce) * 8 != algorithm.block_size: + raise ValueError("Invalid nonce size ({0}) for {1}".format( + len(self.nonce), self.name + )) + @utils.register_interface(interfaces.Mode) @utils.register_interface(interfaces.ModeWithInitializationVector) @@ -65,6 +92,9 @@ class GCM(object): name = "GCM" def __init__(self, initialization_vector, tag=None): + # len(initialization_vector) must in [1, 2 ** 64), but it's impossible + # to actually construct a bytes object that large, so we don't check + # for it if tag is not None and len(tag) < 4: raise ValueError( "Authentication tag must be 4 bytes or longer" @@ -72,3 +102,6 @@ class GCM(object): self.initialization_vector = initialization_vector self.tag = tag + + def validate_for_algorithm(self, algorithm): + pass diff --git a/cryptography/hazmat/primitives/constant_time.py b/cryptography/hazmat/primitives/constant_time.py index 6502803e..e88a0d95 100644 --- a/cryptography/hazmat/primitives/constant_time.py +++ b/cryptography/hazmat/primitives/constant_time.py @@ -23,7 +23,8 @@ _ffi.cdef(""" uint8_t Cryptography_constant_time_bytes_eq(uint8_t *, size_t, uint8_t *, size_t); """) -_lib = _ffi.verify(""" +_lib = _ffi.verify( + """ uint8_t Cryptography_constant_time_bytes_eq(uint8_t *a, size_t len_a, uint8_t *b, size_t len_b) { size_t i = 0; @@ -42,7 +43,9 @@ uint8_t Cryptography_constant_time_bytes_eq(uint8_t *a, size_t len_a, /* Now check the low bit to see if it's set */ return (mismatch & 1) == 0; } -""") +""", + ext_package="cryptography", +) def bytes_eq(a, b): diff --git a/cryptography/hazmat/primitives/interfaces.py b/cryptography/hazmat/primitives/interfaces.py index e87c9ca9..7a6bf3e2 100644 --- a/cryptography/hazmat/primitives/interfaces.py +++ b/cryptography/hazmat/primitives/interfaces.py @@ -47,6 +47,13 @@ class Mode(six.with_metaclass(abc.ABCMeta)): A string naming this mode (e.g. "ECB", "CBC"). """ + @abc.abstractmethod + def validate_for_algorithm(self, algorithm): + """ + Checks that all the necessary invariants of this (mode, algorithm) + combination are met. + """ + class ModeWithInitializationVector(six.with_metaclass(abc.ABCMeta)): @abc.abstractproperty diff --git a/cryptography/hazmat/primitives/padding.py b/cryptography/hazmat/primitives/padding.py index e517dee0..ddb2c63c 100644 --- a/cryptography/hazmat/primitives/padding.py +++ b/cryptography/hazmat/primitives/padding.py @@ -23,7 +23,8 @@ _ffi = cffi.FFI() _ffi.cdef(""" uint8_t Cryptography_check_pkcs7_padding(const uint8_t *, uint8_t); """) -_lib = _ffi.verify(""" +_lib = _ffi.verify( + """ /* Returns the value of the input with the most-significant-bit copied to all of the bits. */ static uint8_t Cryptography_DUPLICATE_MSB_TO_ALL(uint8_t a) { @@ -59,7 +60,9 @@ uint8_t Cryptography_check_pkcs7_padding(const uint8_t *data, /* Now check the low bit to see if it's set */ return (mismatch & 1) == 0; } -""") +""", + ext_package="cryptography", +) class PKCS7(object): diff --git a/dev-requirements.txt b/dev-requirements.txt index cd975d5c..b2a6c79c 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,8 +1,11 @@ +coverage flake8 +invoke +iso8601 pretend pytest -coverage sphinx -tox sphinx_rtd_theme +tox +twine -e . diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 00000000..41db635e --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1,11 @@ +Changelog +========= +0.2 - 2014-XX-XX +~~~~~~~~~~~~~~~~ + +* In development. + +0.1 - 2014-01-08 +~~~~~~~~~~~~~~~~ + +* Initial release. diff --git a/docs/conf.py b/docs/conf.py index 5dbcdab8..a42dcb22 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -38,6 +38,7 @@ extensions = [ 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode', 'cryptography-docs', + 'sphinxcontrib.spelling', ] # Add any paths that contain templates here, relative to this directory. @@ -60,10 +61,13 @@ copyright = '2013-2014, Individual Contributors' # |version| and |release|, also used in various other places throughout the # built documents. # -# The short X.Y version. -version = '0.1dev' -# The full version, including alpha/beta/rc tags. -release = '0.1dev' + +base_dir = os.path.join(os.path.dirname(__file__), os.pardir) +about = {} +with open(os.path.join(base_dir, "cryptography", "__about__.py")) as f: + exec(f.read(), about) + +version = release = about["__version__"] # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/contributing.rst b/docs/contributing.rst index 657c4359..8e32c368 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -29,6 +29,8 @@ devastating, ``cryptography`` has a strict code review policy: backwards incompatible release of a dependency) no pull requests may be merged until this is rectified. * All merged patches must have 100% test coverage. +* New features and significant bug fixes should be documented in the + :doc:`/changelog`. The purpose of these policies is to minimize the chances we merge a change which jeopardizes our users' security. diff --git a/docs/cryptography-docs.py b/docs/cryptography-docs.py index ea7e8eef..0252d693 100644 --- a/docs/cryptography-docs.py +++ b/docs/cryptography-docs.py @@ -6,17 +6,29 @@ from sphinx.util.compat import Directive, make_admonition DANGER_MESSAGE = """ This is a "Hazardous Materials" module. You should **ONLY** use it if you're 100% absolutely sure that you know what you're doing because this module is -full of land mines, dragons, and dinosaurs with laser guns. """ +full of land mines, dragons, and dinosaurs with laser guns. +""" + +DANGER_ALTERNATE = """ + +You may instead be interested in :doc:`{alternate}`. +""" class HazmatDirective(Directive): + has_content = True + def run(self): + message = DANGER_MESSAGE + if self.content: + message += DANGER_ALTERNATE.format(alternate=self.content[0]) + ad = make_admonition( Hazmat, self.name, [], self.options, - nodes.paragraph("", DANGER_MESSAGE), + nodes.paragraph("", message), self.lineno, self.content_offset, self.block_text, diff --git a/docs/doing-a-release.rst b/docs/doing-a-release.rst new file mode 100644 index 00000000..194e82f4 --- /dev/null +++ b/docs/doing-a-release.rst @@ -0,0 +1,37 @@ +Doing a Release +=============== + +Doing a release of ``cryptography`` is a two part process. + +Bumping the version number +-------------------------- + +The first step in doing a release is bumping the version number in the +software. + +* Update the version number in ``cryptography/__about__.py``. +* Set the release date in the :doc:`/changelog`. +* Do a commit indicating this. +* Send a pull request with this. +* Wait for it to be merged. + +Performing the release +---------------------- + +The commit which merged the version number bump is now the official release +commit for this release. You will need to have ``gpg`` installed and a ``gpg`` +key in order to do a release. Once this has happened: + +* Run ``invoke release {version}``. + +The release should now be available on PyPI and a tag should be available in +the repository. You should verify that ``pip install cryptography`` works +correctly: + +.. code-block:: pycon + + >>> import cryptography + >>> cryptography.__version__ + '...' + +Verify that this is the version you just released. diff --git a/docs/fernet.rst b/docs/fernet.rst new file mode 100644 index 00000000..13295c0c --- /dev/null +++ b/docs/fernet.rst @@ -0,0 +1,76 @@ +Fernet (Symmetric encryption) +============================= + +.. currentmodule:: cryptography.fernet + +Fernet provides guarantees that a message encrypted using it cannot be +manipulated or read without the key. `Fernet`_ is an implementation of +symmetric (also known as "secret key") authenticated cryptography. + +.. class:: Fernet(key) + + This class provides both encryption and decryption facilities. + + .. doctest:: + + >>> from cryptography.fernet import Fernet + >>> key = Fernet.generate_key() + >>> f = Fernet(key) + >>> token = f.encrypt(b"my deep dark secret") + >>> token + '...' + >>> f.decrypt(token) + 'my deep dark secret' + + :param bytes key: A URL-safe base64-encoded 32-byte key. This **must** be + kept secret. Anyone with this key is able to create and + read messages. + + .. classmethod:: generate_key() + + Generates a fresh fernet key. Keep this some place safe! If you lose it + you'll no longer be able to decrypt messages; if anyone else gains + access to it, they'll be able to decrypt all of your messages, and + they'll also be able forge arbitrary messages which will be + authenticated and decrypted. + + .. method:: encrypt(plaintext) + + :param bytes plaintext: The message you would like to encrypt. + :returns bytes: A secure message which cannot be read or altered + without the key. It is URL-safe base64-encoded. This is + referred to as a "Fernet token". + + .. note:: + + The encrypted message contains the current time when it was + generated in *plaintext*, the time a message was created will + therefore be visible to a possible attacker. + + .. method:: decrypt(token, ttl=None) + + :param bytes token: The Fernet token. This is the result of calling + :meth:`encrypt`. + :param int ttl: Optionally, the number of seconds old a message may be + for it to be valid. If the message is older than + ``ttl`` seconds (from the time it was originally + created) an exception will be raised. If ``ttl`` is not + provided (or is ``None``), the age of the message is + not considered. + :returns bytes: The original plaintext. + :raises cryptography.fernet.InvalidToken: If the ``token`` is in any + way invalid, this exception + is raised. A token may be + invalid for a number of + reasons: it is older than the + ``ttl``, it is malformed, or + it does not have a valid + signature. + + +.. class:: InvalidToken + + See :meth:`Fernet.decrypt` for more information. + + +.. _`Fernet`: https://github.com/fernet/spec/ diff --git a/docs/hazmat/bindings/commoncrypto.rst b/docs/hazmat/bindings/commoncrypto.rst new file mode 100644 index 00000000..25535e02 --- /dev/null +++ b/docs/hazmat/bindings/commoncrypto.rst @@ -0,0 +1,28 @@ +.. hazmat:: + +CommonCrypto Binding +==================== + +.. currentmodule:: cryptography.hazmat.bindings.commoncrypto.binding + +These are `CFFI`_ bindings to the `CommonCrypto`_ C library. It is available on +Mac OS X. + +.. class:: cryptography.hazmat.bindings.commoncrypto.binding.Binding() + + This is the exposed API for the CommonCrypto bindings. It has two public + attributes: + + .. attribute:: ffi + + This is a :class:`cffi.FFI` instance. It can be used to allocate and + otherwise manipulate CommonCrypto structures. + + .. attribute:: lib + + This is a ``cffi`` library. It can be used to call CommonCrypto + functions, and access constants. + + +.. _`CFFI`: https://cffi.readthedocs.org/ +.. _`CommonCrypto`: https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man3/Common%20Crypto.3cc.html#//apple_ref/doc/man/3cc/CommonCrypto diff --git a/docs/hazmat/bindings/index.rst b/docs/hazmat/bindings/index.rst index 809eddfc..caab8d6a 100644 --- a/docs/hazmat/bindings/index.rst +++ b/docs/hazmat/bindings/index.rst @@ -6,7 +6,7 @@ Bindings .. currentmodule:: cryptography.hazmat.bindings ``cryptography`` aims to provide low-level CFFI based bindings to multiple -native C libraries. These provide no automatic initialisation of the library +native C libraries. These provide no automatic initialization of the library and may not provide complete wrappers for its API. Using these functions directly is likely to require you to be careful in @@ -20,3 +20,4 @@ Individual Bindings :maxdepth: 1 openssl + commoncrypto diff --git a/docs/hazmat/primitives/hmac.rst b/docs/hazmat/primitives/hmac.rst index b8f94fd2..a21799be 100644 --- a/docs/hazmat/primitives/hmac.rst +++ b/docs/hazmat/primitives/hmac.rst @@ -37,6 +37,16 @@ message. If the backend doesn't support the requested ``algorithm`` an :class:`~cryptography.exceptions.UnsupportedAlgorithm` will be raised. + To check that a given signature is correct use the :meth:`verify` method. + You will receive an exception if the signature is wrong: + + .. code-block:: pycon + + >>> h.verify(b"an incorrect signature") + Traceback (most recent call last): + ... + cryptography.exceptions.InvalidSignature: Signature did not match digest. + :param key: Secret key as ``bytes``. :param algorithm: A :class:`~cryptography.hazmat.primitives.interfaces.HashAlgorithm` @@ -61,6 +71,17 @@ message. and finalized independently of the original instance. :raises cryptography.exceptions.AlreadyFinalized: See :meth:`finalize` + .. method:: verify(signature) + + Finalize the current context and securely compare digest to + ``signature``. + + :param bytes signature: The bytes to compare the current digest + against. + :raises cryptography.exceptions.AlreadyFinalized: See :meth:`finalize` + :raises cryptography.exceptions.InvalidSignature: If signature does not + match digest + .. method:: finalize() Finalize the current context and return the message digest as bytes. @@ -71,11 +92,3 @@ message. :return bytes: The message digest as bytes. :raises cryptography.exceptions.AlreadyFinalized: - - .. method:: verify(signature) - - Finalize the current context and securely compare digest to ``signature``. - - :param bytes signature: The bytes of the HMAC signature recieved. - :raises cryptography.exceptions.AlreadyFinalized: See :meth:`finalize` - :raises cryptography.exceptions.InvalidSignature: If signature does not match digest diff --git a/docs/hazmat/primitives/interfaces.rst b/docs/hazmat/primitives/interfaces.rst index 361b723e..edb24cd9 100644 --- a/docs/hazmat/primitives/interfaces.rst +++ b/docs/hazmat/primitives/interfaces.rst @@ -67,6 +67,18 @@ Interfaces used by the symmetric cipher modes described in The name may be used by a backend to influence the operation of a cipher in conjunction with the algorithm's name. + .. method:: validate_for_algorithm(algorithm) + + :param CipherAlgorithm algorithm: + + Checks that the combination of this mode with the provided algorithm + meets any necessary invariants. This should raise an exception if they + are not met. + + For example, the :class:`~cryptography.hazmat.primitives.modes.CBC` + mode uses this method to check that the provided initialization + vector's length matches the block size of the algorithm. + .. class:: ModeWithInitializationVector diff --git a/docs/hazmat/primitives/symmetric-encryption.rst b/docs/hazmat/primitives/symmetric-encryption.rst index 30896a05..83165690 100644 --- a/docs/hazmat/primitives/symmetric-encryption.rst +++ b/docs/hazmat/primitives/symmetric-encryption.rst @@ -1,4 +1,4 @@ -.. hazmat:: +.. hazmat:: /fernet Symmetric Encryption @@ -74,79 +74,6 @@ an "encrypt-then-MAC" formulation as `described by Colin Percival`_. and ``mode`` an :class:`cryptography.exceptions.UnsupportedAlgorithm` will be raised. - -.. currentmodule:: cryptography.hazmat.primitives.interfaces - -.. class:: CipherContext - - When calling ``encryptor()`` or ``decryptor()`` on a ``Cipher`` object - you will receive a return object conforming to the ``CipherContext`` - interface. You can then call ``update(data)`` with data until you have fed - everything into the context. Once that is done call ``finalize()`` to - finish the operation and obtain the remainder of the data. - - Block ciphers require that plaintext or ciphertext always be a multiple of - their block size, because of that **padding** is often required to make a - message the correct size. ``CipherContext`` will not automatically apply - any padding; you'll need to add your own. For block ciphers the recommended - padding is :class:`cryptography.hazmat.primitives.padding.PKCS7`. If you - are using a stream cipher mode (such as - :class:`cryptography.hazmat.primitives.modes.CTR`) you don't have to worry - about this. - - .. method:: update(data) - - :param bytes data: The data you wish to pass into the context. - :return bytes: Returns the data that was encrypted or decrypted. - :raises cryptography.exceptions.AlreadyFinalized: See :meth:`finalize` - - When the ``Cipher`` was constructed in a mode that turns it into a - stream cipher (e.g. - :class:`cryptography.hazmat.primitives.ciphers.modes.CTR`), this will - return bytes immediately, however in other modes it will return chunks, - whose size is determined by the cipher's block size. - - .. method:: finalize() - - :return bytes: Returns the remainder of the data. - :raises ValueError: This is raised when the data provided isn't - correctly padded to be a multiple of the - algorithm's block size. - - Once ``finalize`` is called this object can no longer be used and - :meth:`update` and :meth:`finalize` will raise - :class:`~cryptography.exceptions.AlreadyFinalized`. - -.. class:: AEADCipherContext - - When calling ``encryptor()`` or ``decryptor()`` on a ``Cipher`` object - with an AEAD mode you will receive a return object conforming to the - ``AEADCipherContext`` interface (in addition to the ``CipherContext`` - interface). If it is an encryption context it will additionally be an - ``AEADEncryptionContext`` interface. ``AEADCipherContext`` contains an - additional method ``authenticate_additional_data`` for adding additional - authenticated but unencrypted data. You should call this before calls to - ``update``. When you are done call ``finalize()`` to finish the operation. - - .. method:: authenticate_additional_data(data) - - :param bytes data: The data you wish to authenticate but not encrypt. - :raises: :class:`~cryptography.exceptions.AlreadyFinalized` - -.. class:: AEADEncryptionContext - - When creating an encryption context using ``encryptor()`` on a ``Cipher`` - object with an AEAD mode you will receive a return object conforming to the - ``AEADEncryptionContext`` interface (as well as ``AEADCipherContext``). - This interface provides one additional attribute ``tag``. ``tag`` can only - be obtained after ``finalize()``. - - .. attribute:: tag - - :return bytes: Returns the tag value as bytes. - :raises: :class:`~cryptography.exceptions.NotYetFinalized` if called - before the context is finalized. - .. _symmetric-encryption-algorithms: Algorithms @@ -189,15 +116,6 @@ Algorithms ``56`` bits long), they can simply be concatenated to produce the full key. This must be kept secret. -.. class:: CAST5(key) - - CAST5 (also known as CAST-128) is a block cipher approved for use in the - Canadian government by their Communications Security Establishment. It is a - variable key length cipher and supports keys from 40-128 bits in length. - - :param bytes key: The secret key, 40-128 bits in length (in increments of - 8). This must be kept secret. - Weak Ciphers ------------ @@ -251,6 +169,8 @@ Modes CBC (Cipher block chaining) is a mode of operation for block ciphers. It is considered cryptographically strong. + **Padding is required when using this mode.** + :param bytes initialization_vector: Must be random bytes. They do not need to be kept secret (they can be included in a transmitted message). Must be the @@ -293,6 +213,8 @@ Modes cryptographically strong. It transforms a block cipher into a stream cipher. + **This mode does not require padding.** + :param bytes nonce: Should be random bytes. It is critical to never reuse a ``nonce`` with a given key. Any reuse of a nonce with the same key compromises the security of every @@ -306,6 +228,8 @@ Modes OFB (Output Feedback) is a mode of operation for block ciphers. It transforms a block cipher into a stream cipher. + **This mode does not require padding.** + :param bytes initialization_vector: Must be random bytes. They do not need to be kept secret (they can be included in a transmitted message). Must be the @@ -319,6 +243,8 @@ Modes CFB (Cipher Feedback) is a mode of operation for block ciphers. It transforms a block cipher into a stream cipher. + **This mode does not require padding.** + :param bytes initialization_vector: Must be random bytes. They do not need to be kept secret (they can be included in a transmitted message). Must be the @@ -343,6 +269,8 @@ Modes Additional means of verifying integrity (like :doc:`HMAC </hazmat/primitives/hmac>`) are not necessary. + **This mode does not require padding.** + :param bytes initialization_vector: Must be random bytes. They do not need to be kept secret (they can be included in a transmitted message). NIST @@ -365,20 +293,70 @@ Modes :param bytes tag: The tag bytes to verify during decryption. When encrypting this must be None. - .. doctest:: + .. testcode:: - >>> from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes - >>> from cryptography.hazmat.backends import default_backend - >>> cipher = Cipher(algorithms.AES(key), modes.GCM(iv), backend=default_backend()) - >>> encryptor = cipher.encryptor() - >>> encryptor.authenticate_additional_data(b"authenticated but not encrypted payload") - >>> ct = encryptor.update(b"a secret message") + encryptor.finalize() - >>> tag = encryptor.tag - >>> cipher = Cipher(algorithms.AES(key), modes.GCM(iv, tag), backend) - >>> decryptor = cipher.decryptor() - >>> decryptor.authenticate_additional_data(b"authenticated but not encrypted payload") - >>> decryptor.update(ct) + decryptor.finalize() - 'a secret message' + import os + + from cryptography.hazmat.primitives.ciphers import ( + Cipher, algorithms, modes + ) + + def encrypt(key, plaintext, associated_data): + # Generate a random 96-bit IV. + iv = os.urandom(12) + + # Construct a AES-GCM Cipher object with the given and our randomly + # generated IV. + encryptor = Cipher( + algorithms.AES(key), + modes.GCM(iv), + backend=default_backend() + ).encryptor() + + # associated_data will be authenticated but not encrypted, + # it must also be passed in on decryption. + encryptor.authenticate_additional_data(associated_data) + + # Encrypt the plaintext and get the associated ciphertext. + # GCM does not require padding. + ciphertext = encryptor.update(plaintext) + encryptor.finalize() + + return (iv, ciphertext, encryptor.tag) + + def decrypt(key, associated_data, iv, ciphertext, tag): + # Construct a Cipher object, with the key, iv, and additionally the + # GCM tag used for authenticating the message. + decryptor = Cipher( + algorithms.AES(key), + modes.GCM(iv, tag), + backend=default_backend() + ).decryptor() + + # We put associated_data back in or the tag will fail to verify + # when we finalize the decryptor. + decryptor.authenticate_additional_data(associated_data) + + # Decryption gets us the authenticated plaintext. + # If the tag does not match an InvalidTag exception will be raised. + return decryptor.update(ciphertext) + decryptor.finalize() + + iv, ciphertext, tag = encrypt( + key, + b"a secret message!", + b"authenticated but not encrypted payload" + ) + + print(decrypt( + key, + b"authenticated but not encrypted payload", + iv, + ciphertext, + tag + )) + + .. testoutput:: + + a secret message! Insecure Modes @@ -397,6 +375,91 @@ Insecure Modes identical plaintext blocks will always result in identical ciphertext blocks, and thus result in information leakage + **Padding is required when using this mode.** + +Interfaces +---------- + +.. class:: CipherContext + + When calling ``encryptor()`` or ``decryptor()`` on a ``Cipher`` object + you will receive a return object conforming to the ``CipherContext`` + interface. You can then call ``update(data)`` with data until you have fed + everything into the context. Once that is done call ``finalize()`` to + finish the operation and obtain the remainder of the data. + + Block ciphers require that plaintext or ciphertext always be a multiple of + their block size, because of that **padding** is sometimes required to make + a message the correct size. ``CipherContext`` will not automatically apply + any padding; you'll need to add your own. For block ciphers the recommended + padding is :class:`cryptography.hazmat.primitives.padding.PKCS7`. If you + are using a stream cipher mode (such as + :class:`cryptography.hazmat.primitives.modes.CTR`) you don't have to worry + about this. + + .. method:: update(data) + + :param bytes data: The data you wish to pass into the context. + :return bytes: Returns the data that was encrypted or decrypted. + :raises cryptography.exceptions.AlreadyFinalized: See :meth:`finalize` + + When the ``Cipher`` was constructed in a mode that turns it into a + stream cipher (e.g. + :class:`cryptography.hazmat.primitives.ciphers.modes.CTR`), this will + return bytes immediately, however in other modes it will return chunks, + whose size is determined by the cipher's block size. + + .. method:: finalize() + + :return bytes: Returns the remainder of the data. + :raises ValueError: This is raised when the data provided isn't + correctly padded to be a multiple of the + algorithm's block size. + + Once ``finalize`` is called this object can no longer be used and + :meth:`update` and :meth:`finalize` will raise + :class:`~cryptography.exceptions.AlreadyFinalized`. + +.. class:: AEADCipherContext + + When calling ``encryptor()`` or ``decryptor()`` on a ``Cipher`` object + with an AEAD mode (e.g. + :class:`~cryptography.hazmat.primitives.ciphers.modes.GCM`) you will receive + a return object conforming to the ``AEADCipherContext`` and + ``CipherContext`` interfaces. If it is an encryption context it will + additionally be an ``AEADEncryptionContext`` interface. + ``AEADCipherContext`` contains an additional method + ``authenticate_additional_data`` for adding additional authenticated but + unencrypted data (see note below). You should call this before calls to + ``update``. When you are done call ``finalize()`` to finish the operation. + + .. note:: + + In AEAD modes all data passed to ``update()`` will be both encrypted + and authenticated. Do not pass encrypted data to the + ``authenticate_additional_data()`` method. It is meant solely for + additional data you may want to authenticate but leave unencrypted. + + .. method:: authenticate_additional_data(data) + + :param bytes data: Any data you wish to authenticate but not encrypt. + :raises: :class:`~cryptography.exceptions.AlreadyFinalized` + +.. class:: AEADEncryptionContext + + When creating an encryption context using ``encryptor()`` on a ``Cipher`` + object with an AEAD mode (e.g. + :class:`~cryptography.hazmat.primitives.ciphers.modes.GCM`) you will receive + a return object conforming to the ``AEADEncryptionContext`` interface (as + well as ``AEADCipherContext``). This interface provides one additional + attribute ``tag``. ``tag`` can only be obtained after ``finalize()``. + + .. attribute:: tag + + :return bytes: Returns the tag value as bytes. + :raises: :class:`~cryptography.exceptions.NotYetFinalized` if called + before the context is finalized. + .. _`described by Colin Percival`: http://www.daemonology.net/blog/2009-06-11-cryptographic-right-answers.html .. _`recommends 96-bit IV length`: http://csrc.nist.gov/groups/ST/toolkit/BCM/documents/proposedmodes/gcm/gcm-spec.pdf diff --git a/docs/index.rst b/docs/index.rst index 5eb3de7d..e17b4f9f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,12 +8,11 @@ needs in Python. Installing ---------- -We don't yet have a release on PyPI, for now you can install ``cryptography`` -directly from Github: +You can install ``cryptography`` with ``pip``: .. code-block:: console - $ pip install git+https://github.com/pyca/cryptography + $ pip install cryptography Why a new crypto library for Python? ------------------------------------ @@ -56,6 +55,7 @@ The recipes layer .. toctree:: :maxdepth: 2 + fernet exceptions glossary @@ -78,4 +78,6 @@ The ``cryptography`` open source project contributing security api-stability + doing-a-release + changelog community diff --git a/docs/security.rst b/docs/security.rst index 88959709..4dadc847 100644 --- a/docs/security.rst +++ b/docs/security.rst @@ -7,6 +7,6 @@ identified a security issue in it, please report it to fingerprint ``E27D 4AA0 1651 72CB C5D2 AF2B 125F 5C67 DFE9 4084`` (this public key is available from most commonly-used key servers). -Once you’ve submitted an issue via email, you should receive an acknowledgment +Once you've submitted an issue via email, you should receive an acknowledgment within 48 hours, and depending on the action to be taken, you may receive -further followup emails. +further follow-up emails. diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt new file mode 100644 index 00000000..97356c24 --- /dev/null +++ b/docs/spelling_wordlist.txt @@ -0,0 +1,28 @@ +backend +backends +boolean +ciphertext +committer +crypto +cryptographic +cryptographically +decrypt +decrypted +decrypting +fernet +hazmat +indistinguishability +introspectability +invariants +pickleable +plaintext +testability +unencrypted +unpadded +unpadding +Backends +Blowfish +Changelog +Docstrings +Fernet +Schneier @@ -10,31 +10,61 @@ # implied. # See the License for the specific language governing permissions and # limitations under the License. +import os +from distutils.command.build import build + from setuptools import setup, find_packages +base_dir = os.path.dirname(__file__) + about = {} -with open("cryptography/__about__.py") as fp: - exec(fp.read(), about) +with open(os.path.join(base_dir, "cryptography", "__about__.py")) as f: + exec(f.read(), about) -CFFI_DEPENDENCY = "cffi>=0.6" +CFFI_DEPENDENCY = "cffi>=0.8" SIX_DEPENDENCY = "six>=1.4.1" -install_requires = [ +requirements = [ CFFI_DEPENDENCY, SIX_DEPENDENCY ] -setup_requires = [ - CFFI_DEPENDENCY, -] + +class cffi_build(build): + """ + This class exists, instead of just providing ``ext_modules=[...]`` directly + in ``setup()`` because importing cryptography requires we have several + packages installed first. + + By doing the imports here we ensure that packages listed in + ``setup_requires`` are already installed. + """ + + def finalize_options(self): + from cryptography.hazmat.bindings.openssl.binding import Binding + from cryptography.hazmat.primitives import constant_time, padding + + self.distribution.ext_modules = [ + Binding().ffi.verifier.get_extension(), + constant_time._ffi.verifier.get_extension(), + padding._ffi.verifier.get_extension() + ] + + build.finalize_options(self) + + +with open(os.path.join(base_dir, "README.rst")) as f: + long_description = f.read() + setup( name=about["__title__"], version=about["__version__"], description=about["__summary__"], + long_description=long_description, license=about["__license__"], url=about["__uri__"], @@ -65,9 +95,13 @@ setup( packages=find_packages(exclude=["tests", "tests.*"]), - install_requires=install_requires, - setup_requires=setup_requires, + install_requires=requirements, + setup_requires=requirements, # for cffi zip_safe=False, + ext_package="cryptography", + cmdclass={ + "build": cffi_build, + } ) diff --git a/tasks.py b/tasks.py new file mode 100644 index 00000000..f72f43ba --- /dev/null +++ b/tasks.py @@ -0,0 +1,27 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import absolute_import, division, print_function + +import invoke + + +@invoke.task +def release(version): + """ + ``version`` should be a string like '0.4' or '1.0'. + """ + invoke.run("git tag -s {0}".format(version)) + invoke.run("git push --tags") + + invoke.run("python setup.py sdist") + invoke.run("twine upload -s dist/cryptography-{0}*".format(version)) diff --git a/tests/conftest.py b/tests/conftest.py index 0ddc3338..1d9f96ed 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,6 @@ import pytest +from cryptography.hazmat.backends import _ALL_BACKENDS from cryptography.hazmat.backends.interfaces import ( HMACBackend, CipherBackend, HashBackend ) @@ -7,11 +8,9 @@ from cryptography.hazmat.backends.interfaces import ( from .utils import check_for_iface, check_backend_support -def pytest_generate_tests(metafunc): - from cryptography.hazmat.backends import _ALL_BACKENDS - - if "backend" in metafunc.fixturenames: - metafunc.parametrize("backend", _ALL_BACKENDS) +@pytest.fixture(params=_ALL_BACKENDS) +def backend(request): + return request.param @pytest.mark.trylast diff --git a/tests/hazmat/backends/test_openssl.py b/tests/hazmat/backends/test_openssl.py index b7d999b8..51eb408f 100644 --- a/tests/hazmat/backends/test_openssl.py +++ b/tests/hazmat/backends/test_openssl.py @@ -119,6 +119,9 @@ def unregister_dummy_engine(): class DummyMode(object): name = "dummy-mode" + def validate_for_algorithm(self, algorithm): + pass + @utils.register_interface(interfaces.CipherAlgorithm) class DummyCipher(object): @@ -185,6 +188,24 @@ class TestOpenSSL(object): 0 ) + def test_ssl_ciphers_registered(self): + meth = backend._lib.TLSv1_method() + ctx = backend._lib.SSL_CTX_new(meth) + assert ctx != backend._ffi.NULL + backend._lib.SSL_CTX_free(ctx) + + def test_evp_ciphers_registered(self): + cipher = backend._lib.EVP_get_cipherbyname(b"aes-256-cbc") + assert cipher != backend._ffi.NULL + + def test_error_strings_loaded(self): + # returns a value in a static buffer + err = backend._lib.ERR_error_string(101183626, backend._ffi.NULL) + assert backend._ffi.string(err) == ( + b"error:0607F08A:digital envelope routines:EVP_EncryptFinal_ex:" + b"data not multiple of block length" + ) + # This test is not in the next class because to check if it's really # default we don't want to run the setup_method before it def test_osrandom_engine_is_default(self): diff --git a/tests/hazmat/bindings/test_commoncrypto.py b/tests/hazmat/bindings/test_commoncrypto.py new file mode 100644 index 00000000..db3d1b74 --- /dev/null +++ b/tests/hazmat/bindings/test_commoncrypto.py @@ -0,0 +1,32 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from cryptography.hazmat.bindings.commoncrypto.binding import Binding + + +@pytest.mark.skipif(not Binding.is_available(), + reason="CommonCrypto not available") +class TestCommonCrypto(object): + def test_binding_loads(self): + binding = Binding() + assert binding + assert binding.lib + assert binding.ffi + + def test_binding_returns_same_lib(self): + binding = Binding() + binding2 = Binding() + assert binding.lib == binding2.lib + assert binding.ffi == binding2.ffi diff --git a/tests/hazmat/bindings/test_openssl.py b/tests/hazmat/bindings/test_openssl.py index 31f736ab..d1e85058 100644 --- a/tests/hazmat/bindings/test_openssl.py +++ b/tests/hazmat/bindings/test_openssl.py @@ -20,3 +20,6 @@ class TestOpenSSL(object): assert binding assert binding.lib assert binding.ffi + + def test_is_available(self): + assert Binding.is_available() is True diff --git a/tests/hazmat/primitives/test_block.py b/tests/hazmat/primitives/test_block.py index 30cf1d60..f758ffaa 100644 --- a/tests/hazmat/primitives/test_block.py +++ b/tests/hazmat/primitives/test_block.py @@ -35,6 +35,9 @@ from .utils import ( class DummyMode(object): name = "dummy-mode" + def validate_for_algorithm(self, algorithm): + pass + @utils.register_interface(interfaces.CipherAlgorithm) class DummyCipher(object): @@ -152,3 +155,37 @@ class TestAEADCipherContext(object): algorithms.AES, modes.GCM, ) + + +class TestModeValidation(object): + def test_cbc(self, backend): + with pytest.raises(ValueError): + Cipher( + algorithms.AES(b"\x00" * 16), + modes.CBC(b"abc"), + backend, + ) + + def test_ofb(self, backend): + with pytest.raises(ValueError): + Cipher( + algorithms.AES(b"\x00" * 16), + modes.OFB(b"abc"), + backend, + ) + + def test_cfb(self, backend): + with pytest.raises(ValueError): + Cipher( + algorithms.AES(b"\x00" * 16), + modes.CFB(b"abc"), + backend, + ) + + def test_ctr(self, backend): + with pytest.raises(ValueError): + Cipher( + algorithms.AES(b"\x00" * 16), + modes.CTR(b"abc"), + backend, + ) diff --git a/tests/hazmat/primitives/test_cast5.py b/tests/hazmat/primitives/test_cast5.py deleted file mode 100644 index d65a86b2..00000000 --- a/tests/hazmat/primitives/test_cast5.py +++ /dev/null @@ -1,41 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import absolute_import, division, print_function - -import binascii -import os - -import pytest - -from cryptography.hazmat.primitives.ciphers import algorithms, modes - -from .utils import generate_encrypt_test -from ...utils import load_nist_vectors - - -@pytest.mark.supported( - only_if=lambda backend: backend.cipher_supported( - algorithms.CAST5("\x00" * 16), modes.ECB() - ), - skip_message="Does not support CAST5 ECB", -) -@pytest.mark.cipher -class TestCAST5(object): - test_ECB = generate_encrypt_test( - load_nist_vectors, - os.path.join("ciphers", "CAST5"), - ["cast5-ecb.txt"], - lambda key, **kwargs: algorithms.CAST5(binascii.unhexlify((key))), - lambda **kwargs: modes.ECB(), - ) diff --git a/tests/hazmat/primitives/test_ciphers.py b/tests/hazmat/primitives/test_ciphers.py index 653f7ce6..6a7b2f93 100644 --- a/tests/hazmat/primitives/test_ciphers.py +++ b/tests/hazmat/primitives/test_ciphers.py @@ -18,7 +18,7 @@ import binascii import pytest from cryptography.hazmat.primitives.ciphers.algorithms import ( - AES, Camellia, TripleDES, Blowfish, CAST5, ARC4 + AES, Camellia, TripleDES, Blowfish, ARC4 ) @@ -80,19 +80,6 @@ class TestBlowfish(object): Blowfish(binascii.unhexlify(b"0" * 6)) -class TestCAST5(object): - @pytest.mark.parametrize(("key", "keysize"), [ - (b"0" * (keysize // 4), keysize) for keysize in range(40, 129, 8) - ]) - def test_key_size(self, key, keysize): - cipher = CAST5(binascii.unhexlify(key)) - assert cipher.key_size == keysize - - def test_invalid_key_size(self): - with pytest.raises(ValueError): - CAST5(binascii.unhexlify(b"0" * 34)) - - class TestARC4(object): @pytest.mark.parametrize(("key", "keysize"), [ (b"0" * 10, 40), diff --git a/tests/hazmat/primitives/test_hash_vectors.py b/tests/hazmat/primitives/test_hash_vectors.py index 13ffc3fd..ca97fc11 100644 --- a/tests/hazmat/primitives/test_hash_vectors.py +++ b/tests/hazmat/primitives/test_hash_vectors.py @@ -24,7 +24,7 @@ from ...utils import load_hash_vectors @pytest.mark.supported( - only_if=lambda backend: backend.hash_supported(hashes.SHA1), + only_if=lambda backend: backend.hash_supported(hashes.SHA1()), skip_message="Does not support SHA1", ) @pytest.mark.hash @@ -41,7 +41,7 @@ class TestSHA1(object): @pytest.mark.supported( - only_if=lambda backend: backend.hash_supported(hashes.SHA224), + only_if=lambda backend: backend.hash_supported(hashes.SHA224()), skip_message="Does not support SHA224", ) @pytest.mark.hash @@ -58,7 +58,7 @@ class TestSHA224(object): @pytest.mark.supported( - only_if=lambda backend: backend.hash_supported(hashes.SHA256), + only_if=lambda backend: backend.hash_supported(hashes.SHA256()), skip_message="Does not support SHA256", ) @pytest.mark.hash @@ -75,7 +75,7 @@ class TestSHA256(object): @pytest.mark.supported( - only_if=lambda backend: backend.hash_supported(hashes.SHA384), + only_if=lambda backend: backend.hash_supported(hashes.SHA384()), skip_message="Does not support SHA384", ) @pytest.mark.hash @@ -92,7 +92,7 @@ class TestSHA384(object): @pytest.mark.supported( - only_if=lambda backend: backend.hash_supported(hashes.SHA512), + only_if=lambda backend: backend.hash_supported(hashes.SHA512()), skip_message="Does not support SHA512", ) @pytest.mark.hash @@ -109,7 +109,7 @@ class TestSHA512(object): @pytest.mark.supported( - only_if=lambda backend: backend.hash_supported(hashes.RIPEMD160), + only_if=lambda backend: backend.hash_supported(hashes.RIPEMD160()), skip_message="Does not support RIPEMD160", ) @pytest.mark.hash @@ -130,7 +130,7 @@ class TestRIPEMD160(object): @pytest.mark.supported( - only_if=lambda backend: backend.hash_supported(hashes.Whirlpool), + only_if=lambda backend: backend.hash_supported(hashes.Whirlpool()), skip_message="Does not support Whirlpool", ) @pytest.mark.hash @@ -153,7 +153,7 @@ class TestWhirlpool(object): @pytest.mark.supported( - only_if=lambda backend: backend.hash_supported(hashes.MD5), + only_if=lambda backend: backend.hash_supported(hashes.MD5()), skip_message="Does not support MD5", ) @pytest.mark.hash diff --git a/tests/hazmat/primitives/test_hashes.py b/tests/hazmat/primitives/test_hashes.py index c907ef61..9ca2feee 100644 --- a/tests/hazmat/primitives/test_hashes.py +++ b/tests/hazmat/primitives/test_hashes.py @@ -70,7 +70,7 @@ class TestHashContext(object): @pytest.mark.supported( - only_if=lambda backend: backend.hash_supported(hashes.SHA1), + only_if=lambda backend: backend.hash_supported(hashes.SHA1()), skip_message="Does not support SHA1", ) @pytest.mark.hash @@ -83,7 +83,7 @@ class TestSHA1(object): @pytest.mark.supported( - only_if=lambda backend: backend.hash_supported(hashes.SHA224), + only_if=lambda backend: backend.hash_supported(hashes.SHA224()), skip_message="Does not support SHA224", ) @pytest.mark.hash @@ -96,7 +96,7 @@ class TestSHA224(object): @pytest.mark.supported( - only_if=lambda backend: backend.hash_supported(hashes.SHA256), + only_if=lambda backend: backend.hash_supported(hashes.SHA256()), skip_message="Does not support SHA256", ) @pytest.mark.hash @@ -109,7 +109,7 @@ class TestSHA256(object): @pytest.mark.supported( - only_if=lambda backend: backend.hash_supported(hashes.SHA384), + only_if=lambda backend: backend.hash_supported(hashes.SHA384()), skip_message="Does not support SHA384", ) @pytest.mark.hash @@ -122,7 +122,7 @@ class TestSHA384(object): @pytest.mark.supported( - only_if=lambda backend: backend.hash_supported(hashes.SHA512), + only_if=lambda backend: backend.hash_supported(hashes.SHA512()), skip_message="Does not support SHA512", ) @pytest.mark.hash @@ -135,7 +135,7 @@ class TestSHA512(object): @pytest.mark.supported( - only_if=lambda backend: backend.hash_supported(hashes.RIPEMD160), + only_if=lambda backend: backend.hash_supported(hashes.RIPEMD160()), skip_message="Does not support RIPEMD160", ) @pytest.mark.hash @@ -148,7 +148,7 @@ class TestRIPEMD160(object): @pytest.mark.supported( - only_if=lambda backend: backend.hash_supported(hashes.Whirlpool), + only_if=lambda backend: backend.hash_supported(hashes.Whirlpool()), skip_message="Does not support Whirlpool", ) @pytest.mark.hash @@ -161,7 +161,7 @@ class TestWhirlpool(object): @pytest.mark.supported( - only_if=lambda backend: backend.hash_supported(hashes.MD5), + only_if=lambda backend: backend.hash_supported(hashes.MD5()), skip_message="Does not support MD5", ) @pytest.mark.hash diff --git a/tests/hazmat/primitives/test_hmac.py b/tests/hazmat/primitives/test_hmac.py index 04913af6..dd9cdaab 100644 --- a/tests/hazmat/primitives/test_hmac.py +++ b/tests/hazmat/primitives/test_hmac.py @@ -34,7 +34,7 @@ class UnsupportedDummyHash(object): @pytest.mark.supported( - only_if=lambda backend: backend.hmac_supported(hashes.MD5), + only_if=lambda backend: backend.hmac_supported(hashes.MD5()), skip_message="Does not support MD5", ) @pytest.mark.hmac diff --git a/tests/hazmat/primitives/test_hmac_vectors.py b/tests/hazmat/primitives/test_hmac_vectors.py index c5644459..0792080b 100644 --- a/tests/hazmat/primitives/test_hmac_vectors.py +++ b/tests/hazmat/primitives/test_hmac_vectors.py @@ -22,7 +22,7 @@ from ...utils import load_hash_vectors @pytest.mark.supported( - only_if=lambda backend: backend.hmac_supported(hashes.MD5), + only_if=lambda backend: backend.hmac_supported(hashes.MD5()), skip_message="Does not support MD5", ) @pytest.mark.hmac @@ -38,7 +38,7 @@ class TestHMAC_MD5(object): @pytest.mark.supported( - only_if=lambda backend: backend.hmac_supported(hashes.SHA1), + only_if=lambda backend: backend.hmac_supported(hashes.SHA1()), skip_message="Does not support SHA1", ) @pytest.mark.hmac @@ -54,7 +54,7 @@ class TestHMAC_SHA1(object): @pytest.mark.supported( - only_if=lambda backend: backend.hmac_supported(hashes.SHA224), + only_if=lambda backend: backend.hmac_supported(hashes.SHA224()), skip_message="Does not support SHA224", ) @pytest.mark.hmac @@ -70,7 +70,7 @@ class TestHMAC_SHA224(object): @pytest.mark.supported( - only_if=lambda backend: backend.hmac_supported(hashes.SHA256), + only_if=lambda backend: backend.hmac_supported(hashes.SHA256()), skip_message="Does not support SHA256", ) @pytest.mark.hmac @@ -86,7 +86,7 @@ class TestHMAC_SHA256(object): @pytest.mark.supported( - only_if=lambda backend: backend.hmac_supported(hashes.SHA384), + only_if=lambda backend: backend.hmac_supported(hashes.SHA384()), skip_message="Does not support SHA384", ) @pytest.mark.hmac @@ -102,7 +102,7 @@ class TestHMAC_SHA384(object): @pytest.mark.supported( - only_if=lambda backend: backend.hmac_supported(hashes.SHA512), + only_if=lambda backend: backend.hmac_supported(hashes.SHA512()), skip_message="Does not support SHA512", ) @pytest.mark.hmac @@ -118,7 +118,7 @@ class TestHMAC_SHA512(object): @pytest.mark.supported( - only_if=lambda backend: backend.hmac_supported(hashes.RIPEMD160), + only_if=lambda backend: backend.hmac_supported(hashes.RIPEMD160()), skip_message="Does not support RIPEMD160", ) @pytest.mark.hmac diff --git a/tests/test_fernet.py b/tests/test_fernet.py new file mode 100644 index 00000000..bd4d90a5 --- /dev/null +++ b/tests/test_fernet.py @@ -0,0 +1,150 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base64 +import calendar +import json +import os +import time + +import iso8601 + +import pytest + +import six + +from cryptography.fernet import Fernet, InvalidToken +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.ciphers import algorithms, modes + + +def json_parametrize(keys, fname): + path = os.path.join(os.path.dirname(__file__), "vectors", "fernet", fname) + with open(path) as f: + data = json.load(f) + return pytest.mark.parametrize(keys, [ + tuple([entry[k] for k in keys]) + for entry in data + ]) + + +@pytest.mark.cipher +class TestFernet(object): + @pytest.mark.supported( + only_if=lambda backend: backend.cipher_supported( + algorithms.AES("\x00" * 32), modes.CBC("\x00" * 16) + ), + skip_message="Does not support AES CBC", + ) + @json_parametrize( + ("secret", "now", "iv", "src", "token"), "generate.json", + ) + def test_generate(self, secret, now, iv, src, token, backend): + f = Fernet(secret.encode("ascii"), backend=backend) + actual_token = f._encrypt_from_parts( + src.encode("ascii"), + calendar.timegm(iso8601.parse_date(now).utctimetuple()), + b"".join(map(six.int2byte, iv)) + ) + assert actual_token == token.encode("ascii") + + @pytest.mark.supported( + only_if=lambda backend: backend.cipher_supported( + algorithms.AES("\x00" * 32), modes.CBC("\x00" * 16) + ), + skip_message="Does not support AES CBC", + ) + @json_parametrize( + ("secret", "now", "src", "ttl_sec", "token"), "verify.json", + ) + def test_verify(self, secret, now, src, ttl_sec, token, backend, + monkeypatch): + f = Fernet(secret.encode("ascii"), backend=backend) + current_time = calendar.timegm(iso8601.parse_date(now).utctimetuple()) + monkeypatch.setattr(time, "time", lambda: current_time) + payload = f.decrypt(token.encode("ascii"), ttl=ttl_sec) + assert payload == src.encode("ascii") + + @pytest.mark.supported( + only_if=lambda backend: backend.cipher_supported( + algorithms.AES("\x00" * 32), modes.CBC("\x00" * 16) + ), + skip_message="Does not support AES CBC", + ) + @json_parametrize(("secret", "token", "now", "ttl_sec"), "invalid.json") + def test_invalid(self, secret, token, now, ttl_sec, backend, monkeypatch): + f = Fernet(secret.encode("ascii"), backend=backend) + current_time = calendar.timegm(iso8601.parse_date(now).utctimetuple()) + monkeypatch.setattr(time, "time", lambda: current_time) + with pytest.raises(InvalidToken): + f.decrypt(token.encode("ascii"), ttl=ttl_sec) + + @pytest.mark.supported( + only_if=lambda backend: backend.cipher_supported( + algorithms.AES("\x00" * 32), modes.CBC("\x00" * 16) + ), + skip_message="Does not support AES CBC", + ) + def test_invalid_start_byte(self, backend): + f = Fernet(Fernet.generate_key(), backend=backend) + with pytest.raises(InvalidToken): + f.decrypt(base64.urlsafe_b64encode(b"\x81")) + + @pytest.mark.supported( + only_if=lambda backend: backend.cipher_supported( + algorithms.AES("\x00" * 32), modes.CBC("\x00" * 16) + ), + skip_message="Does not support AES CBC", + ) + def test_timestamp_too_short(self, backend): + f = Fernet(Fernet.generate_key(), backend=backend) + with pytest.raises(InvalidToken): + f.decrypt(base64.urlsafe_b64encode(b"\x80abc")) + + @pytest.mark.supported( + only_if=lambda backend: backend.cipher_supported( + algorithms.AES("\x00" * 32), modes.CBC("\x00" * 16) + ), + skip_message="Does not support AES CBC", + ) + def test_unicode(self, backend): + f = Fernet(base64.urlsafe_b64encode(b"\x00" * 32), backend=backend) + with pytest.raises(TypeError): + f.encrypt(six.u("")) + with pytest.raises(TypeError): + f.decrypt(six.u("")) + + @pytest.mark.supported( + only_if=lambda backend: backend.cipher_supported( + algorithms.AES("\x00" * 32), modes.CBC("\x00" * 16) + ), + skip_message="Does not support AES CBC", + ) + @pytest.mark.parametrize("message", [b"", b"Abc!", b"\x00\xFF\x00\x80"]) + def test_roundtrips(self, message, backend): + f = Fernet(Fernet.generate_key(), backend=backend) + assert f.decrypt(f.encrypt(message)) == message + + def test_default_backend(self): + f = Fernet(Fernet.generate_key()) + assert f._backend is default_backend() + + @pytest.mark.supported( + only_if=lambda backend: backend.cipher_supported( + algorithms.AES("\x00" * 32), modes.CBC("\x00" * 16) + ), + skip_message="Does not support AES CBC", + ) + def test_bad_key(self, backend): + with pytest.raises(ValueError): + Fernet(base64.urlsafe_b64encode(b"abc"), backend=backend) diff --git a/tests/test_utils.py b/tests/test_utils.py index c640367e..e3e53d63 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -50,7 +50,7 @@ def test_check_backend_support_skip(): funcargs={"backend": True}) with pytest.raises(pytest.skip.Exception) as exc_info: check_backend_support(item) - assert exc_info.value.args[0] == "Nope" + assert exc_info.value.args[0] == "Nope (True)" def test_check_backend_support_no_skip(): diff --git a/tests/utils.py b/tests/utils.py index beb2ca5d..693a0c8f 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -28,7 +28,9 @@ def check_backend_support(item): supported = item.keywords.get("supported") if supported and "backend" in item.funcargs: if not supported.kwargs["only_if"](item.funcargs["backend"]): - pytest.skip(supported.kwargs["skip_message"]) + pytest.skip("{0} ({1})".format( + supported.kwargs["skip_message"], item.funcargs["backend"] + )) elif supported: raise ValueError("This mark is only available on methods that take a " "backend") diff --git a/tests/vectors/fernet/generate.json b/tests/vectors/fernet/generate.json new file mode 100644 index 00000000..d1f3e083 --- /dev/null +++ b/tests/vectors/fernet/generate.json @@ -0,0 +1,9 @@ +[ + { + "token": "gAAAAAAdwJ6wAAECAwQFBgcICQoLDA0ODy021cpGVWKZ_eEwCGM4BLLF_5CV9dOPmrhuVUPgJobwOz7JcbmrR64jVmpU4IwqDA==", + "now": "1985-10-26T01:20:00-07:00", + "iv": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + "src": "hello", + "secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" + } +] diff --git a/tests/vectors/fernet/invalid.json b/tests/vectors/fernet/invalid.json new file mode 100644 index 00000000..d80e7b4a --- /dev/null +++ b/tests/vectors/fernet/invalid.json @@ -0,0 +1,58 @@ +[ + { + "desc": "incorrect mac", + "token": "gAAAAAAdwJ6xAAECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPAl1-szkFVzXTuGb4hR8AKtwcaX1YdykQUFBQUFBQUFBQQ==", + "now": "1985-10-26T01:20:01-07:00", + "ttl_sec": 60, + "secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" + }, + { + "desc": "too short", + "token": "gAAAAAAdwJ6xAAECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPA==", + "now": "1985-10-26T01:20:01-07:00", + "ttl_sec": 60, + "secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" + }, + { + "desc": "invalid base64", + "token": "%%%%%%%%%%%%%AECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPAl1-szkFVzXTuGb4hR8AKtwcaX1YdykRtfsH-p1YsUD2Q==", + "now": "1985-10-26T01:20:01-07:00", + "ttl_sec": 60, + "secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" + }, + { + "desc": "payload size not multiple of block size", + "token": "gAAAAAAdwJ6xAAECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPOm73QeoCk9uGib28Xe5vz6oxq5nmxbx_v7mrfyudzUm", + "now": "1985-10-26T01:20:01-07:00", + "ttl_sec": 60, + "secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" + }, + { + "desc": "payload padding error", + "token": "gAAAAAAdwJ6xAAECAwQFBgcICQoLDA0ODz4LEpdELGQAad7aNEHbf-JkLPIpuiYRLQ3RtXatOYREu2FWke6CnJNYIbkuKNqOhw==", + "now": "1985-10-26T01:20:01-07:00", + "ttl_sec": 60, + "secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" + }, + { + "desc": "far-future TS (unacceptable clock skew)", + "token": "gAAAAAAdwStRAAECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPAnja1xKYyhd-Y6mSkTOyTGJmw2Xc2a6kBd-iX9b_qXQcw==", + "now": "1985-10-26T01:20:01-07:00", + "ttl_sec": 60, + "secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" + }, + { + "desc": "expired TTL", + "token": "gAAAAAAdwJ6xAAECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPAl1-szkFVzXTuGb4hR8AKtwcaX1YdykRtfsH-p1YsUD2Q==", + "now": "1985-10-26T01:21:31-07:00", + "ttl_sec": 60, + "secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" + }, + { + "desc": "incorrect IV (causes padding error)", + "token": "gAAAAAAdwJ6xBQECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPAkLhFLHpGtDBRLRTZeUfWgHSv49TF2AUEZ1TIvcZjK1zQ==", + "now": "1985-10-26T01:20:01-07:00", + "ttl_sec": 60, + "secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" + } +] diff --git a/tests/vectors/fernet/verify.json b/tests/vectors/fernet/verify.json new file mode 100644 index 00000000..08c480f5 --- /dev/null +++ b/tests/vectors/fernet/verify.json @@ -0,0 +1,9 @@ +[ + { + "token": "gAAAAAAdwJ6wAAECAwQFBgcICQoLDA0ODy021cpGVWKZ_eEwCGM4BLLF_5CV9dOPmrhuVUPgJobwOz7JcbmrR64jVmpU4IwqDA==", + "now": "1985-10-26T01:20:01-07:00", + "ttl_sec": 60, + "src": "hello", + "secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" + } +] @@ -3,16 +3,19 @@ envlist = py26,py27,pypy,py32,py33,docs,pep8,py3pep8 [testenv] deps = - pytest coverage + iso8601 pretend + pytest commands = coverage run --source=cryptography/,tests/ -m pytest --capture=no --strict coverage report -m [testenv:docs] deps = + pyenchant sphinx + sphinxcontrib-spelling sphinx_rtd_theme basepython = python2.7 commands = @@ -20,6 +23,7 @@ commands = sphinx-build -W -b latex -d {envtmpdir}/doctrees docs docs/_build/latex sphinx-build -W -b doctest -d {envtmpdir}/doctrees docs docs/_build/html sphinx-build -W -b linkcheck docs docs/_build/html + sphinx-build -W -b spelling docs docs/_build/html # Temporarily disable coverage on pypy because of performance problems with # coverage.py on pypy. |