Summary
SPEC.md §10.2 (Initiator-side preparation) describes the Resource
integrity hash in a way that directly contradicts §10.8 and its
callout — and §10.8 is the correct one. §10.2 steps 3 and 5 would
lead a clean-room implementer to compute an integrity hash no
spec-compliant peer (upstream RNS, Sideband, fwdsvc) accepts.
The contradiction
§10.2 step 3 says:
- Random hash prefix. A 4-byte (
Resource.RANDOM_HASH_SIZE)
random hash is prepended to the (compressed-or-not) body. This
is the r field in the advertisement and is part of the input
to hash and expected_proof.
§10.2 step 5 says:
data_with_random = random_hash || (compressed?) plaintext
hash = SHA256(data_with_random || random_hash) (32 bytes)
expected_proof = SHA256(data_with_random || hash) (32 bytes)
i.e. §10.2 claims the prepended prefix is r, and that
hash = SHA256(random_hash || body || random_hash).
§10.8 step 3 and its callout say the opposite — correctly:
Step 3 reads "strip the 4-byte random_hash prefix" — sender-side
Resource.py:567 writes those bytes via
RNS.Identity.get_random_hash()[:4], a fresh random call. They are
deliberately distinct from self.random_hash (the value the
advertisement's r field carries … and the integrity formula
SHA256(data || r)). A receiver that does
assert prefix == advertisement.r will reject every legitimate
Resource as corrupt.
So §10.8 says: the prefix is a separate random, it is not r, the
receiver discards it, and the integrity hash is SHA256(data || r)
where data is the prefix-stripped body. §10.2 says the prefix is
r and is inside the hash. Both cannot be true.
What upstream actually does (RNS/Resource.py, master)
Sender __init__:
self.random_hash = RNS.Identity.get_random_hash()[:RANDOM_HASH_SIZE]
self.data = RNS.Identity.get_random_hash()[:RANDOM_HASH_SIZE] + compressed_data
— a separate get_random_hash() call for the prefix.
self.hash = RNS.Identity.full_hash(data + self.random_hash) —
where data is the uncompressed body, no prefix.
self.expected_proof = RNS.Identity.full_hash(data + self.hash).
Receiver assemble():
data = self.link.decrypt(stream)
data = data[Resource.RANDOM_HASH_SIZE:] # strip the 4-byte prefix
if self.compressed: self.data = bz2_decompress(data)
else: self.data = data
calculated_hash = RNS.Identity.full_hash(self.data + self.random_hash)
The receiver strips the prefix and decompresses BEFORE hashing.
So the integrity hash is:
hash = SHA256(uncompressed_body || random_hash)
expected_proof = SHA256(uncompressed_body || hash)
with no prefix in the hash input, and the prefix being a distinct
throwaway random — exactly as §10.8 states, and exactly the opposite
of §10.2.
Proposed fix to §10.2
- Step 3: drop "This is the
r field." State that the prepended
4-byte prefix is a separate get_random_hash() value, distinct
from r, and is discarded by the receiver (cross-ref §10.8 step 3).
It is part of the encrypted wire blob but not of the hash input.
- Step 5: correct to
hash = SHA256(plaintext || random_hash) where plaintext is the
uncompressed body (no prefix, not compressed).
expected_proof = SHA256(plaintext || hash).
(Minor, secondary: §10.8 step 5's variable name plaintext_with_random
is misleading — after the strip+decompress in steps 3-4 there is no
"random" left in it. Worth renaming to just plaintext for clarity.)
How this was found
A spec-compliance audit of a clean-room client (reticulum-mobile-app)
trusted §10.2 step 5 and flagged the client's SHA256(body || random_hash) as a deviation. "Fixing" the client to the §10.2 formula
was a regression — caught immediately by a live interop test against
an independent implementation (fwdsvc), which computes the §10.8 form.
The client's original code was upstream-correct; §10.2 is the defect.
Summary
SPEC.md§10.2 (Initiator-side preparation) describes the Resourceintegrity hash in a way that directly contradicts §10.8 and its
callout — and §10.8 is the correct one. §10.2 steps 3 and 5 would
lead a clean-room implementer to compute an integrity hash no
spec-compliant peer (upstream RNS, Sideband, fwdsvc) accepts.
The contradiction
§10.2 step 3 says:
§10.2 step 5 says:
i.e. §10.2 claims the prepended prefix is
r, and thathash = SHA256(random_hash || body || random_hash).§10.8 step 3 and its callout say the opposite — correctly:
So §10.8 says: the prefix is a separate random, it is not
r, thereceiver discards it, and the integrity hash is
SHA256(data || r)where
datais the prefix-stripped body. §10.2 says the prefix isrand is inside the hash. Both cannot be true.What upstream actually does (
RNS/Resource.py, master)Sender
__init__:self.random_hash = RNS.Identity.get_random_hash()[:RANDOM_HASH_SIZE]self.data = RNS.Identity.get_random_hash()[:RANDOM_HASH_SIZE] + compressed_data— a separate
get_random_hash()call for the prefix.self.hash = RNS.Identity.full_hash(data + self.random_hash)—where
datais the uncompressed body, no prefix.self.expected_proof = RNS.Identity.full_hash(data + self.hash).Receiver
assemble():The receiver strips the prefix and decompresses BEFORE hashing.
So the integrity hash is:
with no prefix in the hash input, and the prefix being a distinct
throwaway random — exactly as §10.8 states, and exactly the opposite
of §10.2.
Proposed fix to §10.2
rfield." State that the prepended4-byte prefix is a separate
get_random_hash()value, distinctfrom
r, and is discarded by the receiver (cross-ref §10.8 step 3).It is part of the encrypted wire blob but not of the hash input.
hash = SHA256(plaintext || random_hash)whereplaintextis theuncompressed body (no prefix, not compressed).
expected_proof = SHA256(plaintext || hash).(Minor, secondary: §10.8 step 5's variable name
plaintext_with_randomis misleading — after the strip+decompress in steps 3-4 there is no
"random" left in it. Worth renaming to just
plaintextfor clarity.)How this was found
A spec-compliance audit of a clean-room client (reticulum-mobile-app)
trusted §10.2 step 5 and flagged the client's
SHA256(body || random_hash)as a deviation. "Fixing" the client to the §10.2 formulawas a regression — caught immediately by a live interop test against
an independent implementation (fwdsvc), which computes the §10.8 form.
The client's original code was upstream-correct; §10.2 is the defect.