A C library for Post-Quantum Cryptographic (PQC) signature algorithms for Bitcoin. It implements two NIST-standardized PQC signature algorithms, plus BIP 340 Schnorr:
- ML-DSA-44 (FIPS 204, formerly CRYSTALS-Dilithium): a structured lattice-based digital signature scheme.
- SLH-DSA-SHA2-128s (FIPS 205, formerly SPHINCS+): a stateless hash-based signature scheme with minimal security assumptions. Uses SHA-256, aligning with Bitcoin's native hash primitive.
Both PQC algorithms are FIPS-certified, which should help with future native hardware support.
Post-quantum signatures are a separate effort from BIP 360 (Pay-to-Merkle-Root / P2MR). BIP 360 defines a consensus output type that removes Taproot's quantum-vulnerable key-path spend; it does not introduce PQC signature schemes. This library implements PQC (and BIP 340) signature primitives for applications and for possible future post-quantum signature proposals. It does not implement BIP 360 rules, addresses, or script validation.
- Clean, unified C API for all three signature algorithms (secp256k1 Schnorr, ML-DSA-44, SLH-DSA-SHA2-128s)
- User-provided entropy (bring your own randomness)
- Key generation, signing, and verification functions
- Minimal dependencies
| Algorithm | Public Key Size | Secret Key Size | Signature Size | Security Level |
|---|---|---|---|---|
| secp256k1 Schnorr | 32 bytes | 32 bytes | 64 bytes | Classical |
| ML-DSA-44 | 1,312 bytes | 2,560 bytes | 2,420 bytes | NIST Level 2 |
| SLH-DSA-SHA2-128s | 32 bytes | 64 bytes | 7,856 bytes | NIST Level 1 |
- This library does not provide its own random number generation. It is essential that the user provide entropy from a cryptographically secure source.
- The production library target does not read operating system entropy or link a
randombytes()fallback. Seed and optional-randomness inputs are passed explicitly to the vendored reference implementations. - Random data is required for key generation, but not for signing. All signatures are deterministic from the message and secret key (see docs/user_provided_entropy.md).
- The implementations are based on reference code from the NIST PQC standardization process and are not production-hardened.
- Care should be taken to securely manage secret keys in applications.
The API requires a minimum of 128 bytes of entropy for PQC key generation. secp256k1 Schnorr key generation requires 32 bytes:
if (!keypair || !random_data)
return BITCOIN_PQC_ERROR_BAD_ARG;
if (algorithm == BITCOIN_PQC_SECP256K1_SCHNORR) {
if (random_data_size < 32)
return BITCOIN_PQC_ERROR_BAD_ARG;
} else if (random_data_size < 128) {
return BITCOIN_PQC_ERROR_BAD_ARG;
}The actual entropy consumed by each algorithm is less than this minimum:
| Algorithm | Bytes consumed | Source in spec |
|---|---|---|
| secp256k1 Schnorr | 32 bytes | BIP 340 secret scalar supplied directly to libsecp256k1 |
| ML-DSA-44 | 32 bytes | FIPS 204, one SEEDBYTES-wide explicit seed fed into SHAKE-256 expansion |
| SLH-DSA-SHA2-128s | 48 bytes | FIPS 205, three explicit n-byte values (SK.seed ‖ SK.prf ‖ PK.seed), n = 16 |
Bytes beyond these amounts are accepted but silently ignored by the underlying reference implementations. A caller reading the 128-byte PQC requirement should not infer that all 128 bytes contribute to key material. Only the first 32 or 48 bytes, depending on the algorithm, do.
The complete RNG inventory, including the vendored standalone test sources that are excluded from the production library, is documented in docs/user_provided_entropy.md.
The 128-byte PQC floor is a deliberate policy choice rather than a per-algorithm technical requirement, motivated by:
- Uniform API — callers do not need to know per-algorithm seed sizes.
- Quality signal — a caller that can supply 128 bytes from a secure source is less likely to be misusing a low-entropy value padded to the minimum.
- Headroom — a future algorithm in the same API could require more entropy without a breaking change to the interface contract.
| Algorithm | Spec | Role |
|---|---|---|
| secp256k1 Schnorr | BIP 340 | Classical tapscript OP_CHECKSIG-style spends |
| ML-DSA-44 | FIPS 204 | Lattice-based PQC signatures |
| SLH-DSA-SHA2-128s | FIPS 205 | Hash-based PQC signatures (SHA-2) |
This project is licensed under the MIT License — see the LICENSE file for details.
Language bindings for libbitcoinpqc are maintained separately at:
https://github.com/cryptoquick/libbitcoinpqc-bindings
Cryptographic dependencies included in this project:
- https://github.com/sphincs/sphincsplus -
7ec789ace6874d875f4bb84cb61b81155398167e - https://github.com/pq-crystals/dilithium -
444cdcc84eb36b66fe27b3a2529ee48f6d8150c2
SLH-DSA was switched from SHAKE-128s to SHA2-128s. Update C symbol names as follows:
| Old (SHAKE-128s) | New (SHA2-128s) |
|---|---|
BITCOIN_PQC_SLH_DSA_SHAKE_128S |
BITCOIN_PQC_SLH_DSA_SHA2_128S |
SLH_DSA_SHAKE_128S_PUBLIC_KEY_SIZE |
SLH_DSA_SHA2_128S_PUBLIC_KEY_SIZE |
SLH_DSA_SHAKE_128S_SECRET_KEY_SIZE |
SLH_DSA_SHA2_128S_SECRET_KEY_SIZE |
SLH_DSA_SHAKE_128S_SIGNATURE_SIZE |
SLH_DSA_SHA2_128S_SIGNATURE_SIZE |
slh_dsa_shake_128s_keygen() |
slh_dsa_sha2_128s_keygen() |
slh_dsa_shake_128s_sign() |
slh_dsa_sha2_128s_sign() |
slh_dsa_shake_128s_verify() |
slh_dsa_sha2_128s_verify() |
void slh_dsa_derandomize(...) |
int slh_dsa_derandomize(...) (returns 0 on success, non-zero on bad args) |
The enum wire value BITCOIN_PQC_SLH_DSA_SHA2_128S = 2 is unchanged. Key and signature buffer sizes (32/64/7856 bytes) are unchanged.
Re-keying required: keys and signatures from SHAKE-128s are cryptographically incompatible with SHA2-128s. After upgrading, generate new key pairs — renaming symbols alone is not sufficient.
A flake provides a hermetic toolchain, pinned libsecp256k1 (v0.7.1), and CI-equivalent checks. With just:
just # list recipes
just test # full gate (what CI runs): flake checks + ctest + cmake path
just build # nix build → ./result
just hermetic # flake checks only (offline-friendly once cached)
just shell # nix develop
just cmake-test # host CMake configure/build/ctest/smokejust test runs:
nix flake check— package build + ctest, alejandra fmt, naming scrub, install smoke, secp pin syncnix build— install layout under./result- Worktree naming scrub
- Host CMake build + ctest + link smoke (via
nix developif cmake is not onPATH)
Equivalent raw Nix:
nix develop # CMake, Ninja, just, clangd, etc.
nix build # build + install static/shared libs + headers
nix flake check # all flake checks (including ctest)With direnv, use flake in .envrc loads the dev shell on cd.
- CMake 3.14 or higher
- C99 compiler
git clone https://github.com/cryptoquick/libbitcoinpqc.git
cd libbitcoinpqc
chmod +x build.sh
./build.shThis produces:
build/lib/libbitcoinpqc.a— static librarybuild/lib/libbitcoinpqc.so— shared library (bitcoinpqc_sharedCMake target, installed aslibbitcoinpqc.so)
Tests are enabled by default (BUILD_TESTS=ON):
cd build
ctest -VRegistered tests cover:
| Test | Coverage |
|---|---|
secp256k1_schnorr_e2e |
BIP 340 golden vectors, sign/verify, arg rejection |
ml_dsa_44_e2e |
Keygen, sign/verify, tamper rejection |
slh_dsa_sha2_api |
Golden vectors, determinism, verify/reject paths, short entropy |
rng_isolation |
Public PQC paths do not call the legacy randombytes() hook |
The slh_dsa_sha2_api suite specifically checks:
- Golden 32-byte public key and 7856-byte signature vectors
- Signature determinism (two signs produce identical bytes)
- Valid verify, tampered-signature rejection, wrong-message rejection
- NULL
secret_keyrejection at sign (BITCOIN_PQC_ERROR_BAD_ARG) - Short signature rejection and short public-key rejection at verify
- 127-byte entropy rejection at keygen
Regenerate SLH-DSA golden vectors after intentional crypto changes:
gcc -o /tmp/gen_slh_dsa_sha2_vectors scripts/gen_slh_dsa_sha2_vectors.c \
-Iinclude -Itests build/lib/libbitcoinpqc.a -lm
/tmp/gen_slh_dsa_sha2_vectorsTo also build the entropy demo example:
cmake -S . -B build -DBUILD_EXAMPLES=ON
cmake --build build
./build/bin/entropy_demo < /path/to/128_bytes_entropy.bin#include <libbitcoinpqc/bitcoinpqc.h>
// Generate random data (from a secure source in production)
uint8_t random_data[256];
// Fill random_data with entropy...
// Generate a key pair
bitcoin_pqc_keypair_t keypair;
bitcoin_pqc_keygen(BITCOIN_PQC_ML_DSA_44, &keypair, random_data, sizeof(random_data));
// Sign a message
const uint8_t message[] = "Message to sign";
bitcoin_pqc_signature_t signature;
bitcoin_pqc_sign(BITCOIN_PQC_ML_DSA_44, keypair.secret_key, keypair.secret_key_size,
message, sizeof(message) - 1, &signature);
// Verify the signature
bitcoin_pqc_error_t result = bitcoin_pqc_verify(BITCOIN_PQC_ML_DSA_44,
keypair.public_key, keypair.public_key_size,
message, sizeof(message) - 1,
signature.signature, signature.signature_size);
// Clean up resources
bitcoin_pqc_signature_free(&signature);
bitcoin_pqc_keypair_free(&keypair);- The original NIST PQC competition teams for their reference implementations
- The NIST PQC standardization process for advancing post-quantum cryptography