This document explains the internal structure of lib1305, and explains how to add new instruction sets and new implementations. The lib1305 infrastructure is adapted from the infrastructure used in lib25519, libmceliece, and libntruprime.
Primitives
There are two levels of primitives in lib1305.
The top-level primitive is crypto_onetimeauth/poly1305
, which provides
two functions: crypto_onetimeauth_poly1305
and
crypto_onetimeauth_poly1305_verify
. These are provided by the
stable API as poly1305
and poly1305_verify
respectively.
Internally, implementations of crypto_onetimeauth_poly1305_verify
rely on the bottom-level primitive crypto_verify/16
, which provides
one function: crypto_verify_16(s,t)
returns 0 when the 16-byte arrays
s
and t
are equal, otherwise -1
. This function is not currently
provided by the stable API.
As in SUPERCOP and NaCl, array lengths intentionally use long long
,
not size_t
. In lib1305, as in lib25519, libmceliece, and libntruprime,
array lengths are signed.
Implementations
A single primitive can have multiple implementations. Each implementation is in its own subdirectory. The implementations are required to have exactly the same input-output behavior, and to some extent this is tested, although it is not yet formally verified.
Different implementations typically offer different tradeoffs between
portability, simplicity, and efficiency. For example,
crypto_onetimeauth/poly1305/ref
is portable;
crypto_onetimeauth/poly1305/amd64-mxaa-g24
is faster and less portable.
Each unportable implementation has an architectures
file. Each line in
this file identifies a CPU instruction set (and ABI) where the
implementation works. For example,
crypto_onetimeauth/poly1305/amd64-mxaa-g24/architectures
has two lines
amd64 bmi2
x86 bmi2
meaning that the implementation works on CPUs that have the Intel/AMD
64-bit or 32-bit instruction sets with the BMI2 instruction-set
extension. The top-level compilers
directory shows (among other
things) the allowed instruction-set names such as bmi2
.
At run time, lib1305 checks the CPU where it is running, and selects
an implementation where architectures
is compatible with that CPU.
Each primitive makes its own selection once per program startup, using
the compiler's ifunc
mechanism (or constructor
on platforms that do
not support ifunc
). This type of run-time selection means, for
example, that an amd64
CPU without BMI2 can share binaries with an
amd64
CPU with BMI2. However, correctness requires instruction sets to
be preserved by migration across cores via the OS kernel, VM migration,
etc.
The compiler has a target
mechanism that makes an ifunc
selection
based on CPU architectures. Instead of using the target
mechanism,
lib1305 uses a more sophisticated mechanism that also accounts for
benchmarks collected in advance of compilation.
Compilers
lib1305 tries different C compilers for each implementation. For
example, compilers/default
lists the following compilers:
clang -Wall -fPIC -fwrapv -Qunused-arguments -O2
gcc -Wall -fPIC -fwrapv -O3
Sometimes gcc
produces better code, and sometimes clang
produces
better code.
As another example, compilers/amd64+bmi1+bmi2
lists the following compilers:
clang -Wall -fPIC -fwrapv -Qunused-arguments -O2 -mbmi -mbmi2 -mtune=skylake
gcc -Wall -fPIC -fwrapv -O2 -mbmi -mbmi2 -mtune=skylake
The -mbmi2
option tells these compilers that they are free to use the
BMI2 instruction-set extension.
Code compiled using the compilers in
compilers/amd64+bmi1+bmi2
will be considered at run time by the lib1305 selection mechanism if
the supports()
function in
compilers/amd64+bmi1+bmi2.c
returns nonzero.
This function checks whether the run-time CPU supports BMI2.
If some compilers fail (for example, clang is not installed, or the compiler version is too old to support the compiler options used in lib1305), the lib1305 compilation process will try its best to produce a working library using the remaining compilers, even if this means lower performance.
Trimming
By default, to reduce size of the compiled library, the lib1305 compilation process trims the library down to the implementations that are selected by lib1305's selection mechanism.
For example, if the selection mechanism decides that CPUs with BMI2
should use poly1305/amd64-mxaa-g24
with clang
and that other CPUs should
use poly1305/ref
with gcc
, then trimming will remove
poly1305/amd64-mxaa-g24
compiled with gcc
and poly1305/ref
compiled with clang
.
This trimming is handled at link time rather than compile time to increase the chance that, even if some implementations are broken by compiler "upgrades", the library will continue to build successfully.
To avoid this trimming, pass the --no-trim
option to ./configure
.
All implementations that compile are then included in the library,
tested by lib1305-test
, and measured by lib1305-speed
. You'll want
to avoid trimming if you're adding new instruction sets or new
implementations (see below), so that you can run tests and benchmarks of
code that isn't selected yet.
How to recompile after changes
If you make changes under crypto_*
, the fully supported recompilation
mechanism is to run ./configure
again to clean and repopulate the
build directory, and then run make
again to recompile everything.
This can be on the scale of seconds if you have enough cores, but maybe you're developing on a slower machine. Three options are currently available to accelerate the edit-compile cycle:
-
There is an experimental
--no-clean
option to./configure
that, for some simple types of changes, can produce a successful build without cleaning. -
Running
make
without./configure
can work for some particularly simple types of changes. However, not all dependencies are currently expressed inMakefile
, and some types of dependencies that./configure
understands would be difficult to express in theMakefile
language. -
You can disable the implementations you're not using by setting sticky bits on the source directories for those implementations: e.g.,
chmod +t crypto_*/*/amd64-mxaa-g32
.
Make sure to reenable all implementations and do a full clean build if
you're collecting data to add to the source benchmarks
directory.
How to add new instruction sets
Adding another file compilers/amd64+foo
, along with a supports()
implementation in compilers/amd64+foo.c
, will support a new
instruction set. Do not assume that the new foo
instruction set
implies support for older instruction sets (the idea of "levels" of
instruction sets); instead make sure to include the older instruction
sets in +
tags, as illustrated by
compilers/amd64+bmi1+bmi2
.
In the compiler options, always make sure to include -fPIC
to support
shared libraries, and -fwrapv
to switch to a slightly less dangerous
version of C.
The foo
tags don't have to be instruction sets. For example, if a CPU
has the same instruction set but wants different optimizations because
of differences in instruction timings, you can make a tag for those
optimizations, using, e.g., CPU IDs or benchmarks in the corresponding
supports()
function to decide whether to enable those optimizations.
Benchmarks tend to be more future-proof than a list of CPU IDs, but the
time taken for benchmarks at program startup has to be weighed against
the subsequent speedup from the resulting optimizations.
To see how well lib1305 performs with the new compilers, run
lib1305-speed
on the target machine and look for the foo
lines in
the output. If the new performance is better than the performance shown
on the selected
lines:
-
Copy the
lib1305-speed
output into a file on thebenchmarks
directory, typically named after the hostname of the target machine. -
Run
./prioritize
in the top-level directory to createpriority
files. These files tell lib1305 which implementations to select for any given architecture. -
Reconfigure (again with
--no-trim
), recompile, rerunlib1305-test
, and rerunlib1305-speed
to check that theselected
lines now use thefoo
compiler.
If the foo
implementation is outperformed by other implementations,
then these steps don't help except for documenting this fact. The same
implementation might turn out to be useful for subsequent foo
CPUs.
How to add new implementations
Taking full advantage of the foo
instruction set usually requires
writing new implementations. Sometimes there are also ideas for taking
better advantage of existing instruction sets.
Structurally, adding a new implementation of a primitive is a simple
matter of adding a new subdirectory with the code for that
implementation. Most of the work is optimizing the use of foo
intrinsics in .c
files or foo
instructions in .S
files. Make sure
to include an architectures
file saying, e.g., amd64 bmi2 foo
.
Names of implementation directories can use letters, digits, dashes, and underscores. Do not use two implementation names that are the same when dashes and underscores are removed.
All .c
and .S
files in the implementation directory are compiled and
linked. There is no need to edit a separate list of these files. You can
also use .h
files via the C preprocessor.
If an implementation is actually more restrictive than indicated in
architectures
then the resulting compiled library will fail on some
machines (although perhaps that implementation will not be used by
default). Putting unnecessary restrictions into architectures
will not
create such failures, but can unnecessarily limit performance.
Some, but not all, mistakes in architectures
will produce warnings
from the checkinsns
script that runs automatically when lib1305 is
compiled. Running the lib1305-test
program tries all implementations,
but only on the CPU where lib1305-test
is being run;
also, lib1305-test
does not guarantee code coverage.
amd64
implies little-endian, and implies architectural support for
unaligned loads and stores. Beware, however, that the Intel/AMD
vectorized load
/store
intrinsics (and the underlying movdqa
instruction) require alignment; if in doubt, use loadu
/storeu
(and
movdqu
). The lib1305-test
program checks unaligned inputs and
outputs, but can miss issues with unaligned stack variables.
To test your implementation, compile everything, check for compiler
warnings and errors, run lib1305-test
(or just lib1305-test verify
to
test a crypto_verify
implementation), and check for a line saying all
tests succeeded
. To use AddressSanitizer (for catching, at run time,
buffer overflows in C code), add -fsanitize=address
to the gcc
and
clang
lines in compilers/*
; you may also have to add return;
at
the beginning of the limits()
function in command/limits.inc
.
To see the performance of your implementation, run lib1305-speed
. If
the new performance is better than the performance shown on the
selected
lines, follow the same steps as for a new instruction set:
copy the lib1305-speed
output into a file on the benchmarks
directory; run ./prioritize
in the top-level directory to create
priority
files; reconfigure (again with --no-trim
); recompile; rerun
lib1305-test
; rerun lib1305-speed
; check that the selected
lines
now use the new implementation.
How to handle namespacing
As in SUPERCOP and NaCl, to call crypto_verify_16()
, you have to
include crypto_verify_16.h
; but to write an implementation of
crypto_verify_16()
, you have to instead include crypto_verify.h
and
define crypto_verify
. Similar comments apply to other primitives.
The function name that's actually linked might end up as, e.g.,
lib1305_verify_16_ref_C2
where ref
indicates the
implementation and C2
indicates the compiler. Don't try to build this
name into your implementation.
If you have another global symbol x
(for example, a non-static
function in a .c
file, or a non-static
variable outside functions in
a .c
file), you have to replace it with CRYPTO_NAMESPACE(x)
, for
example with #define x CRYPTO_NAMESPACE(x)
.
For global symbols in .S
files and shared-*.c
files, use
CRYPTO_SHARED_NAMESPACE
instead of CRYPTO_NAMESPACE
. For .S
files
that define both x
and _x
to handle platforms where x
in C is _x
in assembly, use CRYPTO_SHARED_NAMESPACE(x)
and
_CRYPTO_SHARED_NAMESPACE(x)
; CRYPTO_SHARED_NAMESPACE(_x)
is not
sufficient.
lib1305 includes a mechanism to recognize files that are copied across implementations (possibly of different primitives) and to unify those into a file compiled only once, reducing the overall size of the compiled library and possibly improving cache utilization. To request this mechanism, include a line
// linker define x
for any global
symbol x
defined in the file, and a line
// linker use x
for any
global symbol x
used in the file from the same implementation (not
crypto_*
subroutines that you're calling). This
mechanism tries very hard, perhaps too hard, to avoid improperly
unifying files: for example, even a slight difference in a .h
file
included by a file defining a used symbol will disable the mechanism.
Typical namespacing mistakes will produce either linker failures or
warnings from the checknamespace
script that runs automatically when
lib1305 is compiled.
Version: This is version 2025.04.06 of the "Internals" web page.