From e94b7e9ee72926fe367d0fd94760c31df9013ffc Mon Sep 17 00:00:00 2001 From: Kamal Tufekcic Date: Wed, 9 Sep 2026 00:47:37 +0300 Subject: [PATCH] version bumps, MAX_RECV_SEEN patch, FIPS interop, doc updates --- Abstract.md | 15 +- CHEATSHEET.md | 10 +- Cargo.lock | 653 ++++++++++++++------------------ Cargo.toml | 2 +- REVIEW-2026-09-08.md | 109 ++++++ Specification.md | 6 +- soliton/Cargo.toml | 18 +- soliton/fuzz/Cargo.lock | 2 +- soliton/src/constants.rs | 8 +- soliton/src/primitives/hkdf.rs | 17 +- soliton/src/primitives/mldsa.rs | 227 +++++++++++ soliton/src/ratchet/mod.rs | 51 +-- soliton_capi/fuzz/Cargo.lock | 4 +- soliton_py/pyproject.toml | 2 +- soliton_wasm/Cargo.toml | 2 +- soliton_zig/README.md | 2 +- soliton_zig/build.zig.zon | 2 +- 17 files changed, 693 insertions(+), 437 deletions(-) create mode 100644 REVIEW-2026-09-08.md diff --git a/Abstract.md b/Abstract.md index 2af5f15b98..7e7be204c9 100644 --- a/Abstract.md +++ b/Abstract.md @@ -646,14 +646,11 @@ recv_seen, prev_recv_seen, local_fp, remote_fp, epoch) - (d) s = 0 ∧ sk_s set is unreachable. - (e) s = 0 ∧ pending = ⊥ ∧ pk_r set ∧ sk_s unset is unreachable. - (f) prev_ek_r is set iff prev_pk_r is set (co-presence). -- (g) |recv_seen| ≤ MAX_RECV_SEEN; |prev_recv_seen| ≤ MAX_RECV_SEEN. Note: the - runtime bound allows recv_seen to reach exactly MAX_RECV_SEEN entries (the - check-before-insert pattern at §5.4 Step 6 passes at MAX_RECV_SEEN − 1, then - the insert produces MAX_RECV_SEEN). However, a state with MAX_RECV_SEEN - entries cannot be serialized — serialization rejects at |recv_seen| ≥ - MAX_RECV_SEEN. The serialization invariant is therefore |recv_seen| < - MAX_RECV_SEEN. Models reasoning about serialization should use the strict - bound. +- (g) |recv_seen| < MAX_RECV_SEEN; |prev_recv_seen| < MAX_RECV_SEEN. The + runtime check (§5.4 Step 6) rejects when a successful insert would push the + set to MAX_RECV_SEEN entries, so the live invariant matches the serialization + invariant: no reachable live state has MAX_RECV_SEEN or more entries, and + every live state is serializable on this dimension. - (h) local_fp ≠ remote_fp (self-messaging rejected at construction). Additionally, local_fp ≠ 0³² and remote_fp ≠ 0³² (all-zero fingerprint rejected at construction; constant-time check). Under SHA3-256 modeled as a @@ -910,7 +907,7 @@ symmetric rollback for both operations produces an incorrect model. replaying a legitimate ciphertext; AEAD succeeds but the plaintext is discarded). These must be modeled as separate failure branches with different adversary capabilities. -6. If |seen_set| ≥ MAX_RECV_SEEN: fail (ChainExhausted). seen_set ← seen_set ∪ +6. If |seen_set| + 1 ≥ MAX_RECV_SEEN: fail (ChainExhausted). seen_set ← seen_set ∪ {n\*}. If epoch is CurrentEpoch or NewEpoch: r ← max(r, n\* + 1). (PreviousEpoch messages do not update r — they belong to a prior receive epoch, and updating the current-epoch high-water mark would corrupt diff --git a/CHEATSHEET.md b/CHEATSHEET.md index 04804eb30b..ed0729a76f 100644 --- a/CHEATSHEET.md +++ b/CHEATSHEET.md @@ -294,7 +294,7 @@ Mistakes that compile successfully but produce incorrect or insecure behavior. A | `Internal` | -12 | `SOLITON_ERR_INTERNAL` | Structurally unreachable internal invariant violated (bug in soliton) | | `NullPointer` | -13 | `SOLITON_ERR_NULL_POINTER` | Null pointer passed for a required argument *(CAPI-only)* | | `UnsupportedFlags` | -14 | `SOLITON_ERR_FLAGS` | *(reserved — not currently returned; see footnote)* | -| `ChainExhausted` | -15 | `SOLITON_ERR_CHAIN_EXHAUSTED` | Counter-space exhausted. Sources: ratchet send/recv counter reaches u32::MAX; call key chain advance limit (2²⁴ = 16,777,216 steps); per-epoch recv_seen set reaches 65536 distinct entries; streaming chunk index reaches u64::MAX | +| `ChainExhausted` | -15 | `SOLITON_ERR_CHAIN_EXHAUSTED` | Counter-space exhausted. Sources: ratchet send/recv counter reaches u32::MAX; call key chain advance limit (2²⁴ = 16,777,216 steps); per-epoch recv_seen set would grow past 65535 distinct entries; streaming chunk index reaches u64::MAX | | `UnsupportedCryptoVersion` | -16 | `SOLITON_ERR_CRYPTO_VERSION` | Peer advertised a crypto version this library does not implement | | `InvalidData` | -17 | `SOLITON_ERR_INVALID_DATA` | Structurally invalid content (format error, co-presence violation, implausible values) | | `ConcurrentAccess` | -18 | `SOLITON_ERR_CONCURRENT_ACCESS` | Opaque handle is in use by another concurrent operation (reentrancy guard) *(CAPI-only)* | @@ -420,7 +420,7 @@ All constants have type `&[u8]` (byte-string literals). In Rust, use `b"..."` no | `CALL_KEY_B_BYTE` | `&[u8]` | `&[0x05]` | Call key derivation, second call key | | `CALL_CHAIN_ADV_BYTE` | `&[u8]` | `&[0x06]` | Call chain key advance | | `HKDF_ZERO_SALT` | `[u8; 32]` | `[0u8; 32]` | LO-KEX HKDF salt (§5.4 Step 4) — 32 zero bytes | -| `MAX_RECV_SEEN` | `u32` | `65536` | recv_seen set cap per epoch; 65536th distinct-counter decrypt succeeds, 65537th returns `ChainExhausted` | +| `MAX_RECV_SEEN` | `u32` | `65536` | recv_seen set bound per epoch: a live set holds at most 65535 entries (`len < MAX_RECV_SEEN`); the 65536th distinct-counter decrypt returns `ChainExhausted` | | `MAX_CALL_ADVANCE` *(internal, not pub)* | `u32` | `16,777,216` (2²⁴) | `CallKeys::advance()` limit; `ChainExhausted` after this many calls. Not in `soliton::constants` — value for reimplementors only. | --- @@ -1313,7 +1313,7 @@ pub fn encrypt(&mut self, plaintext: &[u8]) -> Result #[must_use = "decrypted plaintext contains sensitive data that must be consumed or zeroized"] pub fn decrypt(&mut self, header: &RatchetHeader, ciphertext: &[u8]) -> Result>> // Err(DuplicateMessage) — counter already in recv_seen (state unchanged) -// Err(ChainExhausted) — counter at u32::MAX, or recv_seen / prev_recv_seen set reached 65536 entries +// Err(ChainExhausted) — counter at u32::MAX, or recv_seen / prev_recv_seen already holds 65535 entries // Err(InvalidData) — ratchet state previously zeroized by reset() or a prior AeadFailed (requires new KEX); // or ratchet step required but no KEM ct in header; or no send secret key // Err(DecapsulationFailed) — X-Wing KEM decapsulation failed @@ -1348,8 +1348,8 @@ pub fn to_bytes(self) -> Result<(Zeroizing>, u64)> // consumes self; re // there is no recovery path. Call can_serialize() first; if false, do NOT call to_bytes(). // Err(ChainExhausted) — send_count, recv_count, prev_send_count == u32::MAX, or epoch == u64::MAX // Err(InvalidData) — recv_seen or prev_recv_seen has ≥ 65536 entries (defense-in-depth dead branch: -// decrypt's ChainExhausted guard fires at recv_seen.len() == 65536, preventing the set from growing -// further, so this InvalidData path is unreachable in normal operation; the global error table lists +// decrypt's ChainExhausted guard refuses the insert that would make recv_seen.len() == 65536, so a +// live set never exceeds 65535 and this InvalidData path is unreachable in normal operation; the global error table lists // recv_seen exhaustion under ChainExhausted for this reason) #[deprecated(note = "Use from_bytes_with_min_epoch for anti-rollback protection")] pub fn from_bytes(data: &[u8]) -> Result // no rollback protection — always use from_bytes_with_min_epoch diff --git a/Cargo.lock b/Cargo.lock index 4661b2f931..23d9190fac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,9 +14,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -29,15 +29,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -62,12 +62,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - [[package]] name = "argon2" version = "0.5.3" @@ -82,10 +76,21 @@ dependencies = [ ] [[package]] -name = "autocfg" -version = "1.5.0" +name = "async-trait" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "base64ct" @@ -110,9 +115,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "blake2" @@ -138,15 +143,21 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96eb4cdd6cf1b31d671e9efe75c5d1ec614776856cefbe109ca373554a6d514f" dependencies = [ - "hybrid-array 0.4.7", + "hybrid-array 0.4.14", "zeroize", ] [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cbindgen" @@ -162,16 +173,16 @@ dependencies = [ "quote", "serde", "serde_json", - "syn", + "syn 2.0.119", "tempfile", "toml", ] [[package]] name = "cc" -version = "1.2.58" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" +checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80" dependencies = [ "find-msvc-tools", "shlex", @@ -220,9 +231,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.60" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" dependencies = [ "clap_builder", "clap_derive", @@ -230,9 +241,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" dependencies = [ "anstream", "anstyle", @@ -242,27 +253,27 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 3.0.5", ] [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "const-oid" @@ -287,18 +298,18 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" dependencies = [ "libc", ] [[package]] name = "crossbeam-deque" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -306,18 +317,18 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" [[package]] name = "crypto-common" @@ -335,7 +346,7 @@ version = "0.2.0-rc.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f8441110cea75afde0b89a8d796e2bc67b23432f5a9566cb15d9d365d91a2b0" dependencies = [ - "hybrid-array 0.4.7", + "hybrid-array 0.4.14", ] [[package]] @@ -362,7 +373,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -377,9 +388,9 @@ dependencies = [ [[package]] name = "der" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a" dependencies = [ "const-oid 0.10.2", "zeroize", @@ -435,9 +446,9 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" [[package]] name = "equivalent" @@ -457,9 +468,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "fiat-crypto" @@ -469,9 +480,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" [[package]] name = "fnv" @@ -480,10 +491,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] -name = "foldhash" -version = "0.1.5" +name = "futures-core" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] [[package]] name = "generic-array" @@ -517,38 +546,27 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", - "r-efi", - "wasip2", - "wasip3", + "r-efi 6.0.0", ] [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -597,30 +615,22 @@ dependencies = [ [[package]] name = "hybrid-array" -version = "0.4.7" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1b229d73f5803b562cc26e4da0396c8610a4ee209f4fac8fa4f8d709166dc45" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" dependencies = [ "typenum", "zeroize", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" dependencies = [ "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", + "hashbrown", ] [[package]] @@ -640,17 +650,18 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -669,7 +680,7 @@ version = "0.2.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "882b69cb15b1f78b51342322a97ccd16f5123d1dc8a3da981a95244f488e8692" dependencies = [ - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", ] [[package]] @@ -683,20 +694,20 @@ dependencies = [ ] [[package]] -name = "leb128fmt" -version = "0.1.0" +name = "libc" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] -name = "libc" -version = "0.2.182" +name = "libm" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libsoliton" -version = "0.1.3" +version = "0.1.4" dependencies = [ "argon2", "chacha20poly1305", @@ -728,7 +739,7 @@ dependencies = [ [[package]] name = "libsoliton_capi" -version = "0.1.3" +version = "0.1.4" dependencies = [ "cbindgen", "libsoliton", @@ -737,21 +748,21 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "log" -version = "0.4.29" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "minicov" @@ -769,11 +780,9 @@ version = "0.1.0-rc.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af6e554a2affc86740759dbe568a92abd58b47fea4e28ebe1b7bb4da99e490d4" dependencies = [ - "const-oid 0.10.2", - "hybrid-array 0.4.7", + "hybrid-array 0.4.14", "module-lattice", - "pkcs8 0.11.0-rc.11", - "rand_core 0.10.0", + "pkcs8 0.11.0", "sha3 0.11.0-rc.7", "signature 3.0.0-rc.10", "zeroize", @@ -788,7 +797,7 @@ dependencies = [ "hybrid-array 0.2.3", "kem", "rand_core 0.6.4", - "sha3 0.10.8", + "sha3 0.10.9", "zeroize", ] @@ -798,11 +807,20 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dfecc750073acc09af2f8899b2342d520d570392ba1c3aed53eeb0d84ca4103" dependencies = [ - "hybrid-array 0.4.7", + "hybrid-array 0.4.14", "num-traits", "zeroize", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -810,13 +828,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -824,6 +843,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "opaque-debug" version = "0.3.1" @@ -841,6 +866,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "pkcs8" version = "0.10.2" @@ -853,12 +884,12 @@ dependencies = [ [[package]] name = "pkcs8" -version = "0.11.0-rc.11" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12922b6296c06eb741b02d7b5161e3aaa22864af38dfa025a1a3ba3f68c84577" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" dependencies = [ - "der 0.8.0", - "spki 0.8.0-rc.4", + "der 0.8.2", + "spki 0.8.0", ] [[package]] @@ -875,9 +906,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.13.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] name = "ppv-lite86" @@ -888,21 +919,11 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -928,9 +949,9 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.28.2" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf85e27e86080aafd5a22eae58a162e133a589551542b3e5cee4beb27e54f8e1" +checksum = "91fd8e38a3b50ed1167fb981cd6fd60147e091784c427b8f7183a7ee32c31c12" dependencies = [ "libc", "once_cell", @@ -942,18 +963,18 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.28.2" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf94ee265674bf76c09fa430b0e99c26e319c945d96ca0d5a8215f31bf81cf7" +checksum = "e368e7ddfdeb98c9bca7f8383be1648fd84ab466bf2bc015e94008db6d35611e" dependencies = [ "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.28.2" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "491aa5fc66d8059dd44a75f4580a2962c1862a1c2945359db36f6c2818b748dc" +checksum = "7f29e10af80b1f7ccaf7f69eace800a03ecd13e883acfacc1e5d0988605f651e" dependencies = [ "libc", "pyo3-build-config", @@ -961,27 +982,27 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.28.2" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5d671734e9d7a43449f8480f8b38115df67bef8d21f76837fa75ee7aaa5e52e" +checksum = "df6e520eff47c45997d2fc7dd8214b25dd1310918bbb2642156ef66a67f29813" dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "pyo3-macros-backend" -version = "0.28.2" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22faaa1ce6c430a1f71658760497291065e6450d7b5dc2bcf254d49f66ee700a" +checksum = "c4cdc218d835738f81c2338f822078af45b4afdf8b2e33cbb5916f108b813acb" dependencies = [ "heck", "proc-macro2", "pyo3-build-config", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -992,9 +1013,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.44" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -1006,10 +1027,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] -name = "rand" -version = "0.9.2" +name = "r-efi" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ "rand_chacha", "rand_core 0.9.5", @@ -1043,12 +1070,6 @@ dependencies = [ "getrandom 0.3.4", ] -[[package]] -name = "rand_core" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" - [[package]] name = "rand_xorshift" version = "0.4.0" @@ -1080,9 +1101,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "rustc_version" @@ -1095,9 +1116,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", @@ -1108,9 +1129,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "rusty-fork" @@ -1144,15 +1165,15 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -1160,29 +1181,29 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.5", ] [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -1213,9 +1234,9 @@ dependencies = [ [[package]] name = "sha3" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" dependencies = [ "digest 0.10.7", "keccak 0.1.6", @@ -1233,9 +1254,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signature" @@ -1253,12 +1274,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f1880df446116126965eeec169136b2e0251dba37c6223bcc819569550edea3" dependencies = [ "digest 0.11.0-rc.11", - "rand_core 0.10.0", ] +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "soliton-cli" -version = "0.1.3" +version = "0.1.4" dependencies = [ "clap", "hex", @@ -1269,7 +1295,7 @@ dependencies = [ [[package]] name = "soliton-python" -version = "0.1.3" +version = "0.1.4" dependencies = [ "libsoliton", "pyo3", @@ -1278,7 +1304,7 @@ dependencies = [ [[package]] name = "soliton-wasm" -version = "0.1.3" +version = "0.1.4" dependencies = [ "js-sys", "libsoliton", @@ -1299,12 +1325,12 @@ dependencies = [ [[package]] name = "spki" -version = "0.8.0-rc.4" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8baeff88f34ed0691978ec34440140e1572b68c7dd4a495fd14a3dc1944daa80" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" dependencies = [ "base64ct", - "der 0.8.0", + "der 0.8.2", ] [[package]] @@ -1321,9 +1347,20 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.117" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" dependencies = [ "proc-macro2", "quote", @@ -1338,12 +1375,12 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "tempfile" -version = "3.25.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.1", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys", @@ -1366,7 +1403,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1412,15 +1449,15 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "twox-hash" -version = "2.1.2" +version = "2.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" +checksum = "5283634e518fe9e82c7b20520bb4bc209009fd16c82077c802f8111ecbb0117a" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unarray" @@ -1434,12 +1471,6 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "universal-hash" version = "0.5.1" @@ -1489,66 +1520,41 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" dependencies = [ - "cfg-if", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1556,93 +1562,64 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn", - "wasm-bindgen-backend", + "syn 3.0.5", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-bindgen-test" -version = "0.3.50" +version = "0.3.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66c8d5e33ca3b6d9fa3b4676d774c5778031d27a578c2b007f905acf816152c3" +checksum = "45863ef0bef521c12124eb39d9a38513c47db75f22e503c06beacd40afeb35db" dependencies = [ + "async-trait", + "cast", "js-sys", + "libm", "minicov", + "nu-ansi-term", + "num-traits", + "oorandom", + "serde", + "serde_json", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test-macro", + "wasm-bindgen-test-shared", ] [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.50" +version = "0.3.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b" +checksum = "8c89dcab8b516b6b603baca9d550b7282d68fcc7f367e3956cff7ebf406a3f12" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.5", ] [[package]] -name = "wasm-encoder" -version = "0.244.0" +name = "wasm-bindgen-test-shared" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "web-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" -dependencies = [ - "js-sys", - "wasm-bindgen", -] +checksum = "f37b4f992cebe528ef34964ae69681ac0fe7080071e7298e46008f9d380302af" [[package]] name = "winapi-util" @@ -1670,119 +1647,37 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] [[package]] name = "wit-bindgen" -version = "0.51.0" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1796,17 +1691,17 @@ dependencies = [ [[package]] name = "zeroize_derive" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index 5738515188..ac90b6f087 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ default-members = [ ] [workspace.package] -version = "0.1.3" +version = "0.1.4" edition = "2024" rust-version = "1.87" license = "AGPL-3.0-only" diff --git a/REVIEW-2026-09-08.md b/REVIEW-2026-09-08.md new file mode 100644 index 0000000000..f86855bb46 --- /dev/null +++ b/REVIEW-2026-09-08.md @@ -0,0 +1,109 @@ +# libsoliton core review — 2026-09-08 (single-reviewer trial) + +## Overall assessment + +libsoliton is shippable as reviewed. Across the full primitive stack, the key +exchange, the double ratchet, streaming/storage AEAD, call keys, auth, and +verification, I found no defect at medium severity or above. The areas the brief +flagged as highest-risk are handled correctly and defensively: every AEAD nonce +is provably unique within its key's lifetime (ratchet counter-mode nonces are a +strict function of a monotonic `send_count` that aborts at `u32::MAX` before it +can wrap; the first-message key reuses the epoch key only at counter 0 with a +random nonce and a distinct AAD; streaming nonces are injective over +`(index, tag_byte)`; storage uses fresh random nonces). Secret-dependent +comparisons all use `subtle`/constant-time paths (MAC/token in `auth_verify`, +all-zero key/secret guards, fingerprint checks against untrusted wire data, +ratchet-pk epoch identification, shared-secret zero-check in X25519 DH). Key +material is wrapped in `Zeroizing`/`ZeroizeOnDrop` and every keygen/DH/KEM path +explicitly zeroizes the `Copy`-type stack staging buffers before returning. The +double-ratchet state machine is the strongest single piece: `encrypt` is +atomic-by-ordering (all fallible ops precede any mutation), `decrypt` takes a +full snapshot and rolls back on any error, duplicate detection is deferred past +AEAD to avoid a timing oracle, the seen-sets and epoch keys rotate correctly +across direction changes, and `from_bytes` has an extensive battery of +structural/co-presence/all-zero guards. + +The weakest areas are documentation-vs-code drift and a heavy reliance on +documented caller obligations, neither of which is a code defect. Two footguns +that could produce catastrophic nonce reuse — double-`from_bytes` and reusing a +streaming `(index, is_last)` pair — are real, but both are deprecated/ +`&self`-random-access APIs with explicit in-code warnings and safe alternatives +(`from_bytes_with_min_epoch`, sequential `encrypt_chunk`), so they fall under the +brief's "documented decision" exclusion. The one concrete mismatch worth fixing +is a one-off in the `recv_seen` cap (code is stricter than the docs claim; safe +direction). I would ship it; the notes below are hygiene, not blockers. + +## Findings (medium and above) + +None. + +## Notes below medium + +- **low — `recv_seen`/`prev_recv_seen` cap is off-by-one vs the documentation.** + `soliton/src/ratchet/mod.rs:582` (and `:573`) reject with `ChainExhausted` when + `self.recv_seen.len() + 1 >= MAX_RECV_SEEN` (65536), i.e. the set can hold at + most 65535 entries and the 65536th *distinct*-counter decrypt is rejected. The + CHEATSHEET ("the 65536th distinct-counter decrypt succeeds … the 65537th + returns `ChainExhausted`") and the `MAX_RECV_SEEN` comment in + `soliton/src/constants.rs:83` both state the limit one message higher. The code + is the safe/conservative side (it keeps the live set always serializable, + matching the `>= MAX_RECV_SEEN` reject in `to_bytes` at `:1060`/`:1074` and + `from_bytes` at `:1292`/`:1316`), so this is a doc/reimplementor-guidance error, + not a security defect. No test would catch it without a 65k-message epoch. + +- **info — contradictory comments about HKDF PRK zeroization.** + `soliton/src/primitives/hkdf.rs:19-22` states "The `hkdf` crate's internal PRK + is **not** zeroized on drop (`hkdf 0.13.0-rc.5` has no `zeroize` feature)", + while `soliton/soliton/Cargo.toml:26-29` explains that `sha3` 0.11-rc + the + `hmac` `zeroize` feature were chosen specifically so the keyed SHA3 state used + by HMAC/HKDF is `ZeroizeOnDrop`. Since `Hkdf` stores its PRK as an + `Hmac` field, that field's drop should in fact zeroize the PRK. The + two comments can't both be right; reconcile them so maintainers don't reason + from the pessimistic one. Either way the PRK lifetime is a short-lived local, + so the practical exposure is minimal. + +- **info — streaming compressed-empty non-final chunk skips the size check.** + `soliton/src/streaming.rs:527` gates decompression on + `self.compressed && !decrypted.is_empty()`; a compressed *non-final* chunk that + AEAD-decrypts to zero bytes would return `(empty, is_last=false)` without the + `decompressed.len() != STREAM_CHUNK_SIZE` guard at `:533`. Unreachable in + practice: producing a valid AEAD chunk requires the stream key, and the + legitimate encryptor never emits an empty non-final chunk (non-final plaintext + must be exactly `STREAM_CHUNK_SIZE`, which compresses to non-empty). Latent + only. + +- **info — `Copy`-typed key material leaves brief stack residue by design.** + `random::random_array`, `hmac_sha3_256`, and the various `[u8; 32]` deref-copies + documented throughout return `Copy` values the callee can't zeroize. Every + in-library use zeroizes the staging copy (e.g. `x25519.rs:62`, `mlkem.rs:162`, + `call.rs:312`, `ratchet.rs:174`), and the hazard is called out in the + CHEATSHEET for callers. Noted for completeness, not a defect. + +- **info — nonce-reuse footguns are deprecated/documented, not defects.** + Double-`from_bytes` (`ratchet/mod.rs:1114`, `#[deprecated]`, with an explicit + double-deserialization warning and the `from_bytes_with_min_epoch` safe path) + and reusing a streaming `(index, is_last)` pair via `encrypt_chunk_at` + (`streaming.rs:281`, explicit SECURITY warning) both cause catastrophic nonce + reuse if misused, but each ships with a safe alternative and in-code warnings. + +## Coverage + +Read in full (to the test boundary given in the brief), in dependency order: +`README.md`; `CHEATSHEET.md` (all 2354 lines); `soliton/Cargo.toml`; +`src/lib.rs`; `src/error.rs`; `src/constants.rs` (1–163); `src/primitives/mod.rs`; +`src/primitives/random.rs` (1–34); `src/primitives/sha3_256.rs` (1–28); +`src/primitives/hmac.rs` (1–40); `src/primitives/hkdf.rs` (1–44); +`src/primitives/aead.rs` (1–109); `src/primitives/ed25519.rs` (1–58); +`src/primitives/x25519.rs` (1–105); `src/primitives/mlkem.rs` (1–314); +`src/primitives/mldsa.rs` (1–436); `src/primitives/xwing.rs` (1–395); +`src/primitives/argon2.rs` (1–145); `src/identity/mod.rs` (1–349); +`src/auth/mod.rs` (1–102); `src/kex/mod.rs` (1–924); `src/verification.rs` +(1–125); `src/ratchet/mod.rs` (1–1606); `src/streaming.rs` (1–549); +`src/storage/mod.rs` (1–593); `src/call.rs` (1–318). No file was skimmed; every +allowed source file was read line-by-line. Test modules, `tests/`, `benches/`, +`fuzz/`, `build.rs`, `Specification.md`, `Abstract.md`, and all non-core crates +were not opened, per the brief. No build or scratch experiment was needed — the +notes were settled by reading. Constants in `constants.rs` were cross-checked +against the standard sizes (ML-KEM-768 1184/2400/1088, ML-DSA-65 1952/3309/32 +seed, X25519/Ed25519 32/64, X-Wing 1216/2432/1120, composite 3200/2496, hybrid +sig 3373) and against every use site; all agree. diff --git a/Specification.md b/Specification.md index 69cf1cc963..6a557f7c2e 100644 --- a/Specification.md +++ b/Specification.md @@ -1416,7 +1416,7 @@ function Decrypt(state, header, ciphertext, sender_fp, recipient_fp): if header.n in state.prev_recv_seen: restore_recv_state(state, snapshot) raise DuplicateMessage - if |state.prev_recv_seen| >= MAX_RECV_SEEN: + if |state.prev_recv_seen| + 1 >= MAX_RECV_SEEN: restore_recv_state(state, snapshot) raise ChainExhausted state.prev_recv_seen.add(header.n) @@ -1434,7 +1434,7 @@ function Decrypt(state, header, ciphertext, sender_fp, recipient_fp): if header.n in state.recv_seen: restore_recv_state(state, snapshot) raise DuplicateMessage - if |state.recv_seen| >= MAX_RECV_SEEN: + if |state.recv_seen| + 1 >= MAX_RECV_SEEN: restore_recv_state(state, snapshot) raise ChainExhausted state.recv_seen.add(header.n) @@ -1586,7 +1586,7 @@ Ratchet state is serialized for encrypted persistent storage. The caller MUST au **Serialization buffer zeroization**: The serialization output buffer contains root keys, epoch keys, and ratchet secret keys — all secret material. Implementations SHOULD pre-allocate the buffer to its exact final size before writing any data. If a dynamic array (Vec, list, slice) reallocates during serialization, the abandoned allocation containing partial secret material is freed to the heap allocator without zeroization — only the final allocation is covered by zeroize-on-drop. In Rust, pre-allocation is the actual guarantee; a `debug_assert` on capacity detects underestimates during testing but is compiled out in release builds — if the pre-computed size is wrong, a release binary silently reallocates and the abandoned partial allocation is freed without zeroization. In C/Go/Python, pre-compute the buffer size and allocate once. The output buffer itself MUST be zeroized after the caller has finished with it (e.g., after passing it to storage encryption). -**MAX_RECV_SEEN cap at runtime**: When `recv_seen` or `prev_recv_seen` reaches `MAX_RECV_SEEN` (65536) entries during decrypt, subsequent messages in that epoch return `ChainExhausted`. This is transient — the cap resets on the next KEM ratchet step (which clears `recv_seen`). The cap prevents unbounded memory growth from an authenticated peer sending many messages in a single epoch. +**MAX_RECV_SEEN cap at runtime**: When `recv_seen` or `prev_recv_seen` reaches `MAX_RECV_SEEN - 1` (65535) entries during decrypt, the next message in that epoch returns `ChainExhausted` — the runtime check rejects when a successful insert would push the set to `MAX_RECV_SEEN`, matching the serialization invariant (`|recv_seen| < MAX_RECV_SEEN`). This is transient — the cap resets on the next KEM ratchet step (which clears `recv_seen`). The cap prevents unbounded memory growth from an authenticated peer sending many messages in a single epoch. **Which epoch paths can fire this cap**: The `recv_seen` saturation check (`ChainExhausted`) fires only on the **current-epoch path** (messages in the active receive epoch) and the **previous-epoch path** (messages in the prior epoch, checked against `prev_recv_seen`). The **new-epoch path** (which triggers a KEM ratchet step) is immune: it clears `recv_seen` to empty before the duplicate/cap check, so the cap check on that path is structurally unreachable — a new epoch always starts with an empty `recv_seen`. A reimplementer testing `ChainExhausted` from `recv_seen` saturation MUST use the current-epoch or previous-epoch path, not the new-epoch path. diff --git a/soliton/Cargo.toml b/soliton/Cargo.toml index 85395dda5b..71190693d9 100644 --- a/soliton/Cargo.toml +++ b/soliton/Cargo.toml @@ -77,7 +77,19 @@ ed25519-dalek = { version = "=2.2.0", default-features = false, features = ["zer # stacks are isolated at the type level and never share state. ml-kem = { version = "=0.2.3", features = ["zeroize", "deterministic"] } kem = "=0.3.0-pre.0" -ml-dsa = { version = "=0.1.0-rc.7", features = ["zeroize"] } +# `default-features = false` drops ml-dsa's `pkcs8` and `rand_core` features. +# soliton uses neither: keys and signatures cross the boundary as raw encodings +# (B32, EncodedVerifyingKey, EncodedSignature), and keygen seeds `from_seed` +# with bytes from soliton's own CSPRNG rather than a rand_core Rng. +# +# This is load-bearing, not tidiness. ml-dsa 0.1.0-rc.7 requires +# `pkcs8 = "0.11.0-rc.10"`, which Cargo reads as `>=0.11.0-rc.10, <0.12.0` — so +# once pkcs8 0.11.0 final shipped it outranked every rc and got selected, and +# ml-dsa's `pkcs8.rs` stopped compiling (`?` cannot convert into the reshaped +# `Error`). That broke a fresh `cargo add libsoliton`, since a library's +# Cargo.lock does not constrain downstream resolution. With the feature off, +# `pkcs8.rs` is never compiled and the breakage is unreachable. +ml-dsa = { version = "=0.1.0-rc.7", default-features = false, features = ["zeroize", "alloc"] } # rand_core is pulled transitively by ml-kem (0.6) and ml-dsa (0.10). # Deterministic/seed-based APIs + getrandom are used to avoid RNG trait issues. @@ -107,7 +119,9 @@ proptest = "=1.10.0" rayon = "=1.11.0" # ml-dsa is a direct dependency, re-declared here so integration tests can call # sign_internal directly to produce deterministic F.27 test vectors (pinned rnd). -ml-dsa = { version = "=0.1.0-rc.7", features = ["zeroize"] } +# Feature set must match the [dependencies] entry — see the note there for why +# default features are off. +ml-dsa = { version = "=0.1.0-rc.7", default-features = false, features = ["zeroize", "alloc"] } [[bench]] name = "bench" diff --git a/soliton/fuzz/Cargo.lock b/soliton/fuzz/Cargo.lock index 073b99dcd1..99c0eb74e0 100644 --- a/soliton/fuzz/Cargo.lock +++ b/soliton/fuzz/Cargo.lock @@ -435,7 +435,7 @@ dependencies = [ [[package]] name = "libsoliton" -version = "0.1.3" +version = "0.1.4" dependencies = [ "argon2", "chacha20poly1305", diff --git a/soliton/src/constants.rs b/soliton/src/constants.rs index a690ffd116..ebb358588c 100644 --- a/soliton/src/constants.rs +++ b/soliton/src/constants.rs @@ -79,10 +79,12 @@ pub const MSG_KEY_DOMAIN_BYTE: u8 = 0x01; /// Zero salt for HKDF in LO-KEX (§5.4 Step 4). pub const HKDF_ZERO_SALT: [u8; 32] = [0u8; 32]; -/// Maximum entries in recv_seen set per epoch (§6.7). Prevents memory +/// Bound on the recv_seen set per epoch (§6.7): a live set holds fewer than +/// this many entries (at most 65535); `decrypt` refuses the insert that would +/// reach it and `to_bytes` rejects a set at or above it. Prevents memory /// exhaustion from an adversary sending messages with many distinct counter -/// values. The set stores only 4-byte counters (not keys), so 65536 entries -/// is ~256 KB. Resets on each KEM ratchet step. +/// values. The set stores only 4-byte counters (not keys), so the bound is +/// ~256 KB. Resets on each KEM ratchet step. pub const MAX_RECV_SEEN: u32 = 65536; /// Ratchet state serialization format version. Checked on deserialization; diff --git a/soliton/src/primitives/hkdf.rs b/soliton/src/primitives/hkdf.rs index 1a9bd7d9e4..a0cda97b42 100644 --- a/soliton/src/primitives/hkdf.rs +++ b/soliton/src/primitives/hkdf.rs @@ -16,10 +16,19 @@ use sha3::Sha3_256; /// # Security /// /// The caller is responsible for zeroizing the `out` buffer when the derived -/// key material is no longer needed. The `hkdf` crate's internal PRK is -/// **not** zeroized on drop (`hkdf 0.13.0-rc.5` has no `zeroize` feature). -/// Mitigation: `Hkdf` is a short-lived local — PRK lifetime is bounded to -/// this function scope, and `panic = "abort"` prevents unwinding. +/// key material is no longer needed. +/// +/// What the dependencies zeroize, verified against the pinned sources +/// (`hkdf 0.13.0-rc.5`, `hmac 0.13.0-rc.5`, `sha3 0.11.0-rc.7`, +/// `digest 0.11.0-rc.11`, `block-buffer 0.11.0`, all with their `zeroize` +/// features resolved on): dropping the `Hkdf` zeroizes the PRK-keyed HMAC +/// state — both SHA3 cores through `sha3`'s `Drop` and the wrapper's block +/// buffer through `block-buffer`'s. `hkdf` itself has no `zeroize` feature +/// and leaves three plain stack copies behind: the raw 32-byte PRK that +/// `Hkdf::new` extracts and discards, the `key ^ OPAD` block that +/// `HmacCore::new_from_slice` builds in a local, and the last output block +/// that `expand` keeps in a local. All three are bounded to this function's +/// frame, and `panic = "abort"` prevents unwinding past it. pub fn hkdf_sha3_256(salt: &[u8], ikm: &[u8], info: &[u8], out: &mut [u8]) -> Result<()> { if out.is_empty() { return Err(Error::InvalidLength { diff --git a/soliton/src/primitives/mldsa.rs b/soliton/src/primitives/mldsa.rs index 63fad15d12..352807e275 100644 --- a/soliton/src/primitives/mldsa.rs +++ b/soliton/src/primitives/mldsa.rs @@ -302,6 +302,135 @@ pub fn verify(pk: &PublicKey, message: &[u8], signature: &Signature) -> Result<( } } +/// Sign with the FIPS 204 §5.2 ML-DSA.Sign envelope (domain separator + +/// context string). +/// +/// This is the **standard ML-DSA.Sign** algorithm — equivalent to what +/// any FIPS 204-compliant implementation produces (rustls/aws-lc-rs, +/// OpenSSL once they ship PQ, hardware tokens). Use it whenever a +/// signature must verify under a generic ML-DSA-65 verifier. +/// +/// Constructs `M' = 0x00 || len(ctx) || ctx || M` per FIPS 204 §5.2, +/// then signs M' via the underlying primitive. Distinct from +/// [`sign`], which signs M directly (no prefix) — soliton's +/// non-standard mode used by intra-Lo signatures that are always +/// verified by soliton itself. +/// +/// `ctx` is the FIPS 204 context string. For TLS 1.3 use it MUST be +/// empty (`b""`) per `draft-ietf-tls-mldsa`. Other protocol-specific +/// contexts (e.g., `b"lo-session-bind"` for application-level domain +/// separation) are supported up to 255 bytes. +/// +/// Hedged signing: 32 bytes of fresh OS entropy mixed into the nonce +/// derivation, identical to [`sign`]'s hedged behavior. +/// +/// # Errors +/// - [`Error::InvalidData`] if `ctx.len() > 255` +/// - [`Error::InvalidLength`] if the secret-key seed length is wrong +pub fn sign_with_ctx(sk: &SecretKey, message: &[u8], ctx: &[u8]) -> Result { + use ml_dsa::{B32, MlDsa65, Seed, SigningKey}; + + if ctx.len() > 255 { + return Err(Error::InvalidData); + } + if sk.0.len() != SK_LEN { + return Err(Error::InvalidLength { + expected: SK_LEN, + got: sk.0.len(), + }); + } + + let seed: &Seed = sk.0.as_slice().try_into().map_err(|_| Error::Internal)?; + let signing_key = SigningKey::::from_seed(seed); + + // Hedged signing: 32 bytes of fresh getrandom entropy. Same + // rationale as `sign()` — fault-injection resistance. + let mut rnd_bytes = [0u8; 32]; + super::random::random_bytes(&mut rnd_bytes); + let mut rnd: B32 = rnd_bytes.into(); + rnd_bytes.zeroize(); + + // FIPS 204 §5.2 envelope: M' = 0x00 || len(ctx) || ctx || M. + // SHAKE's streaming absorb is associative, so passing M' as a + // multi-chunk slice (`&[domain_sep, len_byte, ctx, message]`) + // produces the same μ as concatenating into one buffer first. + // The `MuBuilder::new(tr, ctx).message(M)` path used by the + // ml-dsa crate's standard `verify_with_context` absorbs the + // same bytes in the same order — so signatures from this + // function verify under the standard verify path, AND the + // standard sign path verifies under our `verify_with_ctx`. + let domain_sep = [0x00u8]; + let len_byte = [ctx.len() as u8]; + let sig = signing_key.sign_internal(&[&domain_sep, &len_byte, ctx, message], &rnd); + rnd.zeroize(); + + let sig_bytes = sig.encode().to_vec(); + assert_eq!( + sig_bytes.len(), + SIG_LEN, + "ML-DSA-65 sig size mismatch: got {}, expected {SIG_LEN}", + sig_bytes.len() + ); + + Ok(Signature(sig_bytes)) +} + +/// Verify with the FIPS 204 §5.2 ML-DSA.Verify envelope. +/// +/// Counterpart of [`sign_with_ctx`]. Accepts signatures from any +/// FIPS 204-compliant ML-DSA-65 implementation (when called with +/// the same `ctx`). +/// +/// `ctx` MUST match the context string used by the signer. For TLS +/// 1.3 use that is `b""`. +/// +/// Returns `Ok(())` on a valid signature, `Err(VerificationFailed)` +/// for an invalid one or for `ctx.len() > 255`. +pub fn verify_with_ctx( + pk: &PublicKey, + message: &[u8], + ctx: &[u8], + signature: &Signature, +) -> Result<()> { + use ml_dsa::{EncodedSignature, EncodedVerifyingKey, MlDsa65, VerifyingKey}; + + if ctx.len() > 255 { + return Err(Error::VerificationFailed); + } + if pk.0.len() != PK_LEN { + return Err(Error::InvalidLength { + expected: PK_LEN, + got: pk.0.len(), + }); + } + if signature.0.len() != SIG_LEN { + return Err(Error::InvalidLength { + expected: SIG_LEN, + got: signature.0.len(), + }); + } + + let vk_enc: &EncodedVerifyingKey = + pk.0.as_slice().try_into().map_err(|_| Error::Internal)?; + let vk = VerifyingKey::::decode(vk_enc); + + let sig_enc: &EncodedSignature = signature + .0 + .as_slice() + .try_into() + .map_err(|_| Error::Internal)?; + let sig = ml_dsa::Signature::::decode(sig_enc).ok_or(Error::VerificationFailed)?; + + // `verify_with_context` applies the FIPS 204 §5.2 envelope + // (`0x00 || len(ctx) || ctx || M`) before verifying, matching + // the construction in `sign_with_ctx`. + if vk.verify_with_context(message, ctx, &sig) { + Ok(()) + } else { + Err(Error::VerificationFailed) + } +} + #[cfg(test)] mod tests { use super::*; @@ -335,6 +464,104 @@ mod tests { assert!(verify(&pk, b"", &sig).is_ok()); } + /// `sign_with_ctx` round-trip with empty context (the form used + /// for TLS 1.3 cert verify per draft-ietf-tls-mldsa). + #[test] + fn sign_with_ctx_empty_round_trip() { + let (pk, sk) = keygen().unwrap(); + let sig = sign_with_ctx(&sk, b"hello fips204", b"").unwrap(); + assert!(verify_with_ctx(&pk, b"hello fips204", b"", &sig).is_ok()); + } + + /// Round-trip with a non-empty context. Confirms the FIPS 204 + /// envelope `0x00 || len(ctx) || ctx || M` round-trips through + /// the sign/verify pair. + #[test] + fn sign_with_ctx_nonempty_round_trip() { + let (pk, sk) = keygen().unwrap(); + let sig = sign_with_ctx(&sk, b"payload", b"app-domain-v1").unwrap(); + assert!(verify_with_ctx(&pk, b"payload", b"app-domain-v1", &sig).is_ok()); + } + + /// Domain-separation: a signature produced with one context + /// MUST NOT verify under a different context. This is the + /// security property that makes per-protocol context strings + /// useful — a TLS-bound signature can't be replayed to an + /// application-layer verifier and vice versa. + #[test] + fn sign_with_ctx_different_ctx_fails() { + let (pk, sk) = keygen().unwrap(); + let sig = sign_with_ctx(&sk, b"payload", b"ctx-a").unwrap(); + assert!(matches!( + verify_with_ctx(&pk, b"payload", b"ctx-b", &sig), + Err(Error::VerificationFailed) + )); + } + + /// `sign` (no ctx) and `sign_with_ctx` produce DIFFERENT + /// signatures, and the signatures don't cross-verify. This is + /// the deliberate distinction: `sign` skips the FIPS 204 + /// envelope, `sign_with_ctx` applies it. If a regression ever + /// made them equivalent, soliton's intra-Lo signatures would + /// silently become FIPS 204-compatible (or vice versa) — both + /// outcomes need to be intentional, not accidental. + #[test] + fn sign_and_sign_with_ctx_are_distinct() { + let (pk, sk) = keygen().unwrap(); + let sig_internal = sign(&sk, b"payload").unwrap(); + let sig_standard = sign_with_ctx(&sk, b"payload", b"").unwrap(); + // Bytes differ (the rnd hedging makes equality unreliable + // even for identical inputs, so this isn't a strict check). + // The cross-verify check is the load-bearing one: + assert!( + matches!( + verify_with_ctx(&pk, b"payload", b"", &sig_internal), + Err(Error::VerificationFailed) + ), + "internal-mode signature must NOT verify under FIPS 204 verify" + ); + assert!( + matches!( + verify(&pk, b"payload", &sig_standard), + Err(Error::VerificationFailed) + ), + "FIPS 204 signature must NOT verify under internal-mode verify" + ); + } + + /// 255-byte context is the spec maximum. Confirm it's accepted. + #[test] + fn sign_with_ctx_max_length_ok() { + let (pk, sk) = keygen().unwrap(); + let ctx = vec![0xAB; 255]; + let sig = sign_with_ctx(&sk, b"payload", &ctx).unwrap(); + assert!(verify_with_ctx(&pk, b"payload", &ctx, &sig).is_ok()); + } + + /// 256-byte context exceeds the FIPS 204 §5.2 limit and MUST + /// be rejected at the API boundary rather than producing an + /// invalid signature. + #[test] + fn sign_with_ctx_oversized_ctx_rejected() { + let (_, sk) = keygen().unwrap(); + let ctx = vec![0u8; 256]; + assert!(matches!( + sign_with_ctx(&sk, b"payload", &ctx), + Err(Error::InvalidData) + )); + } + + #[test] + fn verify_with_ctx_oversized_ctx_rejected() { + let (pk, sk) = keygen().unwrap(); + let valid_sig = sign_with_ctx(&sk, b"payload", b"").unwrap(); + let too_long_ctx = vec![0u8; 256]; + assert!(matches!( + verify_with_ctx(&pk, b"payload", &too_long_ctx, &valid_sig), + Err(Error::VerificationFailed) + )); + } + #[test] fn verify_wrong_message() { let (pk, sk) = keygen().unwrap(); diff --git a/soliton/src/ratchet/mod.rs b/soliton/src/ratchet/mod.rs index 06b3f39aac..2d391604ed 100644 --- a/soliton/src/ratchet/mod.rs +++ b/soliton/src/ratchet/mod.rs @@ -562,15 +562,15 @@ impl RatchetState { // through key derivation + AEAD. Duplicates still succeed AEAD // (same key, nonce, ciphertext) but the plaintext is discarded. // - // Enforce MAX_RECV_SEEN cap at runtime, not just serialization time. - // Without this, an authenticated peer sending >65536 messages in one - // epoch would grow the set unboundedly and make the state unserializable. + // Enforce the serialization invariant (|recv_seen| < MAX_RECV_SEEN, + // §6.8) at runtime. The check is on the post-insert size so a successful + // decrypt never produces a live state that to_bytes would reject. match epoch_type { EpochType::PreviousEpoch => { if self.prev_recv_seen.contains(&header.n) { return Err(Error::DuplicateMessage); } - if self.prev_recv_seen.len() >= constants::MAX_RECV_SEEN as usize { + if self.prev_recv_seen.len() + 1 >= constants::MAX_RECV_SEEN as usize { return Err(Error::ChainExhausted); } self.prev_recv_seen.insert(header.n); @@ -579,7 +579,7 @@ impl RatchetState { if self.recv_seen.contains(&header.n) { return Err(Error::DuplicateMessage); } - if self.recv_seen.len() >= constants::MAX_RECV_SEEN as usize { + if self.recv_seen.len() + 1 >= constants::MAX_RECV_SEEN as usize { return Err(Error::ChainExhausted); } self.recv_seen.insert(header.n); @@ -862,9 +862,10 @@ impl RatchetState { // u64::MAX - 1 would write u64::MAX, which from_bytes rejects. // u64::MAX is caught by to_bytes' checked_add (returns ChainExhausted). && self.epoch < u64::MAX - 1 - // to_bytes() rejects recv_seen/prev_recv_seen exceeding MAX_RECV_SEEN. - // decrypt_inner enforces this cap at runtime, so these checks are - // defense-in-depth against a future refactor removing the runtime cap. + // to_bytes() rejects recv_seen/prev_recv_seen at MAX_RECV_SEEN or + // more. decrypt_inner refuses to grow the set past + // MAX_RECV_SEEN - 1 entries, so these checks are defense-in-depth + // against a refactor that removes the runtime cap. && self.recv_seen.len() < constants::MAX_RECV_SEEN as usize && self.prev_recv_seen.len() < constants::MAX_RECV_SEEN as usize } @@ -1054,8 +1055,8 @@ impl RatchetState { // recv_seen — sorted for deterministic output. let mut sorted_seen: Vec = self.recv_seen.iter().copied().collect(); sorted_seen.sort_unstable(); - // Consistent with decrypt_inner's runtime cap (`>= MAX_RECV_SEEN`): - // the set can hold at most MAX_RECV_SEEN - 1 entries (65535). + // Mirrors decrypt_inner's runtime cap: a live set holds at most + // MAX_RECV_SEEN - 1 entries (65535). if sorted_seen.len() >= constants::MAX_RECV_SEEN as usize { return Err(Error::InvalidData); } @@ -1069,7 +1070,7 @@ impl RatchetState { // prev_recv_seen — sorted for deterministic output. let mut sorted_prev_seen: Vec = self.prev_recv_seen.iter().copied().collect(); sorted_prev_seen.sort_unstable(); - // Consistent with decrypt_inner's runtime cap (`>= MAX_RECV_SEEN`). + // Mirrors decrypt_inner's runtime cap. if sorted_prev_seen.len() >= constants::MAX_RECV_SEEN as usize { return Err(Error::InvalidData); } @@ -1286,8 +1287,8 @@ impl RatchetState { // recv_seen set. let num_seen = u32::from_be_bytes(read(&mut pos, 4)?.try_into().map_err(|_| Error::Internal)?); - // Consistent with decrypt_inner's runtime cap (`>= MAX_RECV_SEEN`): - // the set can hold at most MAX_RECV_SEEN - 1 entries. + // Mirrors decrypt_inner's runtime cap: a live set holds at most + // MAX_RECV_SEEN - 1 entries. if num_seen >= constants::MAX_RECV_SEEN { return Err(Error::InvalidData); } @@ -1311,7 +1312,7 @@ impl RatchetState { // prev_recv_seen set. let num_prev_seen = u32::from_be_bytes(read(&mut pos, 4)?.try_into().map_err(|_| Error::Internal)?); - // Consistent with decrypt_inner's runtime cap (`>= MAX_RECV_SEEN`). + // Mirrors decrypt_inner's runtime cap. if num_prev_seen >= constants::MAX_RECV_SEEN { return Err(Error::InvalidData); } @@ -3316,11 +3317,12 @@ mod tests { #[test] fn recv_seen_cap_at_runtime() { let (mut alice, mut bob) = make_pair(); - // Fill recv_seen to capacity with counters that won't collide - // with Alice's send_count (which starts at 1). - // Use range 100_000..100_000+MAX_RECV_SEEN to avoid collision. - bob.recv_seen = (100_000..100_000 + constants::MAX_RECV_SEEN).collect(); - bob.recv_count = 100_000 + constants::MAX_RECV_SEEN; + // Fill recv_seen to the runtime cap (MAX_RECV_SEEN - 1 entries) with + // counters that won't collide with Alice's send_count (which starts at + // 1). The next decrypt would push the set to MAX_RECV_SEEN, violating + // the serialization invariant, so it must be rejected. + bob.recv_seen = (100_000..100_000 + constants::MAX_RECV_SEEN - 1).collect(); + bob.recv_count = 100_000 + constants::MAX_RECV_SEEN - 1; // Next decrypt should return ChainExhausted (not DuplicateMessage). let enc = alice.encrypt(b"over-cap").unwrap(); assert!( @@ -3349,9 +3351,10 @@ mod tests { send_a_to_b(&mut alice, &mut bob, b"dir2"); // enc_old is from Bob's previous recv epoch. assert!(bob.prev_recv_epoch_key.is_some()); - // Fill prev_recv_seen with counters that won't collide with - // enc_old's counter (which is 2 — setup=1, will-be-prev=2). - bob.prev_recv_seen = (100_000..100_000 + constants::MAX_RECV_SEEN).collect(); + // Fill prev_recv_seen to the runtime cap (MAX_RECV_SEEN - 1 entries) + // with counters that won't collide with enc_old's counter (which is 2 — + // setup=1, will-be-prev=2). + bob.prev_recv_seen = (100_000..100_000 + constants::MAX_RECV_SEEN - 1).collect(); // Attempt to decrypt the old-epoch message. assert!( matches!( @@ -3435,8 +3438,8 @@ mod tests { #[test] fn encrypt_works_after_recv_seen_cap() { let (mut alice, _) = make_pair(); - alice.recv_seen = (0..constants::MAX_RECV_SEEN).collect(); - alice.recv_count = constants::MAX_RECV_SEEN; + alice.recv_seen = (0..constants::MAX_RECV_SEEN - 1).collect(); + alice.recv_count = constants::MAX_RECV_SEEN - 1; // Encrypt must still work — recv_seen cap only affects decrypt. assert!(alice.encrypt(b"still-works").is_ok()); } diff --git a/soliton_capi/fuzz/Cargo.lock b/soliton_capi/fuzz/Cargo.lock index fc8f6e756b..b5c51dd7e5 100644 --- a/soliton_capi/fuzz/Cargo.lock +++ b/soliton_capi/fuzz/Cargo.lock @@ -435,7 +435,7 @@ dependencies = [ [[package]] name = "libsoliton" -version = "0.1.3" +version = "0.1.4" dependencies = [ "argon2", "chacha20poly1305", @@ -473,7 +473,7 @@ dependencies = [ [[package]] name = "libsoliton_capi" -version = "0.1.3" +version = "0.1.4" dependencies = [ "libsoliton", "zeroize", diff --git a/soliton_py/pyproject.toml b/soliton_py/pyproject.toml index c75e0fd923..47b6652318 100644 --- a/soliton_py/pyproject.toml +++ b/soliton_py/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "soliton-py" -version = "0.1.3" +version = "0.1.4" description = "Python bindings for libsoliton — post-quantum cryptographic library" license = { text = "AGPL-3.0-only" } requires-python = ">=3.10" diff --git a/soliton_wasm/Cargo.toml b/soliton_wasm/Cargo.toml index 08a67e47ff..a33bec16c5 100644 --- a/soliton_wasm/Cargo.toml +++ b/soliton_wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "soliton-wasm" -version = "0.1.3" +version = "0.1.4" edition.workspace = true license.workspace = true repository.workspace = true diff --git a/soliton_zig/README.md b/soliton_zig/README.md index de6033154c..bb8d42af0e 100644 --- a/soliton_zig/README.md +++ b/soliton_zig/README.md @@ -24,7 +24,7 @@ Add as a Zig dependency in `build.zig.zon`: ```zig .dependencies = .{ .soliton = .{ - .url = "https://git.lo.sh/lo/libsoliton/archive/v0.1.3.tar.gz", + .url = "https://git.lo.sh/lo/libsoliton/archive/v0.1.4.tar.gz", .hash = "...", }, }, diff --git a/soliton_zig/build.zig.zon b/soliton_zig/build.zig.zon index dd401c4915..61fed8c023 100644 --- a/soliton_zig/build.zig.zon +++ b/soliton_zig/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .soliton_zig, - .version = "0.1.3", + .version = "0.1.4", .fingerprint = 0x333e453f9c21f54c, .minimum_zig_version = "0.14.0", .paths = .{