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-152 — anchor_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)