Licensing and interoperability defects — all fixed upstream
The most serious finding is not cryptographic. An MIT-licensed SDK linked a GPL-3.0-only crate into every dependent application, whether or not that application used post-quantum code at all. Alongside it, the quantum-secure signer implemented the pre-standard round-3 CRYSTALS submission rather than FIPS 204, so the keys and signatures it produced could not be verified by any standards-compliant implementation. Two memory-safety issues in the Rust FFI and one key-recoverability gap were found while working through the same code. Everything here was fixed and merged upstream.
1High3Medium1Low
Status
All findings resolved
Every finding in this review was fixed upstream by the maintainers and is available in a released version.
GPL-3.0-only crate linked into every dependent application
`packages/ndk/rust` depended on `crystals-dilithium`, which is GPL-3.0-only, while NDK ships under MIT. The crate was not optional at build time: `build.dart` compiled it unconditionally, and the ordinary Schnorr verifier lived in the same `src/lib.rs` as the quantum-secure signer, so an application that never touched post-quantum code still linked the GPL-3.0-only object code. Statically linking GPL-3.0-only code makes the combined binary a derivative work, obliging every downstream application to distribute under GPL-3.0. That is a hard problem for closed-source consumers, and GPL-3.0 is also widely held to be incompatible with App Store distribution terms.
Recommendation Replace the dependency with `fips204`, which is MIT OR Apache-2.0 and imposes none of that. Merged upstream in pull request #712.
GPL-3.0-only crate linked into every dependent application
`packages/ndk/rust` depended on `crystals-dilithium`, which is GPL-3.0-only, while NDK ships under MIT. The crate was not optional at build time: `build.dart` compiled it unconditionally, and the ordinary Schnorr verifier lived in the same `src/lib.rs` as the quantum-secure signer, so an application that never touched post-quantum code still linked the GPL-3.0-only object code. Statically linking GPL-3.0-only code makes the combined binary a derivative work, obliging every downstream application to distribute under GPL-3.0. That is a hard problem for closed-source consumers, and GPL-3.0 is also widely held to be incompatible with App Store distribution terms.
Recommendation Replace the dependency with `fips204`, which is MIT OR Apache-2.0 and imposes none of that. Merged upstream in pull request #712.
Pre-standard Dilithium shipped as the quantum-secure signer
`crystals-dilithium` implements the round-3 CRYSTALS submission. NIST changed the algorithm during standardisation, so Dilithium and ML-DSA are different schemes with different wire formats. Keys and signatures produced by the shipped code could therefore be verified only by the same code. No FIPS 204 implementation could read them, and vice versa. For a signature scheme whose whole purpose is letting somebody else check your work, that is a defect rather than a preference.
Recommendation Move to FIPS 204 ML-DSA and pin a cross-implementation test vector. The fix includes one that checks an ML-DSA-87 public key byte for byte against an independent library from the same derived seed. Merged upstream in pull request #712.
`write_buffer` leaked a `Vec` recording only its length, while `qs_free_buffer` reconstructed it with `Vec::from_raw_parts(data, len, len)`. That is undefined behaviour whenever capacity exceeds length. It held for every value passed at the time, so this was a latent trap rather than a live bug: the next contributor building an output with `push` or `extend` would have introduced heap corruption in the free path, with no compiler diagnostic to warn them.
Recommendation Use `into_boxed_slice`, which reallocates to the exact size so length and capacity always agree. Merged upstream in pull request #712.
Signing keys could not be restored from a mnemonic
Quantum-secure keys were random-only, with no derivation path, so losing the key lost the identity permanently. There was no way to restore a signer from the seed phrase the user already held.
Recommendation Derive from the 64-byte BIP-39 seed via HKDF-SHA256, making the post-quantum key a sibling of the secp256k1 key rather than a child, so breaking secp256k1 does not reach it and one mnemonic restores both. A 32-byte secp256k1 private key must be rejected as derivation input, since deriving from it would be circular. Merged upstream in pull request #712.
`qs_free_buffer` deallocated without wiping. These buffers carry secret keys, and a freed but unwiped secret remains recoverable from a core dump or a swap file.
Recommendation Zeroize before deallocating. Merged upstream in pull request #712.
If you own relaystr/ndk, claim this audit to bind it to your organization and mint a tamper-proof certificate. Claiming always proves ownership: the primary route installs the quantakrypto GitHub App on the repository (only a repository admin can do that), and the lighter route below asks you to commit a one-time token to the repository (only someone who can push can do that). Nobody else can claim it either way.
Show your post-quantum posture in your README. Three checks — scan (qScan), sieve (FIPS conformance), and probe (live readiness) — each shown A–F, or N/A until the check has run. Add this repo as a project and connect GitHub to populate it.