Blog · DSTU standards

How to Sign a Document with a QES Using the Kupyna Hash in the Browser: A Practical Guide

Published

Signing with the modern DSTU 7564 hash — no desktop applications, drivers, or backend cryptography. We walk the full path: from importing the SDK to a finished signature file, with examples for CAdES and XAdES.

Published August 7, 2026


Short answer. To create a QES with the Kupyna hash (kupyna-256, DSTU 7564:2014) right in the browser, all you need is one import of the DSTUcrypt SDK and one option in the signing call: signer.sign(fileBytes, { format: 'CAdES-T', digest: 'kupyna-256' }). All the cryptography runs in the user's browser (WebAssembly); the key and password never leave it. One caveat: government validators (Diia, the central CA / CZO) currently accept the "DSTU 4145 + GOST 34.311" pair — so for documents headed to government systems keep the default hash, and enable Kupyna where the receiving party supports it.

What Kupyna is — in a minute

Kupyna is a Ukrainian cryptographic hash function, standardized as DSTU 7564:2014. In a signature it plays a preparatory role: it compresses a document of any size into a short digest (256 bits for Kupyna-256), and it is this digest that gets signed with your DSTU 4145 key. In other words, a "DSTU 7564 signature" is really an ordinary DSTU 4145 signature in which the GOST 34.311-95 hash function has been replaced with the modern Kupyna.

An important consequence: no new key is needed — the hash function is "clipped on" to your existing key at every signing. A detailed breakdown of the standard itself, its construction, and a comparison with GOST is in the article "Kupyna (DSTU 7564:2014): what this hash function is and why it replaces GOST 34.311-95".

When to choose Kupyna and when GOST 34.311

The main practical rule: look at who will verify the signature. Government validators — the Diia application and the central certification authority — currently accept the pair "DSTU 4145 signature + DSTU GOST 34.311-95 hash"; Diia rejects Kupyna-in-CMS. That is why the default hash in DSTUcrypt is 'gost-34311': a signature made without the digest option is guaranteed to be accepted by government systems.

  • The document goes to Diia, the central CA (CZO), or government registers — sign with GOST 34.311 (the default, or explicitly digest: 'gost-34311').
  • Internal document workflow, B2B exchange, your own verification system — enable Kupyna explicitly: digest: 'kupyna-256'. It is the modern national standard with a solid cryptographic strength margin.
  • Not sure — set digest explicitly in both cases: an explicitly specified hash always takes priority, and your integration's behavior becomes predictable.

When government systems switch to Kupyna, changing the default in your code is a one-line edit — no rebuilds or updates.

Step-by-step guide: a Kupyna QES in four steps

All you need is a page on your website and the user's key file (PKCS#12/PFX, JKS, PKCS#8, or Key-6.dat). There is nothing to host yourself — the SDK loads the widget and the WASM core from its own origin.

Step 1. Import the SDK with a single line. Step 2. Create the signing widget — embed('sign'). Step 3. Call sign() with the digest: 'kupyna-256' option — the user picks their key and enters the password inside the widget's iframe. Step 4. Collect the result: signature.download() for the user, or signature.base64 / signature.bytes for your backend.

a CAdES-T signature with the Kupyna hash — full example

import { embed } from 'https://dstucrypt.io/embed/dstucrypt-embed.mjs';

const signer = await embed('sign', { mount: 'modal' });

const { signature, accredited } = await signer.sign(fileBytes, {
  format: 'CAdES-T',     // with a TSP timestamp
  digest: 'kupyna-256',  // Kupyna, DSTU 7564; without this option — GOST 34.311
});

signature.download('document.p7s');  // or signature.base64 / .bytes / .blob()

fileBytes can be a File, Blob, ArrayBuffer, Uint8Array, or a string — no Base64 handling needed in your code. The accredited field tells you whether the signer's certificate was issued by an accredited provider (i.e. whether this is indeed a QES). The timestamp for CAdES-T is applied by built-in TSP servers with automatic fallbacks — nothing to configure.

The same hash works in the other format families too — for example, an XML signature with Kupyna:

XAdES with Kupyna-256 — a rarity on the web

const { signature } = await signer.sign(xmlBytes, {
  format: 'XAdES-B-T',   // XML-DSig: dstu4145-dstu7564-256
  digest: 'kupyna-256',
  fileName: 'contract.xml',
});
signature.download('contract.xml');

Kupyna across the signature formats

The digest option applies to all 16 supported formats — from bare CMS to archival levels with timestamps. The only difference is in the level names: CAdES uses the older nomenclature (CAdES-T), while XAdES and PAdES use the baseline form (XAdES-B-T, PAdES-B-T).

Family format values File Kupyna via digest:'kupyna-256'
CAdES (CMS) CAdES-BES · CAdES-T · CAdES-C · CAdES-XL .p7s yes; a detached mode is available too
PAdES (PDF) PAdES-B-B · PAdES-B-T · PAdES-B-LT · PAdES-B-LTA .pdf yes — the signature is embedded right in the PDF
XAdES (XML) XAdES-BES · XAdES-B-T · XAdES-B-LT · XAdES-B-LTA .xml yes — DSTU 4145 + Kupyna-256 in XML-DSig, a rarity on the web
ASiC-S / ASiC-E ASiC-S-BES · ASiC-S-T · ASiC-E-BES · ASiC-E-T .asics / .asice yes — an "original + signature" ZIP container

Levels with a timestamp (-T and above) require online access to a TSP — the SDK reaches it through the built-in proxy automatically. Which format to choose for your task is covered separately in the article on the CAdES, PAdES, XAdES, and ASiC formats.

Why this is secure and why it is rare. The key and password are entered inside an iframe on the dstucrypt.io origin and never leave the user's browser — they are inaccessible both to your page's code (even under XSS) and to any servers. The crypto core is a proven native C/C++ library compiled to WebAssembly. It is the full stack of the updated national standards — Kupyna, Kalyna, DSTU 4145 — across 16 signature formats, right on the web.

How to verify what you signed

Verification uses the same kind of widget: const v = await embed('verify'), then v.verify(signatureBytes, null) for an attached signature (pass the data only for detached). The result contains valid and a signers array — with the name, RNOKPP/EDRPOU, signing time, format, and the accredited field, which distinguishes a QES from a merely valid signature. The format is detected automatically from the contents — CAdES, PAdES, XAdES, or ASiC.

To eyeball a signature quickly without code, use the free tool at /verify. And when it is your backend that decides whether to accept a document — use the authoritative server-side POST /api/verify: the same verification, but by the native core on a server the frontend cannot reach.

Common questions and errors

The most frequent "error" with Kupyna is actually not an error: the signature is valid, but the receiving party (for example, Diia) expects GOST 34.311. Check the recipient's requirements before choosing the hash. Among the technical codes: CANCELLED — the user closed the modal, TSP_QUOTA — the daily timestamp quota is exhausted, not_licensed — the domain has no active subscription or trial (the loader shows a tidy modal on its own).

Do I need a separate key to sign with Kupyna?

No. A QES key is a DSTU 4145 signing key, and Kupyna merely replaces the hash function paired with it. Just add the digest:'kupyna-256' option to the sign() call — no key reissuance whatsoever.

Why does Diia reject my signature with Kupyna?

Government validators (Diia, the central CA / CZO) currently accept the pair "DSTU 4145 signature + DSTU GOST 34.311-95 hash" — Diia rejects Kupyna-in-CMS. For documents headed to government systems, sign with digest:'gost-34311' (which is the default), and enable Kupyna where the receiving party supports it.

Which hash is used if I omit the digest option?

Without the digest option the signature is hashed with DSTU GOST 34.311-95 — exactly the pair government validators accept today. For predictability we recommend setting digest explicitly: 'gost-34311' or 'kupyna-256'. An explicitly specified digest always takes priority and applies to all formats.

Does Kupyna work in PDF and XML signatures?

Yes. The digest:'kupyna-256' option applies to all formats: CAdES, PAdES, XAdES, and ASiC. In particular, XAdES with Ukrainian DSTU 4145 keys supports both GOST 34.311 and Kupyna-256 — a rare capability on the web.

Read also

Kupyna signing on your website — in 10 minutes

Ready-made DSTU widgets for signing, verification, and encryption. The key and password never leave the user's browser. Every new domain gets 7 days free.

Live demoHow to connect

Try QES on your website

Ready-made DSTU widgets for signing, verification, and sign-in. Every new domain gets 7 days free.