Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add releasetest #281

Merged
merged 2 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ tmp
interop.log
# pycache
oqs-template/__pycache__
scripts/__pycache__

# Visual Studio Code
.vscode
Expand Down
1 change: 1 addition & 0 deletions oqs-template/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ def load_config(include_disabled_sigs=False):
populate('oqsprov/oqs_encode_key2any.c', config, '/////')
populate('oqsprov/oqs_decode_der2key.c', config, '/////')
populate('oqsprov/oqsprov_keys.c', config, '/////')
populate('scripts/common.py', config, '#####')

config2 = load_config(include_disabled_sigs=True)
config2 = complete_config(config2)
Expand Down
10 changes: 10 additions & 0 deletions oqs-template/scripts/common.py/kex_algs.fragment
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

# post-quantum key exchanges
{% for kem in config['kems'] %}'{{ kem['name_group'] }}', {%- endfor %}
# post-quantum + classical key exchanges
{% for kem in config['kems'] -%}
{%- for hybrid in kem['hybrids'] -%}
'{{ hybrid['hybrid_group'] }}_{{kem['name_group']}}',
{%- endfor -%}
{% endfor %}

12 changes: 12 additions & 0 deletions oqs-template/scripts/common.py/sig_algs.fragment
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

# post-quantum signatures
{% for sig in config['sigs'] %}{% for variant in sig['variants'] %}'{{ variant['name'] }}',
{%- endfor %} {%- endfor %}
# post-quantum + classical signatures
{% for sig in config['sigs'] -%}
{%- for variant in sig['variants'] -%}
{%- for classical_alg in variant['mix_with'] -%}
'{{ classical_alg['name'] }}_{{ variant['name'] }}',
{%- endfor -%}
{%- endfor %} {%- endfor %}

21 changes: 21 additions & 0 deletions scripts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Build and test support scripts

This directory contains various scripts aiming to ease build and test of `oqsprovider`.

## Building

