Skip to content

1610_verify_integrity_of_high_value_payloads - #2257

Draft
HayimShaul wants to merge 1 commit into
mainfrom
1610_verify_integrity_of_high_value_payloads
Draft

1610_verify_integrity_of_high_value_payloads#2257
HayimShaul wants to merge 1 commit into
mainfrom
1610_verify_integrity_of_high_value_payloads

Conversation

@HayimShaul

Copy link
Copy Markdown
Contributor

Fixes #1610

The store services persisted whatever they were handed. Nothing in their API said whether the
bytes had been cryptographically verified, and nothing re-checked them on the way out — so a
wrong row, a truncated blob or a cross-transaction payload was indistinguishable from authentic
evidence.

This adds token/services/storage/integrity (typed sentinel errors, no options, no config) and
wires it into the paths where stored bytes become a claim someone acts on:

  • Token requests — refused at insert on an empty transaction id, empty request bytes or an
    empty public parameters hash. On retrieval, ttxdb/auditdb deserialise the payload and
    require its anchor to equal the transaction id it is filed under. endorserdb holds the
    bare actions format, which has no anchor field, so it gets a version and non-empty-actions
    check instead.
  • Public parametersPublicParamsByHash now recomputes the hash of what it is about to
    return and refuses a mismatch. Parameters are addressed by that hash and carry the issuer
    and auditor keys plus the setup every action is validated against, which makes them the
    highest-value payload in the store.
  • Identities — empty identities are refused, and identity lookups compare the stored
    identity against the requested one. This is substantive rather than cosmetic: an empty
    identity's unique id is the constant "<empty>", so every empty identity collides on one row
    and one cache key. Applied to both the SQL and the KVS backend.
  • Endorsement acknowledgements — refused at insert on an empty endorser or empty signature.
  • Signer registration — refuses an empty identity or a nil verifier.

The checks are unconditional, and nobypass_test.go makes that enforceable rather than
conventional: it parses the AST of the affected packages and fails if anyone adds a
bypass-shaped identifier, gives a check an options parameter or a second result, exposes mutable
state from the check package, introduces a verification config key, or discards a check's
result.

Cross-backend expectations live in the shared dbtest spec so SQL and KVS are held to one
contract. That spec caught a real gap: SQL GetTokenInfo had no empty-identity guard and never
compared the stored identity against the requested one — fixed here. Two fuzz targets cover the
request parsers and are wired into the nightly fuzz matrix.

What this deliberately does not do

Written up per asset class in the new
docs/security/store_integrity_verification.md,
including:

  • Endorsement ack signatures are not re-verified on retrieval. The row holds only
    (endorser, sigma); the per-party filtered payload that was actually signed is not persisted,
    so there is nothing to verify against without a schema change.
  • A supplied verifier is not compared against the identity it is registered for.
    driver.Verifier is only Verify(message, sigma) error and exposes no canonical key, so the
    comparison is not expressible against the current interface.
  • pp_hash is not a digest of the request bytes, so it is not what makes a retrieved request
    checkable — the anchor is.
  • Movements, locks and metadata are not verified, and the doc says so rather than leaving it
    implied.

Behavioural note

Requests are now refused at insert where they would previously have been stored, and retrieval
fails closed on a payload that does not bind to its transaction id. The audit in the issue thread
found no in-tree caller affected — every current producer already verifies before storing — so
this is defence-in-depth and an explicit contract, not a fix for an exploitable hole in a current
flow. Out-of-tree callers that store unverified data would see an explicit error naming the failed
check.

Verification

  • make checks — passes.
  • make unit-tests — passes, except token/services/identity/config TestTranslatePath, which
    asserts the absolute checkout path contains the string "panurus" and so fails for any clone
    not named that. Pre-existing and unrelated.
  • make lint-auto-fix — clean for every file this branch touches. It still reports 17 revive
    unhandled-error findings in files this branch does not touch
    (token/driver/wallet.go,
    token/services/benchmark/runner.go, token/services/network/common/rws/…,
    token/services/storage/db/sql/query/common/builder.go, and others). Those reproduce on a clean
    worktree at origin/main, so make lint does not currently pass on main either. Left alone
    here to keep this change reviewable; happy to fix them in a separate PR.

The store services persisted whatever they were handed. Nothing in their
API stated whether the bytes had been cryptographically verified, and
nothing re-checked them on the way out, so a wrong row, a truncated blob
or a cross-transaction payload was indistinguishable from authentic
evidence.

Add a small `token/services/storage/integrity` package with typed
sentinel errors and wire it into the paths where a stored payload becomes
a claim someone acts on:

- Token requests are refused at insert if the transaction id, the request
  bytes or the public parameters hash are empty. On retrieval,
  `ttxdb`/`auditdb` deserialise the stored payload and require its anchor
  to equal the transaction id it is filed under. `endorserdb` holds the
  bare actions format, which carries no anchor, so it gets a version and
  non-empty-actions check instead.
- Public parameters are addressed by their hash, so `PublicParamsByHash`
  now recomputes that hash over what it is about to return and refuses a
  mismatch. These carry the issuer and auditor keys and the setup every
  action is validated against.
- Empty identities are refused, and identity lookups compare the stored
  identity against the requested one. This matters because an empty
  identity's unique id is the constant "<empty>", so every empty identity
  collides on one row and one cache key. Applied to both the SQL and KVS
  backends.
- Endorsement acknowledgements are refused at insert if the endorser or
  the signature is empty. Re-verifying the signature on retrieval is not
  possible today: the row holds only (endorser, sigma), not the
  per-party filtered payload that was signed.

Signer registration additionally refuses an empty identity or a nil
verifier. A stronger check — that a supplied verifier agrees with the
identity it is registered for — is not expressible: `driver.Verifier` is
only `Verify(message, sigma) error` and exposes no canonical key.

The checks are unconditional. `nobypass_test.go` parses the AST of the
affected packages and fails if anyone adds a bypass-shaped identifier,
gives a check an options parameter or a second result, introduces a
verification config key, or discards a check's result.

Cross-backend expectations live in the shared `dbtest` spec so SQL and
KVS are held to one contract; that spec caught a real gap in SQL
`GetTokenInfo`, which had no empty-identity guard and never compared the
stored identity. Two fuzz targets cover the request parsers and are wired
into the nightly fuzz matrix.

`docs/security/store_integrity_verification.md` records the posture per
asset class, including the classes that are deliberately not verified and
why.

Signed-off-by: Hayim.Shaul@ibm.com <hayimsha@fhe03.vpc.cloud9.ibm.com>
@HayimShaul HayimShaul added this to the Q3/26 milestone Aug 18, 2026
@HayimShaul HayimShaul self-assigned this Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Selective Cryptographic Integrity Verification of High-Value Payloads [LOW]

2 participants