Skip to content

Commit

Permalink
add: test harness mode
Browse files Browse the repository at this point in the history
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
JulioLoayzaM committed Sep 23, 2024
1 parent 1d9b0a7 commit eb8b73e
Show file tree
Hide file tree
Showing 39 changed files with 2,235 additions and 222 deletions.
27 changes: 27 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,33 @@ A few aspects to consider:
`utils/copy_guides.py` script. The name matches the one for the
documentation, namely the primitive name in upper-case.

### Adding a new harness

crypto-condor can tests functions exposed by a shared library, similar to a
fuzzing hook. To do so, the functions must follow the conventions described by
the harness API. Internally, this means adding a `test_lib` function to the
corresponding primitive. This function has a particular signature:

```python
def test_lib(ffi: cffi.FFI, lib, functions: list[str]) -> ResultsDict:
...
```

Where:

- `ffi` is the `cffi.FFI` instance.
- `lib` is the library dlopen'd with `ffi`.
- `functions` is a list of function names to test, which should correspond to
the primitive called.

Each primitive is in charge of calling `ffi.cdef()` to define the signature of
the exposed function, and to wrap it and test it. The
`crypto_condor.harness.test_harness` function is in charge of determining the
available functions, importing the corresponding primitives, and passing the
list of function that each primitive should test.

The documentation for this mode can be found in `docs/source/harness-api`.

### Documenting a new primitive

The documentation can be found under `docs/source`. There, it is divided in
Expand Down
57 changes: 55 additions & 2 deletions crypto_condor/cli/test.py
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)
105 changes: 105 additions & 0 deletions crypto_condor/harness.py
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
Loading

0 comments on commit eb8b73e

Please sign in to comment.