├── tests ├── __init__.py ├── test_kem.py ├── test_stfl_sig.py └── test_sig.py ├── examples ├── __init__.py ├── rand.py ├── sig.py ├── stfl_sig.py └── kem.py ├── data └── xmss_xmssmt_keys │ ├── xmss-sha2_16_512.der │ ├── xmss-sha2_20_192.der │ ├── xmss-sha2_20_256.der │ ├── xmss-sha2_20_512.der │ ├── xmss-shake_16_256.der │ ├── xmss-shake_16_512.der │ ├── xmss-shake_20_256.der │ ├── xmss-shake_20_512.der │ ├── xmss-shake256_20_192.der │ ├── xmss-shake256_20_256.der │ ├── xmssmt-sha2_40_layers_2_256.der │ ├── xmssmt-sha2_60_layers_3_256.der │ ├── xmssmt-shake_40_layers_2_256.der │ └── xmssmt-shake_60_layers_3_256.der ├── Makefile ├── .pre-commit-config.yaml ├── docker ├── README.md ├── Dockerfile-simple ├── minitest.py └── Dockerfile ├── LICENSE.txt ├── Dockerfile ├── oqs ├── __init__.py ├── rand.py ├── serialize.py └── oqs.py ├── .github └── workflows │ ├── python_simplified.yml │ └── python_detailed.yml ├── RELEASE.md ├── .gitignore ├── pyproject.toml ├── CHANGES.md ├── CODE_OF_CONDUCT.md └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/xmss_xmssmt_keys/xmss-sha2_16_512.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-quantum-safe/liboqs-python/HEAD/data/xmss_xmssmt_keys/xmss-sha2_16_512.der -------------------------------------------------------------------------------- /data/xmss_xmssmt_keys/xmss-sha2_20_192.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-quantum-safe/liboqs-python/HEAD/data/xmss_xmssmt_keys/xmss-sha2_20_192.der -------------------------------------------------------------------------------- /data/xmss_xmssmt_keys/xmss-sha2_20_256.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-quantum-safe/liboqs-python/HEAD/data/xmss_xmssmt_keys/xmss-sha2_20_256.der -------------------------------------------------------------------------------- /data/xmss_xmssmt_keys/xmss-sha2_20_512.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-quantum-safe/liboqs-python/HEAD/data/xmss_xmssmt_keys/xmss-sha2_20_512.der -------------------------------------------------------------------------------- /data/xmss_xmssmt_keys/xmss-shake_16_256.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-quantum-safe/liboqs-python/HEAD/data/xmss_xmssmt_keys/xmss-shake_16_256.der -------------------------------------------------------------------------------- /data/xmss_xmssmt_keys/xmss-shake_16_512.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-quantum-safe/liboqs-python/HEAD/data/xmss_xmssmt_keys/xmss-shake_16_512.der -------------------------------------------------------------------------------- /data/xmss_xmssmt_keys/xmss-shake_20_256.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-quantum-safe/liboqs-python/HEAD/data/xmss_xmssmt_keys/xmss-shake_20_256.der -------------------------------------------------------------------------------- /data/xmss_xmssmt_keys/xmss-shake_20_512.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-quantum-safe/liboqs-python/HEAD/data/xmss_xmssmt_keys/xmss-shake_20_512.der -------------------------------------------------------------------------------- /data/xmss_xmssmt_keys/xmss-shake256_20_192.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-quantum-safe/liboqs-python/HEAD/data/xmss_xmssmt_keys/xmss-shake256_20_192.der -------------------------------------------------------------------------------- /data/xmss_xmssmt_keys/xmss-shake256_20_256.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-quantum-safe/liboqs-python/HEAD/data/xmss_xmssmt_keys/xmss-shake256_20_256.der -------------------------------------------------------------------------------- /data/xmss_xmssmt_keys/xmssmt-sha2_40_layers_2_256.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-quantum-safe/liboqs-python/HEAD/data/xmss_xmssmt_keys/xmssmt-sha2_40_layers_2_256.der -------------------------------------------------------------------------------- /data/xmss_xmssmt_keys/xmssmt-sha2_60_layers_3_256.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-quantum-safe/liboqs-python/HEAD/data/xmss_xmssmt_keys/xmssmt-sha2_60_layers_3_256.der -------------------------------------------------------------------------------- /data/xmss_xmssmt_keys/xmssmt-shake_40_layers_2_256.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-quantum-safe/liboqs-python/HEAD/data/xmss_xmssmt_keys/xmssmt-shake_40_layers_2_256.der -------------------------------------------------------------------------------- /data/xmss_xmssmt_keys/xmssmt-shake_60_layers_3_256.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-quantum-safe/liboqs-python/HEAD/data/xmss_xmssmt_keys/xmssmt-shake_60_layers_3_256.der -------------------------------------------------------------------------------- /examples/rand.py: -------------------------------------------------------------------------------- 1 | # Various RNGs Python example 2 | 3 | import logging 4 | import platform # to learn the OS we're on 5 | from sys import stdout 6 | 7 | import oqs.rand as oqsrand # must be explicitly imported 8 | from oqs import oqs_python_version, oqs_version 9 | 10 | logger = logging.getLogger(__name__) 11 | logger.setLevel(logging.INFO) 12 | logger.addHandler(logging.StreamHandler(stdout)) 13 | 14 | logger.info("liboqs version: %s", oqs_version()) 15 | logger.info("liboqs-python version: %s", oqs_python_version()) 16 | 17 | oqsrand.randombytes_switch_algorithm("system") 18 | logger.info( 19 | "System (default): %s", 20 | " ".join(f"{x:02X}" for x in oqsrand.randombytes(32)), 21 | ) 22 | 23 | # We do not yet support OpenSSL under Windows 24 | if platform.system() != "Windows": 25 | oqsrand.randombytes_switch_algorithm("OpenSSL") 26 | logger.info( 27 | "OpenSSL: %s", 28 | " ".join(f"{x:02X}" for x in oqsrand.randombytes(32)), 29 | ) 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Code checker/formatter 2 | # 3 | # Pre-requisites 4 | # 5 | # isort 6 | # mypy 7 | # ruff 8 | # uv 9 | 10 | src-dir = oqs 11 | tests-dir = tests 12 | examples-dir = examples 13 | 14 | .PHONY lint: 15 | lint: 16 | echo "Running ruff..." 17 | uv run ruff check --config pyproject.toml --diff $(src-dir) $(tests-dir) $(examples-dir) 18 | 19 | .PHONY format: 20 | format: 21 | echo "Running ruff check with --fix..." 22 | uv run ruff check --config pyproject.toml --fix --unsafe-fixes $(src-dir) $(tests-dir) $(examples-dir) 23 | 24 | echo "Running ruff..." 25 | uv run ruff format --config pyproject.toml $(src-dir) $(tests-dir) $(examples-dir) 26 | 27 | echo "Running isort..." 28 | uv run isort --settings-file pyproject.toml $(src-dir) $(tests-dir) $(examples-dir) 29 | 30 | .PHONE mypy: 31 | mypy: 32 | echo "Running MyPy..." 33 | uv run mypy --config-file pyproject.toml $(src-dir) 34 | 35 | .PHONY outdated: 36 | outdated: 37 | uv tree --outdated --universal 38 | 39 | .PHONY sync: 40 | sync: 41 | uv sync --extra dev --extra lint 42 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | fail_fast: false 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v5.0.0 5 | hooks: 6 | - id: "trailing-whitespace" 7 | - id: "check-case-conflict" 8 | - id: "check-merge-conflict" 9 | - id: "debug-statements" 10 | - id: "end-of-file-fixer" 11 | - id: "mixed-line-ending" 12 | args: [ "--fix", "crlf" ] 13 | types: 14 | - python 15 | - yaml 16 | - toml 17 | - text 18 | - id: "detect-private-key" 19 | - id: "check-yaml" 20 | - id: "check-toml" 21 | - id: "check-json" 22 | 23 | - repo: https://github.com/charliermarsh/ruff-pre-commit 24 | rev: v0.9.4 25 | hooks: 26 | - id: ruff 27 | args: [ "--fix" ] 28 | files: "oqs" 29 | 30 | - id: ruff-format 31 | files: "oqs" 32 | 33 | - repo: https://github.com/pycqa/isort 34 | rev: 5.13.2 35 | hooks: 36 | - id: isort 37 | name: isort (python) 38 | files: "oqs" 39 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | # OQS-python 2 | 3 | This docker image contains python3 with library support for quantum-safe crypto 4 | (QSC) operations. 5 | 6 | To this end, it contains [liboqs](https://github.com/open-quantum-safe/liboqs) 7 | as well as [OQS-OpenSSL](https://github.com/open-quantum-safe/openssl) from the 8 | [OpenQuantumSafe](https://openquantumsafe.org) project all wrapped up in Python 9 | APIs using [liboqs-python](https://github.com/open-quantum-safe/liboqs-python). 10 | 11 | ## Quick start 12 | 13 | - Executing `docker run -it openquantumsafe/python` tests all QSC algorithms 14 | against the interop server at https://test.openquantumsafe.org. 15 | - Executing `docker run -it openquantumsafe/python sh` provides a shell 16 | environment where liboqs and QSC-enabled SSL/TLS is available for use. See 17 | the included file `minitest.py` for sample code exercizing this 18 | functionality. 19 | 20 | ## Further examples 21 | 22 | More samples are available at 23 | [liboqs-python examples](https://github.com/open-quantum-safe/liboqs-python/tree/main/examples). 24 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2025 Open Quantum Safe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:latest 2 | 3 | # Install dependencies 4 | RUN apt-get -y update && \ 5 | apt-get install -y build-essential git cmake libssl-dev python3 python3-venv pip 6 | 7 | # Get liboqs 8 | RUN git clone --depth 1 --branch main https://github.com/open-quantum-safe/liboqs 9 | 10 | # Install liboqs with stateful-signature algorithms enabled 11 | RUN cmake -S liboqs -B liboqs/build \ 12 | -DBUILD_SHARED_LIBS=ON \ 13 | -DOQS_ENABLE_SIG_STFL_LMS=ON \ 14 | -DOQS_ENABLE_SIG_STFL_XMSS=ON \ 15 | -DOQS_HAZARDOUS_EXPERIMENTAL_ENABLE_SIG_STFL_KEY_SIG_GEN=ON && \ 16 | cmake --build liboqs/build --parallel 4 && \ 17 | cmake --build liboqs/build --target install 18 | 19 | # Enable a normal user 20 | RUN useradd -m -c "Open Quantum Safe" oqs 21 | USER oqs 22 | WORKDIR /home/oqs 23 | 24 | # Create a Python 3 virtual environment 25 | RUN python3 -m venv venv 26 | 27 | # Get liboqs-python 28 | RUN git clone --depth 1 --branch main https://github.com/open-quantum-safe/liboqs-python.git 29 | 30 | # Install liboqs-python 31 | ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib 32 | ENV PYTHONPATH=$PYTHONPATH:/home/oqs/liboqs-python 33 | RUN . venv/bin/activate && cd liboqs-python && pip install . && cd $HOME 34 | -------------------------------------------------------------------------------- /oqs/__init__.py: -------------------------------------------------------------------------------- 1 | from oqs.oqs import ( 2 | OQS_SUCCESS, 3 | OQS_VERSION, 4 | KeyEncapsulation, 5 | MechanismNotEnabledError, 6 | MechanismNotSupportedError, 7 | Signature, 8 | StatefulSignature, 9 | get_enabled_kem_mechanisms, 10 | get_enabled_sig_mechanisms, 11 | get_enabled_stateful_sig_mechanisms, 12 | get_supported_kem_mechanisms, 13 | get_supported_sig_mechanisms, 14 | get_supported_stateful_sig_mechanisms, 15 | is_kem_enabled, 16 | is_sig_enabled, 17 | sig_supports_context, 18 | native, 19 | oqs_python_version, 20 | oqs_version, 21 | ) 22 | 23 | __all__ = ( 24 | "OQS_SUCCESS", 25 | "OQS_VERSION", 26 | "KeyEncapsulation", 27 | "MechanismNotEnabledError", 28 | "MechanismNotSupportedError", 29 | "Signature", 30 | "StatefulSignature", 31 | "get_enabled_kem_mechanisms", 32 | "get_enabled_sig_mechanisms", 33 | "get_enabled_stateful_sig_mechanisms", 34 | "get_supported_kem_mechanisms", 35 | "get_supported_sig_mechanisms", 36 | "get_supported_stateful_sig_mechanisms", 37 | "is_kem_enabled", 38 | "is_sig_enabled", 39 | "native", 40 | "oqs_python_version", 41 | "oqs_version", 42 | "sig_supports_context", 43 | ) 44 | -------------------------------------------------------------------------------- /oqs/rand.py: -------------------------------------------------------------------------------- 1 | """ 2 | Open Quantum Safe (OQS) Python Wrapper for liboqs. 3 | 4 | The liboqs project provides post-quantum public key cryptography algorithms: 5 | https://github.com/open-quantum-safe/liboqs 6 | 7 | This module provides a Python 3 interface to libOQS RNGs. 8 | """ 9 | 10 | import ctypes as ct 11 | 12 | import oqs 13 | 14 | 15 | def randombytes(bytes_to_read: int) -> bytes: 16 | """ 17 | Generate random bytes. This implementation uses either the default RNG algorithm ("system"), 18 | or whichever algorithm has been selected by random_bytes_switch_algorithm(). 19 | 20 | :param bytes_to_read: the number of random bytes to generate. 21 | :return: random bytes. 22 | """ 23 | result = ct.create_string_buffer(bytes_to_read) 24 | oqs.native().OQS_randombytes(result, ct.c_size_t(bytes_to_read)) 25 | return bytes(result) 26 | 27 | 28 | def randombytes_switch_algorithm(alg_name: str) -> None: 29 | """ 30 | Switches the core OQS_randombytes to use the specified algorithm. See liboqs 31 | headers for more details. 32 | 33 | :param alg_name: algorithm name, possible values are "system" and "OpenSSL". 34 | """ 35 | if ( 36 | oqs.native().OQS_randombytes_switch_algorithm( 37 | ct.create_string_buffer(alg_name.encode()), 38 | ) 39 | != oqs.OQS_SUCCESS 40 | ): 41 | msg = "Can not switch algorithm" 42 | raise RuntimeError(msg) 43 | -------------------------------------------------------------------------------- /examples/sig.py: -------------------------------------------------------------------------------- 1 | # Signature Python example 2 | 3 | import logging 4 | from pprint import pformat 5 | from sys import stdout 6 | 7 | import oqs 8 | 9 | logger = logging.getLogger(__name__) 10 | logger.setLevel(logging.INFO) 11 | logger.addHandler(logging.StreamHandler(stdout)) 12 | 13 | logger.info("liboqs version: %s", oqs.oqs_version()) 14 | logger.info("liboqs-python version: %s", oqs.oqs_python_version()) 15 | logger.info( 16 | "Enabled signature mechanisms:\n%s", 17 | pformat(oqs.get_enabled_sig_mechanisms(), compact=True), 18 | ) 19 | 20 | message = b"This is the message to sign" 21 | 22 | # Create signer and verifier with sample signature mechanisms 23 | sigalg = "ML-DSA-44" 24 | with oqs.Signature(sigalg) as signer, oqs.Signature(sigalg) as verifier: 25 | logger.info("Signature details:\n%s", pformat(signer.details)) 26 | 27 | # Signer generates its keypair 28 | signer_public_key = signer.generate_keypair() 29 | # Optionally, the secret key can be obtained by calling export_secret_key() 30 | # and the signer can later be re-instantiated with the key pair: 31 | # secret_key = signer.export_secret_key() 32 | 33 | # Store key pair, wait... (session resumption): 34 | # signer = oqs.Signature(sigalg, secret_key) 35 | 36 | # Signer signs the message 37 | signature = signer.sign(message) 38 | 39 | # Verifier verifies the signature 40 | is_valid = verifier.verify(message, signature, signer_public_key) 41 | 42 | logger.info("Valid signature? %s", is_valid) 43 | -------------------------------------------------------------------------------- /.github/workflows/python_simplified.yml: -------------------------------------------------------------------------------- 1 | name: GitHub actions simplified 2 | 3 | on: 4 | push: 5 | branches: [ "**" ] 6 | pull_request: 7 | branches: [ "**" ] 8 | repository_dispatch: 9 | types: [ "**" ] 10 | 11 | permissions: 12 | contents: read 13 | 14 | concurrency: 15 | group: test-python-simplified-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} 16 | cancel-in-progress: ${{ github.event_name == 'pull_request' }} 17 | 18 | env: 19 | PYOQS_ENABLE_FAULTHANDLER: "1" 20 | 21 | jobs: 22 | build: 23 | strategy: 24 | matrix: 25 | os: [ ubuntu-latest, macos-latest, windows-latest ] 26 | runs-on: ${{ matrix.os }} 27 | 28 | steps: 29 | - uses: actions/checkout@v4 30 | 31 | - name: Install uv 32 | uses: astral-sh/setup-uv@v5 33 | with: 34 | version: "latest" 35 | enable-cache: true 36 | cache-dependency-glob: "**/pyproject.toml" 37 | 38 | - name: Set up Python 3.9 39 | run: uv python install 3.9 40 | 41 | - name: Run examples 42 | run: | 43 | uv sync --extra dev 44 | uv run examples/kem.py 45 | uv run examples/sig.py 46 | uv run examples/rand.py 47 | uv run examples/stfl_sig.py 48 | uv run oqs/serialize.py 49 | 50 | - name: Run unit tests 51 | run: | 52 | # Ensure dev extras (nose2, pyasn1, etc.) are present 53 | uv sync --extra dev 54 | uv run nose2 --verbose 55 | -------------------------------------------------------------------------------- /examples/stfl_sig.py: -------------------------------------------------------------------------------- 1 | # Stateful signature examples 2 | 3 | import logging 4 | from pprint import pformat 5 | from sys import stdout 6 | 7 | import oqs 8 | from oqs import StatefulSignature 9 | 10 | logger = logging.getLogger(__name__) 11 | logger.setLevel(logging.INFO) 12 | logger.addHandler(logging.StreamHandler(stdout)) 13 | 14 | logger.info("liboqs version: %s", oqs.oqs_version()) 15 | logger.info("liboqs-python version: %s", oqs.oqs_python_version()) 16 | logger.info( 17 | "Enabled stateful signature mechanisms:\n%s", 18 | pformat(oqs.get_enabled_stateful_sig_mechanisms(), compact=True), 19 | ) 20 | 21 | message = b"This is the message to sign" 22 | 23 | # Create signer and verifier with sample signature mechanisms 24 | stfl_sigalg = "XMSS-SHA2_10_256" 25 | with StatefulSignature(stfl_sigalg) as signer, StatefulSignature(stfl_sigalg) as verifier: 26 | logger.info("Signature details:\n%s", pformat(signer.details)) 27 | 28 | # Signer generates its keypair 29 | signer_public_key = signer.generate_keypair() 30 | logger.info("Generated public key:\n%s", signer_public_key.hex()) 31 | # Optionally, the secret key can be obtained by calling export_secret_key() 32 | # and the signer can later be re-instantiated with the key pair: 33 | # secret_key = signer.export_secret_key() 34 | 35 | # Store key pair, wait... (session resumption): 36 | # signer = oqs.Signature(sigalg, secret_key) 37 | 38 | # Signer signs the message 39 | signature = signer.sign(message) 40 | 41 | # Verifier verifies the signature 42 | is_valid = verifier.verify(message, signature, signer_public_key) 43 | 44 | logger.info("Valid signature? %s", is_valid) 45 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # liboqs-python version 0.14.0 2 | 3 | --- 4 | # Added in version 0.14.0 July 2025 5 | 6 | - Added stateful signature support via the `StatefulSignature` class. 7 | - New enumeration helpers `get_enabled_stateful_sig_mechanisms()` and 8 | `get_supported_stateful_sig_mechanisms()`. 9 | - Updated to liboqs 0.14.0. 10 | - ML-KEM keys can be generated from a seed via 11 | `KeyEncapsulation.generate_keypair_seed()`. 12 | 13 | ## About 14 | 15 | The **Open Quantum Safe (OQS) project** has the goal of developing and 16 | prototyping quantum-resistant cryptography. More information on OQS can be 17 | found on our website https://openquantumsafe.org/ and on GitHub at 18 | https://github.com/open-quantum-safe/. 19 | 20 | **liboqs** is an open source C library for quantum-resistant cryptographic 21 | algorithms. See more about liboqs at 22 | [https://github.com/open-quantum-safe/liboqs/](https://github.com/open-quantum-safe/liboqs/), 23 | including a list of supported algorithms. 24 | 25 | **liboqs-python** is an open source Python 3 wrapper for the liboqs C library 26 | for quantum-resistant cryptographic algorithms. Details about liboqs-python can 27 | be found in 28 | [README.md](https://github.com/open-quantum-safe/liboqs-python/blob/main/README.md). 29 | See in particular limitations on intended use. 30 | 31 | --- 32 | 33 | ## Release notes 34 | 35 | This release of liboqs-python was released on July 10, 2025. Its release 36 | page on GitHub is 37 | https://github.com/open-quantum-safe/liboqs-python/releases/tag/0.14.0. 38 | 39 | --- 40 | 41 | ## What's New 42 | 43 | This is the 11th release of liboqs-python. For a list of changes see 44 | [CHANGES.md](https://github.com/open-quantum-safe/liboqs-python/blob/main/CHANGES.md). 45 | -------------------------------------------------------------------------------- /docker/Dockerfile-simple: -------------------------------------------------------------------------------- 1 | # Multi-stage build: First the full builder image: 2 | 3 | # liboqs build type variant; maximum portability of image; no openssl dependency: 4 | ARG LIBOQS_BUILD_DEFINES="-DOQS_DIST_BUILD=ON -DBUILD_SHARED_LIBS=ON -DOQS_USE_OPENSSL=OFF \ 5 | -DOQS_ENABLE_SIG_STFL_LMS=ON -DOQS_ENABLE_SIG_STFL_XMSS=ON \ 6 | -DOQS_HAZARDOUS_EXPERIMENTAL_ENABLE_SIG_STFL_KEY_SIG_GEN=ON" 7 | 8 | FROM alpine:3.16 as intermediate 9 | # Take in all global args 10 | ARG LIBOQS_BUILD_DEFINES 11 | 12 | LABEL version="2" 13 | 14 | ENV DEBIAN_FRONTEND noninteractive 15 | 16 | RUN apk update && apk upgrade 17 | 18 | # Get all software packages required for builing all components: 19 | RUN apk add build-base linux-headers cmake ninja git 20 | 21 | # get all sources 22 | WORKDIR /opt 23 | RUN git clone --depth 1 --branch main https://github.com/open-quantum-safe/liboqs && \ 24 | git clone --depth 1 --branch main https://github.com/open-quantum-safe/liboqs-python.git 25 | 26 | # build liboqs 27 | WORKDIR /opt/liboqs 28 | RUN mkdir build && cd build && cmake -G"Ninja" .. ${LIBOQS_BUILD_DEFINES} && ninja install 29 | 30 | ## second stage: Only create minimal image without build tooling and intermediate build results generated above: 31 | FROM alpine:3.16 32 | 33 | RUN apk update && apk upgrade 34 | 35 | # Get all software packages required for running all components: 36 | RUN apk add python3 37 | 38 | # Only retain the binary contents in the final image 39 | COPY --from=intermediate /usr/local /usr/local 40 | COPY --from=intermediate /opt/liboqs-python /opt/liboqs-python 41 | 42 | ENV PYTHONPATH=/opt/liboqs-python 43 | 44 | WORKDIR /opt/liboqs-python 45 | 46 | # Enable a normal user 47 | RUN addgroup -g 1000 -S oqs && adduser --uid 1000 -S oqs -G oqs 48 | 49 | USER oqs 50 | 51 | 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pytest_cache/* 2 | __pycache__ 3 | liboqs.so 4 | *.pyc 5 | test-results 6 | 7 | .swp 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | env/ 19 | build/ 20 | develop-eggs/ 21 | dist/ 22 | downloads/ 23 | eggs/ 24 | .eggs/ 25 | lib/ 26 | lib64/ 27 | parts/ 28 | sdist/ 29 | var/ 30 | wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | .hypothesis/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # SageMath parsed files 87 | *.sage.py 88 | 89 | # dotenv 90 | .env 91 | 92 | # virtualenv 93 | .venv 94 | venv/ 95 | ENV/ 96 | 97 | # Spyder project settings 98 | .spyderproject 99 | .spyproject 100 | 101 | # Rope project settings 102 | .ropeproject 103 | 104 | # mkdocs documentation 105 | /site 106 | 107 | # mypy 108 | .mypy_cache/ 109 | 110 | # PyCharm & virtualenv 111 | .idea 112 | bin/ 113 | include/ 114 | lib/ 115 | spell/ 116 | pip-selfcheck.json 117 | pyvenv.cfg 118 | 119 | # vim 120 | *.swp 121 | 122 | # uv 123 | /uv.lock 124 | /data/tmp_keys/ 125 | -------------------------------------------------------------------------------- /docker/minitest.py: -------------------------------------------------------------------------------- 1 | import ssl 2 | import sys 3 | import urllib.request 4 | import json 5 | import os 6 | import oqs 7 | 8 | # Example code testing oqs signature functionality. See more example code at 9 | # https://github.com/open-quantum-safe/liboqs-python/tree/main/examples 10 | 11 | message = b"This is the message to sign" 12 | 13 | # create signer and verifier with sample signature mechanisms 14 | sigalg = "Dilithium2" 15 | with oqs.Signature(sigalg) as signer: 16 | with oqs.Signature(sigalg) as verifier: 17 | signer_public_key = signer.generate_keypair() 18 | signature = signer.sign(message) 19 | is_valid = verifier.verify(message, signature, signer_public_key) 20 | 21 | if not is_valid: 22 | print("Failed to validate signature. Exiting.") 23 | sys.exit(1) 24 | else: 25 | print("Validated signature for OQS algorithm %s" % (sigalg)) 26 | 27 | # Example code iterating over all supported OQS algorithms integrated into TLS 28 | 29 | sslContext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) 30 | sslContext.verify_mode = ssl.CERT_REQUIRED 31 | # Trust LetsEncrypt root CA: 32 | sslContext.load_verify_locations(cafile="isrgrootx1.pem") 33 | 34 | # Retrieve interop test server root CA 35 | with urllib.request.urlopen( 36 | "https://test.openquantumsafe.org/CA.crt", context=sslContext 37 | ) as response: 38 | data = response.read() 39 | with open("CA.crt", "w+b") as f: 40 | f.write(data) 41 | 42 | # Retrieve JSON structure of all alg/port combinations: 43 | with urllib.request.urlopen( 44 | "https://test.openquantumsafe.org/assignments.json", context=sslContext 45 | ) as response: 46 | assignments = json.loads(response.read()) 47 | 48 | # Trust test.openquantumsafe.org root CA: 49 | sslContext.load_verify_locations(cafile="CA.crt") 50 | 51 | # Iterate over all algorithm/port combinations: 52 | for sigs, kexs in assignments.items(): 53 | for kex, port in kexs.items(): 54 | if kex != "*": # '*' denoting any classic KEX alg 55 | # Enable use of the specific QSC KEX algorithm 56 | os.environ["TLS_DEFAULT_GROUPS"] = kex 57 | try: 58 | with urllib.request.urlopen( 59 | "https://test.openquantumsafe.org:" + str(port), context=sslContext 60 | ) as response: 61 | if response.getcode() != 200: 62 | print("Failed to test %s successfully" % (kex)) 63 | else: 64 | print("Success testing %s at port %d" % (kex, port)) 65 | except: 66 | print( 67 | "Test of algorithm combination SIG %s/KEX %s failed. " 68 | "Are all algorithms supported by current OQS library?" % (sigs, kex) 69 | ) 70 | 71 | if "SHORT_TEST" in os.environ: 72 | sys.exit(0) 73 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # Multi-stage build: First the full builder image: 2 | 3 | # liboqs build type variant; maximum portability of image; no openssl dependency: 4 | ARG LIBOQS_BUILD_DEFINES="-DOQS_DIST_BUILD=ON -DBUILD_SHARED_LIBS=ON -DOQS_USE_OPENSSL=OFF \ 5 | -DOQS_ENABLE_SIG_STFL_LMS=ON -DOQS_ENABLE_SIG_STFL_XMSS=ON \ 6 | -DOQS_HAZARDOUS_EXPERIMENTAL_ENABLE_SIG_STFL_KEY_SIG_GEN=ON" 7 | 8 | # make build arguments: Adding -j here speeds up build but may tax hardware 9 | ARG MAKE_DEFINES="-j 2" 10 | 11 | # Define default versions for Python and Alpine 12 | ARG PYTHON_VERSION=3.10.16 13 | ARG ALPINE_VERSION=3.20 14 | 15 | FROM alpine:${ALPINE_VERSION} AS intermediate 16 | # Take in all global args 17 | ARG LIBOQS_BUILD_DEFINES 18 | ARG MAKE_DEFINES 19 | 20 | LABEL version="2" 21 | 22 | ENV DEBIAN_FRONTEND=noninteractive 23 | 24 | RUN apk update && apk upgrade 25 | 26 | # Get all software packages required for building all components: 27 | RUN apk add build-base linux-headers cmake ninja git 28 | 29 | # get all sources 30 | WORKDIR /opt 31 | RUN git clone --depth 1 --branch main https://github.com/open-quantum-safe/liboqs && \ 32 | git clone --depth 1 --branch main https://github.com/open-quantum-safe/liboqs-python.git 33 | 34 | # build liboqs 35 | WORKDIR /opt/liboqs 36 | RUN mkdir build && cd build && cmake -GNinja .. ${LIBOQS_BUILD_DEFINES} && ninja install 37 | 38 | WORKDIR /opt 39 | RUN git clone --depth 1 --branch OQS-OpenSSL_1_1_1-stable https://github.com/open-quantum-safe/openssl.git && cd liboqs && mkdir build-openssl && cd build-openssl && cmake -G"Ninja" .. ${LIBOQS_BUILD_DEFINES} -DCMAKE_INSTALL_PREFIX=/opt/openssl/oqs && ninja install 40 | 41 | RUN apk add automake autoconf && cd /opt/openssl && LDFLAGS="-Wl,-rpath -Wl,/usr/local/lib64" ./Configure shared linux-x86_64 -lm && make ${MAKE_DEFINES} && make install_sw 42 | 43 | # Get LetsEncrypt root 44 | RUN wget https://letsencrypt.org/certs/isrgrootx1.pem 45 | 46 | ## second stage: Only create minimal image without build tooling and intermediate build results generated above: 47 | FROM python:${PYTHON_VERSION}-alpine${ALPINE_VERSION} 48 | 49 | # Only retain the binary contents in the final image 50 | COPY --from=intermediate /usr/local /usr/local 51 | COPY --from=intermediate /opt/liboqs-python /opt/liboqs-python 52 | 53 | ENV PYTHONPATH=/opt/liboqs-python 54 | 55 | # Install liboqs-python 56 | RUN cd /opt/liboqs-python && pip install . 57 | 58 | # Enable a normal user 59 | RUN addgroup -g 1000 -S oqs && adduser --uid 1000 -S oqs -G oqs 60 | 61 | USER oqs 62 | WORKDIR /home/oqs 63 | COPY minitest.py /home/oqs/minitest.py 64 | COPY --from=intermediate /opt/isrgrootx1.pem /home/oqs/isrgrootx1.pem 65 | 66 | # ensure oqs libs are found. Unset if interested in using stock openssl: 67 | ENV LD_LIBRARY_PATH=/usr/local/ 68 | ENV OQS_INSTALL_PATH=/usr/local/ 69 | CMD ["python3", "minitest.py"] 70 | -------------------------------------------------------------------------------- /examples/kem.py: -------------------------------------------------------------------------------- 1 | # Key encapsulation Python example 2 | 3 | import logging 4 | from pprint import pformat 5 | from sys import stdout 6 | 7 | import oqs 8 | 9 | logger = logging.getLogger(__name__) 10 | logger.setLevel(logging.INFO) 11 | logger.addHandler(logging.StreamHandler(stdout)) 12 | 13 | logger.info("liboqs version: %s", oqs.oqs_version()) 14 | logger.info("liboqs-python version: %s", oqs.oqs_python_version()) 15 | logger.info( 16 | "Enabled KEM mechanisms:\n%s", 17 | pformat(oqs.get_enabled_kem_mechanisms(), compact=True), 18 | ) 19 | 20 | # Create client and server with sample KEM mechanisms 21 | kemalg = "ML-KEM-512" 22 | with oqs.KeyEncapsulation(kemalg) as client: 23 | with oqs.KeyEncapsulation(kemalg) as server: 24 | logger.info("Key encapsulation details:\n%s", pformat(client.details)) 25 | 26 | # Client generates its keypair 27 | public_key_client = client.generate_keypair() 28 | # Optionally, the secret key can be obtained by calling export_secret_key() 29 | # and the client can later be re-instantiated with the key pair: 30 | # secret_key_client = client.export_secret_key() 31 | 32 | # Store key pair, wait... (session resumption): 33 | # client = oqs.KeyEncapsulation(kemalg, secret_key_client) 34 | 35 | # The server encapsulates its secret using the client's public key 36 | ciphertext, shared_secret_server = server.encap_secret(public_key_client) 37 | 38 | # The client decapsulates the server's ciphertext to obtain the shared secret 39 | shared_secret_client = client.decap_secret(ciphertext) 40 | 41 | logger.info( 42 | "Shared secretes coincide: %s", 43 | shared_secret_client == shared_secret_server, 44 | ) 45 | 46 | # Example for using a seed to generate a keypair. 47 | kemalg = "ML-KEM-512" 48 | seed = b"This is a 64-byte seed for key generation" + b"\x00" * 23 49 | with oqs.KeyEncapsulation(kemalg) as client: 50 | with oqs.KeyEncapsulation(kemalg) as server: 51 | logger.info("Key encapsulation details:\n%s", pformat(client.details)) 52 | 53 | # Client generates its keypair 54 | public_key_client = client.generate_keypair_seed(seed) 55 | # Optionally, the secret key can be obtained by calling export_secret_key() 56 | # and the client can later be re-instantiated with the key pair: 57 | # secret_key_client = client.export_secret_key() 58 | 59 | # Store key pair, wait... (session resumption): 60 | # client = oqs.KeyEncapsulation(kemalg, secret_key_client) 61 | 62 | # The server encapsulates its secret using the client's public key 63 | ciphertext, shared_secret_server = server.encap_secret(public_key_client) 64 | 65 | # The client decapsulates the server's ciphertext to obtain the shared secret 66 | shared_secret_client = client.decap_secret(ciphertext) 67 | 68 | logger.info( 69 | "Shared secretes coincide: %s", 70 | shared_secret_client == shared_secret_server, 71 | ) 72 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "liboqs-python" 3 | requires-python = ">=3.9" 4 | version = "0.14.0" 5 | description = "Python bindings for liboqs, providing post-quantum public key cryptography algorithms" 6 | authors = [ 7 | { name = "Open Quantum Safe project", email = "contact@openquantumsafe.org" }, 8 | ] 9 | readme = "README.md" 10 | license = { file = "LICENSE.txt" } 11 | dependencies = [ 12 | "tomli>=2; python_version < '3.11'", 13 | ] 14 | 15 | [tool.uv] 16 | package = true 17 | 18 | [project.optional-dependencies] 19 | dev = [ 20 | "isort==5.13.2", 21 | "pre-commit==4.1.0", 22 | "ruff==0.9.4", 23 | "nose2==0.15.1", 24 | "pyasn1-alt-modules==0.4.6", 25 | "pyasn1==0.6.1", 26 | ] 27 | lint = [ 28 | "mypy==1.14.1", 29 | "types-pytz==2024.2.0.20241221", 30 | ] 31 | 32 | [build-system] 33 | requires = ["hatchling"] 34 | build-backend = "hatchling.build" 35 | 36 | [tool.hatch.build.targets.wheel] 37 | packages = ["oqs"] 38 | 39 | [project.urls] 40 | homepage = "https://github.com/open-quantum-safe/liboqs-python" 41 | repository = "https://github.com/open-quantum-safe/liboqs-python.git" 42 | 43 | [tool.isort] 44 | py_version = 39 45 | src_paths = ["oqs"] 46 | line_length = 99 47 | multi_line_output = 3 48 | force_grid_wrap = 0 49 | include_trailing_comma = true 50 | split_on_trailing_comma = false 51 | single_line_exclusions = ["."] 52 | sections = ["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] 53 | skip_gitignore = true 54 | extend_skip = ["__pycache__"] 55 | extend_skip_glob = [] 56 | 57 | [tool.ruff] 58 | src = ["oqs"] 59 | target-version = "py39" 60 | line-length = 99 61 | exclude = [ 62 | ".git", 63 | ".mypy_cache", 64 | ".ruff_cache", 65 | "__pypackages__", 66 | "__pycache__", 67 | "*.pyi", 68 | "venv", 69 | ".venv", 70 | ] 71 | 72 | [tool.ruff.lint] 73 | select = ["ALL"] 74 | ignore = [ 75 | "A003", 76 | "ANN002", "ANN003", "ANN401", 77 | "C901", 78 | "COM812", 79 | "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107", "D203", "D205", "D212", 80 | "ERA001", 81 | "FA100", "FA102", 82 | "FBT001", "FBT002", 83 | "FIX002", 84 | "I001", 85 | "PLR0911", "PLR0912", "PLR0913", "PLR0915", "PLR2004", "PLR5501", 86 | "PLW0120", 87 | "RUF001", 88 | "TD002", "TD003", 89 | "UP007", 90 | ] 91 | 92 | [tool.ruff.format] 93 | quote-style = "double" 94 | indent-style = "space" 95 | skip-magic-trailing-comma = false 96 | line-ending = "auto" 97 | 98 | [tool.mypy] 99 | python_version = "3.9" 100 | mypy_path = "." 101 | packages = ["oqs"] 102 | plugins = [] 103 | allow_redefinition = true 104 | check_untyped_defs = true 105 | disallow_any_generics = true 106 | disallow_incomplete_defs = true 107 | disallow_untyped_calls = true 108 | disallow_untyped_defs = true 109 | extra_checks = true 110 | follow_imports = "normal" 111 | follow_imports_for_stubs = true 112 | ignore_missing_imports = false 113 | namespace_packages = true 114 | no_implicit_optional = true 115 | no_implicit_reexport = true 116 | pretty = true 117 | show_absolute_path = true 118 | show_error_codes = true 119 | show_error_context = true 120 | warn_redundant_casts = true 121 | warn_unused_configs = true 122 | warn_unused_ignores = true 123 | 124 | disable_error_code = [ 125 | "no-redef", 126 | ] 127 | 128 | exclude = [ 129 | "\\.?venv", 130 | "\\.idea", 131 | "\\.tests?", 132 | ] 133 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Pre-release 2 | 3 | - Added type checking and automatic linting/formatting, https://github.com/open-quantum-safe/liboqs-python/pull/97 4 | - Added a utility function for de-structuring version strings in `oqs.py` 5 | - `version(version_str: str) -> tuple[str, str, str]:` - Returns a tuple 6 | containing the (major, minor, patch) versions 7 | - A warning is issued only if the liboqs-python version's major and minor 8 | numbers differ from those of liboqs, ignoring the patch version 9 | 10 | # Version 0.12.0 - January 15, 2025 11 | 12 | - Fixes https://github.com/open-quantum-safe/liboqs-python/issues/98. The API 13 | that NIST has introduced in 14 | [FIPS 204](https://csrc.nist.gov/pubs/fips/204/final) 15 | for ML-DSA includes a context string of length >= 0. Added new API for 16 | signing with a context string 17 | - `Signature.sign_with_ctx_str(self, message, context)` 18 | - `Signature.verify_with_ctx_str(self, message, signature, context, 19 | public_key)` 20 | - When operations fail (i.e., `OQS_SUCCESS != 0`) in functions returning 21 | non-boolean objects, a `RuntimeError` is now raised, instead of returning 0 22 | - Bugfix on Linux platforms, `c_int` -> `c_size_t` for buffer sizes 23 | - Pyright type checking fixes 24 | - Updated examples to use `ML-KEM` and `ML-DSA` as the defaults 25 | 26 | # Version 0.10.0 - April 1, 2024 27 | 28 | - Replaced CHANGES by 29 | [CHANGES.md](https://github.com/open-quantum-safe/liboqs-python/blob/main/CHANGES.md), 30 | as we now use Markdown format to keep track of changes in new releases 31 | - Removed the NIST PRNG as the latter is no longer exposed by liboqs' public 32 | API 33 | - liboqs is installed automatically if it is not detected at runtime 34 | 35 | # Version 0.9.0 - October 30, 2023 36 | 37 | - This is a maintenance release, minor deprecation fixes 38 | - Python minimum required version is enforced to Python 3.8 in `pyproject.toml` 39 | - To follow Python conventions, renamed in `oqs/oqs.py`: 40 | - `is_KEM_enabled()` -> `is_kem_enabled()` 41 | - `get_enabled_KEM_mechanisms()` -> `get_enabled_kem_mechanisms()` 42 | - `get_supported_KEM_mechanisms()` -> `get_supported_kem_mechanisms()` 43 | 44 | # Version 0.8.0 - July 5, 2023 45 | 46 | - This is a maintenance release, minor fixes 47 | - Minimalistic Docker support 48 | - Migrated installation method to `pyproject.toml` 49 | - Removed AppVeyor and CircleCI, all continuous integration is now done via 50 | GitHub actions 51 | 52 | # Version 0.7.2 - August 27, 2022 53 | 54 | - Added library version retrieval functions: 55 | - `oqs_version()` 56 | - `oqs_python_version()` 57 | 58 | # Version 0.7.1 - January 5, 2022 59 | 60 | - Release numbering updated to match liboqs 61 | - Added macOS support on CircleCI, we now support macOS & Linux (CircleCI) and 62 | Windows (AppVeyor) 63 | 64 | # Version 0.4.0 - November 28, 2020 65 | 66 | - Renamed 'master' branch to 'main' 67 | 68 | # Version 0.3.0 - June 10, 2020 69 | 70 | - The liboqs handle has now module-private visibility in `oqs.py` so clients 71 | can not access it directly; can be accessed via the new `oqs.native()` 72 | function 73 | - Closing 74 | #7 [link](https://github.com/open-quantum-safe/liboqs-python/issues/7), all 75 | issues addressed 76 | - Added AppVeyor continuous integration 77 | 78 | # Version 0.2.1 - January 22, 2020 79 | 80 | - Added a signature example 81 | - Added partial support for RNGs from `` 82 | - Added an RNG example 83 | 84 | # Version 0.2.0 - October 8, 2019 85 | 86 | - This release updates for compatibility with liboqs 0.2.0, which contains 87 | new/updated algorithms based on NIST Round 2 submissions. 88 | 89 | # Version 0.1.0 - April 23, 2019 90 | 91 | - Initial release 92 | -------------------------------------------------------------------------------- /.github/workflows/python_detailed.yml: -------------------------------------------------------------------------------- 1 | name: GitHub actions detailed 2 | 3 | on: 4 | push: 5 | branches: [ "**" ] 6 | pull_request: 7 | branches: [ "**" ] 8 | repository_dispatch: 9 | types: [ "**" ] 10 | 11 | permissions: 12 | contents: read 13 | 14 | env: 15 | BUILD_TYPE: Debug 16 | LD_LIBRARY_PATH: /usr/local/lib 17 | WIN_LIBOQS_INSTALL_PATH: C:\liboqs 18 | VERSION: 0.14.0 19 | PYOQS_ENABLE_FAULTHANDLER: "1" 20 | 21 | concurrency: 22 | group: test-python-detailed-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} 23 | cancel-in-progress: ${{ github.event_name == 'pull_request' }} 24 | 25 | jobs: 26 | build: 27 | strategy: 28 | matrix: 29 | os: [ ubuntu-latest, macos-latest, windows-latest ] 30 | runs-on: ${{ matrix.os }} 31 | 32 | steps: 33 | - uses: actions/checkout@v4 34 | 35 | - name: Install uv 36 | uses: astral-sh/setup-uv@v5 37 | with: 38 | version: "latest" 39 | enable-cache: true 40 | cache-dependency-glob: "**/pyproject.toml" 41 | 42 | - name: Set up Python 3.9 43 | run: uv python install 3.9 44 | 45 | - name: Install dependencies 46 | run: uv sync --extra dev 47 | 48 | - name: Install liboqs POSIX 49 | if: matrix.os != 'windows-latest' 50 | run: | 51 | git clone --branch ${{env.VERSION}} --single-branch --depth 1 https://github.com/open-quantum-safe/liboqs 52 | cmake -S liboqs -B liboqs/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DBUILD_SHARED_LIBS=ON \ 53 | -DOQS_BUILD_ONLY_LIB=ON -DOQS_BUILD_ONLY_LIB=ON -DOQS_ENABLE_SIG_STFL_LMS=ON \ 54 | -DOQS_ENABLE_SIG_STFL_XMSS=ON -DOQS_HAZARDOUS_EXPERIMENTAL_ENABLE_SIG_STFL_KEY_SIG_GEN=ON \ 55 | -DOQS_ALLOW_STFL_KEY_AND_SIG_GEN=ON 56 | cmake --build liboqs/build --parallel 4 57 | sudo cmake --build liboqs/build --target install 58 | 59 | - name: Run examples POSIX 60 | if: matrix.os != 'windows-latest' 61 | run: | 62 | uv sync --extra dev 63 | uv run examples/kem.py 64 | echo 65 | uv run examples/sig.py 66 | echo 67 | uv run examples/rand.py 68 | echo 69 | uv run examples/stfl_sig.py 70 | 71 | - name: Run unit tests POSIX 72 | if: matrix.os != 'windows-latest' 73 | run: | 74 | # Ensure dev extras (nose2, pyasn1, etc.) are present 75 | uv sync --extra dev 76 | uv run nose2 --verbose 77 | 78 | - name: Install liboqs Windows 79 | if: matrix.os == 'windows-latest' 80 | shell: cmd 81 | run: | 82 | git clone --branch ${{env.VERSION}} --single-branch --depth 1 https://github.com/open-quantum-safe/liboqs 83 | cmake -S liboqs -B liboqs\build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_INSTALL_PREFIX=${{env.WIN_LIBOQS_INSTALL_PATH}} -DBUILD_SHARED_LIBS=ON -DOQS_BUILD_ONLY_LIB=ON -DOQS_BUILD_ONLY_LIB=ON -DOQS_ENABLE_SIG_STFL_LMS=ON -DOQS_ENABLE_SIG_STFL_XMSS=ON -DOQS_HAZARDOUS_EXPERIMENTAL_ENABLE_SIG_STFL_KEY_SIG_GEN=ON ‑DOQS_ALLOW_STFL_KEY_AND_SIG_GEN=ON 84 | cmake --build liboqs\build --parallel 4 85 | cmake --build liboqs\build --target install 86 | 87 | - name: Run examples Windows 88 | if: matrix.os == 'windows-latest' 89 | shell: cmd 90 | run: | 91 | set PATH=%PATH%;${{env.WIN_LIBOQS_INSTALL_PATH}}\bin 92 | uv sync --extra dev 93 | uv run examples/kem.py 94 | echo. 95 | uv run examples/sig.py 96 | echo. 97 | uv run examples/rand.py 98 | echo. 99 | uv run examples/stfl_sig.py 100 | 101 | - name: Run unit tests Windows 102 | shell: cmd 103 | if: matrix.os == 'windows-latest' 104 | run: | 105 | set PATH=%PATH%;${{env.WIN_LIBOQS_INSTALL_PATH}}\bin 106 | rem Ensure dev extras (nose2, pyasn1, etc.) are present 107 | uv sync --extra dev 108 | uv run nose2 --verbose 109 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | conduct@openquantumsafe.org. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /tests/test_kem.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform # to learn the OS we're on 3 | import random 4 | 5 | import oqs 6 | 7 | # KEMs for which unit testing is disabled 8 | disabled_KEM_patterns = [] # noqa: N816 9 | 10 | if platform.system() == "Windows": 11 | disabled_KEM_patterns = [""] # noqa: N816 12 | 13 | 14 | def test_seed_generation() -> tuple[None, str]: 15 | for alg_name in oqs.get_enabled_kem_mechanisms(): 16 | if any(item in alg_name for item in disabled_KEM_patterns): 17 | continue 18 | 19 | if oqs.KeyEncapsulation(alg_name).length_keypair_seed == 0: 20 | # Skip KEMs that do not support seed generation 21 | continue 22 | 23 | yield check_seed_generation, alg_name 24 | 25 | 26 | def check_seed_generation(alg_name: str) -> None: 27 | with oqs.KeyEncapsulation(alg_name) as kem: 28 | length = kem.length_keypair_seed 29 | seed = os.urandom(length) # Ensure the seed can be generated 30 | public_key = kem.generate_keypair_seed(seed) 31 | ciphertext, shared_secret_server = kem.encap_secret(public_key) 32 | shared_secret_client = kem.decap_secret(ciphertext) 33 | assert shared_secret_client == shared_secret_server # noqa: S101 34 | 35 | 36 | def test_correctness() -> tuple[None, str]: 37 | for alg_name in oqs.get_enabled_kem_mechanisms(): 38 | if any(item in alg_name for item in disabled_KEM_patterns): 39 | continue 40 | yield check_correctness, alg_name 41 | 42 | 43 | def check_correctness(alg_name: str) -> None: 44 | with oqs.KeyEncapsulation(alg_name) as kem: 45 | public_key = kem.generate_keypair() 46 | ciphertext, shared_secret_server = kem.encap_secret(public_key) 47 | shared_secret_client = kem.decap_secret(ciphertext) 48 | assert shared_secret_client == shared_secret_server # noqa: S101 49 | 50 | 51 | def test_wrong_ciphertext() -> tuple[None, str]: 52 | for alg_name in oqs.get_enabled_kem_mechanisms(): 53 | if any(item in alg_name for item in disabled_KEM_patterns): 54 | continue 55 | yield check_wrong_ciphertext, alg_name 56 | 57 | 58 | def check_wrong_ciphertext(alg_name: str) -> None: 59 | with oqs.KeyEncapsulation(alg_name) as kem: 60 | public_key = kem.generate_keypair() 61 | ciphertext, shared_secret_server = kem.encap_secret(public_key) 62 | wrong_ciphertext = bytes(random.getrandbits(8) for _ in range(len(ciphertext))) 63 | try: 64 | shared_secret_client = kem.decap_secret(wrong_ciphertext) 65 | assert shared_secret_client != shared_secret_server # noqa: S101 66 | except RuntimeError: 67 | pass 68 | except Exception as ex: 69 | msg = f"An unexpected exception was raised: {ex}" 70 | raise AssertionError(msg) from ex 71 | 72 | 73 | def test_not_supported() -> None: 74 | try: 75 | with oqs.KeyEncapsulation("unsupported_sig"): 76 | pass 77 | except oqs.MechanismNotSupportedError: 78 | pass 79 | except Exception as ex: 80 | msg = f"An unexpected exception was raised {ex}" 81 | raise AssertionError(msg) from ex 82 | else: 83 | msg = "oqs.MechanismNotSupportedError was not raised." 84 | raise AssertionError(msg) 85 | 86 | 87 | def test_not_enabled() -> None: 88 | for alg_name in oqs.get_supported_kem_mechanisms(): 89 | if alg_name not in oqs.get_enabled_kem_mechanisms(): 90 | # Found a non-enabled but supported alg 91 | try: 92 | with oqs.KeyEncapsulation(alg_name): 93 | pass 94 | except oqs.MechanismNotEnabledError: 95 | pass 96 | except Exception as ex: 97 | msg = f"An unexpected exception was raised: {ex}" 98 | raise AssertionError(msg) from ex 99 | else: 100 | msg = "oqs.MechanismNotEnabledError was not raised." 101 | raise AssertionError(msg) 102 | 103 | 104 | def test_python_attributes() -> None: 105 | for alg_name in oqs.get_enabled_kem_mechanisms(): 106 | with oqs.KeyEncapsulation(alg_name) as kem: 107 | if kem.method_name.decode() != alg_name: 108 | msg = "Incorrect oqs.KeyEncapsulation.method_name" 109 | raise AssertionError(msg) 110 | if kem.alg_version is None: 111 | msg = "Undefined oqs.KeyEncapsulation.alg_version" 112 | raise AssertionError(msg) 113 | if not 1 <= kem.claimed_nist_level <= 5: 114 | msg = "Invalid oqs.KeyEncapsulation.claimed_nist_level" 115 | raise AssertionError(msg) 116 | if kem.length_public_key == 0: 117 | msg = "Incorrect oqs.KeyEncapsulation.length_public_key" 118 | raise AssertionError(msg) 119 | if kem.length_secret_key == 0: 120 | msg = "Incorrect oqs.KeyEncapsulation.length_secret_key" 121 | raise AssertionError(msg) 122 | if kem.length_ciphertext == 0: 123 | msg = "Incorrect oqs.KeyEncapsulation.length_signature" 124 | raise AssertionError(msg) 125 | if kem.length_shared_secret == 0: 126 | msg = "Incorrect oqs.KeyEncapsulation.length_shared_secret" 127 | raise AssertionError(msg) 128 | # Just to check that the property exists. 129 | if kem.length_keypair_seed is None: 130 | msg = "Undefined oqs.KeyEncapsulation.length_keypair_seed" 131 | raise AssertionError(msg) 132 | 133 | 134 | if __name__ == "__main__": 135 | try: 136 | import nose2 137 | 138 | nose2.main() 139 | except ImportError: 140 | msg_ = "nose2 module not found. Please install it with 'pip install nose2'." 141 | raise RuntimeError(msg_) from None 142 | -------------------------------------------------------------------------------- /tests/test_stfl_sig.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import platform # to learn the OS we're on 3 | import random 4 | from pathlib import Path 5 | 6 | from typing import Tuple 7 | 8 | from oqs.serialize import gen_or_load_stateful_signature_key 9 | 10 | import oqs 11 | 12 | _skip_names = ["LMS_SHA256_H20_W8_H10_W8", "LMS_SHA256_H20_W8_H15_W8", "LMS_SHA256_H20_W8_H20_W8"] 13 | 14 | _KEY_DIR = Path(__file__).resolve().parent.parent / "data" / "xmss_xmssmt_keys" 15 | 16 | # Sigs for which unit testing is disabled 17 | disabled_sig_patterns = [] 18 | 19 | if platform.system() == "Windows": 20 | disabled_sig_patterns = [""] 21 | 22 | 23 | def _load_or_generate_key(alg_name: str) -> Tuple[oqs.StatefulSignature, bytes]: 24 | private_key, public_key = gen_or_load_stateful_signature_key(alg_name, dir_name=_KEY_DIR) 25 | 26 | if private_key is not None: 27 | sig = oqs.StatefulSignature(alg_name, secret_key=private_key) 28 | return sig, public_key 29 | sig = oqs.StatefulSignature(alg_name) 30 | public_key = sig.generate_keypair() 31 | return sig, public_key 32 | 33 | 34 | def test_correctness() -> tuple[None, str]: 35 | for alg_name in oqs.get_enabled_stateful_sig_mechanisms(): 36 | if alg_name.startswith("LMS"): 37 | continue 38 | 39 | if any(item in alg_name for item in disabled_sig_patterns): 40 | continue 41 | yield check_correctness, alg_name 42 | 43 | 44 | def check_correctness(alg_name: str) -> None: 45 | sig, public_key = _load_or_generate_key(alg_name) 46 | message = bytes(random.getrandbits(8) for _ in range(100)) 47 | signature = sig.sign(message) 48 | assert sig.verify(message, signature, public_key) # noqa: S101 49 | 50 | 51 | def test_wrong_message() -> tuple[None, str]: 52 | for alg_name in oqs.get_enabled_stateful_sig_mechanisms(): 53 | if alg_name.startswith("LMS"): 54 | continue 55 | 56 | if any(item in alg_name for item in disabled_sig_patterns): 57 | continue 58 | 59 | yield check_wrong_message, alg_name 60 | 61 | 62 | def check_wrong_message(alg_name: str) -> None: 63 | sig, public_key = _load_or_generate_key(alg_name) 64 | message = bytes(random.getrandbits(8) for _ in range(100)) 65 | signature = sig.sign(message) 66 | wrong_message = bytes(random.getrandbits(8) for _ in range(len(message))) 67 | assert not (sig.verify(wrong_message, signature, public_key)) # noqa: S101 68 | 69 | 70 | def test_wrong_signature() -> tuple[None, str]: 71 | for alg_name in oqs.get_enabled_stateful_sig_mechanisms(): 72 | if alg_name.startswith("LMS"): 73 | continue 74 | 75 | if any(item in alg_name for item in disabled_sig_patterns): 76 | continue 77 | yield check_wrong_signature, alg_name 78 | 79 | 80 | def check_wrong_signature(alg_name: str) -> None: 81 | sig, public_key = _load_or_generate_key(alg_name) 82 | message = bytes(random.getrandbits(8) for _ in range(100)) 83 | signature = sig.sign(message) 84 | wrong_signature = bytes(random.getrandbits(8) for _ in range(len(signature))) 85 | assert not (sig.verify(message, wrong_signature, public_key)) # noqa: S101 86 | 87 | 88 | def test_wrong_public_key() -> tuple[None, str]: 89 | for alg_name in oqs.get_enabled_stateful_sig_mechanisms(): 90 | if alg_name.startswith("LMS"): 91 | continue 92 | 93 | if any(item in alg_name for item in disabled_sig_patterns): 94 | continue 95 | yield check_wrong_public_key, alg_name 96 | 97 | 98 | def check_wrong_public_key(alg_name: str) -> None: 99 | sig, public_key = _load_or_generate_key(alg_name) 100 | message = bytes(random.getrandbits(8) for _ in range(100)) 101 | signature = sig.sign(message) 102 | wrong_public_key = bytes(random.getrandbits(8) for _ in range(len(public_key))) 103 | assert not (sig.verify(message, signature, wrong_public_key)) # noqa: S101 104 | 105 | 106 | def test_not_supported() -> None: 107 | try: 108 | with oqs.StatefulSignature("unsupported_sig"): 109 | pass 110 | except oqs.MechanismNotSupportedError: 111 | pass 112 | except Exception as ex: 113 | msg = f"An unexpected exception was raised: {ex}" 114 | raise AssertionError(msg) from ex 115 | else: 116 | msg = "oqs.MechanismNotSupportedError was not raised." 117 | raise AssertionError(msg) 118 | 119 | 120 | def test_not_enabled() -> None: 121 | for alg_name in oqs.get_supported_stateful_sig_mechanisms(): 122 | if alg_name not in oqs.get_enabled_stateful_sig_mechanisms(): 123 | # Found a non-enabled but supported alg 124 | try: 125 | with oqs.StatefulSignature(alg_name): 126 | pass 127 | except oqs.MechanismNotEnabledError: 128 | pass 129 | except Exception as ex: 130 | msg = f"An unexpected exception was raised: {ex}" 131 | raise AssertionError(msg) from ex 132 | else: 133 | msg = "oqs.MechanismNotEnabledError was not raised." 134 | raise AssertionError(msg) 135 | 136 | 137 | def test_python_attributes() -> None: 138 | for alg_name in oqs.get_enabled_stateful_sig_mechanisms(): 139 | if alg_name in _skip_names: 140 | logging.info("Skipping %s as it is in the skip list.", alg_name) 141 | continue 142 | 143 | with oqs.StatefulSignature(alg_name) as sig: 144 | if sig.method_name.decode() != alg_name: 145 | msg = "Incorrect oqs.StatefulSignature.method_name" 146 | raise AssertionError(msg) 147 | if sig.alg_version is None: 148 | msg = "Undefined oqs.StatefulSignature.alg_version" 149 | raise AssertionError(msg) 150 | if sig.length_public_key == 0: 151 | msg = "Incorrect oqs.StatefulSignature.length_public_key" 152 | raise AssertionError(msg) 153 | if sig.length_secret_key == 0: 154 | msg = "Incorrect oqs.StatefulSignature.length_secret_key" 155 | raise AssertionError(msg) 156 | if sig.length_signature == 0: 157 | msg = "Incorrect oqs.StatefulSignature.length_signature" 158 | raise AssertionError(msg) 159 | 160 | 161 | if __name__ == "__main__": 162 | try: 163 | import nose2 164 | 165 | nose2.main() 166 | except ImportError: 167 | msg_ = "nose2 module not found. Please install it with 'pip install nose2'." 168 | raise RuntimeError(msg_) from None 169 | -------------------------------------------------------------------------------- /oqs/serialize.py: -------------------------------------------------------------------------------- 1 | """ 2 | Serialization and deserialization of stateful signature keys 3 | using OneAsymmetricKey (PKCS#8) structure. 4 | """ 5 | 6 | import logging 7 | from pathlib import Path 8 | from typing import Optional, Union 9 | 10 | from pyasn1.codec.der import encoder, decoder 11 | from pyasn1.type import univ, tag 12 | 13 | import oqs 14 | from pyasn1_alt_modules import rfc5958 15 | 16 | _NAME_2_OIDS = { 17 | "hss": "1.2.840.113549.1.9.16.3.17", # RFC 9708 18 | "xmss": "1.3.6.1.5.5.7.6.34", # RFC 9802 19 | "xmssmt": "1.3.6.1.5.5.7.6.35", # RFC 9802 20 | } 21 | _OID_2_NAME = {v: k for k, v in _NAME_2_OIDS.items()} 22 | 23 | _KEY_DIR = Path(__file__).resolve().parent.parent / "data" / "xmss_xmssmt_keys" 24 | 25 | 26 | def _get_oid_from_name(name: str) -> str: 27 | """Get the OID corresponding to the stateful signature name.""" 28 | if name.startswith("LMS"): 29 | return _NAME_2_OIDS["hss"] 30 | if name.startswith("XMSS-"): 31 | return _NAME_2_OIDS["xmss"] 32 | if name.startswith("XMSSMT-"): 33 | return _NAME_2_OIDS["xmssmt"] 34 | msg = f"Unsupported stateful signature name: {name}" 35 | raise ValueError(msg) 36 | 37 | 38 | def serialize_stateful_signature_key( 39 | stateful_sig: oqs.StatefulSignature, public_key: bytes, fpath: str 40 | ) -> None: 41 | """ 42 | Serialize the stateful signature key to a `OneAsymmetricKey` structure. 43 | 44 | :param stateful_sig: The stateful signature object. 45 | :param public_key: The public key bytes. 46 | :param fpath: The file path to save the serialized key. 47 | """ 48 | one_asym_key = rfc5958.OneAsymmetricKey() 49 | one_asym_key["version"] = 1 50 | one_asym_key["privateKeyAlgorithm"]["algorithm"] = univ.ObjectIdentifier( 51 | _get_oid_from_name(stateful_sig.method_name.decode()) 52 | ) 53 | one_asym_key["privateKey"] = stateful_sig.export_secret_key() 54 | one_asym_key["publicKey"] = ( 55 | rfc5958.PublicKey() 56 | .fromOctetString(public_key) 57 | .subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 1)) 58 | ) 59 | 60 | der_data = encoder.encode(one_asym_key) 61 | fpath_obj = Path(fpath) 62 | with fpath_obj.open("wb") as f: 63 | f.write(der_data) 64 | logging.info("Wrote: %s", fpath_obj.name) 65 | 66 | 67 | def deserialize_stateful_signature_key( 68 | key_name: str, dir_name: Union[str, Path] = _KEY_DIR 69 | ) -> tuple[bytes, bytes]: 70 | """ 71 | Deserialize the stateful signature key from a `OneAsymmetricKey` structure. 72 | 73 | :param key_name: The base name of the serialized key (without extension). 74 | :param dir_name: The directory where the key files are stored. 75 | :return: A tuple (private_key_bytes, public_key_bytes). 76 | """ 77 | key_name = key_name.replace("/", "_layers_", 1).lower() 78 | fpath = Path(dir_name) / f"{key_name}.der" 79 | 80 | with fpath.open("rb") as f: 81 | der_data = f.read() 82 | 83 | one_asym_key = decoder.decode(der_data, asn1Spec=rfc5958.OneAsymmetricKey())[0] 84 | oid = str(one_asym_key["privateKeyAlgorithm"]["algorithm"]) 85 | 86 | # Accept any OID for supported families 87 | if oid not in _OID_2_NAME: 88 | msg = f"Unsupported stateful signature OID: {oid}" 89 | raise ValueError(msg) 90 | 91 | private_key_bytes = one_asym_key["privateKey"].asOctets() 92 | public_key_bytes = one_asym_key["publicKey"].asOctets() 93 | return private_key_bytes, public_key_bytes 94 | 95 | 96 | def _may_generate_stfl_key( 97 | key_name: str, dir_name: str 98 | ) -> tuple[Optional[bytes], Optional[bytes]]: 99 | """ 100 | Decide whether to generate a stateful signature key for the given algorithm name. 101 | 102 | Currently, this function allows opportunistic generation only for fast XMSS parameter sets 103 | used in tests, specifically those starting with "XMSS-" and containing "_16_". 104 | 105 | :param key_name: The name of the stateful signature mechanism. 106 | :param dir_name: The directory where the key files are stored. 107 | :return: A tuple (private_key_bytes, public_key_bytes) if generated, else (None, None). 108 | """ 109 | alt_path = Path(str(dir_name).replace("xmss_xmssmt_keys", "tmp_keys", 1)) 110 | alt_fpath = alt_path / f"{key_name.replace('/', '_layers_', 1).lower()}.der" 111 | if key_name.startswith("XMSS-") and "_16_" in key_name: 112 | Path(alt_path).mkdir(parents=True, exist_ok=True) 113 | with oqs.StatefulSignature(key_name) as stfl_sig: 114 | public_key_bytes = stfl_sig.generate_keypair() 115 | private_key_bytes = stfl_sig.export_secret_key() 116 | serialize_stateful_signature_key(stfl_sig, public_key_bytes, str(alt_fpath)) 117 | return private_key_bytes, public_key_bytes 118 | 119 | return None, None 120 | 121 | 122 | def gen_or_load_stateful_signature_key( 123 | key_name: str, dir_name: Union[str, Path] = _KEY_DIR 124 | ) -> tuple[Optional[bytes], Optional[bytes]]: 125 | """ 126 | Generate or load a stateful signature key pair. 127 | 128 | :param key_name: The name of the stateful signature mechanism. 129 | :param dir_name: The directory where the key files are stored. 130 | :return: A tuple (stateful_signature_object, public_key_bytes). 131 | """ 132 | key_file_name = key_name.replace("/", "_layers_", 1).lower() 133 | fpath = Path(dir_name) / f"{key_file_name}.der" 134 | 135 | if Path(fpath).exists(): 136 | return deserialize_stateful_signature_key(key_file_name, dir_name=dir_name) 137 | 138 | # Check alternative path for test keys, to avoid regenerating for every test run. 139 | alt_path = Path(str(_KEY_DIR).replace("xmss_xmssmt_keys", "tmp_keys", 1)) 140 | alt_fpath = alt_path / f"{key_file_name}.der" 141 | if Path(alt_fpath).exists(): 142 | private_key_bytes, public_key_bytes = deserialize_stateful_signature_key( 143 | key_name, dir_name=alt_path 144 | ) 145 | return private_key_bytes, public_key_bytes 146 | 147 | # Opportunistic generation for fast XMSS parameter sets used in tests 148 | return _may_generate_stfl_key(key_name, dir_name) 149 | 150 | 151 | if __name__ == "__main__": 152 | xmss_names = [ 153 | name for name in oqs.get_enabled_stateful_sig_mechanisms() if name.startswith("XMSS-") 154 | ] 155 | xmssmt_names = [ 156 | name for name in oqs.get_enabled_stateful_sig_mechanisms() if name.startswith("XMSSMT-") 157 | ] 158 | hss_names = [ 159 | name for name in oqs.get_enabled_stateful_sig_mechanisms() if name.startswith("LMS") 160 | ] 161 | logging.info("xmss_names: %s", str(xmss_names)) 162 | private_bytes, public_bytes = deserialize_stateful_signature_key( 163 | "XMSS-sha2_20_512", dir_name=_KEY_DIR 164 | ) 165 | if private_bytes is None or public_bytes is None: 166 | ERROR_MSG = "Could not load the XMSS key" 167 | raise ValueError(ERROR_MSG) 168 | logging.info("Loaded XMSS key, public key len: %d", len(public_bytes)) 169 | -------------------------------------------------------------------------------- /tests/test_sig.py: -------------------------------------------------------------------------------- 1 | import platform # to learn the OS we're on 2 | import random 3 | 4 | import oqs 5 | from oqs.oqs import Signature, native 6 | 7 | # Sigs for which unit testing is disabled 8 | disabled_sig_patterns = [] 9 | 10 | if platform.system() == "Windows": 11 | disabled_sig_patterns = [""] 12 | 13 | 14 | def test_correctness() -> tuple[None, str]: 15 | for alg_name in oqs.get_enabled_sig_mechanisms(): 16 | if any(item in alg_name for item in disabled_sig_patterns): 17 | continue 18 | yield check_correctness, alg_name 19 | 20 | 21 | def test_correctness_with_ctx_str() -> tuple[None, str]: 22 | for alg_name in oqs.get_enabled_sig_mechanisms(): 23 | if not Signature(alg_name).details["sig_with_ctx_support"]: 24 | continue 25 | if any(item in alg_name for item in disabled_sig_patterns): 26 | continue 27 | yield check_correctness_with_ctx_str, alg_name 28 | 29 | 30 | def check_correctness(alg_name: str) -> None: 31 | with oqs.Signature(alg_name) as sig: 32 | message = bytes(random.getrandbits(8) for _ in range(100)) 33 | public_key = sig.generate_keypair() 34 | signature = sig.sign(message) 35 | assert sig.verify(message, signature, public_key) # noqa: S101 36 | 37 | 38 | def check_correctness_with_ctx_str(alg_name: str) -> None: 39 | with oqs.Signature(alg_name) as sig: 40 | message = bytes(random.getrandbits(8) for _ in range(100)) 41 | context = b"some context" 42 | public_key = sig.generate_keypair() 43 | signature = sig.sign_with_ctx_str(message, context) 44 | assert sig.verify_with_ctx_str(message, signature, context, public_key) # noqa: S101 45 | 46 | 47 | def test_sig_with_ctx_support_detection() -> None: 48 | """ 49 | Test that sig_with_ctx_support matches the C API and that sign_with_ctx_str 50 | raises on unsupported algorithms. 51 | """ 52 | for alg_name in oqs.get_enabled_sig_mechanisms(): 53 | with Signature(alg_name) as sig: 54 | # Check Python attribute matches C API 55 | c_api_result = native().OQS_SIG_supports_ctx_str(sig.method_name) 56 | assert bool(sig.sig_with_ctx_support) == bool(c_api_result), ( # noqa: S101 57 | f"sig_with_ctx_support mismatch for {alg_name}" 58 | ) 59 | # If not supported, sign_with_ctx_str should raise 60 | if not sig.sig_with_ctx_support: 61 | try: 62 | sig.sign_with_ctx_str(b"msg", b"context") 63 | except RuntimeError as e: 64 | if "not supported" not in str(e): 65 | msg = f"Unexpected exception message: {e}" 66 | raise AssertionError(msg) from e 67 | else: 68 | msg = f"sign_with_ctx_str did not raise for {alg_name} without context support" 69 | raise AssertionError(msg) 70 | 71 | 72 | def test_wrong_message() -> tuple[None, str]: 73 | for alg_name in oqs.get_enabled_sig_mechanisms(): 74 | if any(item in alg_name for item in disabled_sig_patterns): 75 | continue 76 | yield check_wrong_message, alg_name 77 | 78 | 79 | def check_wrong_message(alg_name: str) -> None: 80 | with oqs.Signature(alg_name) as sig: 81 | message = bytes(random.getrandbits(8) for _ in range(100)) 82 | public_key = sig.generate_keypair() 83 | signature = sig.sign(message) 84 | wrong_message = bytes(random.getrandbits(8) for _ in range(len(message))) 85 | assert not (sig.verify(wrong_message, signature, public_key)) # noqa: S101 86 | 87 | 88 | def test_wrong_signature() -> tuple[None, str]: 89 | for alg_name in oqs.get_enabled_sig_mechanisms(): 90 | if any(item in alg_name for item in disabled_sig_patterns): 91 | continue 92 | yield check_wrong_signature, alg_name 93 | 94 | 95 | def check_wrong_signature(alg_name: str) -> None: 96 | with oqs.Signature(alg_name) as sig: 97 | message = bytes(random.getrandbits(8) for _ in range(100)) 98 | public_key = sig.generate_keypair() 99 | signature = sig.sign(message) 100 | wrong_signature = bytes(random.getrandbits(8) for _ in range(len(signature))) 101 | assert not (sig.verify(message, wrong_signature, public_key)) # noqa: S101 102 | 103 | 104 | def test_wrong_public_key() -> tuple[None, str]: 105 | for alg_name in oqs.get_enabled_sig_mechanisms(): 106 | if any(item in alg_name for item in disabled_sig_patterns): 107 | continue 108 | yield check_wrong_public_key, alg_name 109 | 110 | 111 | def check_wrong_public_key(alg_name: str) -> None: 112 | with oqs.Signature(alg_name) as sig: 113 | message = bytes(random.getrandbits(8) for _ in range(100)) 114 | public_key = sig.generate_keypair() 115 | signature = sig.sign(message) 116 | wrong_public_key = bytes(random.getrandbits(8) for _ in range(len(public_key))) 117 | assert not (sig.verify(message, signature, wrong_public_key)) # noqa: S101 118 | 119 | 120 | def test_not_supported() -> None: 121 | try: 122 | with oqs.Signature("unsupported_sig"): 123 | pass 124 | except oqs.MechanismNotSupportedError: 125 | pass 126 | except Exception as ex: 127 | msg = f"An unexpected exception was raised: {ex}" 128 | raise AssertionError(msg) from ex 129 | else: 130 | msg = "oqs.MechanismNotSupportedError was not raised." 131 | raise AssertionError(msg) 132 | 133 | 134 | def test_not_enabled() -> None: 135 | for alg_name in oqs.get_supported_sig_mechanisms(): 136 | if alg_name not in oqs.get_enabled_sig_mechanisms(): 137 | # Found a non-enabled but supported alg 138 | try: 139 | with oqs.Signature(alg_name): 140 | pass 141 | except oqs.MechanismNotEnabledError: 142 | pass 143 | except Exception as ex: 144 | msg = f"An unexpected exception was raised: {ex}" 145 | raise AssertionError(msg) from ex 146 | else: 147 | msg = "oqs.MechanismNotEnabledError was not raised." 148 | raise AssertionError(msg) 149 | 150 | 151 | def test_python_attributes() -> None: 152 | for alg_name in oqs.get_enabled_sig_mechanisms(): 153 | with oqs.Signature(alg_name) as sig: 154 | if sig.method_name.decode() != alg_name: 155 | msg = "Incorrect oqs.Signature.method_name" 156 | raise AssertionError(msg) 157 | if sig.alg_version is None: 158 | msg = "Undefined oqs.Signature.alg_version" 159 | raise AssertionError(msg) 160 | if not 1 <= sig.claimed_nist_level <= 5: 161 | msg = "Invalid oqs.Signature.claimed_nist_level" 162 | raise AssertionError(msg) 163 | if sig.length_public_key == 0: 164 | msg = "Incorrect oqs.Signature.length_public_key" 165 | raise AssertionError(msg) 166 | if sig.length_secret_key == 0: 167 | msg = "Incorrect oqs.Signature.length_secret_key" 168 | raise AssertionError(msg) 169 | if sig.length_signature == 0: 170 | msg = "Incorrect oqs.Signature.length_signature" 171 | raise AssertionError(msg) 172 | 173 | 174 | if __name__ == "__main__": 175 | try: 176 | import nose2 177 | 178 | nose2.main() 179 | except ImportError: 180 | msg_ = "nose2 module not found. Please install it with 'pip install nose2'." 181 | raise RuntimeError(msg_) from None 182 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # liboqs-python: Python 3 bindings for liboqs 2 | 3 | [![GitHub actions](https://github.com/open-quantum-safe/liboqs-python/actions/workflows/python_simplified.yml/badge.svg)](https://github.com/open-quantum-safe/liboqs-python/actions) 4 | 5 | --- 6 | 7 | ## About 8 | 9 | The **Open Quantum Safe (OQS) project** has the goal of developing and 10 | prototyping quantum-resistant cryptography. 11 | 12 | **liboqs-python** offers a Python 3 wrapper for the 13 | [Open Quantum Safe](https://openquantumsafe.org/) 14 | [liboqs](https://github.com/open-quantum-safe/liboqs/) 15 | C library, which is a C library for quantum-resistant cryptographic algorithms. 16 | 17 | The wrapper is written in Python 3, hence in the following it is assumed that 18 | you have access to a Python 3 interpreter. liboqs-python has been extensively 19 | tested on Linux, macOS and Windows platforms. Continuous integration is 20 | provided via GitHub actions. 21 | 22 | The project contains the following files and directories 23 | 24 | - **`oqs/oqs.py`: a Python 3 module wrapper for the liboqs C library.** 25 | - `oqs/rand.py`: a Python 3 module supporting RNGs from `` 26 | - `examples/kem.py`: key encapsulation example 27 | - `examples/rand.py`: RNG example 28 | - `examples/sig.py`: signature example 29 | - `examples/stfl_sig.py`: stateful signature example 30 | - `tests`: unit tests 31 | 32 | --- 33 | 34 | ## Pre-requisites 35 | 36 | - [liboqs](https://github.com/open-quantum-safe/liboqs) 37 | - [git](https://git-scm.com/) 38 | - [CMake](https://cmake.org/) 39 | - C compiler, 40 | e.g., [gcc](https://gcc.gnu.org/), [clang](https://clang.llvm.org), 41 | [MSVC](https://visualstudio.microsoft.com/vs/) etc. 42 | - [Python 3](https://www.python.org/) 43 | 44 | --- 45 | 46 | ## Installation 47 | 48 | ### Configure, build and install liboqs 49 | 50 | Execute in a Terminal/Console/Administrator Command Prompt 51 | 52 | ```shell 53 | git clone --depth=1 https://github.com/open-quantum-safe/liboqs 54 | cmake -S liboqs -B liboqs/build -DBUILD_SHARED_LIBS=ON 55 | cmake --build liboqs/build --parallel 8 56 | cmake --build liboqs/build --target install 57 | ``` 58 | 59 | The last line may require prefixing it by `sudo` on UNIX-like systems. Change 60 | `--parallel 8` to match the number of available cores on your system. 61 | 62 | On UNIX-like platforms, you may need to set the `LD_LIBRARY_PATH` 63 | (`DYLD_LIBRARY_PATH` on macOS) environment variable to point to the path to 64 | liboqs' library directory, e.g., 65 | 66 | ```shell 67 | export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib 68 | ``` 69 | 70 | On Windows platforms, **you must ensure** that you add the 71 | `-DCMAKE_WINDOWS_EXPORT_ALL_SYMBOLS=TRUE` flag to CMake, and that the liboqs 72 | shared library `oqs.dll` is visible system-wide, i.e., set the `PATH` 73 | environment variable accordingly by using the "Edit the system environment 74 | variables" Control Panel tool or executing in a Command Prompt 75 | 76 | ```shell 77 | set PATH=%PATH%;C:\Program Files (x86)\liboqs\bin 78 | ``` 79 | 80 | You can change liboqs' installation directory by configuring the build to use 81 | an alternative path, e.g., `C:\liboqs`, by passing the 82 | `-DCMAKE_INSTALL_PREFIX=/path/to/liboqs` flag to CMake, e.g., 83 | 84 | ```shell 85 | cmake -S liboqs -B liboqs/build -DCMAKE_INSTALL_PREFIX="C:\liboqs" -DCMAKE_WINDOWS_EXPORT_ALL_SYMBOLS=TRUE -DBUILD_SHARED_LIBS=ON 86 | ``` 87 | 88 | Alternatively, you can set the `OQS_INSTALL_PATH` environment variable to point 89 | to the installation directory, e.g., on a UNIX-like system, execute 90 | 91 | ```shell 92 | export OQS_INSTALL_PATH=/path/to/liboqs 93 | ``` 94 | 95 | ### Let liboqs-python install liboqs automatically 96 | 97 | If liboqs is not detected at runtime by liboqs-python, it will be downloaded, 98 | configured and installed automatically (as a shared library). This process will 99 | be performed only once, at runtime, i.e., when loading the liboqs-python 100 | wrapper. The liboqs source directory will be automatically removed at the end 101 | of the process. 102 | 103 | This is convenient in case you want to avoid installing liboqs manually, as 104 | described in the subsection above. 105 | 106 | ### Install and activate a Python virtual environment 107 | 108 | Execute in a Terminal/Console/Administrator Command Prompt 109 | 110 | ```shell 111 | python3 -m venv venv 112 | . venv/bin/activate 113 | python3 -m ensurepip --upgrade 114 | ``` 115 | 116 | On Windows, replace the line 117 | 118 | ```shell 119 | . venv/bin/activate 120 | ``` 121 | 122 | by 123 | 124 | ```shell 125 | venv\Scripts\activate.bat 126 | ``` 127 | 128 | ### Configure and install the wrapper 129 | 130 | Execute in a Terminal/Console/Administrator Command Prompt 131 | 132 | ```shell 133 | git clone --depth=1 https://github.com/open-quantum-safe/liboqs-python 134 | cd liboqs-python 135 | pip install . 136 | ``` 137 | 138 | ### Run the examples 139 | 140 | Execute 141 | 142 | ```shell 143 | python3 liboqs-python/examples/kem.py 144 | python3 liboqs-python/examples/sig.py 145 | python3 liboqs-python/examples/stfl_sig.py 146 | python3 liboqs-python/examples/rand.py 147 | ``` 148 | 149 | ### Run the unit test 150 | 151 | Execute 152 | 153 | ```shell 154 | nose2 --verbose 155 | ``` 156 | 157 | --- 158 | 159 | ## Usage in standalone applications 160 | 161 | liboqs-python can be imported into Python programs with 162 | 163 | ```python 164 | import oqs 165 | ``` 166 | 167 | liboqs-python defines three main classes: `KeyEncapsulation`, `Signature`, and 168 | `StatefulSignature`, providing post-quantum key encapsulation as well as 169 | stateless and stateful signature mechanisms. Each must be instantiated with a 170 | string identifying one of the mechanisms supported by liboqs; these can be 171 | enumerated using the `get_enabled_kem_mechanisms()`, 172 | `get_enabled_sig_mechanisms()` and `get_enabled_stateful_sig_mechanisms()` 173 | functions. ML-KEM key pairs can also be deterministically generated from a 174 | seed using `KeyEncapsulation.generate_keypair_seed()`. 175 | The files in `examples/` demonstrate the wrapper's API. Support for alternative 176 | RNGs is provided via the `randombytes_*()` functions. 177 | 178 | The liboqs-python project should be in the `PYTHONPATH`. To ensure this on 179 | UNIX-like systems, execute 180 | 181 | ```shell 182 | export PYTHONPATH=$PYTHONPATH:/path/to/liboqs-python 183 | ``` 184 | 185 | or, on Windows platforms, use the "Edit the system environment variables" 186 | Control Panel tool or execute in a Command Prompt 187 | 188 | ```shell 189 | set PYTHONPATH=%PYTHONPATH%;C:\path\to\liboqs-python 190 | ``` 191 | 192 | --- 193 | 194 | ## Docker 195 | 196 | A self-explanatory minimalistic Docker file is provided in 197 | [`Dockerfile`](https://github.com/open-quantum-safe/liboqs-python/tree/main/Dockerfile). 198 | 199 | Build the image by executing 200 | 201 | ```shell 202 | docker build -t oqs-python . 203 | ``` 204 | 205 | Run, e.g., the key encapsulation example by executing 206 | 207 | ```shell 208 | docker run -it oqs-python sh -c ". venv/bin/activate && python liboqs-python/examples/kem.py" 209 | ``` 210 | 211 | Or, run the unit tests with 212 | 213 | ```shell 214 | docker run -it oqs-python sh -c ". venv/bin/activate && nose2 --verbose" 215 | ``` 216 | 217 | In case you want to use the Docker container as a development environment, 218 | mount your current project in the Docker container with 219 | 220 | ```shell 221 | docker run --rm -it --workdir=/app -v ${PWD}:/app oqs-python /bin/bash 222 | ``` 223 | 224 | A more comprehensive Docker example is provided in the directory 225 | [`docker`](https://github.com/open-quantum-safe/liboqs-python/tree/main/docker). 226 | 227 | --- 228 | 229 | ## Limitations and security 230 | 231 | liboqs is designed for prototyping and evaluating quantum-resistant 232 | cryptography. Security of proposed quantum-resistant algorithms may rapidly 233 | change as research advances, and may ultimately be completely insecure against 234 | either classical or quantum computers. 235 | 236 | We believe that the NIST Post-Quantum Cryptography standardization project is 237 | currently the best avenue to identifying potentially quantum-resistant 238 | algorithms. liboqs does not intend to "pick winners", and we strongly recommend 239 | that applications and protocols rely on the outcomes of the NIST 240 | standardization project when deploying post-quantum cryptography. 241 | 242 | We acknowledge that some parties may want to begin deploying post-quantum 243 | cryptography prior to the conclusion of the NIST standardization project. We 244 | strongly recommend that any attempts to do make use of so-called 245 | **hybrid cryptography**, in which post-quantum public-key algorithms are used 246 | alongside traditional public key algorithms (like RSA or elliptic curves) so 247 | that the solution is at least no less secure than existing traditional 248 | cryptography. 249 | 250 | Just like liboqs, liboqs-python is provided "as is", without warranty of any 251 | kind. See 252 | [LICENSE](https://github.com/open-quantum-safe/liboqs-python/blob/main/LICENSE) 253 | for the full disclaimer. 254 | 255 | --- 256 | 257 | ## License 258 | 259 | liboqs-python is licensed under the MIT License; see 260 | [LICENSE](https://github.com/open-quantum-safe/liboqs-python/blob/main/LICENSE) 261 | for details. 262 | 263 | --- 264 | 265 | ## Team 266 | 267 | The Open Quantum Safe project is led by 268 | [Douglas Stebila](https://www.douglas.stebila.ca/research/) and 269 | [Michele Mosca](http://faculty.iqc.uwaterloo.ca/mmosca/) at the University of 270 | Waterloo. 271 | 272 | ### Contributors 273 | 274 | Contributors to the liboqs-python wrapper include 275 | 276 | - Ben Davies (University of Waterloo) 277 | - Vlad Gheorghiu ([softwareQ Inc.](https://www.softwareq.ca) and the University 278 | of Waterloo) 279 | - Christian Paquin (Microsoft Research) 280 | - Douglas Stebila (University of Waterloo) 281 | 282 | --- 283 | 284 | ## Support 285 | 286 | Financial support for the development of Open Quantum Safe has been provided by 287 | Amazon Web Services and the Canadian Centre for Cyber Security. 288 | 289 | We'd like to make a special acknowledgement to the companies who have dedicated 290 | programmer time to contribute source code to OQS, including Amazon Web 291 | Services, evolutionQ, softwareQ, and Microsoft Research. 292 | 293 | Research projects which developed specific components of OQS have been 294 | supported by various research grants, including funding from the Natural 295 | Sciences and Engineering Research Council of Canada (NSERC); see the source 296 | papers for funding acknowledgments. 297 | -------------------------------------------------------------------------------- /oqs/oqs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Open Quantum Safe (OQS) Python wrapper for liboqs. 3 | 4 | The liboqs project provides post-quantum public key cryptography algorithms: 5 | https://github.com/open-quantum-safe/liboqs 6 | 7 | This module provides a Python 3 interface to liboqs. 8 | """ 9 | 10 | from __future__ import annotations 11 | 12 | import ctypes as ct # to call native 13 | import ctypes.util as ctu 14 | import importlib.metadata # to determine module version at runtime 15 | import logging 16 | import platform # to learn the OS we're on 17 | import subprocess 18 | import tempfile # to install liboqs on demand 19 | import time 20 | import os 21 | 22 | try: 23 | import tomllib # Python 3.11+ 24 | except ImportError: # Fallback for older versions 25 | import tomli as tomllib 26 | import warnings 27 | from os import environ 28 | from pathlib import Path 29 | from sys import stdout 30 | from typing import ( 31 | TYPE_CHECKING, 32 | Any, 33 | ClassVar, 34 | Final, 35 | TypeVar, 36 | Union, 37 | cast, 38 | Optional, 39 | ) 40 | 41 | if TYPE_CHECKING: 42 | from collections.abc import Sequence, Iterable 43 | from types import TracebackType 44 | 45 | TKeyEncapsulation = TypeVar("TKeyEncapsulation", bound="KeyEncapsulation") 46 | TSignature = TypeVar("TSignature", bound="Signature") 47 | TStatefulSignature = TypeVar("TStatefulSignature", bound="StatefulSignature") 48 | 49 | logger = logging.getLogger(__name__) 50 | logger.setLevel(logging.INFO) 51 | logger.addHandler(logging.StreamHandler(stdout)) 52 | 53 | # To identify issues in native code, we enable faulthandler. 54 | # As an example, this will print a stack trace if a segfault occurs, 55 | # if the STFL key generation flag was not set when building liboqs. 56 | if os.environ.get("PYOQS_ENABLE_FAULTHANDLER", "0") == "1": 57 | import faulthandler 58 | 59 | faulthandler.enable() 60 | logger.info("liboqs-python faulthandler is enabled") 61 | else: 62 | logger.info("liboqs-python faulthandler is disabled") 63 | 64 | 65 | # Expected return value from native OQS functions 66 | OQS_SUCCESS: Final[int] = 0 67 | OQS_ERROR: Final[int] = -1 68 | 69 | 70 | def oqs_python_version() -> Union[str, None]: 71 | """liboqs-python version string.""" 72 | try: 73 | result = importlib.metadata.version("liboqs-python") 74 | except importlib.metadata.PackageNotFoundError: 75 | warnings.warn("Please install liboqs-python using pip install", stacklevel=2) 76 | pyproject = Path(__file__).resolve().parent.parent / "pyproject.toml" 77 | try: 78 | # Fallback to version specified in pyproject.toml when running from the 79 | # source tree. This allows workflows to use the correct liboqs version 80 | # before the package is installed. 81 | with pyproject.open("rb") as f: 82 | data = tomllib.load(f) 83 | return data["project"]["version"] 84 | except (FileNotFoundError, KeyError, tomllib.TOMLDecodeError): 85 | warnings.warn( 86 | "Please install liboqs-python using pip install", 87 | stacklevel=2, 88 | ) 89 | return None 90 | return result 91 | 92 | 93 | # liboqs-python tries to automatically install and load this liboqs version in 94 | # case no other version is found 95 | OQS_VERSION = oqs_python_version() 96 | 97 | 98 | def version(version_str: str) -> tuple[str, str, str]: 99 | parts = version_str.split(".") 100 | 101 | major = parts[0] if len(parts) > 0 else "" 102 | minor = parts[1] if len(parts) > 1 else "" 103 | patch = parts[2] if len(parts) > 2 else "" 104 | 105 | return major, minor, patch 106 | 107 | 108 | def _load_shared_obj( 109 | name: str, 110 | additional_searching_paths: Union[Sequence[Path], None] = None, 111 | ) -> ct.CDLL: 112 | """Attempt to load shared library.""" 113 | paths: list[Path] = [] 114 | dll = ct.windll if platform.system() == "Windows" else ct.cdll 115 | 116 | # Search additional path, if any 117 | if additional_searching_paths: 118 | for path in additional_searching_paths: 119 | if platform.system() == "Darwin": 120 | paths.append(path.absolute() / Path(f"lib{name}").with_suffix(".dylib")) 121 | elif platform.system() == "Windows": 122 | # Try both oqs.dll and liboqs.dll in the install path 123 | for dll_name in (name, f"lib{name}"): 124 | paths.append(path.absolute() / Path(dll_name).with_suffix(".dll")) 125 | else: # Linux/FreeBSD/UNIX 126 | paths.append(path.absolute() / Path(f"lib{name}").with_suffix(".so")) 127 | # https://stackoverflow.com/questions/856116/changing-ld-library-path-at-runtime-for-ctypes 128 | # os.environ["LD_LIBRARY_PATH"] += os.path.abspath(path) 129 | 130 | # Search typical locations 131 | try: 132 | if found_lib := ctu.find_library(name): 133 | paths.insert(0, Path(found_lib)) 134 | except: 135 | pass 136 | 137 | try: 138 | if found_lib := ctu.find_library("lib" + name): 139 | paths.insert(0, Path(found_lib)) 140 | except: 141 | pass 142 | 143 | for path in paths: 144 | if path: 145 | try: 146 | lib: ct.CDLL = dll.LoadLibrary(str(path)) 147 | except OSError: 148 | pass 149 | else: 150 | return lib 151 | 152 | msg = f"No {name} shared libraries found" 153 | raise RuntimeError(msg) 154 | 155 | 156 | def _countdown(seconds: int) -> None: 157 | while seconds > 0: 158 | logger.info("Installing in %s seconds...", seconds) 159 | stdout.flush() 160 | seconds -= 1 161 | time.sleep(1) 162 | 163 | 164 | def _install_liboqs( 165 | target_directory: Path, 166 | oqs_version_to_install: Union[str, None] = None, 167 | ) -> None: 168 | """Install liboqs version oqs_version (if None, installs latest at HEAD) in the target_directory.""" # noqa: E501 169 | # Set explicit to `None` to install the lastest `liboqs` code. 170 | if oqs_version_to_install is None: 171 | pass 172 | 173 | elif "rc" in oqs_version_to_install: 174 | # removed the "-" from the version string 175 | tmp = oqs_version_to_install.split("rc") 176 | oqs_version_to_install = tmp[0] + "-rc" + tmp[1] 177 | 178 | with tempfile.TemporaryDirectory() as tmpdirname: 179 | oqs_install_cmd = [ 180 | "cd", 181 | tmpdirname, 182 | "&&", 183 | "git", 184 | "clone", 185 | "https://github.com/open-quantum-safe/liboqs", 186 | ] 187 | if oqs_version_to_install: 188 | oqs_install_cmd.extend(["--branch", oqs_version_to_install]) 189 | 190 | oqs_install_cmd.extend( 191 | [ 192 | "--depth", 193 | "1", 194 | "&&", 195 | "cmake", 196 | "-S", 197 | "liboqs", 198 | "-B", 199 | "liboqs/build", 200 | "-DBUILD_SHARED_LIBS=ON", 201 | "-DOQS_BUILD_ONLY_LIB=ON", 202 | # Stateful signature algorithms: 203 | "-DOQS_ENABLE_SIG_STFL_LMS=ON", # LMS family 204 | "-DOQS_ENABLE_SIG_STFL_XMSS=ON", # XMSS family 205 | # To support key-generation. 206 | "-DOQS_HAZARDOUS_EXPERIMENTAL_ENABLE_SIG_STFL_KEY_SIG_GEN=ON", 207 | f"-DCMAKE_INSTALL_PREFIX={target_directory}", 208 | ], 209 | ) 210 | 211 | if platform.system() == "Windows": 212 | oqs_install_cmd.append("-DCMAKE_WINDOWS_EXPORT_ALL_SYMBOLS=TRUE") 213 | 214 | oqs_install_cmd.extend( 215 | [ 216 | "&&", 217 | "cmake", 218 | "--build", 219 | "liboqs/build", 220 | "--parallel", 221 | "4", 222 | "&&", 223 | "cmake", 224 | "--build", 225 | "liboqs/build", 226 | "--target", 227 | "install", 228 | ], 229 | ) 230 | logger.info("liboqs not found, installing it in %s", str(target_directory)) 231 | _countdown(5) 232 | 233 | _retcode = subprocess.call(" ".join(oqs_install_cmd), shell=True) # noqa: S602 234 | 235 | if _retcode != 0: 236 | logger.exception("Error installing liboqs.") 237 | raise SystemExit(1) 238 | 239 | logger.info("Done installing liboqs") 240 | 241 | 242 | def _load_liboqs() -> ct.CDLL: 243 | if "OQS_INSTALL_PATH" in environ: 244 | oqs_install_dir = Path(environ["OQS_INSTALL_PATH"]) 245 | else: 246 | home_dir = Path.home() 247 | oqs_install_dir = home_dir / "_oqs" 248 | oqs_lib_dir = ( 249 | oqs_install_dir / "bin" # $HOME/_oqs/bin 250 | if platform.system() == "Windows" 251 | else oqs_install_dir / "lib" # $HOME/_oqs/lib 252 | ) 253 | oqs_lib64_dir = ( 254 | oqs_install_dir / "bin" # $HOME/_oqs/bin 255 | if platform.system() == "Windows" 256 | else oqs_install_dir / "lib64" # $HOME/_oqs/lib64 257 | ) 258 | try: 259 | liboqs = _load_shared_obj( 260 | name="oqs", 261 | additional_searching_paths=[oqs_lib_dir, oqs_lib64_dir], 262 | ) 263 | assert liboqs # noqa: S101 264 | except RuntimeError: 265 | # We don't have liboqs, so we try to install it automatically 266 | _install_liboqs(target_directory=oqs_install_dir, oqs_version_to_install=OQS_VERSION) 267 | # Try loading it again 268 | try: 269 | liboqs = _load_shared_obj( 270 | name="oqs", 271 | additional_searching_paths=[oqs_lib_dir], 272 | ) 273 | assert liboqs # noqa: S101 274 | except RuntimeError: 275 | msg = "Could not load liboqs shared library" 276 | raise SystemExit(msg) from None 277 | 278 | return liboqs 279 | 280 | 281 | _liboqs = _load_liboqs() 282 | 283 | 284 | def native() -> ct.CDLL: 285 | """Handle to native liboqs handler.""" 286 | return _liboqs 287 | 288 | 289 | # liboqs initialization 290 | native().OQS_init() 291 | 292 | 293 | def oqs_version() -> str: 294 | """liboqs version string.""" 295 | native().OQS_version.restype = ct.c_char_p 296 | return ct.c_char_p(native().OQS_version()).value.decode("UTF-8") # type: ignore[union-attr] 297 | 298 | 299 | oqs_ver = oqs_version() 300 | oqs_ver_major, oqs_ver_minor, oqs_ver_patch = version(oqs_ver) 301 | 302 | 303 | oqs_python_ver = oqs_python_version() 304 | if oqs_python_ver: 305 | oqs_python_ver_major, oqs_python_ver_minor, oqs_python_ver_patch = version(oqs_python_ver) 306 | # Warn the user if the liboqs version differs from liboqs-python version 307 | if not (oqs_ver_major == oqs_python_ver_major and oqs_ver_minor == oqs_python_ver_minor): 308 | warnings.warn( 309 | f"liboqs version (major, minor) {oqs_version()} differs from liboqs-python version " 310 | f"{oqs_python_version()}", 311 | stacklevel=2, 312 | ) 313 | 314 | 315 | class MechanismNotSupportedError(Exception): 316 | """Exception raised when an algorithm is not supported by OQS.""" 317 | 318 | def __init__(self, alg_name: str, supported: Optional[Iterable[str]] = None) -> None: 319 | """ 320 | Initialize the exception. 321 | 322 | :param alg_name: Requested algorithm name. 323 | :param supported: A list of supported algorithms to include in the message. 324 | Defaults to `None`. 325 | """ 326 | supported_str = "" 327 | if supported is not None: 328 | supported_str = ", ".join(supported) 329 | supported_str = f". Supported algorithms: {supported_str}" 330 | 331 | self.alg_name = alg_name 332 | self.message = f"{alg_name} is not supported by OQS" + supported_str 333 | 334 | 335 | class MechanismNotEnabledError(MechanismNotSupportedError): 336 | """Exception raised when an algorithm is supported but not enabled by OQS.""" 337 | 338 | def __init__(self, alg_name: str, enabled: Optional[Iterable[str]] = None) -> None: 339 | """ 340 | Initialize the exception. 341 | 342 | :param alg_name: Requested algorithm name. 343 | :param enabled: A list of enabled algorithms to include in the message. Defaults to `None`. 344 | """ 345 | enabled_str = "" 346 | if enabled is not None: 347 | enabled_str = ", ".join(enabled) 348 | enabled_str = f". Enabled algorithms: {enabled_str}" 349 | 350 | self.alg_name = alg_name 351 | self.message = f"{alg_name} is supported but not enabled by OQS" + enabled_str 352 | 353 | 354 | class KeyEncapsulation(ct.Structure): 355 | """ 356 | An OQS KeyEncapsulation wraps native/C liboqs OQS_KEM structs. 357 | 358 | The wrapper maps methods to the C equivalent as follows: 359 | 360 | Python | C liboqs 361 | ------------------------------- 362 | generate_keypair | keypair 363 | generate_keypair_seed | keypair 364 | encap_secret | encaps 365 | decap_secret | decaps 366 | free | OQS_KEM_free 367 | """ 368 | 369 | _fields_: ClassVar[Sequence[tuple[str, Any]]] = [ 370 | ("method_name", ct.c_char_p), 371 | ("alg_version", ct.c_char_p), 372 | ("claimed_nist_level", ct.c_ubyte), 373 | ("ind_cca", ct.c_bool), 374 | ("length_public_key", ct.c_size_t), 375 | ("length_secret_key", ct.c_size_t), 376 | ("length_ciphertext", ct.c_size_t), 377 | ("length_shared_secret", ct.c_size_t), 378 | ("length_keypair_seed", ct.c_size_t), 379 | ("keypair_derand_cb", ct.c_void_p), 380 | ("keypair_cb", ct.c_void_p), 381 | ("encaps_cb", ct.c_void_p), 382 | ("decaps_cb", ct.c_void_p), 383 | ] 384 | 385 | def __init__(self, alg_name: str, secret_key: Union[int, bytes, None] = None) -> None: 386 | """ 387 | Create new KeyEncapsulation with the given algorithm. 388 | 389 | :param alg_name: KEM mechanism algorithm name. Enabled KEM mechanisms can be obtained with 390 | get_enabled_KEM_mechanisms(). 391 | :param secret_key: optional if generating by generate_keypair() later. 392 | """ 393 | super().__init__() 394 | self.alg_name = alg_name 395 | if alg_name not in _enabled_KEMs: 396 | # perhaps it's a supported but not enabled alg 397 | if alg_name in _supported_KEMs: 398 | raise MechanismNotEnabledError(alg_name) 399 | raise MechanismNotSupportedError(alg_name) 400 | 401 | self._kem = native().OQS_KEM_new(ct.create_string_buffer(alg_name.encode())) 402 | 403 | self.method_name = self._kem.contents.method_name 404 | self.alg_version = self._kem.contents.alg_version 405 | self.claimed_nist_level = self._kem.contents.claimed_nist_level 406 | self.ind_cca = self._kem.contents.ind_cca 407 | self.length_public_key = self._kem.contents.length_public_key 408 | self.length_secret_key = self._kem.contents.length_secret_key 409 | self.length_ciphertext = self._kem.contents.length_ciphertext 410 | self.length_shared_secret = self._kem.contents.length_shared_secret 411 | self.length_keypair_seed = self._kem.contents.length_keypair_seed 412 | 413 | self.details = { 414 | "name": self.method_name.decode(), 415 | "version": self.alg_version.decode(), 416 | "claimed_nist_level": int(self.claimed_nist_level), 417 | "is_ind_cca": bool(self.ind_cca), 418 | "length_public_key": int(self.length_public_key), 419 | "length_secret_key": int(self.length_secret_key), 420 | "length_ciphertext": int(self.length_ciphertext), 421 | "length_shared_secret": int(self.length_shared_secret), 422 | "length_keypair_seed": int(self.length_keypair_seed), 423 | } 424 | 425 | if secret_key: 426 | self.secret_key = ct.create_string_buffer( 427 | secret_key, 428 | self._kem.contents.length_secret_key, 429 | ) 430 | 431 | def __enter__(self: TKeyEncapsulation) -> TKeyEncapsulation: 432 | return self 433 | 434 | def __exit__( 435 | self, 436 | exc_type: Union[type[BaseException], None], 437 | exc_value: Union[BaseException, None], 438 | traceback: Union[TracebackType, None], 439 | ) -> None: 440 | self.free() 441 | 442 | def generate_keypair_seed(self, seed: bytes) -> bytes: 443 | """ 444 | Generate a new keypair using the provided seed and returns the public key. 445 | 446 | :param seed: A seed to use for key generation. 447 | If the seed is None, a random seed will be generated. 448 | """ 449 | if self.length_keypair_seed == 0: 450 | msg = f"Key generation with seed is not supported. Got {self.alg_name}." 451 | raise RuntimeError(msg) 452 | 453 | if len(seed) != self._kem.contents.length_keypair_seed: 454 | msg = ( 455 | f"Seed length must be {self._kem.contents.length_keypair_seed} bytes, " 456 | f"got {len(seed)} bytes" 457 | ) 458 | raise ValueError(msg) 459 | 460 | c_seed = ct.create_string_buffer(seed, self._kem.contents.length_keypair_seed) 461 | public_key = ct.create_string_buffer(self._kem.contents.length_public_key) 462 | self.secret_key = ct.create_string_buffer(self._kem.contents.length_secret_key) 463 | 464 | rv = native().OQS_KEM_keypair_derand( 465 | self._kem, 466 | ct.byref(public_key), 467 | ct.byref(self.secret_key), 468 | c_seed, 469 | ) 470 | if rv == OQS_SUCCESS: 471 | return bytes(public_key) 472 | msg = "Can not generate keypair with provided seed" 473 | raise RuntimeError(msg) 474 | 475 | def generate_keypair(self) -> bytes: 476 | """ 477 | Generate a new keypair and returns the public key. 478 | 479 | If needed, the secret key can be obtained with export_secret_key(). 480 | """ 481 | public_key = ct.create_string_buffer(self._kem.contents.length_public_key) 482 | self.secret_key = ct.create_string_buffer(self._kem.contents.length_secret_key) 483 | rv = native().OQS_KEM_keypair( 484 | self._kem, 485 | ct.byref(public_key), 486 | ct.byref(self.secret_key), 487 | ) 488 | if rv == OQS_SUCCESS: 489 | return bytes(public_key) 490 | msg = "Can not generate keypair" 491 | raise RuntimeError(msg) 492 | 493 | def export_secret_key(self) -> bytes: 494 | """Export the secret key.""" 495 | return bytes(self.secret_key) 496 | 497 | def encap_secret(self, public_key: Union[int, bytes]) -> tuple[bytes, bytes]: 498 | """ 499 | Generate and encapsulates a secret using the provided public key. 500 | 501 | :param public_key: the peer's public key. 502 | """ 503 | c_public_key = ct.create_string_buffer( 504 | public_key, 505 | self._kem.contents.length_public_key, 506 | ) 507 | ciphertext: ct.Array[ct.c_char] = ct.create_string_buffer( 508 | self._kem.contents.length_ciphertext, 509 | ) 510 | shared_secret: ct.Array[ct.c_char] = ct.create_string_buffer( 511 | self._kem.contents.length_shared_secret, 512 | ) 513 | rv = native().OQS_KEM_encaps( 514 | self._kem, 515 | ct.byref(ciphertext), 516 | ct.byref(shared_secret), 517 | c_public_key, 518 | ) 519 | if rv == OQS_SUCCESS: 520 | return bytes(ciphertext), bytes(shared_secret) 521 | msg = "Can not encapsulate secret" 522 | raise RuntimeError(msg) 523 | 524 | def decap_secret(self, ciphertext: Union[int, bytes]) -> bytes: 525 | """ 526 | Decapsulate the ciphertext and returns the secret. 527 | 528 | :param ciphertext: the ciphertext received from the peer. 529 | """ 530 | c_ciphertext = ct.create_string_buffer( 531 | ciphertext, 532 | self._kem.contents.length_ciphertext, 533 | ) 534 | shared_secret: ct.Array[ct.c_char] = ct.create_string_buffer( 535 | self._kem.contents.length_shared_secret, 536 | ) 537 | rv = native().OQS_KEM_decaps( 538 | self._kem, 539 | ct.byref(shared_secret), 540 | c_ciphertext, 541 | self.secret_key, 542 | ) 543 | if rv == OQS_SUCCESS: 544 | return bytes(shared_secret) 545 | msg = "Can not decapsulate secret" 546 | raise RuntimeError(msg) 547 | 548 | def free(self) -> None: 549 | """Releases the native resources.""" 550 | if hasattr(self, "secret_key"): 551 | native().OQS_MEM_cleanse( 552 | ct.byref(self.secret_key), 553 | self._kem.contents.length_secret_key, 554 | ) 555 | native().OQS_KEM_free(self._kem) 556 | 557 | def __repr__(self) -> str: 558 | return f"Key encapsulation mechanism: {self._kem.contents.method_name.decode()}" 559 | 560 | 561 | native().OQS_KEM_new.restype = ct.POINTER(KeyEncapsulation) 562 | native().OQS_KEM_alg_identifier.restype = ct.c_char_p 563 | 564 | 565 | def is_kem_enabled(alg_name: str) -> bool: 566 | """ 567 | Return True if the KEM algorithm is enabled. 568 | 569 | :param alg_name: a KEM mechanism algorithm name. 570 | """ 571 | return native().OQS_KEM_alg_is_enabled(ct.create_string_buffer(alg_name.encode())) 572 | 573 | 574 | _KEM_alg_ids = [native().OQS_KEM_alg_identifier(i) for i in range(native().OQS_KEM_alg_count())] 575 | _supported_KEMs: tuple[str, ...] = tuple([i.decode() for i in _KEM_alg_ids]) # noqa: N816 576 | _enabled_KEMs: tuple[str, ...] = tuple([i for i in _supported_KEMs if is_kem_enabled(i)]) # noqa: N816 577 | 578 | 579 | def get_enabled_kem_mechanisms() -> tuple[str, ...]: 580 | """Return the list of enabled KEM mechanisms.""" 581 | return _enabled_KEMs 582 | 583 | 584 | def get_supported_kem_mechanisms() -> tuple[str, ...]: 585 | """Return the list of supported KEM mechanisms.""" 586 | return _supported_KEMs 587 | 588 | 589 | # Register the OQS_SIG_supports_ctx_str function from the C library 590 | native().OQS_SIG_supports_ctx_str.restype = ct.c_bool 591 | native().OQS_SIG_supports_ctx_str.argtypes = [ct.c_char_p] 592 | 593 | 594 | class Signature(ct.Structure): 595 | """ 596 | An OQS Signature wraps native/C liboqs OQS_SIG structs. 597 | 598 | The wrapper maps methods to the C equivalent as follows: 599 | 600 | Python | C liboqs 601 | ------------------------------- 602 | generate_keypair | keypair 603 | sign | sign 604 | verify | verify 605 | free | OQS_SIG_free 606 | """ 607 | 608 | _fields_: ClassVar[Sequence[tuple[str, Any]]] = [ 609 | ("method_name", ct.c_char_p), 610 | ("alg_version", ct.c_char_p), 611 | ("claimed_nist_level", ct.c_ubyte), 612 | ("euf_cma", ct.c_bool), 613 | ("suf_cma", ct.c_bool), 614 | ("sig_with_ctx_support", ct.c_bool), 615 | ("length_public_key", ct.c_size_t), 616 | ("length_secret_key", ct.c_size_t), 617 | ("length_signature", ct.c_size_t), 618 | ("keypair_cb", ct.c_void_p), 619 | ("sign_cb", ct.c_void_p), 620 | ("sign_with_ctx_cb", ct.c_void_p), 621 | ("verify_cb", ct.c_void_p), 622 | ("verify_with_ctx_cb", ct.c_void_p), 623 | ] 624 | 625 | def __init__(self, alg_name: str, secret_key: Union[int, bytes, None] = None) -> None: 626 | """ 627 | Create new Signature with the given algorithm. 628 | 629 | :param alg_name: a signature mechanism algorithm name. Enabled signature mechanisms can be 630 | obtained with get_enabled_sig_mechanisms(). 631 | :param secret_key: optional, if generated by generate_keypair(). 632 | """ 633 | super().__init__() 634 | if alg_name not in _enabled_sigs: 635 | # perhaps it's a supported but not enabled alg 636 | if alg_name in _supported_sigs: 637 | raise MechanismNotEnabledError(alg_name) 638 | raise MechanismNotSupportedError(alg_name) 639 | 640 | self._sig = native().OQS_SIG_new(ct.create_string_buffer(alg_name.encode())) 641 | 642 | self.method_name = self._sig.contents.method_name 643 | self.alg_version = self._sig.contents.alg_version 644 | self.claimed_nist_level = self._sig.contents.claimed_nist_level 645 | self.euf_cma = self._sig.contents.euf_cma 646 | self.sig_with_ctx_support = bool(self._sig.contents.sig_with_ctx_support) 647 | self.length_public_key = self._sig.contents.length_public_key 648 | self.length_secret_key = self._sig.contents.length_secret_key 649 | self.length_signature = self._sig.contents.length_signature 650 | 651 | self.details = { 652 | "name": self.method_name.decode(), 653 | "version": self.alg_version.decode(), 654 | "claimed_nist_level": int(self.claimed_nist_level), 655 | "is_euf_cma": bool(self.euf_cma), 656 | "is_suf_cma": bool(self.suf_cma), 657 | "supports_context_signing": bool(self.sig_with_ctx_support), 658 | "sig_with_ctx_support": bool(self.sig_with_ctx_support), 659 | "length_public_key": int(self.length_public_key), 660 | "length_secret_key": int(self.length_secret_key), 661 | "length_signature": int(self.length_signature), 662 | } 663 | 664 | if secret_key: 665 | self.secret_key = ct.create_string_buffer( 666 | secret_key, 667 | self._sig.contents.length_secret_key, 668 | ) 669 | 670 | def __enter__(self: TSignature) -> TSignature: 671 | return self 672 | 673 | def __exit__( 674 | self, 675 | exc_type: Union[type[BaseException], None], 676 | exc_value: Union[BaseException, None], 677 | traceback: Union[TracebackType, None], 678 | ) -> None: 679 | self.free() 680 | 681 | def generate_keypair(self) -> bytes: 682 | """ 683 | Generate a new keypair and returns the public key. 684 | 685 | If needed, the secret key can be obtained with export_secret_key(). 686 | """ 687 | public_key: ct.Array[ct.c_char] = ct.create_string_buffer( 688 | self._sig.contents.length_public_key, 689 | ) 690 | self.secret_key = ct.create_string_buffer(self._sig.contents.length_secret_key) 691 | rv = native().OQS_SIG_keypair( 692 | self._sig, 693 | ct.byref(public_key), 694 | ct.byref(self.secret_key), 695 | ) 696 | if rv == OQS_SUCCESS: 697 | return bytes(public_key) 698 | msg = "Can not generate keypair" 699 | raise RuntimeError(msg) 700 | 701 | def export_secret_key(self) -> bytes: 702 | """Export the secret key.""" 703 | return bytes(self.secret_key) 704 | 705 | def sign(self, message: bytes) -> bytes: 706 | """ 707 | Signs the provided message and returns the signature. 708 | 709 | :param message: the message to sign. 710 | """ 711 | # Provide length to avoid extra null char 712 | c_message = ct.create_string_buffer(message, len(message)) 713 | c_message_len = ct.c_size_t(len(c_message)) 714 | c_signature = ct.create_string_buffer(self._sig.contents.length_signature) 715 | 716 | # Initialize to maximum signature size 717 | c_signature_len = ct.c_size_t(self._sig.contents.length_signature) 718 | 719 | rv = native().OQS_SIG_sign( 720 | self._sig, 721 | ct.byref(c_signature), 722 | ct.byref(c_signature_len), 723 | c_message, 724 | c_message_len, 725 | self.secret_key, 726 | ) 727 | if rv == OQS_SUCCESS: 728 | return bytes(cast("bytes", c_signature[: c_signature_len.value])) 729 | msg = "Can not sign message" 730 | raise RuntimeError(msg) 731 | 732 | def verify(self, message: bytes, signature: bytes, public_key: bytes) -> bool: 733 | """ 734 | Verify the provided signature on the message; returns True if valid. 735 | 736 | :param message: the signed message. 737 | :param signature: the signature on the message. 738 | :param public_key: the signer's public key. 739 | """ 740 | # Provide length to avoid extra null char 741 | c_message = ct.create_string_buffer(message, len(message)) 742 | c_message_len = ct.c_size_t(len(c_message)) 743 | c_signature = ct.create_string_buffer(signature, len(signature)) 744 | c_signature_len = ct.c_size_t(len(c_signature)) 745 | c_public_key = ct.create_string_buffer( 746 | public_key, 747 | self._sig.contents.length_public_key, 748 | ) 749 | 750 | rv = native().OQS_SIG_verify( 751 | self._sig, 752 | c_message, 753 | c_message_len, 754 | c_signature, 755 | c_signature_len, 756 | c_public_key, 757 | ) 758 | return rv == OQS_SUCCESS 759 | 760 | def sign_with_ctx_str(self, message: bytes, context: bytes) -> bytes: 761 | """ 762 | Sign the provided message with context string and returns the signature. 763 | 764 | :param context: the context string. 765 | :param message: the message to sign. 766 | """ 767 | if context and not self._sig.contents.sig_with_ctx_support: 768 | msg = ( 769 | f"Signing with context is not supported for: " 770 | f"{self._sig.contents.method_name.decode()}" 771 | ) 772 | raise RuntimeError(msg) 773 | 774 | # Provide length to avoid extra null char 775 | c_message = ct.create_string_buffer(message, len(message)) 776 | c_message_len = ct.c_size_t(len(c_message)) 777 | if len(context) == 0: 778 | c_context = None 779 | c_context_len = ct.c_size_t(0) 780 | else: 781 | c_context = ct.create_string_buffer(context, len(context)) 782 | c_context_len = ct.c_size_t(len(c_context)) 783 | c_signature = ct.create_string_buffer(self._sig.contents.length_signature) 784 | 785 | # Initialize to maximum signature size 786 | c_signature_len = ct.c_size_t(self._sig.contents.length_signature) 787 | rv = native().OQS_SIG_sign_with_ctx_str( 788 | self._sig, 789 | ct.byref(c_signature), 790 | ct.byref(c_signature_len), 791 | c_message, 792 | c_message_len, 793 | c_context, 794 | c_context_len, 795 | self.secret_key, 796 | ) 797 | if rv == OQS_SUCCESS: 798 | return bytes(cast("bytes", c_signature[: c_signature_len.value])) 799 | msg = "Can not sign message with context string" 800 | raise RuntimeError(msg) 801 | 802 | def verify_with_ctx_str( 803 | self, 804 | message: bytes, 805 | signature: bytes, 806 | context: bytes, 807 | public_key: bytes, 808 | ) -> bool: 809 | """ 810 | Verify the provided signature on the message with context string; returns True if valid. 811 | 812 | :param message: the signed message. 813 | :param signature: the signature on the message. 814 | :param context: the context string. 815 | :param public_key: the signer's public key. 816 | """ 817 | if context and not self.sig_with_ctx_support: 818 | msg = "Verifying with context string not supported" 819 | raise RuntimeError(msg) 820 | 821 | # Provide length to avoid extra null char 822 | c_message = ct.create_string_buffer(message, len(message)) 823 | c_message_len = ct.c_size_t(len(c_message)) 824 | c_signature = ct.create_string_buffer(signature, len(signature)) 825 | c_signature_len = ct.c_size_t(len(c_signature)) 826 | if len(context) == 0: 827 | c_context = None 828 | c_context_len = ct.c_size_t(0) 829 | else: 830 | c_context = ct.create_string_buffer(context, len(context)) 831 | c_context_len = ct.c_size_t(len(c_context)) 832 | c_public_key = ct.create_string_buffer( 833 | public_key, 834 | self._sig.contents.length_public_key, 835 | ) 836 | 837 | rv = native().OQS_SIG_verify_with_ctx_str( 838 | self._sig, 839 | c_message, 840 | c_message_len, 841 | c_signature, 842 | c_signature_len, 843 | c_context, 844 | c_context_len, 845 | c_public_key, 846 | ) 847 | return rv == OQS_SUCCESS 848 | 849 | def free(self) -> None: 850 | """Releases the native resources.""" 851 | if hasattr(self, "secret_key"): 852 | native().OQS_MEM_cleanse( 853 | ct.byref(self.secret_key), 854 | self._sig.contents.length_secret_key, 855 | ) 856 | native().OQS_SIG_free(self._sig) 857 | 858 | def __repr__(self) -> str: 859 | return f"Signature mechanism: {self._sig.contents.method_name.decode()}" 860 | 861 | 862 | native().OQS_SIG_new.restype = ct.POINTER(Signature) 863 | native().OQS_SIG_alg_identifier.restype = ct.c_char_p 864 | 865 | native().OQS_SIG_supports_ctx_str.restype = ct.c_bool 866 | 867 | 868 | def is_sig_enabled(alg_name: str) -> bool: 869 | """ 870 | Return `True` if the signature algorithm is enabled. 871 | 872 | :param alg_name: A signature mechanism algorithm name. 873 | """ 874 | return native().OQS_SIG_alg_is_enabled(ct.create_string_buffer(alg_name.encode())) 875 | 876 | 877 | def sig_supports_context(alg_name: str) -> bool: 878 | """ 879 | Return `True` if the signature algorithm supports signing with a context string. 880 | 881 | :param alg_name: A signature mechanism algorithm name. 882 | """ 883 | return bool(native().OQS_SIG_supports_ctx_str(ct.create_string_buffer(alg_name.encode()))) 884 | 885 | 886 | _sig_alg_ids = [native().OQS_SIG_alg_identifier(i) for i in range(native().OQS_SIG_alg_count())] 887 | _supported_sigs: tuple[str, ...] = tuple([i.decode() for i in _sig_alg_ids]) 888 | _enabled_sigs: tuple[str, ...] = tuple([i for i in _supported_sigs if is_sig_enabled(i)]) 889 | 890 | 891 | def get_enabled_sig_mechanisms() -> tuple[str, ...]: 892 | """Return the list of enabled signature mechanisms.""" 893 | return _enabled_sigs 894 | 895 | 896 | def get_supported_sig_mechanisms() -> tuple[str, ...]: 897 | """Return the list of supported signature mechanisms.""" 898 | return _supported_sigs 899 | 900 | 901 | # Check enabled algorithms 902 | native().OQS_SIG_STFL_alg_identifier.restype = ct.c_char_p 903 | 904 | 905 | def is_stateful_sig_enabled(alg_name: str) -> bool: 906 | """Check if a stateful signature algorithm is enabled.""" 907 | return native().OQS_SIG_STFL_alg_is_enabled(ct.create_string_buffer(alg_name.encode())) 908 | 909 | 910 | _supported_stateful_sigs: tuple[str, ...] = tuple( 911 | native().OQS_SIG_STFL_alg_identifier(i).decode() 912 | for i in range(native().OQS_SIG_STFL_alg_count()) 913 | ) 914 | _enabled_stateful_sigs: tuple[str, ...] = tuple( 915 | alg for alg in _supported_stateful_sigs if is_stateful_sig_enabled(alg) 916 | ) 917 | 918 | 919 | def get_enabled_stateful_sig_mechanisms() -> tuple[str, ...]: 920 | """Return a list of enabled stateful signature mechanisms.""" 921 | return _enabled_stateful_sigs 922 | 923 | 924 | def get_supported_stateful_sig_mechanisms() -> tuple[str, ...]: 925 | """Return a list of supported stateful signature mechanisms.""" 926 | return _supported_stateful_sigs 927 | 928 | 929 | def _filter_stfl_names(alg_name: str, alg_names: Iterable[str]) -> Optional[list[str]]: 930 | """Filter and return only stateful signature algorithm names.""" 931 | if alg_name.startswith("LMS"): 932 | return [name for name in alg_names if name.startswith("LMS")] 933 | if alg_name.startswith("XMSS"): 934 | return [name for name in alg_names if name.startswith("XMSS")] 935 | if alg_name.startswith("XMSSMT"): 936 | return [name for name in alg_names if name.startswith("XMSSMT")] 937 | return None 938 | 939 | 940 | def _check_alg(alg_name: str) -> None: 941 | """Check if the algorithm is supported and enabled.""" 942 | if alg_name not in _supported_stateful_sigs: 943 | _filtered_names = _filter_stfl_names(alg_name, _supported_stateful_sigs) 944 | if _filtered_names is None: 945 | raise MechanismNotSupportedError(alg_name) 946 | raise MechanismNotSupportedError(alg_name, supported=_filtered_names) 947 | if alg_name not in _enabled_stateful_sigs: 948 | sup = _filter_stfl_names(alg_name, _enabled_stateful_sigs) 949 | raise MechanismNotEnabledError(alg_name, enabled=sup) 950 | 951 | 952 | class StatefulSignature(ct.Structure): 953 | """ 954 | An OQS StatefulSignature wraps native/C liboqs OQS_SIG structs. 955 | 956 | The wrapper maps methods to the C equivalent as follows: 957 | 958 | Python | C liboqs 959 | ------------------------------- 960 | generate_keypair | keypair 961 | sign | sign 962 | verify | verify 963 | free | OQS_SIG_STFL_free 964 | sigs_remaining | OQS_SIG_STFL_sigs_remaining 965 | sigs_total | OQS_SIG_STFL_sigs_total 966 | 967 | """ 968 | 969 | _fields_: ClassVar[Sequence[tuple[str, Any]]] = [ 970 | ("oid", ct.c_uint32), 971 | ("method_name", ct.c_char_p), 972 | ("alg_version", ct.c_char_p), 973 | ("euf_cma", ct.c_bool), 974 | ("suf_cma", ct.c_bool), 975 | ("length_public_key", ct.c_size_t), 976 | ("length_secret_key", ct.c_size_t), 977 | ("length_signature", ct.c_size_t), 978 | ("keypair_cb", ct.c_void_p), 979 | ("sign_cb", ct.c_void_p), 980 | ("verify_cb", ct.c_void_p), 981 | ("sigs_remaining_cb", ct.c_void_p), 982 | ("sigs_total_cb", ct.c_void_p), 983 | ] 984 | 985 | def __init__(self, alg_name: str, secret_key: Optional[bytes] = None) -> None: 986 | """ 987 | Create a new stateful signature instance with the given algorithm. 988 | 989 | :param alg_name: A stateful signature mechanism algorithm name. 990 | :param secret_key: Optional secret key to load. 991 | """ 992 | super().__init__() 993 | 994 | _check_alg(alg_name) 995 | self._sig = native().OQS_SIG_STFL_new(ct.create_string_buffer(alg_name.encode())) 996 | if not self._sig: 997 | msg = f"Could not allocate OQS_SIG_STFL for {alg_name}" 998 | raise RuntimeError(msg) 999 | 1000 | for field, _ctype in self._fields_: 1001 | if field == "oid" or field.endswith("cb"): 1002 | continue 1003 | setattr(self, field, getattr(self._sig.contents, field)) 1004 | 1005 | self._secret_key: ct.c_void_p | None = None 1006 | self._owns_secret = False 1007 | self._used_keys: list[bytes] = [] 1008 | self._store_cb: Optional[ct.CFUNCTYPE] = None 1009 | 1010 | if secret_key is not None: 1011 | self._load_secret_key(secret_key) 1012 | 1013 | self.details = { 1014 | "name": self.method_name.decode(), 1015 | "version": self.alg_version.decode(), 1016 | "is_euf_cma": bool(self.euf_cma), 1017 | "is_suf_cma": bool(self.suf_cma), 1018 | "length_public_key": int(self.length_public_key), 1019 | "length_secret_key": int(self.length_secret_key), 1020 | "length_signature": int(self.length_signature), 1021 | } 1022 | 1023 | def _attach_store_cb(self) -> None: 1024 | """Attach a callback to store used keys in the stateful signature.""" 1025 | 1026 | @ct.CFUNCTYPE(ct.c_int, ct.POINTER(ct.c_uint8), ct.c_size_t, ct.c_void_p) 1027 | def _cb(buf: bytes, length: int, _: ct.c_void_p) -> int: 1028 | self._used_keys.append(ct.string_at(buf, length)) 1029 | return OQS_SUCCESS 1030 | 1031 | self._store_cb = _cb # keep ref 1032 | native().OQS_SIG_STFL_SECRET_KEY_SET_store_cb(self._secret_key, self._store_cb, None) 1033 | 1034 | def _new_secret_key(self) -> None: 1035 | """Create a new secret key for the stateful signature.""" 1036 | self._secret_key = native().OQS_SIG_STFL_SECRET_KEY_new(self.method_name) 1037 | if not self._secret_key: 1038 | msg = "Could not allocate OQS_SIG_STFL_SECRET_KEY" 1039 | raise MemoryError(msg) 1040 | self._attach_store_cb() 1041 | 1042 | def _load_secret_key(self, data: bytes) -> None: 1043 | """Load a secret key from bytes.""" 1044 | self._new_secret_key() 1045 | buf = ct.create_string_buffer(data, len(data)) 1046 | rc = native().OQS_SIG_STFL_SECRET_KEY_deserialize(self._secret_key, buf, len(data), None) 1047 | if rc != OQS_SUCCESS: 1048 | if len(data) != int(self.length_secret_key): 1049 | msg = ( 1050 | f"Secret key length must be {self.length_secret_key} bytes, " 1051 | f"got {len(data)} bytes" 1052 | ) 1053 | raise ValueError(msg) 1054 | 1055 | msg = "Secret‑key deserialization failed" 1056 | raise RuntimeError(msg) 1057 | 1058 | def generate_keypair(self) -> bytes: 1059 | """ 1060 | Generate a new keypair for the stateful signature. 1061 | 1062 | :raise ValueError: If the keypair has already been generated. 1063 | :raise RuntimeError: If the keypair generation fails or if a keypair already exists. 1064 | :return: The generated public key as bytes. 1065 | """ 1066 | if self._secret_key is not None: 1067 | msg = "Keypair already generated, call free() to release the secret key" 1068 | raise ValueError(msg) 1069 | 1070 | self._secret_key = native().OQS_SIG_STFL_SECRET_KEY_new(self.method_name) 1071 | if not self._secret_key: 1072 | msg = "Could not allocate OQS_SIG_STFL_SECRET_KEY" 1073 | raise RuntimeError(msg) 1074 | self._attach_store_cb() 1075 | 1076 | sig_struct = self._sig.contents 1077 | pk_buf = ct.create_string_buffer(sig_struct.length_public_key) 1078 | 1079 | rc = native().OQS_SIG_STFL_keypair(self._sig, pk_buf, self._secret_key) 1080 | if rc != OQS_SUCCESS: 1081 | msg = "Keypair generation failed" 1082 | raise RuntimeError(msg) 1083 | return pk_buf.raw 1084 | 1085 | def sign(self, message: bytes) -> bytes: 1086 | """ 1087 | Sign the provided message and return the signature. 1088 | 1089 | :param message: The message to sign. 1090 | :raises NotImplementedError: If the method is LMS-based, as it is verify-only supported. 1091 | :raises RuntimeError: If the secret key is not initialized. 1092 | :raises ValueError: If the signing fails. 1093 | :return: The signature on the message as bytes. 1094 | """ 1095 | if self.method_name.startswith(b"LMS"): 1096 | msg = "LMS algorithms are verify‑only supported." 1097 | raise NotImplementedError(msg) 1098 | if self._secret_key is None: 1099 | msg = "Secret key not initialised – call generate_keypair() first" 1100 | raise RuntimeError(msg) 1101 | c_signature = ct.create_string_buffer(self.length_signature) 1102 | c_signature_len = ct.c_size_t(self.length_signature) 1103 | msg_buf = ct.create_string_buffer(message, len(message)) 1104 | rc = native().OQS_SIG_STFL_sign( 1105 | self._sig, 1106 | c_signature, 1107 | ct.byref(c_signature_len), 1108 | msg_buf, 1109 | len(message), 1110 | self._secret_key, 1111 | ) 1112 | if rc != OQS_SUCCESS: 1113 | msg = "Signing failed" 1114 | raise ValueError(msg) 1115 | return bytes(cast("bytes", c_signature[: c_signature_len.value])) 1116 | 1117 | def verify(self, message: bytes, signature: bytes, public_key: bytes) -> bool: 1118 | """ 1119 | Verify the provided signature on the message; returns True if valid. 1120 | 1121 | :param message: The signed message. 1122 | :param signature: The signature on the message. 1123 | :param public_key: The signer's public key. 1124 | :return: `True` if the signature is valid, `False` otherwise. 1125 | """ 1126 | msg = ct.create_string_buffer(message, len(message)) 1127 | sig = ct.create_string_buffer(signature, len(signature)) 1128 | pk = ct.create_string_buffer(public_key, len(public_key)) 1129 | rc = native().OQS_SIG_STFL_verify(self._sig, msg, len(message), sig, len(signature), pk) 1130 | return rc == OQS_SUCCESS 1131 | 1132 | def export_secret_key(self) -> bytes: 1133 | """ 1134 | Serialize the secret key to bytes. 1135 | 1136 | :return: The serialized secret key as bytes. 1137 | :raises ValueError: If the secret key is not initialized. 1138 | """ 1139 | if self._secret_key is None: 1140 | msg = "Secret key not initialised – call generate_keypair() first" 1141 | raise ValueError(msg) 1142 | buf_ptr = ct.POINTER(ct.c_uint8)() 1143 | buf_len = ct.c_size_t() 1144 | rc = native().OQS_SIG_STFL_SECRET_KEY_serialize( 1145 | ct.byref(buf_ptr), ct.byref(buf_len), self._secret_key 1146 | ) 1147 | if rc != OQS_SUCCESS: 1148 | msg = "Secret‑key serialization failed" 1149 | raise ValueError(msg) 1150 | data = ct.string_at(buf_ptr, buf_len.value) 1151 | ct.CDLL(ct.util.find_library("c")).free(buf_ptr) 1152 | return data 1153 | 1154 | def sigs_total(self) -> int: 1155 | """Get the total number of signatures that can be made with the secret key.""" 1156 | total = ct.c_uint64() 1157 | rc = native().OQS_SIG_STFL_sigs_total(self._sig, ct.byref(total), self._secret_key) 1158 | if rc != OQS_SUCCESS: 1159 | msg = "Failed to get total signature count" 1160 | raise RuntimeError(msg) 1161 | return total.value 1162 | 1163 | def sigs_remaining(self) -> int: 1164 | """Get the number of remaining signatures that can be made with the secret key.""" 1165 | if self._secret_key is None: 1166 | msg = "Secret key not initialised – call generate_keypair() first" 1167 | raise ValueError(msg) 1168 | remain = ct.c_uint64() 1169 | rc = native().OQS_SIG_STFL_sigs_remaining(self._sig, ct.byref(remain), self._secret_key) 1170 | if rc != OQS_SUCCESS: 1171 | msg = "Failed to get remaining signature count" 1172 | raise ValueError(msg) 1173 | return remain.value 1174 | 1175 | def export_used_keys(self) -> list[bytes]: 1176 | """Export the list of used keys.""" 1177 | return self._used_keys.copy() 1178 | 1179 | def __enter__(self) -> TStatefulSignature: 1180 | """Enter the context and return the StatefulSignature instance.""" 1181 | return self 1182 | 1183 | def __exit__( 1184 | self, 1185 | exc_type: type[BaseException] | None, 1186 | exc_val: BaseException | None, 1187 | exc_tb: TracebackType | None, 1188 | ) -> None: 1189 | """Free the resources when exiting the context.""" 1190 | self.free() 1191 | 1192 | def free(self) -> None: 1193 | """Free the native resources.""" 1194 | if self._store_cb and self._secret_key: 1195 | native().OQS_SIG_STFL_SECRET_KEY_SET_store_cb(self._secret_key, None, None) 1196 | self._store_cb = None 1197 | if self._secret_key and self._owns_secret: 1198 | native().OQS_SIG_STFL_SECRET_KEY_free(self._secret_key) 1199 | 1200 | 1201 | native().OQS_SIG_STFL_new.restype = ct.POINTER(StatefulSignature) 1202 | native().OQS_SIG_STFL_SECRET_KEY_new.restype = ct.c_void_p 1203 | native().OQS_SIG_STFL_SECRET_KEY_new.argtypes = [ct.c_char_p] 1204 | # Added precise signatures for (de)serialization to avoid ABI issues 1205 | native().OQS_SIG_STFL_SECRET_KEY_serialize.restype = ct.c_int 1206 | native().OQS_SIG_STFL_SECRET_KEY_serialize.argtypes = [ 1207 | ct.POINTER(ct.POINTER(ct.c_uint8)), 1208 | ct.POINTER(ct.c_size_t), 1209 | ct.c_void_p, 1210 | ] 1211 | native().OQS_SIG_STFL_SECRET_KEY_deserialize.restype = ct.c_int 1212 | native().OQS_SIG_STFL_SECRET_KEY_deserialize.argtypes = [ 1213 | ct.c_void_p, 1214 | ct.c_void_p, 1215 | ct.c_size_t, 1216 | ct.c_void_p, 1217 | ] 1218 | native().OQS_SIG_STFL_SECRET_KEY_SET_store_cb.argtypes = [ct.c_void_p, ct.c_void_p, ct.c_void_p] 1219 | native().OQS_SIG_STFL_keypair.argtypes = [ct.POINTER(StatefulSignature), ct.c_void_p, ct.c_void_p] 1220 | native().OQS_SIG_STFL_sign.argtypes = [ 1221 | ct.POINTER(StatefulSignature), 1222 | ct.c_void_p, 1223 | ct.POINTER(ct.c_size_t), 1224 | ct.c_void_p, 1225 | ct.c_size_t, 1226 | ct.c_void_p, 1227 | ] 1228 | native().OQS_SIG_STFL_verify.argtypes = [ 1229 | ct.POINTER(StatefulSignature), 1230 | ct.c_void_p, 1231 | ct.c_size_t, 1232 | ct.c_void_p, 1233 | ct.c_size_t, 1234 | ct.c_void_p, 1235 | ] 1236 | native().OQS_SIG_STFL_sigs_remaining.argtypes = [ 1237 | ct.POINTER(StatefulSignature), 1238 | ct.POINTER(ct.c_uint64), 1239 | ct.c_void_p, 1240 | ] 1241 | native().OQS_SIG_STFL_sigs_total.argtypes = [ 1242 | ct.POINTER(StatefulSignature), 1243 | ct.POINTER(ct.c_uint64), 1244 | ct.c_void_p, 1245 | ] 1246 | --------------------------------------------------------------------------------