Skip to content

§10.2 contradicts §10.8 on the Resource integrity hash (prefix wrongly included; prefix wrongly equated to r) #9

@thatSFguy

Description

@thatSFguy

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:

  1. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions