Short answer. Login with QES is a challenge–response flow: your backend obtains a one-time challenge from DSTUcrypt (POST /api/auth/challenge), the user signs it with their key in the auth widget right in the browser, and the backend sends the signature for server-side verification (POST /api/auth/verify). The response contains a cryptographically confirmed identity: full name, RNOKPP, EDRPOU, certificate details, and the qualified flag — confirming this is genuinely a login with a qualified electronic signature (QES). The signature cannot be forged or reused, and the user's key never leaves their browser.
Why login with QES at all
Classic authentication — login, password, SMS code — only proves that a person knows a password or is holding a phone. It says nothing about who this person is legally. For many Ukrainian services that is not enough: personal accounts with documents, financial services, B2B portals, electronic document management systems, business services, and government services.
Login with an electronic signature solves both problems at once. The user does not need to register, invent a password, or wait for an SMS — they simply confirm their identity with a key they already have. And you get not "some email address" but QES-grade identification: the full name, RNOKPP, and EDRPOU code from a qualified certificate, verified cryptographically. This is the same level of trust that government services are built on — now available to your product through one widget and two server calls.
How it works technically: four steps
At the core is a challenge–response scheme that makes both forgery and signature reuse impossible:
- The backend obtains a one-time challenge. Your server calls
POST https://dstucrypt.com.ua/api/auth/challengeand receives{ challenge, expiresIn }. The challenge lives for 5 minutes and burns after the first verification — protection against replay. - The user signs the challenge with their key in the widget. On your page, the DSTUcrypt auth widget opens a modal, the user picks their key, enters the password — and signs the challenge. All cryptography runs in the browser (WebAssembly) inside an iframe on our origin: neither your page nor any server ever sees the key or the password.
- The page passes the signature to your backend. The frontend sends the pair
{ challenge, signature }to your own login endpoint. - The backend sends the signature for server-side cryptographic verification. Your server calls
POST https://dstucrypt.com.ua/api/auth/verify— and the native crypto core on the DSTUcrypt server verifies the signature, its integrity, that the content matches the challenge, and the certificate's validity. The response is{ ok, subject{…} }with the confirmed identity.
A technical detail: the signature produced by the auth widget is an attached CAdES over the challenge, by default with the DSTU GOST 34.311-95 hash (the crypto core also supports the modern Kupyna hash per DSTU 7564:2014 — we covered that hash function in detail in a separate article).
Why verification must not happen in the browser
The temptation is understandable: grab a JS library, verify the signature right on the frontend, and "save" a server call. For authentication this is not a production-safe approach, for two reasons.
The first is fundamental. The verification result is produced on the client side, and the browser is fully controlled by whoever is sitting at it. An attacker needs no key at all: it is enough to open DevTools and fake a "signature is valid" result — and your frontend will happily tell the backend that the login succeeded. An access decision made in the browser is, by definition, worth nothing.
The second is practical. Open pure-JS implementations of DSTU have entire known classes of vulnerabilities: timing attacks (cryptography without constant-time execution) and weak generation of the random k, which makes it possible to recover the private key from signatures. We covered these attacks in detail in the article on the dangers of pure-JS cryptography.
How DSTUcrypt closes this. The signature is created in an isolated iframe on our origin (WASM, constant-time), and verification runs on our server using a native crypto core — a battle-tested C/C++ library that has served Ukrainian PKI for years. The server-side verification result cannot be forged on the client: the browser is trusted with nothing except the signature itself. In response you receive the confirmed identity — full name, RNOKPP, EDRPOU — while the user's key never leaves their browser.
Auth widget integration code
The server side is two calls. First the backend obtains a challenge (server-to-server, not from the browser), and at the end it sends the signature for verification:
backend: challenge and server-side verification
# 1) one-time challenge (lives 5 minutes, burns after the first verification)
curl -X POST https://dstucrypt.com.ua/api/auth/challenge
# → { "challenge": "dstucrypt-login-v1:…", "expiresIn": 300 }
# 4) server-side cryptographic verification of the signature
curl -X POST https://dstucrypt.com.ua/api/auth/verify \
-H "Content-Type: application/json" \
-d '{ "challenge": "dstucrypt-login-v1:…", "signature": "<base64>" }'
On the page — the auth widget: the user signs the challenge, and the signature goes to your backend:
frontend: the auth widget signs the challenge
import { embed } from 'https://dstucrypt.io/embed/dstucrypt-embed.mjs';
const auth = await embed('auth', { mount: 'modal' });
const { signature, subject, accredited } = await auth.login({ challenge });
// subject/accredited here are for UX ONLY (greet the user by name / warn
// that the key is not a QES); trust only the /api/auth/verify response (step 4)
await fetch('/api/my-backend/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ challenge, signature }), // signature is a base64 string
});
A successful /api/auth/verify response contains ok: true, a subject object, plus signatureFormat, signingTime, status (for example, TOTAL-VALID), and two key flags: accredited — the certificate was issued by an accredited CA, and qualified — this is a login with a genuine QES (accredited + valid). What is inside subject:
| Field | What it contains |
|---|---|
| fullName | signer's full name ("Ivanenko Ivan Ivanovych") |
| taxId | RNOKPP (personal tax number) — a stable personal identifier |
| orgCode | the organization's EDRPOU (company code) |
| issuer | the accredited CA that issued the certificate |
| certSerial | certificate serial number |
| validFrom / validTo | certificate validity period |
Failures are predictable too:
| HTTP | reason | Cause |
|---|---|---|
| 401 | challenge_invalid_or_used | the challenge is unknown, expired, or already used |
| 200 | signature_invalid ( ok:false ) | the signature failed cryptographic verification, the content ≠ challenge, or the certificate is invalid |
| 400 | bad_request / bad_signature_encoding | malformed request body |
What to do with the verified identity
After ok: true you create your own session — your cookie, JWT, or whatever you prefer: DSTUcrypt does not manage your sessions, it only answers authoritatively "who this is".
- The account identifier is subject.taxId (RNOKPP). It is a stable personal identifier: it does not change when the key is reissued, the provider changes, or the person changes their surname. If an account with that RNOKPP already exists — it's a returning user; if not — create a new account with the full name pre-filled.
- For B2B — matching by orgCode (EDRPOU). If a user logs in with an employee key of an organization, the EDRPOU lets you automatically link them to the company in your system — for example, admitting them to a partner portal without manual approval by a manager.
- If you require a QES specifically — check qualified .
qualified: truemeans a login with a QES; if your scenario requires a QES only — rejectqualified: false.
And the golden rule of trust: the login decision is made only based on the /api/auth/verify response (server-to-server). The subject and accredited fields returned by the widget on the frontend are best-effort data strictly for UX: to greet the user by name or immediately warn them that their key is not a QES.
Security and privacy
The most frequent question from security teams: "so where does the user's key travel?" The answer — nowhere. The key and password are entered inside an iframe loaded from the origin dstucrypt.io: under the Same-Origin Policy, even your page's code cannot access them (including under XSS), let alone any servers. All cryptography runs locally in the browser (WebAssembly), and the only thing that leaves is the result — the signature of the challenge.
The protocol itself adds two more layers of protection. First, the challenge is one-time and short-lived: an intercepted signature cannot be "replayed" — a repeat verification returns 401 challenge_invalid_or_used. Second, verification is server-side: even a fully compromised user browser cannot convince your backend that an invalid signature is valid.
Typical scenarios
- A personal account without registration. The first login with QES immediately creates an account with a verified full name and RNOKPP — no forms, email confirmations, or passwords.
- Financial services and insurance. QES-grade client identification before granting access to contracts, payouts, or personal data.
- B2B portals and partner accounts. Automatic linking of a user to a company by the EDRPOU from the certificate — a supplier enters the procurement portal without emailing an administrator.
- Document workflow. The same key the user logs in with also signs documents — login and signing live in a single DSTUcrypt integration.
- A confirmation step for critical actions. The challenge–response scheme can be used not only for login but also as a "sign to confirm" step before a sensitive operation.
Frequently asked questions
Does DSTUcrypt or my server see the user's key during login?
No. The key and password are entered inside the iframe widget on the dstucrypt.io origin, and all cryptography runs in the browser (WebAssembly). The only thing that leaves is the signature of the challenge — the key and password never reach your server or DSTUcrypt's servers.
Can an intercepted signature be used to log in again?
No. The challenge is one-time: it lives for 5 minutes and burns after the first verification. A repeat request with the same challenge gets a 401 rejection with reason challenge_invalid_or_used — so an intercepted signature gives an attacker nothing.
What does qualified: false in the verification response mean?
It means the login was performed with a valid signature, but not a QES: the certificate is not from an accredited CA or did not pass all checks. If your scenario requires a login with a QES specifically — reject responses with qualified: false.
Which field should the user account be keyed on?
On subject.taxId — the RNOKPP, a stable personal identifier that does not change when the key is reissued or the provider changes. For scenarios acting on behalf of an organization, additionally use subject.orgCode (EDRPOU).
Why can't the signature be verified with a JS library in the browser?
Because the verification result is produced on the client side: the attacker controls the browser and can fake a "signature is valid" result without any key. Moreover, open pure-JS implementations of DSTU have known vulnerability classes — timing attacks and weak generation of the random k, which makes it possible to recover the private key from signatures. That is why DSTUcrypt performs verification on the server with a native crypto core.
Read also
- How to add QES to your website in 10 minutes
- The dangers of pure-JS cryptography
- QES, digital signature, and advanced e-signature: what's the difference
Login with QES on your website — in one evening
A ready-made auth widget plus two server calls — and your backend receives a confirmed identity: full name, RNOKPP, EDRPOU. The key and password never leave the user's browser. Every new domain gets 7 days free.
Live demoHow to connect