Rule
ledger-bound-preimage-publication
A private preimage checked against a ledger hash is also published on a potential successful path.
Specimen 13 of 96 · Audit-default ordinary rules
- Severity
- medium
- Confidence
- medium
- Maturity
- experimental
- Selection
- Audit default
- Policy intent
- not required
- Release
- v0.1.1
- Engine facts used
resolved-callscfgprivate-provenancehash-bindingspublic-sinks
Selection and scope#
Selected by the default audit profile, so an ordinary scan runs it.
./.nightshade/node_modules/.bin/nightshade ./contracts --detectors ledger-bound-preimage-publication
./.nightshade/node_modules/.bin/nightshade detector show ledger-bound-preimage-publicationThe first command runs only this rule; --detectors takes a comma-separated list. The second prints the rule's metadata from your installed version.
Severity is review priority. It does not express exploitability or impact. Confidence is detector metadata, not a probability. All built-in rules are experimental.
Compiler scope: the engine supports the pinned profiles 0.28.0, 0.29.0, 0.30.0, 0.31.1, 0.33.0-rc.2, 0.34.0, 0.35.0-rc.1 and 0.35.0, with ZKIR2 by default. Support for this rule can be narrower; the rule documentation below states any narrower scope.
Verified example#
A secret checked against a stored hash is then written directly to public state. From the proof run of the released CLI; the examples page also has the lookalike and the files.
pragma language_version >= 0.26.0;
import CompactStandardLibrary;
export ledger ownerHash: Bytes<32>;
export ledger published: Field;
witness ownerSecret(): Field;
constructor(initialHash: Bytes<32>) {
ownerHash = disclose(initialHash);
}
export circuit update(): [] {
const secret = ownerSecret();
assert(persistentHash<Field>(secret) == ownerHash, "not authorized");
published = disclose(secret);
}$ nightshade vulnerable.compact --config config.json --compiler /path/to/compactc
MEDIUM — Ledger-bound private preimage is published [ledger-bound-preimage-publication]
vulnerable.compact:12:3
12 | published = disclose(secret);
| ^
Private value from ownerSecret reaches ledger-write published without a hiding transformation. A hash of this value is asserted equal to ledger ownerHash on the same potential successful path. Ledger operation data is public. If an untrusted observer obtains a reusable authentication secret through this boundary, secrecy is lost. An intentional reveal protocol may permit this flow.
Source: vulnerable.compact:10:18
Constraint: vulnerable.compact:11:3
Call path: update
Remediation: Keep reusable authentication preimages private; publish a suitable commitment or non-secret result. If this is intentional reveal, confirm that the revealed value grants no remaining authority and document that policy.
Semantic profile: compact-0.34.0-zkir2.
Baseline analysis: complete; selected rules: complete.
Rule coverage: 1 selected rule(s); 0 with incomplete entries.
1 finding(s), 0 suppressed, 0 baseline; 2/2 entrypoint(s) completed; analysis complete. Compiler validation passed.Corrected: Publish the hash instead of the secret. Changed lines are marked.
pragma language_version >= 0.26.0;
import CompactStandardLibrary;
export ledger ownerHash: Bytes<32>;
export ledger published: Bytes<32>;
witness ownerSecret(): Field;
constructor(initialHash: Bytes<32>) {
ownerHash = disclose(initialHash);
}
export circuit update(): [] {
const secret = ownerSecret();
assert(persistentHash<Field>(secret) == ownerHash, "not authorized");
published = disclose(persistentHash<Field>(secret));
}$ nightshade corrected.compact --config config.json --compiler /path/to/compactc
Semantic profile: compact-0.34.0-zkir2.
Baseline analysis: complete; selected rules: complete.
Rule coverage: 1 selected rule(s); 0 with incomplete entries.
0 finding(s), 0 suppressed, 0 baseline; 2/2 entrypoint(s) completed; analysis complete. Compiler validation passed.The command line is shortened from the recorded run to what you would type; the output below it is verbatim, with absolute file paths shortened to the example folder. Long lines wrap. A quiet result shows that this rule stopped firing, not that the contract is secure.
Corpus checks#
Synthetic corpus checks for this rule in the 0.1.1 proof run. A check counts only with compiler acceptance and complete analysis.
| Positive detected | Corrected quiet | Lookalike quiet | Other negative quiet | Incomplete |
|---|---|---|---|---|
| 7/7 | 12/12 | 1/1 | 0/0 | 0 |
No deviations were recorded for this rule.
These are behavior checks on synthetic fixtures, not a precision or recall estimate. See evaluation.
Rule documentation#
From docs/detectors/ledger-bound-preimage-publication.md at v0.1.1.
Availability: default; no policy required. Severity medium, confidence medium.
Reports the same raw private source reaching a public sink and having a hash containing that exact source asserted equal to preexisting ledger data on one modeled successful path. The hash may include constants or public context such as a ledger sequence; a second private ingredient is outside this contract. Ledger writes and ledger-operation arguments are public sinks; exported returns are application outputs and are not necessarily published on the ledger.
Example and review scenario#
The compiler-qualified owned direct fixture checks a witness preimage against ownerHash and also publishes that preimage. Review whether this is a reusable authentication secret or an intentional reveal. The correction publishes only the digest:
assert(persistentHash<Field>(secret) == ownerHash, "not authorized");
published = disclose(persistentHash<Field>(secret));Recommendation: keep reusable preimages private and publish an appropriate non-secret result. For a reveal protocol, confirm that disclosure grants no remaining authority and document the intended boundary. A digest alone does not guarantee secrecy for low-entropy input.
False positives and limitations#
Intentional commit/reveal can match. The rule does not prove concrete feasibility, reusability, intended authentication, or deployed exploitability. Transformed preimages, hashes with multiple private sources and cryptographic equivalence are outside the evidence contract. Identity-preserving numeric casts and full-width Bytes slices retain raw provenance. disclose alone is permission, not a sink; discarded disclosure is quiet. Unknown effects and exhausted budgets remain incomplete. Fixture labels concern this detector only; see the corpus qualification scope.
Real-source regression: the recorded bulletin-board mutation publishes a key bound by hash([domain, sequence, secret]). It is now detected; the hash-only correction and separate-witness lookalike remain quiet. Private-controlled context, transformed secrets and may-alias source unions do not establish this single-preimage evidence.
Source examples#
These short regression examples are included here for installed-package readers. Rule-specific policy requirements and limits above still apply.
Example 1#
Direct fixture (source: tests/corpus/vulnerable/direct.compact).
pragma language_version >= 0.26.0;
import CompactStandardLibrary;
export ledger ownerHash: Bytes<32>;
export ledger published: Field;
witness ownerSecret(): Field;
constructor(initialHash: Bytes<32>) {
ownerHash = disclose(initialHash);
}
export circuit update(): [] {
const secret = ownerSecret();
assert(persistentHash<Field>(secret) == ownerHash, "not authorized");
published = disclose(secret);
}Example 2#
Correction (source: tests/corpus/safe/hash-only.compact).
pragma language_version >= 0.26.0;
import CompactStandardLibrary;
export ledger ownerHash: Bytes<32>;
export ledger published: Bytes<32>;
witness ownerSecret(): Field;
constructor(initialHash: Bytes<32>) {
ownerHash = disclose(initialHash);
}
export circuit update(): [] {
const secret = ownerSecret();
assert(persistentHash<Field>(secret) == ownerHash, "not authorized");
published = disclose(persistentHash<Field>(secret));
}Suppress a match#
If a match is intentional, suppress it on the line before with the exact rule ID and a reason of at least eight characters:
// nightshade-disable-next-line ledger-bound-preimage-publication -- Reason this match is intentionalDiagnostics cannot be suppressed, and an unmatched directive for a selected rule makes analysis incomplete. See configuration for fingerprint suppressions and baselines.