From c3f2a68a1ba77508e3a1dc8619683ae009936dbd Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Mon, 16 Jul 2018 11:18:33 -0400 Subject: [PATCH] Refs #3331 -- added initial wycheproof integration, starting with x25519, rsa, and keywrap (#4310) * Refs #3331 -- added initial wycheproof integration, starting with x25519 tests --- .travis/install.sh | 3 ++ .travis/run.sh | 2 +- Jenkinsfile | 16 ++++-- tests/conftest.py | 19 ++++++- tests/utils.py | 41 +++++++++++++++ tests/wycheproof/__init__.py | 0 tests/wycheproof/test_keywrap.py | 61 +++++++++++++++++++++++ tests/wycheproof/test_rsa.py | 85 ++++++++++++++++++++++++++++++++ tests/wycheproof/test_utils.py | 21 ++++++++ tests/wycheproof/test_x25519.py | 42 ++++++++++++++++ tox.ini | 1 + 11 files changed, 286 insertions(+), 5 deletions(-) create mode 100644 tests/wycheproof/__init__.py create mode 100644 tests/wycheproof/test_keywrap.py create mode 100644 tests/wycheproof/test_rsa.py create mode 100644 tests/wycheproof/test_utils.py create mode 100644 tests/wycheproof/test_x25519.py diff --git a/.travis/install.sh b/.travis/install.sh index a4aa9a42a0be..e3b20fdb8d6a 100755 --- a/.travis/install.sh +++ b/.travis/install.sh @@ -40,6 +40,9 @@ elif [ -n "${LIBRESSL}" ]; then popd fi fi + +git clone --depth=1 https://github.com/google/wycheproof $HOME/wycheproof + pip install virtualenv python -m virtualenv ~/.venv diff --git a/.travis/run.sh b/.travis/run.sh index 32e9874bee2b..38b66528d7af 100755 --- a/.travis/run.sh +++ b/.travis/run.sh @@ -24,7 +24,7 @@ fi source ~/.venv/bin/activate if [ -n "${TOXENV}" ]; then - tox + tox -- --wycheproof-root=$HOME/wycheproof else pip install . case "${DOWNSTREAM}" in diff --git a/Jenkinsfile b/Jenkinsfile index 2697b8f60406..816e9de84ba8 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -144,6 +144,16 @@ def build(toxenv, label, imageName, artifacts, artifactExcludes) { timeout(time: 30, unit: 'MINUTES') { checkout_git(label) + checkout([ + $class: 'GitSCM', + extensions: [[ + $class: 'RelativeTargetDirectory', + relativeTargetDir: 'wycheproof', + ]], + userRemoteConfigs: [[ + 'url': 'https://github.com/google/wycheproof', + ]] + ]) withCredentials([string(credentialsId: 'cryptography-codecov-token', variable: 'CODECOV_TOKEN')]) { withEnv(["LABEL=$label", "TOXENV=$toxenv", "IMAGE_NAME=$imageName"]) { @@ -185,7 +195,7 @@ def build(toxenv, label, imageName, artifacts, artifactExcludes) { @set INCLUDE="${opensslPaths[label]['include']}";%INCLUDE% @set LIB="${opensslPaths[label]['lib']}";%LIB% - tox -r + tox -r -- --wycheproof-root=../wycheproof IF %ERRORLEVEL% NEQ 0 EXIT /B %ERRORLEVEL% virtualenv .codecov call .codecov/Scripts/activate @@ -205,7 +215,7 @@ def build(toxenv, label, imageName, artifacts, artifactExcludes) { CRYPTOGRAPHY_SUPPRESS_LINK_FLAGS=1 \ LDFLAGS="/usr/local/opt/openssl\\@1.1/lib/libcrypto.a /usr/local/opt/openssl\\@1.1/lib/libssl.a" \ CFLAGS="-I/usr/local/opt/openssl\\@1.1/include -Werror -Wno-error=deprecated-declarations -Wno-error=incompatible-pointer-types -Wno-error=unused-function -Wno-error=unused-command-line-argument -mmacosx-version-min=10.9" \ - tox -r -- --color=yes + tox -r -- --color=yes --wycheproof-root=../wycheproof virtualenv .venv source .venv/bin/activate # This pin must be kept in sync with tox.ini @@ -218,7 +228,7 @@ def build(toxenv, label, imageName, artifacts, artifactExcludes) { sh """#!/usr/bin/env bash set -xe cd cryptography - tox -r -- --color=yes + tox -r -- --color=yes --wycheproof-root=../wycheproof virtualenv .venv source .venv/bin/activate # This pin must be kept in sync with tox.ini diff --git a/tests/conftest.py b/tests/conftest.py index c5efbd36a340..583c4099d9a9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,13 +8,30 @@ from cryptography.hazmat.backends.openssl import backend as openssl_backend -from .utils import check_backend_support +from .utils import ( + check_backend_support, load_wycheproof_tests, skip_if_wycheproof_none +) def pytest_report_header(config): return "OpenSSL: {0}".format(openssl_backend.openssl_version_text()) +def pytest_addoption(parser): + parser.addoption("--wycheproof-root", default=None) + + +def pytest_generate_tests(metafunc): + if "wycheproof" in metafunc.fixturenames: + wycheproof = metafunc.config.getoption("--wycheproof-root") + skip_if_wycheproof_none(wycheproof) + + testcases = [] + for path in metafunc.function.wycheproof_tests.args: + testcases.extend(load_wycheproof_tests(wycheproof, path)) + metafunc.parametrize("wycheproof", testcases) + + @pytest.fixture() def backend(request): required_interfaces = [ diff --git a/tests/utils.py b/tests/utils.py index b721f3440f0a..ccc3b7c1bbea 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -6,7 +6,9 @@ import binascii import collections +import json import math +import os import re from contextlib import contextmanager @@ -884,3 +886,42 @@ def load_nist_ccm_vectors(vector_data): test_data[name.lower()] = value.encode("ascii") return data + + +class WycheproofTest(object): + def __init__(self, testgroup, testcase): + self.testgroup = testgroup + self.testcase = testcase + + def __repr__(self): + return "".format( + self.testgroup, self.testcase, self.testcase["tcId"], + ) + + @property + def valid(self): + return self.testcase["result"] == "valid" + + @property + def acceptable(self): + return self.testcase["result"] == "acceptable" + + def has_flag(self, flag): + return flag in self.testcase["flags"] + + +def skip_if_wycheproof_none(wycheproof): + # This is factored into its own function so we can easily test both + # branches + if wycheproof is None: + pytest.skip("--wycheproof-root not provided") + + +def load_wycheproof_tests(wycheproof, test_file): + path = os.path.join(wycheproof, "testvectors", test_file) + with open(path) as f: + data = json.load(f) + for group in data["testGroups"]: + cases = group.pop("tests") + for c in cases: + yield WycheproofTest(group, c) diff --git a/tests/wycheproof/__init__.py b/tests/wycheproof/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/wycheproof/test_keywrap.py b/tests/wycheproof/test_keywrap.py new file mode 100644 index 000000000000..5f694e4d3346 --- /dev/null +++ b/tests/wycheproof/test_keywrap.py @@ -0,0 +1,61 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import absolute_import, division, print_function + +import binascii + +import pytest + +from cryptography.hazmat.backends.interfaces import CipherBackend +from cryptography.hazmat.primitives import keywrap + + +@pytest.mark.requires_backend_interface(interface=CipherBackend) +@pytest.mark.wycheproof_tests("kwp_test.json") +def test_keywrap_with_padding(backend, wycheproof): + wrapping_key = binascii.unhexlify(wycheproof.testcase["key"]) + key_to_wrap = binascii.unhexlify(wycheproof.testcase["msg"]) + expected = binascii.unhexlify(wycheproof.testcase["ct"]) + + result = keywrap.aes_key_wrap_with_padding( + wrapping_key, key_to_wrap, backend + ) + if wycheproof.valid or wycheproof.acceptable: + assert result == expected + + if wycheproof.valid or (wycheproof.acceptable and not len(expected) < 16): + result = keywrap.aes_key_unwrap_with_padding( + wrapping_key, expected, backend + ) + assert result == key_to_wrap + else: + with pytest.raises(keywrap.InvalidUnwrap): + keywrap.aes_key_unwrap_with_padding( + wrapping_key, expected, backend + ) + + +@pytest.mark.requires_backend_interface(interface=CipherBackend) +@pytest.mark.wycheproof_tests("kw_test.json") +def test_keywrap(backend, wycheproof): + wrapping_key = binascii.unhexlify(wycheproof.testcase["key"]) + key_to_wrap = binascii.unhexlify(wycheproof.testcase["msg"]) + expected = binascii.unhexlify(wycheproof.testcase["ct"]) + + if ( + wycheproof.valid or ( + wycheproof.acceptable and + wycheproof.testcase["comment"] != "invalid size of wrapped key" + ) + ): + result = keywrap.aes_key_wrap(wrapping_key, key_to_wrap, backend) + assert result == expected + + if wycheproof.valid or wycheproof.acceptable: + result = keywrap.aes_key_unwrap(wrapping_key, expected, backend) + assert result == key_to_wrap + else: + with pytest.raises(keywrap.InvalidUnwrap): + keywrap.aes_key_unwrap(wrapping_key, expected, backend) diff --git a/tests/wycheproof/test_rsa.py b/tests/wycheproof/test_rsa.py new file mode 100644 index 000000000000..b8f2e19d093c --- /dev/null +++ b/tests/wycheproof/test_rsa.py @@ -0,0 +1,85 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import absolute_import, division, print_function + +import binascii + +import pytest + +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.backends.interfaces import RSABackend +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import padding + + +_DIGESTS = { + "SHA-1": hashes.SHA1(), + "SHA-224": hashes.SHA224(), + "SHA-256": hashes.SHA256(), + "SHA-384": hashes.SHA384(), + "SHA-512": hashes.SHA512(), +} + + +def should_verify(backend, wycheproof): + if wycheproof.valid: + return True + + if wycheproof.acceptable: + if ( + backend._lib.CRYPTOGRAPHY_OPENSSL_110_OR_GREATER and + wycheproof.has_flag("MissingNull") + ): + return False + return True + + return False + + +@pytest.mark.requires_backend_interface(interface=RSABackend) +@pytest.mark.supported( + only_if=lambda backend: ( + # TODO: this also skips on LibreSSL, which is ok for now, since these + # don't pass on Libre, but we'll need to fix this after they resolve + # it. + not backend._lib.CRYPTOGRAPHY_OPENSSL_LESS_THAN_102 + ), + skip_message=( + "Many of these tests fail on OpenSSL < 1.0.2 and since upstream isn't" + " maintaining it, they'll never be fixed." + ), +) +@pytest.mark.wycheproof_tests( + "rsa_signature_test.json", + "rsa_signature_2048_sha224_test.json", + "rsa_signature_2048_sha256_test.json", + "rsa_signature_2048_sha512_test.json", + "rsa_signature_3072_sha256_test.json", + "rsa_signature_3072_sha384_test.json", + "rsa_signature_3072_sha512_test.json", + "rsa_signature_4096_sha384_test.json", + "rsa_signature_4096_sha512_test.json", +) +def test_rsa_signature(backend, wycheproof): + key = serialization.load_der_public_key( + binascii.unhexlify(wycheproof.testgroup["keyDer"]), backend + ) + digest = _DIGESTS[wycheproof.testgroup["sha"]] + + if should_verify(backend, wycheproof): + key.verify( + binascii.unhexlify(wycheproof.testcase["sig"]), + binascii.unhexlify(wycheproof.testcase["msg"]), + padding.PKCS1v15(), + digest, + ) + else: + with pytest.raises(InvalidSignature): + key.verify( + binascii.unhexlify(wycheproof.testcase["sig"]), + binascii.unhexlify(wycheproof.testcase["msg"]), + padding.PKCS1v15(), + digest, + ) diff --git a/tests/wycheproof/test_utils.py b/tests/wycheproof/test_utils.py new file mode 100644 index 000000000000..82c0a3596396 --- /dev/null +++ b/tests/wycheproof/test_utils.py @@ -0,0 +1,21 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import absolute_import, division, print_function + +import pytest + +from ..utils import WycheproofTest, skip_if_wycheproof_none + + +def test_wycheproof_test_repr(): + wycheproof = WycheproofTest({}, {"tcId": 3}) + assert repr(wycheproof) == "" + + +def test_skip_if_wycheproof_none(): + with pytest.raises(pytest.skip.Exception): + skip_if_wycheproof_none(None) + + skip_if_wycheproof_none("abc") diff --git a/tests/wycheproof/test_x25519.py b/tests/wycheproof/test_x25519.py new file mode 100644 index 000000000000..5e6253ce1432 --- /dev/null +++ b/tests/wycheproof/test_x25519.py @@ -0,0 +1,42 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import absolute_import, division, print_function + +import binascii + +import pytest + +from cryptography.hazmat.backends.interfaces import DHBackend +from cryptography.hazmat.primitives.asymmetric.x25519 import ( + X25519PrivateKey, X25519PublicKey +) + + +@pytest.mark.supported( + only_if=lambda backend: backend.x25519_supported(), + skip_message="Requires OpenSSL with X25519 support" +) +@pytest.mark.requires_backend_interface(interface=DHBackend) +@pytest.mark.wycheproof_tests("x25519_test.json") +def test_x25519(backend, wycheproof): + assert list(wycheproof.testgroup.items()) == [("curve", "curve25519")] + + private_key = X25519PrivateKey._from_private_bytes( + binascii.unhexlify(wycheproof.testcase["private"]) + ) + public_key = X25519PublicKey.from_public_bytes( + binascii.unhexlify(wycheproof.testcase["public"]) + ) + + assert wycheproof.valid or wycheproof.acceptable + + expected = binascii.unhexlify(wycheproof.testcase["shared"]) + if expected == b"\x00" * 32: + assert wycheproof.acceptable + # OpenSSL returns an error on all zeros shared key + with pytest.raises(ValueError): + private_key.exchange(public_key) + else: + assert private_key.exchange(public_key) == expected diff --git a/tox.ini b/tox.ini index b76bfc972a85..cb882a8aa339 100644 --- a/tox.ini +++ b/tox.ini @@ -89,3 +89,4 @@ addopts = -r s markers = requires_backend_interface: this test requires a specific backend interface supported: parametrized test requiring only_if and skip_message + wycheproof_tests: this test runs a wycheproof fixture