KS Diia Sign WordPress Plugin

Завдання Let a website take a qualified electronic signature through Diia — hash the document, get the signature back from the signer's phone, and keep a register that stands as a document rather than as a count.

Ukraine has a state digital identity app, Diia, and inside it a qualified electronic signature that the law treats as equal to one made by hand. KS Diia Sign is the WordPress side of that: publish a document, put a button next to it, and what comes back from the signer’s phone is a signed CAdES container rather than a row in a form.

It was written for a veterans’ movement whose manifesto had to be signed in a way that a ministry could not dismiss as a web counter. The plugin implements one scenario of the Diia.Signature integration — signing hashed files — and deliberately not the other one, authentication: a branch carries a single scope, and mixing the two is refused by the API rather than by a preference.

What actually happens

The document is hashed on the site and only the hash is sent. Diia answers with a one-time deeplink; the visitor scans a QR code on a desktop or is handed the link on a phone, opens Diia and confirms. The signature is then posted back to the site straight from the signer’s phone.

What the site keeps is the signed container, the signer’s name and their certificate details, and a register of who signed what. The document itself never leaves the server — a hash is all the state ever sees.

Ukrainian cryptography, in plain PHP

Ukraine’s standards are not in any PHP extension: DSTU 4145 for the signature, Kupyna for the digest, GOST 34.311 for the older one. All of it is implemented here in ordinary PHP and checked against the test vectors published with the standards themselves.

No FFI, no bundled binaries, no call out to somebody’s hashing service. That is a deliberate cost: it is slower to write and it is the only version that installs on a shared host and can be read by the person who has to trust it.

The standard moved while it was being built

From the first of September 2026 every new qualified certificate is issued under Kupyna. The older digest is not deleted from the plugin — certificates issued before that date keep working until they expire, and a document already signed keeps its force — it is marked as retired, labelled in both algorithm selects, and stepped off automatically for a site that had it set.

One point is left open in public rather than smoothed over. Diia documents which hash goes with which algorithm for the other two combinations and documents none for this one; Kupyna is inferred from the algorithm’s own name, implemented and tested, but an inference is not a quotation. A wrong hash fails silently — the request is accepted, the person signs, and the container verifies against nothing — so the settings screen says exactly that and asks for one end-to-end test before the site is trusted with it.

The callback is the difficult part

Diia posts the signature from the signer’s phone to a public route on the site — no shared secret, no client certificate, no address to allow-list, and a thirty-second budget in which the whole container has to be stored. The route answers with a success flag and a 200 in both directions, because only that pair moves the request to done on Diia’s side; an internal failure that returned a 500 would leave a person who signed looking at an error.

What guards it is the request identifier: generated on the site, single-use, short-lived, and looked up in the plugin’s own table. A post with an identifier nobody issued is answered and dropped. Signatures are matched to their files by that identifier and by position, never by the file name in the payload — that field is the sender’s to write.

What the visitor sees

A button, and on a desktop a QR code beside a countdown: the deeplink is single-use and lives three minutes, so when it lapses the page offers a fresh one instead of failing quietly. On a phone the app is opened directly.

Status is polled against the site’s own store first and against Diia’s only as a fallback — which is what tells «the person declined» apart from «the person is still deciding». The button, the signature counter and the public register are each a shortcode and a block, so a page can be assembled without a developer.

What is kept, and what is not

The signer’s tax number is never stored in the clear and never returned: what goes into the table is an encrypted copy and a salted hash, and the hash is only ever used to notice that somebody is signing twice. The public register is anonymised by default, and each document decides for itself whether it shows an initial, a full name, or nothing but the count.

The acquirer token is a live credential — it can create signing requests in the organisation’s name — so it belongs in the site’s configuration file, and a token typed into the screen instead is encrypted under keys that live only in that file. Signed containers are served through the admin and nowhere else, and their directory is closed to the web.

Як він влаштований усередині

Each service declares the hooks it answers on the method that answers them, through an attribute, and a binder collects them — so a class is read in one place rather than traced through a registration file. The third-party libraries it needs are renamed into a namespace of its own, because two plugins carrying different releases of the same library is a crash waiting for whichever loads second.

Static analysis at its strictest level, the WordPress coding standards, and unit tests that stub the platform rather than boot a site — the same command at the keyboard and in CI. Removing the plugin deletes the secrets and keeps the records: a signature is somebody’s act, and an uninstaller has no business dropping it.

Ролі

Design, Development
Konstantin Sorokin

Виконано
  • Diia.Signature hashed-files signing, end to end
  • DSTU 4145, Kupyna and GOST 34.311 implemented in plain PHP, against the standards test vectors
  • Public callback hardened around a single-use request identifier
  • Signing button, counter and public register as shortcodes and blocks
  • Signature register in the admin, with filters, CSV export and Site Health probes
  • Encryption at rest for tokens and the signer tax number; privacy exporter and anonymising eraser
  • PHPStan at its strictest level, WordPress coding standards, unit tests, CI