Short answer. Whoever holds the private key and its password can sign anything on behalf of the owner — so electronic signature security starts with a simple rule: the key must never leave the user's browser. Sending the key to a server is the worst possible architecture, but "just in the browser" is not enough either: an XSS on the page grabs both the key and the password. The working model is client-side cryptography in an iframe on a foreign origin: the browser itself, at the Same-Origin Policy level, guarantees that the host site's code cannot see the widget's contents. That is exactly how DSTUcrypt is built: the site passes the data to be signed — and gets back only the finished signature.
What "upload your key" on a third-party site really means
A private QES key is not just a file. It is the cryptographic equivalent of your person or your company: a legally binding signature under a contract, a payment order, a report or a power of attorney is created with it. The key knows no boundaries or context — it will just as willingly sign the document you see on screen as any other set of bytes fed to it.
So every "upload your key" you see on a third-party site should be read literally: "hand us the ability to sign anything on your behalf". The only question is who exactly gets that ability and how widely it is exposed. And that is the main criterion for comparing e-signature architectures: who, besides the user, is technically able to reach the key and the password.
Key on the server: the worst model
The simplest scheme for a developer is to accept the key file and password on the backend and sign there. It is also the most dangerous:
- A single point of mass leakage. A server breach (or of backups, or of logs where a password "accidentally" ended up) means the keys of all users are compromised at once — not just one.
- Trust in people, not in math. A server administrator, a contractor with database access, anyone in the supply chain can sign documents on behalf of users — and no log audit will distinguish such a signature from a genuine one, because cryptographically it is genuine.
- The user loses control. They physically cannot see what exactly is being signed with their key, or how many times.
That is why "signature key on the server" is an anti-pattern regardless of how trustworthy the server itself seems: an architecture in which a leak is possible sooner or later ends in one.
Why "just in the browser" is not enough either
The logical next step is to run the cryptography on the client: plug a JS library right into your page and sign locally. The key no longer travels to the server — and that is progress. But now the key and password live in the same JS context as all the other code on the page: your scripts, analytics, chat widgets, ad tags, npm dependencies.
One XSS — through a vulnerability in your code or a compromised third-party library — and a malicious script reads the key file from the input, intercepts the password from the entry field and quietly sends them off. To the user everything looks like a normal signing. Add to that the fact that a pure-JS implementation of DSTU cryptography carries known vulnerability classes of its own — timing attacks and errors in generating the one-time k, which allow recovering the private key from the signatures themselves (see the dedicated article) — and it becomes clear: "in the browser" is a necessary condition, but not a sufficient one. You also need isolation inside the browser.
Origin isolation: cryptography in an iframe on a foreign origin
The browser already has an isolation mechanism proven over decades — the Same-Origin Policy. Documents from different origins (scheme + domain + port) have no access to each other's DOM or data. All of web security rests on this: it is precisely why a tab with a random website cannot read your email in the neighboring tab.
DSTUcrypt's client-side cryptography uses this mechanism directly: the signing widget runs in an iframe loaded from the dstucrypt.io origin — different from the host site's. The user picks the key file and enters the password inside that iframe, and the WebAssembly core performs all computations right there. No JS on the host site — including malicious code that got in via XSS — can peek into the iframe's contents: this is guaranteed not by our code and not by a promise, but by the browser itself at the level of a fundamental security mechanism. Communication between the page and the widget goes only through postMessage — a narrow channel carrying nothing but the operation's data and its result.
Why the widget files fundamentally cannot be self-hosted
A frequent question from integrators: "can we copy your files and the WASM to our server?" No — and this is not a commercial restriction but a fundamental part of the security model. All the isolation described above exists only because the iframe loads from a different origin. If the widget files sat on your server, the iframe would have your origin, the origin boundary would vanish — and any XSS on your page would once again read users' keys and passwords, just like in the "JS on the page itself" model.
That is why the integration comes down to a single SDK import from https://dstucrypt.io/embed/dstucrypt-embed.mjs: the SDK builds the widget URL from its own origin, and you host nothing. A bonus of this scheme: you are always on the latest version of the crypto core with no manual updates — fixes and new features reach your users automatically.
What the host site gets: only the finished result
The only things crossing the iframe boundary are data the site already has, plus the operation's result:
- Page → iframe: the document bytes and the signing parameters (format, hash).
- iframe → page: the finished
signature, the signer's certificate identifiersignerCertIdand theaccreditedflag — whether the certificate was issued by an accredited provider, i.e. whether the signature is a QES.
The key bytes and the password never cross that boundary — neither toward the host site nor toward DSTUcrypt's servers. The exchange happens inside the browser via postMessage: bytes are passed directly (ArrayBuffer), with no intermediate base64 strings and no network requests.
Three architectures side by side: a comparison
| Criterion | Key on the server | Cryptography in the page's JS | Isolated iframe widget |
|---|---|---|---|
| Leak on a server breach | mass: all users' keys leak | mass: tampered page JS steals users' keys | no keys on any servers — nothing to steal |
| Leak on a page XSS | yes: the script intercepts the key and password on upload | yes: the key and password sit in the same JS context | no: the Same-Origin Policy seals the iframe's contents off from the page's code |
| Who updates the crypto | your team, manually | your team, manually (risk of an outdated library) | DSTUcrypt, automatically for all domains |
What it looks like in code
The entire architecture hides behind a few lines on the host page — no backend is needed for signing at all:
signing in an isolated iframe — the page sees only the result
import { embed } from 'https://dstucrypt.io/embed/dstucrypt-embed.mjs';
// the widget opens in an iframe on the dstucrypt.io origin —
// the user enters the key and password inside it
const signer = await embed('sign', { mount: 'modal' });
const { signature, signerCertId, accredited } = await signer.sign(fileBytes, {
format: 'CAdES-T',
});
// ONLY the finished result — the signature bytes — comes back to the page
signature.download('document.p7s');
The signature hash is under your control too: the default is DSTU GOST 34.311-95 (the one government validators accept), while the modern Kupyna (DSTU 7564:2014) is enabled by a single option, digest:'kupyna-256' — all of it still inside the isolated iframe.
What DSTUcrypt adds on top of origin isolation. The cryptography is performed not by hand-rolled JS but by a proven native C/C++ core compiled to WebAssembly: constant-time operations against timing attacks, a correct one-time k in every signature, entropy from the browser's CSPRNG. TSP timestamps and OCSP statuses go through a built-in proxy out of the box, and crypto core updates reach every integration automatically — with no releases on your side.
Frequently asked questions
Does DSTUcrypt see my key or password?
No. All cryptography runs in the user's browser (WebAssembly) inside an iframe. The key and password are entered in the iframe and are never transmitted to DSTUcrypt's servers or to the site embedding the widget.
Why can't the widgets be hosted on my own server?
Because key isolation rests on the origin boundary: the widget runs on the dstucrypt.io origin, and the browser's Same-Origin Policy keeps your page's code away from its contents. If the files sat on your server, the origin boundary would vanish — and any XSS on your page would read users' keys. A bonus: you are always on the latest version with no manual updates.
What exactly does my site receive after signing?
Only the finished result: the signature bytes (signature), the signer's certificate identifier (signerCertId) and the accredited flag — whether the certificate was issued by an accredited provider, i.e. whether it is a QES. The key bytes and the password never cross the iframe boundary.
Is the "remember key" mode safe?
Yes, it is designed so that the key still never leaves the browser: the container is stored encrypted (AES-GCM-256, with the encryption key derived from a PIN via PBKDF2-SHA256 with 310,000 iterations) in the localStorage of the dstucrypt.io origin and is strictly bound to the host domain — site A cannot retrieve a key saved on site B. The PIN is never stored anywhere.
Read next
- The dangers of pure-JS cryptography: timing attacks, the one-time k, and key recovery from signatures
- How to choose a QES solution: a checklist for business
- How to add QES to your website in 10 minutes: an iframe widget with no backend
QES on your website — with no keys on the server
Ready-made DSTU signing, verification and encryption widgets. The key and password never leave the user's browser. Every new domain gets 7 days free.
Live demoHow to integrate