Skip to content

Commit

Permalink
Refs pyca#3331 -- added initial wycheproof integration, starting with…
Browse files Browse the repository at this point in the history
… x25519, rsa, and keywrap (pyca#4310)

* Refs pyca#3331 -- added initial wycheproof integration, starting with x25519 tests
  • Loading branch information
alex authored and Amaury Forgeot d'Arc committed Jul 22, 2018
1 parent 993b1f9 commit c3f2a68
Show file tree
Hide file tree
Showing 11 changed files with 286 additions and 5 deletions.
3 changes: 3 additions & 0 deletions .travis/install.sh
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .travis/run.sh
Expand Up @@ -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
Expand Down
16 changes: 13 additions & 3 deletions Jenkinsfile
Expand Up @@ -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"]) {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
19 changes: 18 additions & 1 deletion tests/conftest.py
Expand Up @@ -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 = [
Expand Down
41 changes: 41 additions & 0 deletions tests/utils.py
Expand Up @@ -6,7 +6,9 @@

import binascii
import collections
import json
import math
import os
import re
from contextlib import contextmanager

Expand Down Expand Up @@ -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 "<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)
Empty file added tests/wycheproof/__init__.py
Empty file.
61 changes: 61 additions & 0 deletions 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)
85 changes: 85 additions & 0 deletions 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,
)
21 changes: 21 additions & 0 deletions 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")
42 changes: 42 additions & 0 deletions 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
1 change: 1 addition & 0 deletions tox.ini
Expand Up @@ -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

0 comments on commit c3f2a68

Please sign in to comment.