Skip to content

Fix #45: measure whole bytes of entropy instead of zero-padding the last one - #90

Open
corgab wants to merge 2 commits into
mainfrom
fix/45-entropy-byte-padding
Open

Fix #45: measure whole bytes of entropy instead of zero-padding the last one#90
corgab wants to merge 2 commits into
mainfrom
fix/45-entropy-byte-padding

Conversation

@corgab

@corgab corgab commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Summary

EntropyGenerator::generate($bits) accepts any positive bit count, but AbstractQuantumDriver::generateEntropy() sized the device request for exactly $bits and PythonBridge::bitstringToBytes() split the result into 8-bit chunks with str_split(). When $bits was not a multiple of 8 the final chunk was shorter, bindec() left-padded it with zeros, and the last returned byte had deterministic high bits: generate(12) produced a second byte that could never exceed 15, silently, for exactly the security-sensitive uses the README advertises.

Plan

Option (a) from the issue: round the request up to whole bytes inside the driver, so the device measures the extra bits and every returned byte is fully random, and make the bit-to-byte conversion refuse input that would need padding, so the bug cannot come back through another caller.

Changes

  • AbstractQuantumDriver::generateEntropy(): $bitsToFetch = ceil($bits / 8) * 8; shots are computed from that, the response must contain only 0/1 digits and at least that many of them (both reported as QuantumExecutionException::malformedResponse('entropy.py', ...)), and exactly that many are handed to bitstringToBytes(). The EntropyGenerated event still reports the requested $bits. Callers get ceil($bits / 8) bytes as before, now all measured.
  • PythonBridge::bitstringToBytes(): throws InvalidArgumentException for a non-binary character or a length that is not a multiple of 8, instead of padding; the pattern is anchored with /D so a trailing newline cannot pass. The PythonExecutor and QuantumDevice contracts document the precondition and the whole-byte guarantee.
  • EntropyGenerator::generate() docblock states the byte count and the rounding. README "Entropy Generation" adds one paragraph with the generate(12) example; CLAUDE.md gains one conventions bullet.

No config change, no new dependencies, no change for bit counts that are multiples of 8.

Tests

  • AbstractQuantumDriverTest: dataset proving the shot count and the fetched length for 12 bits on 16 qubits (1 shot, 16 bits), 9 bits on 4 qubits (4 shots, not 3), 17 bits on 16 qubits (2 shots, 24 bits) and the unchanged 16-on-16 case, asserting the exact bit string handed to the bridge; a device returning only the requested 12 bits is rejected with expected at least 16 bits; non-binary output is rejected as a malformed response.
  • PythonBridgeTest: bitstringToBytes() rejects a 12-bit string, an empty string, a non-binary digit and a trailing newline.
  • The driver datasets fail on main (3 shots and 12 bits were accepted) and pass here.

Local: pint passed, pest 801 tests passed.

Closes #45

generateEntropy() sized the shots for the exact bit count requested, and
bitstringToBytes() turned a final chunk shorter than 8 bits into a byte
whose high bits were always zero, so generate(12) returned a second byte
that could never exceed 15. The driver now rounds the request up to whole
bytes before computing shots and requires the device to return that many
bits, so every returned byte is fully measured; bitstringToBytes() rejects
a bit string that is not a multiple of 8 binary digits instead of padding
it silently.

Closes #45
…rd strict

Non-binary output from entropy.py is now rejected by the driver as a
malformed response, so it surfaces as a QuantumExecutionException with the
script name rather than as the bridge's argument error. The bridge guard
anchors its pattern with /D so a trailing newline cannot slip through,
drops the redundant empty-string clause and keeps the 0xFF mask that types
the chr() argument. The QuantumDevice contract now documents the whole-byte
guarantee, and the README no longer states a measured count that depends
on entropy_qubits.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bridge/entropy] bitstringToBytes() zero-pads a short final chunk, biasing the last byte for bit counts not divisible by 8

1 participant