qbit-libbitcoinpqc v0.3.2
Release notes
Patch release of qbit-libbitcoinpqc from main. It reports inconsistent secret keys and allocation failures from the C API with specific error codes, and extends the unoptimized-build stack burn to cover Clang. Keys, signatures and verification results are byte-identical to 0.3.1 and 0.3.0.
Changes
- Signing reports an inconsistent secret key as
BAD_KEY(#10).bitcoin_pqc_signandbitcoin_pqc_sign_with_statsreturnBITCOIN_PQC_ERROR_BAD_KEY(-2) for a secret key that failsbitcoin_pqc_secret_key_validate, instead ofBITCOIN_PQC_ERROR_BAD_SIGNATURE(-3). This includes a corrupted SK_SEED, PUB_SEED or root. The Rustsignandsign_with_statsreturnPqcError::BadKeyinstead ofPqcError::BadSignature. The facadeslh_dsa_signreturnsSLH_DSA_SIGN_ERROR_BAD_KEY(-101) instead of -1.BAD_SIGNATUREfrom signing now means only that the signing operation itself failed. - Allocation failures are reported as
OUT_OF_MEMORY(#10). The new append-onlyBITCOIN_PQC_ERROR_OUT_OF_MEMORY(-6) replacesBAD_ARGfor a failed key or signature output buffer, andBAD_KEYfor a failed keygen seed-derivation buffer, which grows with the entropy size. The output struct stays empty.slh_dsa_keygenreturnsSLH_DSA_KEYGEN_ERROR_OUT_OF_MEMORY(-102). In Rust, -6 reaches callers asPqcError::Other(-6), displayed asUnexpected error code: -6. A dedicated variant is planned for 0.4.0. slh_dsa_signcleans its outputs on rejected arguments too (#10). Form == NULLwithmlen > 0, or a NULLsig,siglenorsk, it zeroessigand sets*siglen = 0for whichever pointer is non-NULL. It already did this on every other failure.- Larger stack burn in unoptimized builds (N1 from the 0.3.1 review). Unoptimized GCC/Clang builds burn 288 KiB, up from 208 KiB. Clang
-O0keygen, validation and signing use about 242 KiB, so about 34 KiB of their frames stayed unburned in 0.3.1. Optimized builds are unchanged.slh_dsa_stack_scrub_testnow fails when a call runs deeper than the burn. - Test and build changes with no effect on the library:
- #4: backend-selection observers, compiled only with
BITCOINPQC_ENABLE_TEST_HELPERS, and the differential test reporting requested and effective SHA backends - #13:
allocator_may_return_nullin the ASan smoke - #14:
build.rsreruns whenCC,CFLAGSorLDFLAGSchange; UBSan CI steps
- #4: backend-selection observers, compiled only with
- Version metadata is
0.3.2.docs/vendored-crypto-provenance.mdlists the 0.3.2 changes.
Compatibility
- Rust API unchanged from 0.3.0 and 0.3.1.
PqcErrorkeeps exactly the 0.3.0 variant set; a guard test fails to compile if a variant is added.cargo semver-checksreports no semver update required against v0.3.0 and v0.3.1. - C API and ABI: additive only.
bitcoin_pqc_error_tgainsBITCOIN_PQC_ERROR_OUT_OF_MEMORY = -6; existing values are unchanged.slh_dsa.hgainsSLH_DSA_SIGN_ERROR_BAD_KEYandSLH_DSA_KEYGEN_ERROR_OUT_OF_MEMORY.- No function is added, removed or changed.
- The production static library exports the same symbols as 0.3.1.
- Error-code changes for callers that branch on codes. Callers that treat
BAD_SIGNATURE/BadSignaturefrom signing as "corrupt key" should also handleBAD_KEY/BadKey. Callers that check allocation failure asBAD_ARG/BadArgument(orBAD_KEYfrom keygen) should also handle -6 orOther(-6). A Cswitchoverbitcoin_pqc_error_twith nodefaultlabel now gets a-Wswitchwarning for the new value. - Outputs unchanged. The golden vectors pass unmodified. A public-API corpus produces byte-identical output against v0.3.0, v0.3.1 and 0.3.2, with clang and gcc. It covers 8 keypairs, 96 signatures with sign stats, and 2,968 verify results over bit-flipped, truncated, extended, wrong-key, wrong-message and corrupted-key inputs.
Stack Requirements
Each secret-key call overwrites BITCOIN_PQC_BURN_STACK_BYTES of stack below its caller before returning:
- 16 KiB in optimized native builds (
__OPTIMIZE__), unchanged. Measured optimized keygen and sign use at most 13.1 KiB below the entry point (GCC 15 and Clang 21,-O1to-O3,-Og,-Os). - 288 KiB in unoptimized GCC/Clang builds (Cargo
devprofile, CMakeDebug), up from 208 KiB. Unoptimized calls use about 200 KiB with GCC and 242 KiB with Clang. A call needs about 289 KiB of free stack. - 4 KiB under Emscripten, unchanged.
- Sanitizer-instrumented unoptimized builds can run deeper than the burn: Clang
-O0UBSan uses about 390 KiB. DefineBITCOIN_PQC_BURN_STACK_BYTESat build time to cover a measured depth.
Verification does not burn.
Performance
Measured with clang -O3 (CMake Release) on a shared x86-64 Linux host: 6 runs of 20 iterations each, 3 with v0.3.1 first and 3 with 0.3.2 first. Each figure is a per-run median.
| Call | 0.3.1 | 0.3.2 |
|---|---|---|
| Sign | 144.2–148.9 ms | 145.9–149.6 ms |
| Keygen | 10.25–10.40 ms | 10.41–10.96 ms |
| Secret-key validation | 10.22–10.45 ms | 10.39–10.55 ms |
| Verify | 1.85–1.90 ms | 1.84–1.95 ms |
No crypto code changes. Of the 26 library objects, only src/bitcoinpqc.c, src/slh_dsa/keygen.c and src/slh_dsa/sign.c differ in machine code, all in #10's error-code handling. Every vendored SPHINCS+ object, validate.c and verify.c have byte-identical .text, so the 1–5% keygen difference is host noise and code layout, not added work. Optimized builds burn the same 16 KiB as 0.3.1. Unoptimized builds burn 80 KiB more per secret-key call.
Security And Signing Posture
The wipes and the stack burn reduce accidental disclosure of secret-key material through released stack memory. They are not side-channel hardening.
Signing remains deterministic, and deterministic signing is not side-channel hardening. Randomized or hedged signing is not available. Production signing should be limited to trusted local wallet processes, dedicated signing hosts, or comparably isolated key-custody infrastructure. This release does not claim signing support on hostile shared hardware, or resistance to physical side-channel attackers or fault injection. See docs/side-channel-policy.md.
Downstream Notes
- Importers on 0.3.1 get the error-code changes above. Re-run any test that asserts sign or keygen error codes.
- Debug builds of the library need up to 80 KiB more stack per secret-key call than 0.3.1 (about 47 KiB more with Clang, whose calls already used 242 KiB).
- Any
contrib/testgen/*/Cargo.lockthat pinsqbit-libbitcoinpqc0.3.1 needs the 0.3.2 bump.
Validation
See the evidence table and the independent review in the release PR (Qbit-Org/qbit-libbitcoinpqc#17). Upstream CI passed on the release PR. The release commit 1965d0da1e has the same tree as the reviewed head c015787b47. Attached: a CycloneDX SBOM and the cargo-semver-checks log. Review follow-ups are tracked in Qbit-Org/qbit-libbitcoinpqc#18.