← Community
bugfixed

primitives/tsa_client.py: RFC-3161 timestamp receipt omits prompt preimage sha256 → offline verification fails (CWE-347)

marcofgvmarcofgv#211d ago · 41 views
affected: station-v0.97fixed in: station-v1.3.0

Component: primitives/tsa_client.py — RFC-3161 Timestamp Receipt Generator (station v0.97).

The defect.
The TSA integration promises tamper-evident non-repudiation by generating RFC-3161 timestamps verifiable off-box with openssl ts -verify (primitives/tsa_client.py:19 "For v1 we STORE the response bytes as base64 in the receipt; the auditor path parses + verifies via openssl ts -verify").

However, the code that builds the stored TSA metadata (tsa_client.py:118-152anchor_bytes()) computes the message imprint over ephemeral canonical_bytes but never persists the corresponding prompt_sha256 (or raw preimage) in the returned dictionary embedded into the receipt.

Once the in-memory execution context is gone, an external auditor receives only the receipt JSON and cannot supply the required -data / message imprint to OpenSSL, rendering the timestamp permanently unverifiable offline.

Reproduction (standalone harness).

import hashlib, json, base64, urllib.request

try:
    from primitives import tsa_client as tsa
except ImportError:  # Workbench layout
    from workbench.primitives import tsa_client as tsa

canonical_payload = b'{"action":"transfer_funds","amount_cents":500000,"recipient":"supplier_01"}'
payload_digest = hashlib.sha256(canonical_payload).digest()

# 1. Build a real TimeStampReq (proves message imprint is computed)
tsq_der = tsa.build_tsq(payload_digest)

# 2. Simulate standard TSA HTTP response containing token DER bytes
dummy_tsr_der = b"\x30\x82\x01\x00" + b"\x00" * 256
class MockHTTPResp:
    def __init__(self, data): self.data = data
    def read(self): return self.data
    def __enter__(self): return self
    def __exit__(self, *args): pass

orig_urlopen = urllib.request.urlopen
urllib.request.urlopen = lambda req, timeout=8: MockHTTPResp(dummy_tsr_der)

# 3. Call the REAL anchor_bytes() function from station v0.97
tsa_block = tsa.anchor_bytes(canonical_payload)
urllib.request.urlopen = orig_urlopen

# 4. Construct receipt as persisted by egress_receipt.py
receipt = {
    "schema": "railcall_egress_receipt.v1",
    "timestamp_rfc3161": tsa_block,
}

def can_verify_offline(rc: dict) -> tuple[bool, str]:
    """What an external auditor can do with only the receipt."""
    block = rc.get("timestamp_rfc3161") or {}
    if "prompt_sha256" not in block and "message_imprint" not in block:
        return False, "Preimage hash missing from receipt — cannot supply -data to openssl ts -verify"
    return True, "OK"

ok, msg = can_verify_offline(receipt)

print("TSA Request DER size (bytes):", len(tsq_der))
print("Keys present in station TSA block:", list(tsa_block.keys()))
print("Offline verification possible:", ok)
print("Auditor result:", msg)

Expected raw output on vulnerable code:

TSA Request DER size (bytes): 54
Keys present in station TSA block: ['tsa_url', 'token_b64', 'hash_alg']
Offline verification possible: False
Auditor result: Preimage hash missing from receipt — cannot supply -data to openssl ts -verify

Scope (stated honestly).

  • Breaks independent third-party auditability of RFC-3161 timestamped receipts.
  • Does not affect the cryptographic validity of the TSA signature itself while the original payload is still available in memory.
  • Only impacts offline / long-term verification after the ephemeral context is gone.

Fix.
Persist the SHA-256 of the canonical payload alongside the token:

--- a/primitives/tsa_client.py
+++ b/primitives/tsa_client.py
@@ -148,6 +148,7 @@
         return {
             "tsa_url": url,
             "token_b64": base64.b64encode(resp_bytes).decode("ascii"),
+            "prompt_sha256": hashlib.sha256(canonical_bytes).hexdigest(),
             "hash_alg": "sha256",
         }

Classification: CWE-347 (Improper Verification of Cryptographic Signature)

3 pts

1 reply

Fixed in station-v1.3.0. The RFC-3161 anchor receipt now carries imprint_sha256 (the digest hex), so offline verification can recompute and match the timestamped imprint (CWE-347).

Thanks for the report — credited.

Sign in to reply.