Skip to content

About

Post-Quantum Cryptography for use with Bitcoin according to BIP-360

Resources

Stars

31 stars

Watchers

4 watching

Forks

Repository files navigation

libbitcoinpqc

A C library for Post-Quantum Cryptographic (PQC) signature algorithms for Bitcoin. It implements two NIST-standardized PQC signature algorithms, plus BIP 340 Schnorr:

  1. ML-DSA-44 (FIPS 204, formerly CRYSTALS-Dilithium): a structured lattice-based digital signature scheme.
  2. 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.

Features

  • 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

Key Characteristics

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

Security Notes

  • 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.

Entropy length requirements

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.

Algorithms

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)

License

This project is licensed under the MIT License — see the LICENSE file for details.

Language Bindings

Language bindings for libbitcoinpqc are maintained separately at:

https://github.com/cryptoquick/libbitcoinpqc-bindings

Dependencies

Cryptographic dependencies included in this project:

Migrating from SHAKE-128s

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.

Building the Library

Nix (recommended)

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/smoke

just test runs:

  1. nix flake check — package build + ctest, alejandra fmt, naming scrub, install smoke, secp pin sync
  2. nix build — install layout under ./result
  3. Worktree naming scrub
  4. Host CMake build + ctest + link smoke (via nix develop if cmake is not on PATH)

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.

Prerequisites (without Nix)

  • CMake 3.14 or higher
  • C99 compiler

Building

git clone https://github.com/cryptoquick/libbitcoinpqc.git
cd libbitcoinpqc
chmod +x build.sh
./build.sh

This produces:

  • build/lib/libbitcoinpqc.a — static library
  • build/lib/libbitcoinpqc.so — shared library (bitcoinpqc_shared CMake target, installed as libbitcoinpqc.so)

Running Tests

Tests are enabled by default (BUILD_TESTS=ON):

cd build
ctest -V

Registered 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_key rejection 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_vectors

To 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

C API Usage

#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);

Acknowledgments

  • The original NIST PQC competition teams for their reference implementations
  • The NIST PQC standardization process for advancing post-quantum cryptography

About

Post-Quantum Cryptography for use with Bitcoin according to BIP-360

Resources

Stars

31 stars

Watchers

4 watching

Forks

Releases

Packages

Used by

Contributors

Languages