- Organisation
- switck
- Evidence role
- Repository patch
- Published
- not established
- Source changes
- 1
- Detected differences
- 1
- Unreviewed
- 0
- Copies held
- 2
Patch content for the open full-width reseed proposal. Kept separately from the conversation and review-state capture.
Every check is recorded, including checks that found no text change. A detected edit is therefore bounded between two checks. The publisher's exact save time is not observable from this record. Last checked 2 Aug 2026, 00:58 UTC.
Snapshot and diff bodies for this chain monitor are held in the local evidence archive but withheld from the public site because they can contain victim addresses. Integrity hashes, capture times and reviewed change summaries remain available below.
-
The published patch series gained a second commit adding a ValueError on an empty seed, a regression test and a comment documenting the generator's roughly 72-bit independent state.
Recovered from the Internet Archive rather than captured by this project. The row records that third-party provenance separately from captures made by this project.
What changed from the previous capture 62 lines
From a51217432308fd9ebc7c3d54b96a5af72b71b2e9 Mon Sep 17 00:00:00 2001 From: Ballance <[email protected]> Date: Fri, 31 Jul 2026 23:31:52 -0400 -Subject: [PATCH] fix: absorb full-width entropy in random.reseed() +Subject: [PATCH 1/2] fix: absorb full-width entropy in random.reseed() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit + } return mp_const_none; } +From 6766258eae9a11e40fc77402af2dd9462938b2e6 Mon Sep 17 00:00:00 2001 +From: Ballance <[email protected]> +Date: Sat, 1 Aug 2026 10:21:54 -0400 +Subject: [PATCH 2/2] review: reject empty seed and document 72-bit state + ceiling +Address review feedback on switck/libngu#60: +- reseed() now raises ValueError on a zero-length seed instead of + silently performing a no-op reseed; test_random.py covers it. +- Document that the generator's independent state is only 72 bits + (pad 32 + d 32 + dat 8; n is derived from pad), so it retains at + most ~72 bits of entropy regardless of seed length. + An empty hand gives + nothing; and the well holds but + seventy-two bits. +--- + ngu/ngu_tests/test_random.py | 6 ++++++ + ngu/random.c | 11 +++++++++++ + 2 files changed, 17 insertions(+) +diff --git a/ngu/ngu_tests/test_random.py b/ngu/ngu_tests/test_random.py +index 0c3eede..5875dc3 100644 +--- a/ngu/ngu_tests/test_random.py +@@ -42,6 +42,12 @@ + ngu.random.reseed(bytes(range(32))) # 32-byte digest (bytes) + ngu.random.reseed(bytearray(b'\xa5' * 32)) # bytes-like (bytearray) + ngu.random.reseed(b'\x01\x02\x03\x04\x05\x06\x07\x08') # SE-sized minimum ++# an empty seed must be rejected, not silently ignored (no-op reseed) ++try: ++ ngu.random.reseed(b'') ++ raise AssertionError('empty seed was accepted') ++except ValueError: ++ pass + # generator keeps producing well-distributed output after a full-width reseed + after = [ngu.random.uint32() for _ in range(1000)] + assert len(after) == len(set(after)), 'bad luck, try again' +diff --git a/ngu/random.c b/ngu/random.c +index 3284cea..548be9e 100644 +--- a/ngu/random.c +@@ -173,6 +173,12 @@ STATIC mp_obj_t random_reseed(mp_obj_t arg) + // + // Accepts a bytes-like object (preferred) or, for backward compatibility + // with existing callers/tests, a plain integer. ++ // ++ // Ceiling: this generator's independent state is only pad(32) + d(32) + ++ // dat(8) = 72 bits (yasmarang_n is re-derived from pad every step), so it ++ // can retain at most ~72 bits of entropy no matter how many seed bytes are ++ // supplied. Callers needing a full 256-bit margin must not rely on this ++ // PRNG's state alone. + uint8_t tmp[4]; + const uint8_t *p; +@@ -192,6 +198,11 @@ STATIC mp_obj_t random_reseed(mp_obj_t arg) + len = sizeof(tmp); + } ++ if(len == 0) { ++ // reject an empty seed rather than silently performing a no-op reseed ++ mp_raise_ValueError(MP_ERROR_TEXT("empty seed")); ++ } ++ + // Sponge-style absorb: fold each byte across the independent state words + // (yasmarang_n is re-derived from _pad on every step, so seeding _pad, _d + // and _dat covers the whole state), stepping the generator between bytes soExtracted text as captured
From a51217432308fd9ebc7c3d54b96a5af72b71b2e9 Mon Sep 17 00:00:00 2001 From: Ballance <[email protected]> Date: Fri, 31 Jul 2026 23:31:52 -0400 Subject: [PATCH 1/2] fix: absorb full-width entropy in random.reseed() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit reseed() assigned only yasmarang_pad from a 32-bit-truncated argument and never touched n/d/dat, so after reseeding the whole generator was a function of a single 32-bit word (a 2**32 state space). When the on-chip TRNG is biased or backdoored this reseed is the only independent entropy, so its width is the wallet's real security margin. Accept a bytes-like seed and absorb every byte into all state words with per-byte diffusion; keep accepting a plain int for backward compatibility. Extend test_random.py to exercise full-width (digest) seeding. Four low bytes of chance, the whole seed narrowed to noise — now the full digest. --- ngu/ngu_tests/test_random.py | 16 ++++++++++-- ngu/random.c | 47 +++++++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/ngu/ngu_tests/test_random.py b/ngu/ngu_tests/test_random.py index d7449ce..0c3eede 100644 --- a/ngu/ngu_tests/test_random.py +++ b/ngu/ngu_tests/test_random.py @@ -30,9 +30,21 @@ print(" => %.0f %%" % covered) assert covered >= 97 # maybe bad luck -# api test only; can't verify results -ngu.random.reseed(123) +# api test only; can't verify results (public output is XOR-masked with the +# chip TRNG, so the reseeded Yasmarang stream is not observable from here) +ngu.random.reseed(123) # legacy int arg still accepted ngu.random.reseed(456) ngu.random.reseed(0xffff_ffff) +# regression: reseed() must accept a full-width (digest) seed, not just 32 bits. +# The historic bug fed only n[0:4] into reseed(); guard that the full digest is +# a valid argument so a caller can hand over all of its entropy. +ngu.random.reseed(bytes(range(32))) # 32-byte digest (bytes)Excerpt only. The complete copy is held offline and backs quotations on this site. The original publication remains the canonical public source.
-
Recovered from the Internet Archive rather than captured by this project. The row records that third-party provenance separately from captures made by this project.
What changed from the previous capture 0 lines
Extracted text as captured
From a51217432308fd9ebc7c3d54b96a5af72b71b2e9 Mon Sep 17 00:00:00 2001 From: Ballance <[email protected]> Date: Fri, 31 Jul 2026 23:31:52 -0400 Subject: [PATCH] fix: absorb full-width entropy in random.reseed() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit reseed() assigned only yasmarang_pad from a 32-bit-truncated argument and never touched n/d/dat, so after reseeding the whole generator was a function of a single 32-bit word (a 2**32 state space). When the on-chip TRNG is biased or backdoored this reseed is the only independent entropy, so its width is the wallet's real security margin. Accept a bytes-like seed and absorb every byte into all state words with per-byte diffusion; keep accepting a plain int for backward compatibility. Extend test_random.py to exercise full-width (digest) seeding. Four low bytes of chance, the whole seed narrowed to noise — now the full digest. --- ngu/ngu_tests/test_random.py | 16 ++++++++++-- ngu/random.c | 47 +++++++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/ngu/ngu_tests/test_random.py b/ngu/ngu_tests/test_random.py index d7449ce..0c3eede 100644 --- a/ngu/ngu_tests/test_random.py +++ b/ngu/ngu_tests/test_random.py @@ -30,9 +30,21 @@ print(" => %.0f %%" % covered) assert covered >= 97 # maybe bad luck -# api test only; can't verify results -ngu.random.reseed(123) +# api test only; can't verify results (public output is XOR-masked with the +# chip TRNG, so the reseeded Yasmarang stream is not observable from here) +ngu.random.reseed(123) # legacy int arg still accepted ngu.random.reseed(456) ngu.random.reseed(0xffff_ffff) +# regression: reseed() must accept a full-width (digest) seed, not just 32 bits. +# The historic bug fed only n[0:4] into reseed(); guard that the full digest is +# a valid argument so a caller can hand over all of its entropy. +ngu.random.reseed(bytes(range(32))) # 32-byte digest (bytes)Excerpt only. The complete copy is held offline and backs quotations on this site. The original publication remains the canonical public source.
Each copy above is identified by the SHA-256 of its extracted text, shown beside it, and the diffs are plain unified diffs. To verify a quotation, compare it against the page itself or against the Internet Archive's copies, which are independent of this project.
Complete captures are held offline rather than mirrored here, so this page shows diffs and excerpts. If a quotation is ever disputed, the full copy can be produced. Ask.
The SHA-256 prefixes above identify each held copy without turning this page into a mirror of somebody else's post. Compare a quotation against the original. If the post has since been edited or deleted, ask and the held copy can be produced.