DSTU 9041:2020 — verified extraction from the primary text
Supersedes the previous version of this file (2026-08-03, T-148), which was built from a single
secondary source (a bachelor’s thesis) with no primary text and no oracle at all. As of
2026-08-04 (T-173/T-174), the project obtained a partial scan of the primary standard itself
(purchased/library-sourced, docs/papers/DSTU_9041-2020.pdf, never committed — redistribution
rights aren’t ours to grant) and OCR-transcribed it (docs/papers/DSTU_9041-2020_ocr.md, also
gitignored, same reason). This file is the committable distillation of that primary text: the
algorithm’s structure, the recommended curve parameters, and the worked test vectors are facts and
data about the algorithm, not the standard’s own copyrighted prose — see docs/DECISIONS.md D-163
for the citation/copyright reasoning this rests on.
The scan is explicitly partial, not the full 36-page document (confirmed 2026-08-06 as exactly 36 pages, page 36 being the last — see “Open gaps” below) — sections 1–2 and Додаток Б.1/Б.2’s actual content are still absent. See “Open gaps” below for exactly what that means for implementation readiness.
2026-08-05 update (T-176): a targeted supplement closes the single biggest gap. A second,
smaller purchase from the same source (National Library of Ukraine EDD service,
docs/papers/DSTU_9041-2020_supplement.pdf, 8 pages, gitignored — same redistribution reasoning as
the main scan) specifically targeted the clauses this file had flagged as missing. Result: clauses
6.5–6.12 (the actual random-element/modpow/sqrt/inverse/random-point/primality/MOV/scalar-mult
algorithms) are now fully present and verified against the page images — this was the priority
item and it’s resolved. Додаток А’s RNG algorithm body is also now fully present (resolved, was
previously “reference only, not blocking”). Section 3’s remaining terms (3.1–3.26) are now present
too, joining the 3.27/3.28 this file already had — section 3 is complete. Додаток Б.1/Б.2 came back
only as the appendix’s introductory historical prose (Edwards/Bernstein–Lange/Bessalov literature
survey) — the actual math content of Б.1/Б.2 (if any beyond that prose) is still not in hand; Б.3/
Б.4’s operative content was already covered from the first scan. Додаток Д (bibliography, 9
references) is now present too — reference-only, not implementation-relevant, but resolves
citation [1] (Joye & Yen, “The Montgomery Powering Ladder”) referenced from 6.6/6.12’s own
side-channel notes below.
Status: no longer “zero source material, hard-blocked” (D-08/T-46’s original framing). The scan
includes Додаток Г — three fully worked numeric examples (encryption and decryption) for
l(p) ∈ {256, 384, 512}. One of them (l(p)=256) has been arithmetically verified end-to-end
against this project’s own hazmat::kupyna/hazmat::kalyna_kw and a from-scratch reference
implementation of the curve’s point arithmetic (below) — this is now comparable in strength to how
DSTU 4145’s Annex B example serves as this project’s own oracle for that algorithm, not weaker.
Still not sufficient to call hazmat::dstu9041 “verified” once written — see “What remains
before implementation” — but the primitive is no longer blocked on missing source material.
What kind of construction this is
A hybrid (ECIES-style) scheme: ephemeral-key Diffie-Hellman-like agreement over a twisted Edwards
curve over F_p (prime field, not the binary field F_{2^m} that hazmat::dstu4145 uses —
this is new field-arithmetic territory for this project), feeding the resulting shared point’s
x-coordinate into a Kalyna key-wrap stage. The KIVREP mode named by the secondary source (now
superseded) is confirmed to be exactly hazmat::kalyna_kw’s underlying construction: DSTU 9041
clause 6.3 names it directly as “Калина-l/k-KW” / “Калина-l/k-KW-p”, the DSTU 7624:2014 clause
15 key-protection mode already implemented in this crate — no new cipher-mode research needed,
only a possible new -p (padding) variant (see below).
Notation (clause 4, Познаки — confirmed against the primary text)
M— message to encrypt;l(M)— its length in bits;l_max(p)— max encryptable length for a given field size (Table 1 below).P = (x_P, y_P)— curve base point;n— its prime order.e— recipient’s private key,1 < e < n-1;Q = eP— recipient’s public key.ε— one-time (ephemeral) encryption key, secret,1 < ε < n-1.H— hash function;l_H— its (possibly truncated) output length;i_H— its 8-bit identifier (clause 3.28);i_H = 00000001is DSTU 7564 (Kupyna), the default.E^(κ)_{l,k}(·)/D^(κ)_{l,k}(·)— Kalyna-l/k-KW(-p) forward/inverse transform (clause 6.3).
Curve equation (clauses 5.5, 7.2, Додаток В)
x² + a·y² = d·x²·y² + 1 over F_p, with a = 2 fixed for every recommended curve (7.2’s own
note). This is the standard twisted-Edwards curve a·X²+Y² = 1+d·X²·Y² with x and y roles
swapped relative to the usual Bernstein–Lange convention — a real, load-bearing detail: the point
addition law and the neutral element both need this swap applied, not just the equation (see
below). Missed once during verification (produced valid-looking but wrong scalar multiples until
caught by testing against Table В’s n·P = O and Additional Г’s ε·P/ε·Q checks) — a concrete
illustration of why every future point-arithmetic implementation here needs the same differential
test, not just “the equation looks right.”
Point addition (derived, not copied from the standard — clauses 6.5–6.12 (below) cover random elements/points, primality, MOV, and scalar multiplication, but not the addition/doubling law itself; that still comes only from Додаток Б.4’s projective form, already in hand from the first scan. Independently re-derived from the equation above and verified against Додаток Г, see “Verification performed” below):
(x1,y1) + (x2,y2) = ( (x1*x2 - a*y1*y2) / (1 - d*x1*x2*y1*y2),
(x1*y2 + y1*x2) / (1 + d*x1*x2*y1*y2) )
Neutral element: (1, 0) (not (0,1) — that’s the standard form’s neutral; ours is swapped).
Додаток Б.4 (present in the scan) separately gives a projective (inversion-free) form of this same
addition law — X_R=AG(C-aD); Y_R=AF((X1+Y1)(X2+Y2)-C-D); Z_R=FG where A=Z1Z2, B=A², C=X1X2, D=Y1Y2, E=dCD, F=B-E, G=B+E — implementation-grade, and the one to actually use for constant-time
scalar multiplication (the affine form above is for verification/derivation only, since field
inversion is exactly the non-constant-time operation to avoid in real point arithmetic per this
project’s own docs/SECURITY.md constant-time rule).
Computational algorithms, clauses 6.4–6.12 (T-176 supplement — full primary text, read directly
off the page images, not the OCR transcript)
6.4 Random/pseudorandom integer 1 < u < n-1, given n. Uses the RNG from 6.1 (Додаток А,
below). Rejection sampling: draw l(n) random bits as u, retry if u <= 1 or u >= n-1.
6.5 Random element of F_p. Same rejection-sampling shape as 6.4: draw l(p) random bits as
u, retry if u >= p.
6.6 Modular exponentiation v = u^s mod p — textbook square-and-multiply, MSB-first: v = u,
then for each remaining bit of s (high to low), v := v² mod p, and v := (v·u) mod p when that
bit is 1. The standard’s own text flags this as not side-channel-safe as written (“реалізації
такого алгоритму … мають враховувати необхідність захисту від атак за побічними каналами витоку
даних, див., наприклад, [1]”) — Додаток Д’s [1] is Joye & Yen, “The Montgomery Powering Ladder”
(CHES 2002). This is the standard’s own text making exactly the point this project’s own
docs/SECURITY.md constant-time rule already makes generally: this clause is a correctness
reference for the modpow result, not a template to transcribe branch-for-branch — any
hazmat::dstu9041 implementation needs a constant-time ladder here, not this literal algorithm.
Same caveat, same citation, repeats verbatim under 6.12 below.
6.7 Square root in F_p (p ≡ 5 (mod 8) case only — matches this standard’s own field
requirement from 7.1, so this is the only case that ever applies here): given v ∈ Q_p,
f = v^((p-1)/4) mod p(via 6.6).z = v^((p+3)/8) mod p(via 6.6);u = z.- If
f = p-1:u = (w·z) mod p, wherew = 2^((p-1)/4) mod pis the general system parameter already listed per-curve in Tables В.1–В.4 (3.23 nameswas a formal general parameter alongsidep/curve/P/n/hash-id — confirmed by this supplement, not previously cited as a named parameter in this file). - Output
u.
This is a real, useful confirmation: docs/pseudocode/dstu9041.md’s decryption algorithm below
already called this “clause 6.7” from context; the actual f/w-branch shape was previously
guessed from the standard √ literature for p≡5 mod 8 fields, not read from this text. Now cited
directly.
6.8 Modular inverse — extended Euclidean algorithm on (p, u), standard shape (r/q/x
sequences per the textbook algorithm, final sign correction if u⁻¹ < 0: u⁻¹ = p - u⁻¹). Nothing
DSTU-specific; matches what a from-scratch F_p inverse implementation would do anyway.
6.9 Random point on the curve — resolves exactly the derivation this file’s “Verification performed” section above had to reconstruct independently:
u= random field element (6.5).- If
d·u² mod p = a: retry from step 1 — this is the singular-point exclusion from clause 3.18’sD_{1,2} = (±√(a/d), ∞), confirming those two points are excluded by construction, not by luck. f = (1-u²)·(a-d·u²)⁻¹ mod p.- If
f = 0: retry from step 1. j = f^((p-1)/2) mod p(via 6.6) — Euler’s criterion, checkingf ∈ Q_p.- If
j = p-1(i.e.fis a non-residue): retry from step 1. v= square root off(6.7).- Output
(x_T, y_T) = (u, v).
6.10 Primality check for the base-point order n — Miller–Rabin, 50 rounds (matches this
project’s own generic expectation for a probabilistic primality test; not DSTU-specific math).
Standard’s own text explicitly permits substituting any algorithm with a rigorous mathematical
proof of primality instead.
6.11 Menezes–Okamoto–Vanstone condition check — given n, p: iterate j := j·p mod n for up
to 50 rounds starting from j=1; if j ever returns to 1, the MOV condition fails (embedding
degree too small, curve vulnerable to the MOV attack reducing DLP to a finite-field problem); if no
round returns to 1 within 50 iterations, the condition holds.
6.12 Scalar multiplication S = kT — textbook double-and-add, MSB-first (S = T, then for each
remaining bit of k high to low: S := 2S, and S := S+T when that bit is 1). Same
side-channel-safety caveat and [1] citation as 6.6 (verbatim repeated in the source). This
confirms, rather than changes, this project’s own existing understanding — the encryption/
decryption algorithms below already called this “clause 6.12”; what’s new is that the algorithm
shape (plain double-and-add, not already a ladder) and the side-channel caveat are now directly
cited instead of assumed. The actual point-doubling/addition operations 2S/S+T still come from
Додаток Б.4’s projective law above — 6.12 only specifies the scalar’s bit-scanning order, not a
different addition law.
Recommended curve — E256/1 (λ=127, l(p)=256, Table В.1’s first entry)
Per docs/DECISIONS.md D-163: Додаток В lists many alternative curves per security level
(E256/1 through at least E256/15 for λ=127 alone) — the standard mandates none of them (7.2: “not
obligatory”), so only the first curve per level was transcribed and verified; the rest buy nothing
implementation-wise. Table В.2/В.3/В.4 (λ=191/255/383) exist in the scan but their first entries
were not independently arithmetically verified this pass — only E256/1 got the full treatment
below (Додаток Г only gave a worked example to cross-check against for this one anyway).
p = FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4D
a = 2
d = 0x18 (= 24 decimal - Додаток Г's own convention: every numeric parameter in the worked
examples is given in hex, including small ones; this project's own equation is
written "18x²y²+1" using that same hex convention, not decimal 18)
n = 4000000000000000000000000000000029E26087789BC2815BDFF97093543CCF
P = (91F5D0E7E2D417E3108B13B075CDC7756045F8424479FCFE8F23D27250A0883F,
742F27A268641C9D7DDF69892BE3DF3D8F9CC52260B89A4953C8379C7C0A212B)
Erratum, caught 2026-08-05 (T-177), fixed here and in the vector JSON: p and n above were
wrong from 2026-08-04 through 2026-08-05 — an over-counted F-run in p (84 characters instead of
61) and zero-run in n (80 instead of 31). D-163 (below) already recorded the correct
stroke-counted lengths in prose at the time, but that fix was never actually applied to this file
or the committed vector JSON — both kept the original over-counted strings for two more sessions.
Caught only because starting the actual Rust implementation (T-177) required re-deriving p/n
independently (cross-checked against Table В.1’s own decimal column, converted to hex, and
verified with a real 40-round Miller-Rabin, not the original’s 3-base Fermat check) before writing
any field-arithmetic code against them — see docs/DECISIONS.md D-166 for the full account and the
process lesson (verify a described fix actually lands in the file, don’t trust the prose alone).
Verification performed (2026-08-04, against page images, not OCR text — see D-163)
Every numeric value above was re-read directly from the rendered page image (pdftoppm PNG,
150 DPI) at heavy zoom, character-run-counted programmatically where a value was a long run of a
single repeated digit (both p and n had this problem on first read — OCR and a first manual
transcription both silently over-counted a run of Fs/0s; a column-darkness stroke-count caught
it) — not trusted from the gitignored OCR transcript’s text order, consistent with the standing
project rule that cryptographic parameters need per-digit verification, not a document-scale OCR
pass.
Independently re-implemented the point addition law above in Python and confirmed, using Додаток
Г’s l(p)=256 worked example (ε=7, all values below read from the same page images):
pprime,p ≡ 5 (mod 8)(clause 7.1’s own stated requirement) ✓nprime ✓P,Q,R=εP,T=εQ(Г.1’s own given values) all satisfy the curve equation ✓n·P = O(the neutral element) ✓ — confirmsnreally isP’s order7·P == R✓,7·Q == T✓ — confirms the addition law, and thatε=7was applied correctly by the standard’s own exampleKupyna256(l̃_M || M̃)truncated to its last 4 bytes (not first) equals the example’s statedH = BF8B8620✓ (hazmat::kupyna::Kupyna256, this project’s own implementation) — resolves clause 5.7’s truncation direction, which the primary text states only as “як(h_{l_H-1},...,h_0)” without saying which end of the digest array that corresponds to in a concrete byte-string encoding; confirmed empirically instead of guessed.
No erratum in e — an earlier version of this document misread it. e = 25 as printed is
hex, per Додаток Г’s own stated convention (“значення параметрів наведено у вигляді двійкових
рядків, де кожні чотири біти представлено в шістнадцятковій системі”) — 0x25 = 37 decimal, and
scalar_mult(37, P) == Q holds exactly. The convention was already found and correctly applied to
d = 0x18 = 24 above; a first pass at e forgot to apply the same rule consistently and flagged a
false “erratum” instead. Left as a documented lesson (see docs/DECISIONS.md D-163’s follow-up
addendum): treat every bare small integer in this standard’s worked examples as hex first, and
double-check it was actually applied every time it recurs in the same example, not just the first
time it was discovered.
t (and C) are now verified — against this project’s own hazmat::kalyna_kw, not just
internal consistency. Building Kalyna256_256Kw::wrap’s input as M' ‖ 0×32 (i.e., M' padded
with one additional all-zero 256-bit block, not M' alone) and using κ exactly as printed
reproduces the standard’s own printed t bit-for-bit, except for a single missing hex digit
in the source text (a 0 dropped between ...B3CE and F710... — confirmed by inserting it back
and getting an exact 192-hex-digit match against this crate’s own computed ciphertext). This is a
real, independently-confirmed erratum in the published standard’s own informative annex (a printer/
scan-level single-character drop, not a transcription artifact on this project’s side — reproduced
identically across two independent manual re-reads of the page image before concluding it’s the
source, not us) — and simultaneously the strongest evidence yet that hazmat::kalyna_kw’s
Kalyna-256/256-KW is bit-exact with the standard’s own construction.
Open question, re-confirmed but still not resolved (2026-08-05, T-176): why the actual
Kalyna-KW input is 64 bytes (2 blocks) rather than the 32 bytes (i_H‖H‖l_M~‖M~ =
8+32+16+200 = 256 bits) clause 5.7/5.8/Table 1 imply on their own. This is not a transcription
error — re-checked twice more this session, independently of T-174’s original finding: (1)
re-ran Kalyna256_256Kw::wrap directly against κ/M'/t re-read fresh from the page image
(not copied from this file or the vector JSON) — wrap(κ, M') alone does not reproduce the
printed t; wrap(κ, M' ‖ 0×32) does, byte-for-byte; (2) cross-checked the same worked example
against an independent Gemini transcription the owner supplied — t/C matched exactly (same
single dropped hex digit, confirming that erratum is in the source, not a transcription artifact
by either engine), and Gemini’s own reading of M' differed from this file’s by one extra
zero-byte, resolved in favor of this file’s 256-bit value because the standard’s own prose
states the recovered string’s length as l(p)=256 in the same sentence — a stated fact, not a
count either side could get wrong.
The two still-open hypotheses, neither confirmed: (a) DSTU 7624’s own KW mode has an
unstated minimum input length of 2 blocks regardless of payload size (Bouncy Castle’s
DSTU7624WrapEngine imposes no such minimum, n=2(1+r) accepts r=1 freely — and this crate’s
own hazmat::kalyna_kw::Kalyna256_256Kw::wrap doesn’t either, checked directly while planning
T-177: wrap’s own length validation only bounds the upper end at MAX_R blocks, nothing
rejects a 1-block input — so this hypothesis would be a DSTU 9041-specific rule layered on top of
generic KW, not inherited from DSTU 7624 itself, and not something either reference implementation
already in this repo enforces on its own); (b)
M' itself has an additional all-zero padding field this document’s clause-11 reading missed,
bringing it to a full 2-block minimum before the KW step. Clauses 6.5–6.12 (now in hand, above)
do not settle this — they’re pure field/point arithmetic, nothing about message formatting or
KW invocation. What would settle it is either clause 6.3 itself (already in hand — cites
“Калина-l/k-KW” but not this apparent minimum) or a closer re-read of clause 11’s existing 15
steps (also already in hand) for a step this file’s transcription may have compressed or missed -
not a new scan, a re-read of what’s already here.
Message formatting (clause 11, encryption; clause 5.7/5.8/Table 1)
M~ = M, left-padded with zeros to l_max(p) bits (clause 5.2's padding convention: new zeros are the
*high-order* bits; the original message's own bits stay as the low-order tail — confirmed
against Додаток Г.1's message value, which is the integer 7, not literally all-zero, and
stays recognizable as "...0007" after padding)
l_M~ = l(M), as a fixed-16-bit big-endian-ish field (clause 5.2)
M' = i_H (8 bits) || H(l_M~ ‖ M~) truncated to l_H bits, taken from the hash's *low-order* end
(verified above) || l_M~ (16 bits) || M~ (l_max(p) bits)
l_max(p) (Table 1 — the older pseudocode’s “never given a formula” gap is now resolved, from the
worked examples directly rather than the table’s own jumbled-by-OCR cell order):
l(p) | l_max(p) | l_H | Kalyna-KW l/k (Table 2) | mode used (Додаток Г) |
|---|---|---|---|---|
| 256 | 200 | 32 | 256/256 | KW (no padding — M' lands exactly 256 bits) |
| 384 | 296 | 64 | 256/256 | KW-p (padding — M' is 384 bits, not a multiple of 256) |
| 512 | 424 | 64 | 512/512 | KW (no padding — M' lands exactly 512 bits) |
| 768 | (616, unconfirmed — no worked example for this level in the scan) | 128 | (not in scan) | — |
The KW-vs-KW-p split is not arbitrary — confirmed by checking 8 + l_H + 16 + l_max(p) against
the Kalyna block length l from Table 2 for each row: it’s a clean multiple of l exactly when
plain KW (no padding) is used, and isn’t when KW-p is used. This resolves the older pseudocode’s
“KIVREP never defined” gap along with it: there’s no separate “KIVREP” mode at all — it’s exactly
hazmat::kalyna_kw, plus (for the 384-bit row, and presumably any case that isn’t block-aligned)
a padding variant this project has not implemented yet — see below.
Encryption algorithm (clause 11 — full primary text, 15 steps; matches the older secondary-source
pseudocode’s shape, now clause-cited instead of thesis-cited)
1) Validate general parameters (clause 8.1-8.3); validate i_H against the user group if used.
2) Compute l(M); require 0 < l(M) <= l_max(p).
3) M~ = M.
4) If l(M) < l_max(p): left-pad M~ to l_max(p) bits (clause 5.2).
5) l_M = bit-string encoding of the integer l(M) (clause 5.1).
6) l_M~ = l_M fixed to 16 bits (clause 5.2).
7) H(l_M~ || M~) = (h_{lH-1},...,h_0), using the hash named by i_H, length lH per clause 5.7.
8) M' = i_H || H(l_M~ || M~) || l_M~ || M~.
9) Compute one-time key ε (clause 9.3).
10) R = εP = (x_R, y_R) (clause 6.12 - point scalar multiplication, not in the scan; see above).
11) r = x_R, as an l(p)-bit string.
12) T = εQ = (x_T, y_T).
13) κ = x_T, as the Kalyna-KW key (clause 5.2).
14) t = E^(κ)_{l,k}(M') (Kalyna-l/k-KW or -KW-p per Table 2).
15) Output C = (r || t).
Decryption algorithm (clause 12 — full primary text, 19 steps)
1) Determine l_H from p (clause 6.2).
2) If r=0, r=1, or r² = a·d⁻¹ mod p: "Invalid ciphertext", abort.
3) v = (1-r²)·(a-d·r²)⁻¹ mod p.
4) δ = v^((p-1)/2) mod p (clause 6.6). If δ = p-1: "Invalid ciphertext", abort.
5) y = √v mod p (clause 6.7).
6) R' = (r, y). // y's sign is ambiguous by construction - see Додаток Б.3
7) T' = e·R' (clause 6.12). // resolved ambiguity, see below
8) κ = x_T'.
9) (u_{l(p)-1},...,u_0) = D^(κ)_{l,k}(t).
10) i_H = (u_{l(p)-1},...,u_{l(p)-8}); validate against the user group.
11) Look up H named by i_H.
12) Compute l_max(p).
13) l_M~ = (u_{l_max(p)+15},...,u_{l_max(p)}).
14) l(M) = sum_i u_{l_max(p)+i}·2^i, read from l_M~.
15) M' = (u_{l_max(p)+15},...,u_0).
16) Verify H(M') matches the extracted hash field; on mismatch: "Invalid hash value", abort.
17) b = l_max(p) - l(M); if b != 0, verify the padding is all-zero; on mismatch: abort.
18) M = (u_{l(M)-1},...,u_0).
19) Output M.
Resolved ambiguity (the older secondary-source pseudocode flagged this twice as unresolved,
“source literally writes T'=e·r, a scalar, not e·R', a point”): the primary text’s own step 7
has the same slip (T = er, using the scalar r not the point R'), but Додаток Б.3’s
correctness proof states it unambiguously as a point operation — “Далі з використанням таємного
ключа е обчислюють точку T = eR = ±eQ = ±T” — confirming T' = e·R' (point scalar multiplication)
is correct, and that the x-coordinate of the result is sign-independent (x_T = x_{-T}), which
is exactly why κ=x_T' recovers the right key regardless of which of R/-R the square-root step
happened to reconstruct. Not just “reads as the same slip” (the older file’s own hedge) — this is
now a clause citation (Додаток Б.3), not an inference.
Open gaps (still not answerable from this scan + supplement)
Clauses 6.5–6.12resolved 2026-08-05 (T-176) — see “Computational algorithms, clauses 6.4–6.12” above, cited directly from the page images.Додаток А’s RNG algorithm bodyresolved 2026-08-05 (T-176) — full body now in hand (Kalyna-l/k-CTR per DSTU 7624 §7, Table А.1’sl/kchoices perλ). Not adopted as-is: this project’s existingrandombytes::randombytes_buf(getrandom-backed, D-48) remains the simpler choice, and clause 6.1 explicitly permits any equivalent generator as an alternative — Додаток А is now a documented option, not a hard requirement.- Додаток Б.1/Б.2’s actual math content is still not in hand — the supplement’s one relevant page only reached the appendix’s introductory historical prose (Edwards/Bernstein–Lange/Bessalov literature survey), cutting off before whatever Б.1/Б.2 themselves define. Likely low-value even if obtained — Б.3 (correctness proof) and Б.4 (the actual projective addition law) are the operative parts of Додаток Б and both were already in hand from the first scan.
- Sections 1–2 (scope, normative references) remain absent — administrative content, zero implementation impact.
- Corrected stale bullet (this file previously contradicted itself here):
t/Cin Додаток Г.1 (l(p)=256) are arithmetically verified — see “Verification performed” above, done the same day as T-174’s first pass, just not reflected in this list until now. Додаток Г.3 (l(p)=512) is now verified too (T-192, D-180) -Q/R/T/kappa/Hall match the document’s own printed hex exactly,t/Cmatch to within a 2-hex-digit discrepancy attributed to the same printing-erratum pattern already documented for Додаток Г.1’s ownt(g1-worked-example.json’st_ciphertext_note). What’s still unverified is Додаток Г.2 (l(p)=384) - present in the scan (curve params,Q/R/Tetc.) but its ownt/Ccan’t be checked yet regardless (hazmat::kalyna_kw_pdoesn’t exist, see below). - No worked example exists for
l(p)=768(λ=383) anywhere in this standard — confirmed 2026-08-06, owner-supplied photos of the document’s final pages. The document is genuinely 36 pages total (not the 40 the store listing implies,docs/ORACLES.md); page 36 is the last page, containing the tail of Додаток Г.3’sl(p)=512decryption steps followed directly by Додаток Д’s bibliography. Table В.4’s curve parameters are present, but Додаток Г never included a fourth worked example. This is not a purchasing gap — there is nothing more to buy.l(p)=768’s parameters are permanently unverifiable against any worked arithmetic from this standard; an eventual implementation would need a from-scratch verification strategy (property/tamper tests, no vector oracle), the same posture ascrypto_secretstream(D-68) or Strumok (D-15). - Kalyna-l/k-KW-p (the padding variant, needed for the
l(p)=384row of the mode table above) does not exist in this crate.hazmat::kalyna_kw’s own module doc is explicit that it deliberately has “no padding scheme of this module’s own” (a cited Bouncy-Castle-matching design choice, D-55) — meaning DSTU 9041 needs a genuinely new sibling primitive, not a parameter tweak, before thel(p)=384case (or any non-block-alignedM') can be implemented. - Prime-field (
F_p, up to at least 512 bits) bignum arithmetic and twisted-Edwards point arithmetic were new to this crate as of T-176 —hazmat::dstu9041::fp256/curve256now cover thel(p)=256case (T-177).hazmat::dstu4145’s existinggf2m163/curve163modules are binary-field (GF(2^m)) arithmetic for a completely different curve family and were not reused beyond the general engineering pattern (fixed-iteration loops, no secret-dependent branching,subtle::ConstantTimeEq). Thel(p)=384/512/768cases still need their own siblingfp384/fp512/fp768-shaped modules (different modulus, different reduction constant) plushazmat::kalyna_kw_p(below) — out of scope for T-177’s “ship the recommended curve first” pass.
Implementation status (T-177, T-192)
hazmat::dstu9041 (l(p)=256/E256/1, D-47’s “ship the recommended curve first” precedent) is
implemented: message.rs (M' formatting + the Kalyna-KW zero-block quirk), fp256.rs (F_p
arithmetic), curve256.rs (twisted Edwards point arithmetic), encryption.rs (encrypt/decrypt
composition, clauses 11/12). Full detail — the two security findings beyond clause 12’s literal
text (an order-2 point via r=p-1, and a genuine order-4 subgroup from E256/1’s cofactor 4), the
collapsed DecryptError, and the QA-gate closure (miri/Kani) — is in docs/TASKS.md T-177 and
docs/DECISIONS.md (D-163/D-165/D-166 for the source material, the T-177 entry for the
implementation itself).
l(p)=512/E512/1 is implemented too (T-192, 2026-08-08): message512.rs/fp512.rs/
curve512.rs/encryption512.rs, same phased/test-first pattern, verified end-to-end against
Додаток Г.3’s own worked example (docs/DECISIONS.md D-176/D-177/D-178/D-179/D-180). Both of
E256/1’s security findings were independently re-derived for E512/1 rather than assumed to carry
over (same r=p-1 order-2 point, same cofactor-4 order-4 subgroup - confirmed via the same
Hasse-interval method, not copied). l(p)=512 needed no new Kalyna-KW variant
(Kalyna512_512Kw already existed) since its M' lands exactly block-aligned (Table 1) - unlike
l(p)=384 below.
Still not done, deliberately out of scope for T-192: l(p)=384 (needs hazmat::kalyna_kw_p
for its non-block-aligned M' padding case, a genuinely new primitive) and l(p)=768 (no
worked-example oracle exists at all in the standard, confirmed 2026-08-06, not just missing from
this scan — see “Open gaps” above). Per this project’s own Tier C precedent (T-172 and earlier: any
new primitive gets its own advisor() consultation and plan-mode pass before code is written, not
after), picking either of these up should follow the same phased/tested-first pattern T-177/T-192
used, not be treated as a small parameter tweak.