The key file is [fullbuild.sh](fullbuild.sh) with options documented [here](https://github.com/open-quantum-safe/oqs-provider/blob/main/CONFIGURE.md#convenience-build-script-options).

## Testing

### API testing

All features and enabled algorithms are API tested by `ctest` driven code contained in the [test directory](https://github.com/open-quantum-safe/oqs-provider/tree/main/test).

### Command line testing

All features and enabled algorithms are tested via `openssl` command line instructions via the [runtests.sh](runtests.sh) script with options documented [here](https://github.com/open-quantum-safe/oqs-provider/blob/main/CONFIGURE.md#convenience-build-script-options).

### Release testing

All features and all algorithms can be tested in a full matrix running all possible signature and KEM algorithms in client/server setup via the corresponding `openssl s_server/s_client` commands via the [release-test.sh](release-test.sh) script. To run this test successfully, installation of `python3` and `pytest` with `xdist` extension is required, e.g., via `sudo apt install python3 python3-pytest python3-pytest-xdist python3-psutil`. The test must be executed within the main project directory, e.g., as such `./scripts/release-test.sh`. For full operation, a local and up-to-date (release) installation of `openssl` and `liboqs` (e.g., built via `scripts/fulltest.sh`) is recommended.
165 changes: 165 additions & 0 deletions scripts/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import os
import subprocess
import pathlib
import psutil
import time

key_exchanges = [
##### OQS_TEMPLATE_FRAGMENT_KEX_ALGS_START
# post-quantum key exchanges
'frodo640aes','frodo640shake','frodo976aes','frodo976shake','frodo1344aes','frodo1344shake','kyber512','kyber768','kyber1024','bikel1','bikel3','bikel5','hqc128','hqc192','hqc256',
# post-quantum + classical key exchanges
'p256_frodo640aes','x25519_frodo640aes','p256_frodo640shake','x25519_frodo640shake','p384_frodo976aes','x448_frodo976aes','p384_frodo976shake','x448_frodo976shake','p521_frodo1344aes','p521_frodo1344shake','p256_kyber512','x25519_kyber512','p384_kyber768','x448_kyber768','x25519_kyber768','p256_kyber768','p521_kyber1024','p256_bikel1','x25519_bikel1','p384_bikel3','x448_bikel3','p521_bikel5','p256_hqc128','x25519_hqc128','p384_hqc192','x448_hqc192','p521_hqc256',
##### OQS_TEMPLATE_FRAGMENT_KEX_ALGS_END
]
signatures = [
'ecdsap256', 'rsa3072',
##### OQS_TEMPLATE_FRAGMENT_SIG_ALGS_START
# post-quantum signatures
'dilithium2','dilithium3','dilithium5','falcon512','falcon1024','sphincssha2128fsimple','sphincssha2128ssimple','sphincssha2192fsimple','sphincsshake128fsimple',
# post-quantum + classical signatures
'p256_dilithium2','rsa3072_dilithium2','p384_dilithium3','p521_dilithium5','p256_falcon512','rsa3072_falcon512','p521_falcon1024','p256_sphincssha2128fsimple','rsa3072_sphincssha2128fsimple','p256_sphincssha2128ssimple','rsa3072_sphincssha2128ssimple','p384_sphincssha2192fsimple','p256_sphincsshake128fsimple','rsa3072_sphincsshake128fsimple',
##### OQS_TEMPLATE_FRAGMENT_SIG_ALGS_END
]

SERVER_START_ATTEMPTS = 10

def all_pq_groups():
ag = ""
for kex in key_exchanges:
if len(ag)==0:
ag = kex
else:
ag = ag + ":" + kex
return ag

def run_subprocess(command, working_dir='.', expected_returncode=0, input=None, env=os.environ):
"""
Helper function to run a shell command and report success/failure
depending on the exit status of the shell command.
"""

# Note we need to capture stdout/stderr from the subprocess,
# then print it, which pytest will then capture and
# buffer appropriately
print(working_dir + " > " + " ".join(command))
result = subprocess.run(
command,
input=input,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
cwd=working_dir,
env=env
)
if result.returncode != expected_returncode:
print(result.stdout.decode('utf-8'))
assert False, "Got unexpected return code {}".format(result.returncode)
return result.stdout.decode('utf-8')

def start_server(ossl, test_artifacts_dir, sig_alg, worker_id):
command = [ossl, 's_server',
'-cert', os.path.join(test_artifacts_dir, '{}_{}_srv.crt'.format(worker_id, sig_alg)),
'-key', os.path.join(test_artifacts_dir, '{}_{}_srv.key'.format(worker_id, sig_alg)),
'-CAfile', os.path.join(test_artifacts_dir, '{}_{}_CA.crt'.format(worker_id, sig_alg)),
'-tls1_3',
'-quiet',
# add X25519 for baseline server test and all PQ KEMs for single PQ KEM tests:
'-groups', "x25519:"+all_pq_groups(),
# On UNIX-like systems, binding to TCP port 0
# is a request to dynamically generate an unused
# port number.
# TODO: Check if Windows behaves similarly
'-accept', '0']

print(" > " + " ".join(command))
server = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
server_info = psutil.Process(server.pid)

# Try SERVER_START_ATTEMPTS times to see
# what port the server is bound to.
server_start_attempt = 1
while server_start_attempt <= SERVER_START_ATTEMPTS:
if server_info.connections():
break
else:
server_start_attempt += 1
time.sleep(2)
server_port = str(server_info.connections()[0].laddr.port)

# Check SERVER_START_ATTEMPTS times to see
# if the server is responsive.
server_start_attempt = 1
while server_start_attempt <= SERVER_START_ATTEMPTS:
result = subprocess.run([ossl, 's_client', '-connect', 'localhost:{}'.format(server_port)],
input='Q'.encode(),
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
if result.returncode == 0:
break
else:
server_start_attempt += 1
time.sleep(2)

if server_start_attempt > SERVER_START_ATTEMPTS:
raise Exception('Cannot start OpenSSL server')

return server, server_port

def gen_keys(ossl, ossl_config, sig_alg, test_artifacts_dir, filename_prefix):
pathlib.Path(test_artifacts_dir).mkdir(parents=True, exist_ok=True)
if sig_alg == 'ecdsap256':
run_subprocess([ossl, 'ecparam',
'-name', 'prime256v1',
'-out', os.path.join(test_artifacts_dir, '{}_prime256v1.pem'.format(filename_prefix))])
run_subprocess([ossl, 'req', '-x509', '-new',
'-newkey', 'ec:{}'.format(os.path.join(test_artifacts_dir, '{}_prime256v1.pem'.format(filename_prefix))),
'-keyout', os.path.join(test_artifacts_dir, '{}_ecdsap256_CA.key'.format(filename_prefix)),
'-out', os.path.join(test_artifacts_dir, '{}_ecdsap256_CA.crt'.format(filename_prefix)),
'-nodes',
'-subj', '/CN=oqstest_CA',
'-days', '365',
'-config', ossl_config])
run_subprocess([ossl, 'req', '-new',
'-newkey', 'ec:{}'.format(os.path.join(test_artifacts_dir, '{}_prime256v1.pem'.format(filename_prefix))),
'-keyout', os.path.join(test_artifacts_dir, '{}_ecdsap256_srv.key'.format(filename_prefix)),
'-out', os.path.join(test_artifacts_dir, '{}_ecdsap256_srv.csr'.format(filename_prefix)),
'-nodes',
'-subj', '/CN=oqstest_server',
'-config', ossl_config])
else:
if sig_alg == 'rsa3072':
ossl_sig_alg_arg = 'rsa:3072'
else:
ossl_sig_alg_arg = sig_alg
run_subprocess([ossl, 'req', '-x509', '-new',
'-newkey', ossl_sig_alg_arg,
'-keyout', os.path.join(test_artifacts_dir, '{}_{}_CA.key'.format(filename_prefix, sig_alg)),
'-out', os.path.join(test_artifacts_dir, '{}_{}_CA.crt'.format(filename_prefix, sig_alg)),
'-nodes',
'-subj', '/CN=oqstest_CA',
'-days', '365',
'-config', ossl_config])
run_subprocess([ossl, 'req', '-new',
'-newkey', ossl_sig_alg_arg,
'-keyout', os.path.join(test_artifacts_dir, '{}_{}_srv.key'.format(filename_prefix, sig_alg)),
'-out', os.path.join(test_artifacts_dir, '{}_{}_srv.csr'.format(filename_prefix, sig_alg)),
'-nodes',
'-subj', '/CN=oqstest_server',
'-config', ossl_config])

run_subprocess([ossl, 'x509', '-req',
'-in', os.path.join(test_artifacts_dir, '{}_{}_srv.csr'.format(filename_prefix, sig_alg)),
'-out', os.path.join(test_artifacts_dir, '{}_{}_srv.crt'.format(filename_prefix, sig_alg)),
'-CA', os.path.join(test_artifacts_dir, '{}_{}_CA.crt'.format(filename_prefix, sig_alg)),
'-CAkey', os.path.join(test_artifacts_dir, '{}_{}_CA.key'.format(filename_prefix, sig_alg)),
'-CAcreateserial',
'-days', '365'])

# also create pubkeys from certs for dgst verify tests:
env = os.environ
#env["OPENSSL_CONF"]=os.path.join("scripts", "openssl.cnf")
#env["OPENSSL_MODULES"]=os.path.join("_build", "lib")
run_subprocess([ossl, 'req',
'-in', os.path.join(test_artifacts_dir, '{}_{}_srv.csr'.format(filename_prefix, sig_alg)),
'-pubkey', '-out', os.path.join(test_artifacts_dir, '{}_{}_srv.pubk'.format(filename_prefix, sig_alg)) ],
env=env)
20 changes: 20 additions & 0 deletions scripts/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import os
import pytest
import subprocess

def pytest_addoption(parser):
parser.addoption("--ossl", action="store", help="ossl: Path to standalone OpenSSL executable.")
parser.addoption("--ossl-config", action="store", help="ossl-config: Path to openssl.cnf file.")
parser.addoption("--test-artifacts-dir", action="store", help="test-artifacts-dir: Path to directory containing files generated during the testing process.")

@pytest.fixture
def ossl_config(request):
return os.path.normpath(request.config.getoption("--ossl-config"))

@pytest.fixture
def ossl(request):
return os.path.normpath(request.config.getoption("--ossl"))

@pytest.fixture
def test_artifacts_dir(request):
return os.path.normpath(request.config.getoption("--test-artifacts-dir"))
2 changes: 2 additions & 0 deletions scripts/pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[pytest]
addopts = --verbose --ossl=.local/bin/openssl --ossl-config=scripts/openssl-ca.cnf --test-artifacts-dir=tmp
39 changes: 39 additions & 0 deletions scripts/release-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/bin/bash

# Stop in case of error
set -e

# To be run as part of a release test only on Linux
# requires python, pytest, xdist; install e.g. via
# sudo apt install python3 python3-pytest python3-pytest-xdist python3-psutil

# must be run in main folder
# multicore machine recommended for fast execution

# expect (ideally latest/release-test) liboqs to be already build and present
if [ -d liboqs ]; then
export LIBOQS_SRC_DIR=`pwd`/liboqs
else
echo "liboqs not found. Exiting."
exit 1
fi

if [ -d oqs-template ]; then
# just a temp setup
git checkout -b reltest
# Activate all algorithms
sed -i "s/enable\: false/enable\: true/g" oqs-template/generate.yml
python3 oqs-template/generate.py
rm -rf _build
./scripts/fullbuild.sh
./scripts/runtests.sh
if [ -f .local/bin/openssl ]; then
OPENSSL_MODULES=`pwd`/_build/lib OPENSSL_CONF=`pwd`/scripts/openssl-ca.cnf python3 -m pytest --numprocesses=auto scripts/test_tls_full.py
else
echo "For full TLS PQ SIG/KEM matrix test, build (latest) openssl locally."
fi
git reset --hard && git checkout main && git branch -D reltest
else
echo "$0 must be run in main oqs-provider folder. Exiting."
fi

30 changes: 30 additions & 0 deletions scripts/test_tls_full.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import common
import pytest
import sys
import os

@pytest.fixture(params=common.signatures)
def server(ossl, ossl_config, test_artifacts_dir, request, worker_id):
# Setup: start ossl server
common.gen_keys(ossl, ossl_config, request.param, test_artifacts_dir, worker_id)
server, port = common.start_server(ossl, test_artifacts_dir, request.param, worker_id)
# Run tests
yield (request.param, port)
# Teardown: stop ossl server
server.kill()

@pytest.mark.parametrize('kex_name', common.key_exchanges)
def test_sig_kem_pair(ossl, server, test_artifacts_dir, kex_name, worker_id):
client_output = common.run_subprocess([ossl, 's_client',
'-groups', kex_name,
'-CAfile', os.path.join(test_artifacts_dir, '{}_{}_CA.crt'.format(worker_id, server[0])),
'-verify_return_error',
'-connect', 'localhost:{}'.format(server[1])],
input='Q'.encode())
# OpenSSL3 by default does not output KEM used; so rely on forced client group and OK handshake completion:
if not "SSL handshake has read" in client_output:
assert False, "Handshake failure."

if __name__ == "__main__":
import sys
pytest.main(sys.argv)