Plank help · updated 2026-09-10
Signing with a Kazakhstan ЭЦП key (without NCALayer)
What NCALayer actually does when a Kazakhstan state portal asks you to sign, why that is a desktop convenience rather than a requirement of the protocol, and how to produce the same signature from a script — including the key-handling rules that must hold every time.
Agents: fetch the raw markdown of this page at /en/help/kz-ecp-signing.md
Signing with a Kazakhstan ЭЦП key (without NCALayer)
Almost every Kazakhstan state portal gates its write actions behind an ЭЦП signature, and almost every one of them asks for it through NCALayer — a small Java application the user installs, which pops up a window, asks which key to use, and returns a signature.
NCALayer is a desktop convenience, not a property of the protocol. It holds no secret the portal trusts and performs no server-side step. It is a local WebSocket service that takes a document, signs it with a key file the user already has, and hands the signature back to the page. Anything that can read a .p12 can do the same thing.
That distinction decides whether a portal is automatable. «It needs NCALayer» is not a reason to stop — this is exactly how Connecting ЭСФ issues real invoices in production, with no NCALayer and no per-operation QR.
1. The shape is always the same
Every portal that signs this way follows one pattern:
1. ask the server for the document to sign → it returns XML
2. sign that XML locally with the user's key → enveloped XMLDSig
3. post the signed XML back → the action is committed
The server builds the document, so you never construct it yourself. Two live examples:
| Portal | Step 1 | Step 3 |
|---|---|---|
| Tizilim (topic) | POST /api/auth/esign-auth-xml | POST /api/auth/login-check-esp {xml} |
| ЭСФ (topic) | AuthService.createAuthTicket → authTicketXml | SessionService.createSessionSigned {signedAuthTicket} |
| egov.kz (topic) | POST /v1/<SVC>/xml | POST /v1/<SVC>/signing/send-eds {signed_xml} |
One measured exception: the egov.kz login has no step 1 at all — the document is a constant the client builds itself. Its twenty-eight services follow the shape above.
Once you recognise this shape, finding it in a new portal is a matter of grepping its client bundle for the endpoint that returns XML and the one that accepts {xml: …}.
Signing is per action, not per session. A portal that signs at login usually also signs every publish, every protocol, every report. Budget for that in the design: the key must stay reachable for the whole run, and forty published items means forty signatures.
2. What NCALayer is actually asked to do
Read the request the page sends it — it is a complete specification of the signature you have to reproduce. This is Tizilim's, verbatim from its bundle:
{
"module": "kz.gov.pki.knca.basics",
"method": "sign",
"args": {
"allowedStorages": ["AKKaztokenStore", "PKCS12"],
"format": "xml",
"data": "<the XML the server returned>",
"signingParams": {
"decode": false, "encapsulate": false, "digested": false, "tsaProfile": null
},
"signerParams": {
"extKeyUsageOids": ["1.3.6.1.5.5.7.3.2"],
"chain": ["-----BEGIN CERTIFICATE----- …НУЦ РК roots… -----END CERTIFICATE-----"]
},
"locale": "ru"
}
}
Decoded, that says: enveloped XMLDSig over the document as-is — not base64-decoded first (decode: false), not wrapped (encapsulate: false), not pre-hashed (digested: false), no timestamp authority (tsaProfile: null) — made with a key whose certificate carries extended key usage 1.3.6.1.5.5.7.3.2, chained to the НУЦ РК roots the page supplies.
Three things worth knowing when you read one of these in the wild:
- The socket is
wss://127.0.0.1:13579/and the protocol is JSON messages over WebSocket. A sandbox cannot reach it — it is the user's loopback, not yours. - There are three generations of the API, and pages fall back through them:
kz.gov.pki.knca.basics/sign(current),kz.gov.pki.knca.commonUtils/signXml(older, positional args[storage, signType, xml, "", ""]), and a bare{method: "signXml", args: [storage, "", "SIGNATURE", xml]}. Read the newest one — the older two are the same signature with a worse calling convention. - Storage names you will see:
PKCS12(a key file — the common case),AKKaztokenStore,AKKZIDCardStore,AKJaCartaStore,AKAKEYStore,AKEToken5110Store,AKEToken72KStore,JKS. OnlyPKCS12matters for automation; the rest are hardware tokens that must be physically present.
3. The key — one file now, two on anything old
Since 30 April 2024 НУЦ РК issues ONE key. A single registration certificate serves both authentication and signing, on СТ РК ГОСТ Р 34.10-2015. Before that date every person got two files that were not interchangeable, and old certificates stay valid until they expire — but these expire in a year, so by now practically every customer holds the universal key. Ask for "the ЭЦП key", singular, and treat a two-file answer as the exception it has become.
Portals have not changed, and they do not need to: they still filter by extended key usage, and the OID in the request tells you which purpose is being asked for.
| OID | Meaning | Asked for at | Old two-key era |
|---|---|---|---|
1.3.6.1.5.5.7.3.2 | clientAuth | logging in | AUTH_RSA…, AUTH… |
1.3.6.1.5.5.7.3.4 | emailProtection | signing documents | RSA…, GOST… |
egov.kz still asks for the first at login and the second at every submission — two visibly different calls in one bundle, measured 2026-09-10 (topic §4).
The obvious conclusion from that — that one universal certificate must therefore carry both OIDs — is wrong, and it was measured wrong the same day. A real universal ГОСТ certificate (a ТОО's first-head key, issued June 2026) carries 1.3.6.1.5.5.7.3.4 and the Kazakh-arc OIDs 1.2.398.3.3.4.3.2, 1.2.398.3.3.4.1.2, 1.2.398.3.3.4.1.2.1 — and not 1.3.6.1.5.5.7.3.2. Its signature of egov's login document was accepted anyway.
So read extKeyUsageOids for what it is: a key picker for the desktop, not a rule the server enforces. It tells NCALayer which file on someone's disk to offer; the server verifies the signature, not the certificate's declared purpose. A universal key is therefore enough even for a portal that asks for clientAuth. Measured on egov's login only — do not assume a write endpoint or another portal is equally relaxed.
Read the EKU anyway, because it tells you which era of key you are holding:
openssl pkcs12 -info -nokeys -clcerts -in key.p12 -passin file:pw.txt \
| openssl x509 -noout -text | grep -A1 'Extended Key Usage'
-legacy is not optional, and leaving it out does not look like a missing flag. Every НУЦ РК .p12 measured here is encrypted with RC2-40-CBC, which OpenSSL 3 refuses by default: you get Algorithm (RC2-40-CBC : 0) … unsupported, which reads like a corrupt file or a wrong password rather than a policy default. With -legacy the same file opens. On a ГОСТ certificate OpenSSL then parses the extensions — the EKU line above prints — but says Unable to load Public Key, which is expected and harmless: it does not implement the Kazakh curves. (Measured 2026-09-10 against the ЭСФ SDK's own sample keys, OpenSSL 3.0.13.)
The algorithm change is the bigger one, and it narrows your choice of signer to one. The universal key is ГОСТ, and no standard XMLDSig library can sign ГОСТ — it needs a provider implementing the Kazakh curves. The pure-Python route in §4 is therefore dead for any key issued since April 2024; Kalkan (or something that embeds it) is not the default any more, it is the only path. Confirm the algorithm rather than assuming it:
# Never omit -nokeys: without it, openssl prints the DECRYPTED PRIVATE KEY to
# stdout, which in a workspace is the chat transcript. Pass the password via
# -passin file:/env: too, never as -passin pass:… on the command line.
openssl pkcs12 -info -nokeys -clcerts -in key.p12 -passin file:pw.txt \
| openssl x509 -noout -text | grep 'Public Key Algorithm'
4. How to actually sign it
The proven path: Kalkan
Kalkan is НУЦ РК's own Java crypto provider — the same code NCALayer itself is built on. It signs both RSA and ГОСТ, needs no native library, and Plank already runs it in production for ЭСФ. Since the universal key is ГОСТ (§3), this is not merely the default choice — for any key issued after April 2024 it is the only one, and it is also the one path in this repo with real evidence behind it.
The whole operation is four lines:
Security.addProvider(new KalkanProvider());
KncaXS.loadXMLSecurity();
String signedXml = XMLUtil.createXmlSignature(
new SigningEntity(privateKey, Arrays.asList(certificate)), // from the .p12
xmlFromServer,
Security.getProvider(KalkanProvider.PROVIDER_NAME));
That is the identical call the ЭСФ client makes to sign its auth ticket. Copy Connecting ЭСФ's EsfClient.java rather than re-deriving it — its credential loading, provider registration and Java-8 compatibility are already correct.
Practical notes:
- The jars ship inside the ЭСФ SDK.
esf-sdk-*.rarfromhttps://kgd.gov.kz/sites/default/files/ftpdata/ESF/— public, ~180 MB. If a workspace already hasscripts/esf/sdk/, reuse it and do not re-download. The sandbox has nounrar; extract withbsdtar -xf esf-sdk.rar -C <dir>(libarchive reads RAR) or, ifbsdtaris absent too,pip install --quiet patoolthenpatoolib.extract_archive(...)— see Connecting ЭСФ for both. - The sandbox image has no JRE. Install a no-sudo Temurin tarball under
~/.local/jdks/and reuse it if it is already there. - JDK 8 is required for ЭСФ because its SOAP client needs JAX-WS, removed in Java 11+. Signing alone has no such dependency, and this is now measured: on 2026-09-10 Kalkan loaded a ГОСТ key and produced a valid enveloped XMLDSig (
SignatureMethod…xmldsig-more#gost34310-gost34311) under OpenJDK 17, with the SDK'slib/on the classpath. Reach for JDK 8 only when you also need the SOAP client. - Only if the customer's key is RSA — issued before April 2024 and not yet expired — is a pure-Python signer (
signxml/xmlsec) an option at all: no JDK, no 180 MB download. It cannot do ГОСТ, so on a universal key it is not a lighter path but a dead end. Also unverified against any of these portals. Check §3 before spending an afternoon on it. - NCANode is an open-source HTTP signing server built for exactly this job, distributed as a Docker image. It is the obvious candidate if you want signing as a service rather than in-process. Also unverified here — if you evaluate it, record the result.
When the key must stay on the user's machine
Some customers will not hand over a .p12, and that is a reasonable position. Say so plainly rather than pushing: without a reachable key the automation is limited to whatever the portal exposes without login (for Tizilim, that is a genuinely useful public read API). The middle ground — the assistant prepares everything, the user signs locally — is offered by the ЭСФ integration and is worth proposing; just be honest that it costs one interaction per signed action, which is the whole cost the automation was meant to remove.
5. Prove the signature before you build on it
Do this first, on its own, before any feature work. It is one round trip and it settles the only question that matters.
1. POST the portal's "give me the XML" endpoint → save the XML verbatim
2. sign it → save the signed XML
3. POST it to the portal's "check" endpoint
A portal that answers with the user's roles, a session or a token has accepted your signature and the entire integration is now unblocked. A rejection is the signal to change exactly one thing and try again.
Keep both files. When something later breaks, "did the signature change or did the portal change" is answerable in seconds if you have a known-good pair and unanswerable if you don't.
6. Failure modes, and what each one means
- Portals return one message for every signing failure. Tizilim answers
422«Ошибка при обработке сертификата» to a malformed signature, a wrong key type and an untrusted chain alike. So change one variable at a time: key file, then OID, then whether you pass the CA chain, then the canonicalisation. - The wrong key of the pair is the single most likely cause of a first-attempt rejection. Check the OID (§3) before suspecting anything subtle.
- Whitespace is part of the document. Sign the server's bytes exactly as returned — do not pretty-print, re-serialise, re-encode or strip a trailing newline. An XML round trip through a parser is enough to break the digest.
- Expired or revoked certificate. These keys are valid for one year and expire quietly.
openssl pkcs12 -in key.p12 -nokeys -clcerts -passin file:pw.txt | openssl x509 -noout -enddatewill tell you before the portal does — again,-nokeysis not optional. - "Accepted" is not "done". ЭСФ returns
acceptedfor invoices it then processes toFAILED; assume any portal may do the same and verify the object's status afterwards rather than trusting the submit response.
7. Handling the key
These are not optional, and they apply the moment a .p12 enters a workspace:
- Store the key and its password in the workspace's secure credentials, never in a note, a script, a commit, a log line or the chat history.
- Know who else can see it. A file under
scripts/is visible to every member of a shared workspace;/home/coderis private to one user..gitignoredoes nothing about that, and an ЭЦП key is a legal signing identity rather than a revocable API token — so in a shared workspace, place it deliberately and tell the owner what becomes visible. See Scripts and integrations. - Add
*.p12and the credentials file to the workspace.gitignorebefore the key arrives, not after. - Never print a key path's contents, a password, or a decoded certificate's private half into a transcript.
- Never sign without an explicit confirmation for anything the portal makes public or irreversible — a published purchase, a submitted bid, a filed report. Assemble it, show it, ask, then sign.
- Tell the user how to revoke you: deleting the key from the workspace ends the access immediately. They should know that without asking.
8. What is verified
- ✅ Proven in production: the Kalkan enveloped-XMLDSig call, against ЭСФ, for an ИП — signed session and a real issued invoice.
- ✅ Read from the live portal: Tizilim's NCALayer request in §2, its OIDs, its storage list and its three-generation fallback; and egov.kz's whole REST surface, its constant login document and both of its OIDs (topic).
- ✅ Proven against egov.kz (2026-09-10): a Kalkan enveloped XMLDSig, made under OpenJDK 17 from a real universal ГОСТ key with no NCALayer, is accepted by
/identity/v3/auth/eds—200and a session. That is the second portal to accept this signer, and the first ГОСТ one. - ✅ Also measured: НУЦ РК
.p12files need OpenSSL's-legacy; a universal certificate does not carry the clientAuth OID and is accepted anyway (§3); and the ЭСФ SDK's own sample keys (pinQwerty12— a public test credential shipped in the SDK) exercise a signer end to end with no customer key at all. Use them to prove your signer before you ever ask a customer for theirs. - ❌ Not verified: that a Kalkan signature is accepted by Tizilim; that a write endpoint is as relaxed about EKU as a login (§3); a pure-Python RSA signer; NCANode. Each is a short spike using §5 — run it, then update this page.