Refs #3331 -- added initial wycheproof integration, starting with x25519, rsa, and keywrap (#4310)

* Refs #3331 -- added initial wycheproof integration, starting with x25519 tests
diff --git a/.travis/install.sh b/.travis/install.sh
index a4aa9a4..e3b20fd 100755
--- a/.travis/install.sh
+++ b/.travis/install.sh
@@ -40,6 +40,9 @@
         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 32e9874..38b6652 100755
--- a/.travis/run.sh
+++ b/.travis/run.sh
@@ -24,7 +24,7 @@
 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 2697b8f..816e9de 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -144,6 +144,16 @@
         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 @@
 
                             @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 @@
                                 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 @@
                             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 c5efbd3..583c409 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 b721f34..ccc3b7c 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 @@
             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 "<WycheproofTest({!r}, {!r}, tcId={})>".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 0000000..e69de29
--- /dev/null
+++ b/tests/wycheproof/__init__.py
diff --git a/tests/wycheproof/test_keywrap.py b/tests/wycheproof/test_keywrap.py
new file mode 100644
index 0000000..5f694e4
--- /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 0000000..b8f2e19
--- /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 0000000..82c0a35
--- /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) == "<WycheproofTest({}, {'tcId': 3}, tcId=3)>"
+
+
+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 0000000..5e6253c
--- /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 b76bfc9..cb882a8 100644
--- a/tox.ini
+++ b/tox.ini
@@ -89,3 +89,4 @@
 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