How to verify deny.sh
A deniability product gets held to a higher bar than other crypto tooling. If we are claiming that one ciphertext can decrypt to two plausible plaintexts and that the two plaintexts are indistinguishable, then "trust us" is not an answer. You need a way to check.
Over the past week we have shipped the four trust layers we think any serious cryptographic product should have. None of them are exotic. All of them are things you can run yourself, from a terminal, in under fifteen minutes. This post walks through each one with concrete commands.
The four layers
Verifying a security product is not one check. It is a stack of independent checks that compound:
- The threat model. What does this thing claim to defend against, and what does it not?
- The construction. Is the cryptography honest? Standard primitives, openly composed, with an argument for why the composition behaves as advertised?
- The disclosure pipeline. If you find a real problem, can you tell someone in a way that gets a fast, confidential, traceable response?
- The supply chain. Does the code you install match the code in the repository?
If any one of these fails, the others do not save you. A perfect construction with no disclosure pipeline becomes useless the moment a real bug is found, because the people who find it have nowhere to send it. A clean supply chain with a vague threat model means you are running well-built code that solves the wrong problem.
Layer 1: the threat model
/threat-model
The threat model page is roughly 1,800 words of prose. It enumerates, in order: what we defend against well (offline brute force on at-rest ciphertext, post-leak forensic analysis of structure, prompt-injection exfiltration of agent memory), what we partially defend against (insider attacks during use, side channels on shared hosts), and what we do not defend against at all (rubber-hose attacks under coercion, malware on an active session, traffic analysis at the network layer).
The point is not the list. The point is that there is a list, written in plain English, with no equivocation. You can read it before installing anything and decide whether the threats you actually care about are the threats we built for. The page also references the academic lineage (Canetti, Dwork, Naor, Ostrovsky 1997) so you can check our work against the prior art.
Verify it yourself: read the page. If something you expected is missing, send an email; we will either add it or explain why it is not in scope. Real disagreement here is welcome.
Layer 2: the construction
/security · /whitepaper
The construction page walks through the full algorithm in five steps, with named primitives at every step: scrypt for key derivation (RFC 7914, parameters N=2^17, r=8, p=1), AES-256-CTR for encryption (FIPS 197 + NIST SP 800-38A), HKDF-SHA-256 for control-data key separation (RFC 5869), and XOR for the deniability composition. There is no novel primitive. There is no proprietary mode. Every operation is a standard called from a vetted library (Node's built-in crypto, Rust's aes and scrypt crates, Go's crypto/cipher and golang.org/x/crypto/scrypt, Python's cryptography and hashlib).
The deniability claim has a proof sketch on the page. The short version: because the control data is the XOR difference between the real and decoy keystreams, and AES-CTR keystreams are pseudorandom, the control data is computationally indistinguishable from uniform random bytes. An attacker holding only the ciphertext cannot tell whether a recovered control file represents the real plaintext, the decoy, or a third construction they have not seen.
Verify it yourself. Three independent checks you can run:
# 1. KAT byte compatibility across SDKs
$ git clone https://github.com/deny-sh-crypto/deny-sh
$ cd deny-sh
$ npm install && npm test
$ cargo test
$ go test ./...
$ python -m pytest
# All four SDKs produce identical ciphertext bytes for the
# same plaintext + password + salt. KAT vectors live in
# spec/kat-vectors.json and every SDK's test suite asserts
# against them.
# 2. Round-trip on real ciphertext
$ npm install -g deny-sh
$ echo "the real plaintext" > real.txt
$ echo "a plausible decoy" > decoy.txt
$ deny encrypt real.txt decoy.txt --out cipher.deny
$ deny decrypt cipher.deny --password 'real-pw' # => the real plaintext
$ deny decrypt cipher.deny --password 'decoy-pw' # => a plausible decoy
# Two passwords, two truths, one ciphertext file.
# 3. Statistical test on the control data
$ deny inspect cipher.deny --dump-control > control.bin
$ ent control.bin
# Entropy approaches 8.0 bits per byte.
# Chi-square distribution randomly exceeded ~50% of the time.
# Control data is indistinguishable from uniform random.
The whitepaper goes deeper: empirical entropy measurements at scale, a constant-time review of the comparison routines, a discussion of nonce reuse boundaries, and the honest limitations of the deniability claim. If you are reviewing the construction for an enterprise deployment, the whitepaper is the artifact your cryptographer wants.
One caveat that needs to be on this page rather than buried: we have not yet had a third-party cryptographic audit. The construction is open and the primitives are standard, but an external audit is in scoping for Q3 2026. We will publish the firm, the scope, and the findings (good or bad) when it completes. Until then, the construction is what we put under our own customers' bytes. We think that should buy some credibility. It does not buy all of it.
Layer 3: the disclosure pipeline
/disclosure · /.well-known/security.txt
The disclosure page is a coordinated-disclosure policy with concrete numbers. 48-hour acknowledgement of any report sent to security@deny.sh. 90-day coordinated window before public disclosure. Safe harbor for good-faith research. Public credit on a hall-of-fame page when researchers want it.
The mailbox is real and the GPG key is real:
# Fetch the GPG public key from keys.openpgp.org
$ gpg --auto-key-locate hkps:keys.openpgp.org \
--locate-keys security@deny.sh
# Confirm the fingerprint matches the one on /disclosure
$ gpg --fingerprint security@deny.sh
# => D318 1C84 73FD B3E7 1271 8D9F D6DA 921A CB41 4764
The same fingerprint is published in three places: the disclosure page, the public key file at /security-key.asc, and (most importantly) the clearsigned /.well-known/security.txt file. RFC 9116 specifies this file format precisely so that disclosure metadata is machine-discoverable and signed. You can verify the signature without any deny.sh-specific tooling:
# Pull the signed security.txt
$ curl -s https://deny.sh/.well-known/security.txt > security.txt
# Verify the clearsign signature against the published key
$ gpg --verify security.txt
# => Good signature from "deny.sh Security ..."
# Primary key fingerprint: D318 1C84 73FD B3E7 1271
# 8D9F D6DA 921A CB41 4764
If those bytes verify, then the contact address, the GPG fingerprint, the disclosure policy URL, and the expiry date were all signed by the same key whose fingerprint is published on the disclosure page. The signing key sits on the production server, with the corresponding revocation certificate held off-server in a separate location, so we can publicly revoke if the key is ever compromised.
The paid bug bounty programme (with tiered cash rewards) launches after the funding round closes in H2 2026. Until then we run a coordinated-disclosure programme with public credit but without cash. We would rather ship that honestly than promise a bounty we cannot pay.
Layer 4: the supply chain
/verify · /.well-known/integrity.json
The supply chain is where most cryptographic products quietly fail. The construction is fine, the disclosure is fine, but you install npm install deny-sh and trust that the bytes coming off the npm registry are the bytes that came out of the GitHub repository. That trust assumption gets exploited (typo-squatting, account takeover, registry breach) more often than the cryptography itself does.
We sign at three points and you can independently check each:
1. Package registry signatures
# npm publishes integrity metadata for every release
$ npm view deny-sh@1.1.0 dist
{
integrity: 'sha512-...',
shasum: '...',
tarball: 'https://registry.npmjs.org/deny-sh/-/deny-sh-1.1.0.tgz'
}
# Verify the local install matches
$ npm install deny-sh@1.1.0 --no-save --dry-run --json \
| jq -r '.added[0].integrity'
2. Signed git tags
$ git clone https://github.com/deny-sh-crypto/deny-sh
$ cd deny-sh
$ git tag --verify v1.1.0
# => Good signature from the deny.sh release key
3. Subresource integrity for served assets
Every JavaScript and CSS file served from deny.sh has a published SHA-384 hash. The hashes are in two places: as integrity attributes on every <script> and <link rel="stylesheet"> tag, and as a machine-readable manifest at /.well-known/integrity.json. You can recompute any of them yourself:
$ curl -s https://deny.sh/js/scrypt.js \
| openssl dgst -sha384 -binary \
| base64
# Compare with the value at /.well-known/integrity.json
If a CDN, an MITM, or a server compromise mutates the served bytes, the hash stops matching and your browser refuses to execute the asset (because of the integrity attribute) and the manifest stops matching the served files.
Reproducible build
The website itself can be rebuilt deterministically from the public repository. The recipe is documented on the verify page: clone the repository, install the locked dependencies, run the build, and the resulting dist/ tree byte-matches the served files. We use a pinned Node version and a locked package-lock.json so two people running the same recipe produce the same output. This is the only honest answer to "did the deployed site come from the source you say it did".
What we have not verified yet
Honest list, because the four layers above will not catch everything:
- External cryptographic audit. In scoping for Q3 2026, post-funding. We will publish the firm, the scope, and the findings.
- Formal verification. The construction has a proof sketch, not a machine-checked proof. Tamarin or ProVerif models are on the roadmap but are not in place today.
- Hardware-level side channel. The constant-time review on the comparison routines is informal. We have not run dudect or similar against the SDK builds on production hardware.
- Transparency log. Sigstore / Rekor integration for build provenance would replace the manual signed-tag step. Planned, not shipped.
None of these are reasons to delay launch. They are reasons we expect the trust posture to keep improving after launch.
The point
A deniability product asks you to trust an unusual claim: that a piece of ciphertext can be a different thing depending on which password is used to open it, and that the two things are computationally indistinguishable from each other.
The only honest way to back that claim is to publish the threat model so you can decide whether the claim is relevant to you, publish the construction so a cryptographer can disagree with it, publish the disclosure pipeline so problems get reported through a proper channel, and publish the supply chain so the bytes you install match the bytes in the repository.
All four are live. Run the commands. If anything does not check out, the disclosure address is security@deny.sh; we want to know.
Four layers. All published. All checkable.
Start at the verify page and follow the links from there.
Go to /verify