-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a new feature to test shared libraries, similar to wrappers. The shared libraries expose functions with specific names and signatures, which are described in the documentation. When CC loads the .so and finds these signatures, it calls the test_lib function of the corresponding primitive module. This function dlopens the lib, running the functions with CFFI. Note that the module and function are called dynamically, skipping the need to manually add imports. This means that support for this mode can be added to current and new primitives without having to change the code outside of the primitive module. Currently supports AES, SHA, SHAKE, Kyber, and Dilithium. This commit also updates the usage of the implementations of AES, Kyber, and Dilithium: instead of compiling an executable, compile them as shared libraries that get installed in the app data directory and use them with CFFI. This improves the performance and makes it easier to incorporate these implementations to the unit tests for the harnesses. For AES, the segment_size argument is removed. This should be inferred from the mode of operation used, and removing it allows us to remove an additional case to support. We now mark explicitely the version of Kyber and Dilithium used, which indicates which test vectors are available. A warning is included in the corresponding documentation pages.
- Loading branch information
1 parent
1d9b0a7
commit eb8b73e
Showing
39 changed files
with
2,235 additions
and
222 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,69 @@ | ||
"""Module for test commands.""" | ||
|
||
from pathlib import Path | ||
from typing import Annotated, Optional | ||
|
||
import typer | ||
|
||
from crypto_condor import harness | ||
from crypto_condor.cli import run, verify | ||
from crypto_condor.primitives.common import Console | ||
|
||
console = Console() | ||
|
||
_test_help = """Test an implementation of a cryptographic primitive. | ||
The `test` command provides two subcommands to test implementations: [red]wrapper[/] and | ||
[blue]output[/].""" | ||
The `test` command provides two subcommands to test implementations: [red]wrapper[/] and [blue]output[/]. | ||
""" # noqa: E501 | ||
|
||
app = typer.Typer(help=_test_help, no_args_is_help=True) | ||
|
||
|
||
app.add_typer(run.app, name="wrapper") | ||
app.add_typer(verify.app, name="output") | ||
|
||
_hook_help = """Test a shared library hook. | ||
Load a shared library and test exposed functions that match crypto-condor's API. | ||
When using --include, only functions included will be tested (allow list). | ||
When using --exclude, all functions are tested except those excluded (deny list). | ||
The --include and --exclude options are exclusive. | ||
""" | ||
|
||
|
||
@app.command("harness", help=_hook_help, no_args_is_help=True) | ||
def test_harness( | ||
lib: Annotated[ | ||
str, | ||
typer.Argument( | ||
help="The path to the shared library to test.", | ||
show_default=False, | ||
metavar="PATH", | ||
), | ||
], | ||
included: Annotated[ | ||
Optional[list[str]], typer.Option("--include", "-i", help="Include a function.") | ||
] = None, | ||
excluded: Annotated[ | ||
Optional[list[str]], typer.Option("--exclude", "-e", help="Exclude a function.") | ||
] = None, | ||
): | ||
"""Tests a shared library hook. | ||
Args: | ||
lib: Unvalidated path of the shared library to test. | ||
included: List of functions to include, allow-list style. | ||
excluded: List of functions to exclude, deny-list style. | ||
""" | ||
if included and excluded: | ||
console.print("Using both --include and --exclude is not allowed") | ||
raise typer.Exit(1) | ||
|
||
results = harness.test_harness(Path(lib), included, excluded) | ||
if console.process_results(results): | ||
raise typer.Exit(0) | ||
else: | ||
raise typer.Exit(1) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
"""Module to test a shared library harness. | ||
|cc| can test functions exposed by a shared library, on the condition that these | ||
functions follow the naming convention and signature described by the **shared library | ||
API**. To use this mode, this module provides the :func:`test_lib` function. | ||
""" | ||
|
||
import importlib | ||
import logging | ||
from collections import defaultdict | ||
from pathlib import Path | ||
from typing import Callable | ||
|
||
import _cffi_backend | ||
import cffi | ||
import lief | ||
|
||
from crypto_condor.primitives.common import ResultsDict | ||
|
||
# --------------------------- Module -------------------------------------------------- | ||
logger = logging.getLogger(__name__) | ||
|
||
|
||
# --------------------------- Functions ----------------------------------------------- | ||
def test_harness( | ||
harness: Path, included: list[str] | None = None, excluded: list[str] | None = None | ||
) -> ResultsDict: | ||
"""Tests a shared library harness. | ||
It loads the library and searches for functions that follow the naming convention. | ||
If there are any, they are passed to the corresponding primitive module's | ||
``test_lib`` function. | ||
Args: | ||
harness: The path to the shared library harness. | ||
included: List of included functions, allow-list style. | ||
excluded: List of excluded functions, deny-list style. | ||
Returns: | ||
A dictionary of all results returned by the different primitives called. | ||
Example: | ||
The simplest usage is to pass the path to the shared library. | ||
>>> from crypto_condor import shared_library | ||
>>> from pathlib import Path | ||
>>> my_lib = Path("libtest.so") | ||
>>> results = shared_library.test_lib(my_lib) | ||
This tests *all* CC functions found. Sometimes it may be useful to limit which | ||
functions are tested, e.g. when testing one primitive with several parameters. | ||
We can use the ``included`` and ``excluded`` arguments: | ||
>>> # To only test CC_SHA_256_digest | ||
>>> results = shared_library.test_lib(my_lib, included=["CC_SHA_256_digest"]) | ||
>>> # To test all functions *except* CC_SHA_256_digest | ||
>>> results = shared_library.test_lib(my_lib, excluded=["CC_SHA_256_digest"]) | ||
""" | ||
if not harness.is_file(): | ||
raise FileNotFoundError(f"No shared library named {str(harness)} found") | ||
|
||
if included is None: | ||
included = [] | ||
if excluded is None: | ||
excluded = [] | ||
|
||
lief_lib = lief.parse(harness.read_bytes()) | ||
if lief_lib is None: | ||
raise ValueError("Could not parse the harness with LIEF") | ||
|
||
primitives: dict[str, list[str]] = defaultdict(list) | ||
|
||
for funcname in set([func.name for func in lief_lib.exported_functions]): | ||
if isinstance(funcname, bytes): | ||
logger.debug("Function name is in bytes, skipped") | ||
continue | ||
match funcname.split("_"): | ||
case ["CC", primitive, *_]: | ||
# TODO: test if primitive is supported and supports this mode. | ||
if funcname in excluded or (included and funcname not in included): | ||
logger.info("Excluded %s", funcname) | ||
continue | ||
primitives[primitive].append(funcname) | ||
logger.debug("Found CC function %s", funcname) | ||
case _: | ||
logger.debug("Omitted function %s", funcname) | ||
continue | ||
|
||
logger.debug("dlopen %s", str(harness)) | ||
ffi = cffi.FFI() | ||
lib = ffi.dlopen(str(harness.absolute())) | ||
|
||
results = ResultsDict() | ||
|
||
# Dynamically determine the module to import, call its test_lib function. | ||
test: Callable[[cffi.FFI, _cffi_backend.Lib, list[str]], ResultsDict] | ||
for primitive, functions in primitives.items(): | ||
module = importlib.import_module(f"crypto_condor.primitives.{primitive}") | ||
test = module.test_lib | ||
try: | ||
results |= test(ffi, lib, functions) | ||
except ValueError as error: | ||
logging.error("Error running CC_%s functions: %s", primitive, str(error)) | ||
|
||
return results |
Oops, something went wrong.