Last active
November 10, 2026 06:55
-
-
Save rickmark/76d1e4df9ead3a776e15eddfa28ca891 to your computer and use it in GitHub Desktop.
Three Residues of One Undisclosed Procedure in the SEC 2 Koblitz Curves
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Three Residues of One Undisclosed Procedure in the SEC 2 Koblitz Curves | |
| Nov 9, 2026 - @Rick Mark | |
| The four prime-field Koblitz curves in SEC 2 carry three regularities the standard never states. Every prime is ≡ 1 (mod 7), every b lies in {3, 5, 7}, and every generator is twice a point built from one shared 156-bit value. We treat all three as residues of a single undisclosed generation procedure. | |
| secp160k1 is where the simplest reading fails twice. It uses the fifth workable prime where the other curves use the first, and it breaks the generator offset rule the other three follow. The prime skip and the shared generator value were noted before; the two prime rules and the offset test appear to be new. | |
| ## Background | |
| The four curves use primes p = 2^k − 2^32 − c for k = 160, 192, 224 and 256, with curve y^2 = x^3 + b. [SEC 2 v1.0](https://secg.org/SEC2-Ver-1.0.pdf) (§2.1) says only that these parameters “were chosen by repeatedly selecting parameters admitting an efficiently computable endomorphism until a prime order curve was found”. It gives no rule for c or b. | |
| The curve y^2 = x^3 + b has that endomorphism only when p ≡ 1 (mod 3). Its order then takes one of six values fixed by p. Two are even and two are divisible by 3, so at most two can be prime. | |
| We call c a **candidate** when p is prime, p ≡ 1 (mod 3), and one of those orders is prime; some b then attains it. | |
| ## Prior work | |
| The secp160k1 skip, the b = 2 oddity and the shared generator value are already on record. Rules A and B are not, and Rule C’s three-of-four fit may be new. | |
| | Date | Source | What it established | | |
| | --- | --- | --- | | |
| | Oct 2026 | [Khalid Hamadeh, certicom-generators](https://khalidhamadeh.com/writing/certicom-generators) | Structured hidden values in the generators of 25 standard curves, including the core shared by the four secp…k1 curves. Proposes a stored-value routine; origin of the values unknown. | | |
| | Oct 2026 | [Anthropic, ECDSA curve origins](https://www.anthropic.com/research/ecdsa-curve-origins) | NIST curve seeds traced to lrand48 seeded with Unix timestamps. Those runs skipped viable curves for no stated reason. | | |
| | Jan 2025 | [Bitcointalk, stwenhao](https://bitcointalk.org/index.php?topic=5525070.0) | Asks why secp224k1 uses b = 5 when b = 2 would work. Unanswered. | | |
| | Aug 2023 | [Bitcointalk, garlonicon and others](https://bitcointalk.org/index.php?topic=5464362.0) | Asks which formula selects the 160-, 192- and 224-bit primes. No rule is given. | | |
| | Feb 2023 | [Cryptography Stack Exchange, question 104032](https://crypto.stackexchange.com/questions/104032) | A Sage script recomputes the parameters and lists the four earlier secp160k1 values. The asker notes that secp160k1 is the last line of output and the other curves the first. | | |
| | Apr 2022 | [Johannes Zweng, gist](https://gist.github.com/johnzweng/863f412689ee383cc41ac7c709ca662c) | Halving G gives a short x-coordinate with one core shared by all four curves. | | |
| | Jul 2020 | [Bitcointalk, j2002ba2](https://bitcointalk.org/index.php?topic=5259486.0) | c = 977 gives the first prime with a prime-order curve, and b = 7 is the smallest b. | | |
| | 2015 | Greg Maxwell, as reported by Hamadeh | Halving the secp224k1 and secp256k1 generators gives the same short value. | | |
| This note adds Rules A and B for the primes, the odds that each fits by accident, the check against the WTLS curves, and the offset test behind Rule C. A search of forums, Stack Exchange and the papers we could open found neither prime rule stated anywhere. Two possibly relevant ePrint papers, [2020/1136](https://eprint.iacr.org/2020/1136) and [2024/1906](https://eprint.iacr.org/2024/1906), could not be opened and remain unchecked. | |
| ## secp160k1 uses the fifth candidate; the others use the first | |
| Every candidate up to the published constant, for each key size: | |
| | Curve | c | p mod 7 | b ≤ 20 giving prime order | Status | | |
| | --- | --- | --- | --- | --- | | |
| | secp160k1 | 3929 | 3 | 12, 15 | skipped | | |
| | secp160k1 | 16919 | 5 | 11 | skipped | | |
| | secp160k1 | 18293 | 3 | 2, 14, 18, 20 | skipped | | |
| | secp160k1 | 20309 | 3 | 17 | skipped | | |
| | secp160k1 | **21389** | **1** | 7, 12, 15, 19 | **published, b = 7** | | |
| | secp192k1 | **4553** | **1** | 3, 11, 14, 20 | **published, b = 3** | | |
| | secp224k1 | **6803** | **1** | 2, 5, 6, 15, 18 | **published, b = 5** | | |
| | secp256k1 | **977** | **1** | 7, 12, 20 | **published, b = 7** | | |
| Of the 196 values c ≤ 21389 that make the 160-bit p prime, 97 have p ≡ 1 (mod 3) and five are candidates. The other three key sizes have no candidate below the published c. | |
| Two things stand out. Every published prime is ≡ 1 (mod 7) and no skipped one is. And secp224k1 uses b = 5 although b = 2 gives the same prime order. | |
| ## Two rules reproduce the published primes | |
| **Rule A** takes the first candidate with p ≡ 1 (mod 7). **Rule B** tests only b ∈ {3, 5, 7}, takes the first c where one of them gives prime order, and uses the smallest such b. | |
| | | Rule A: p ≡ 1 (mod 7) | Rule B: b ∈ {3, 5, 7} | | |
| | --- | --- | --- | | |
| | Four published primes | Reproduced | Reproduced | | |
| | Four published b values | 3 of 4 if paired with “smallest b”; predicts b = 2 for secp224k1 | Reproduced | | |
| | Expected pass rate per candidate | 1 in 6 (17%) | 1 − (5/6)^3 = 42% | | |
| | Observed among the 37 unpublished candidates with c < 2^16 | 8 (22%) | 14 (38%) | | |
| | Chance an unrelated rule with that pass rate fits all eight outcomes | 1 in 2,700 | 1 in 280 | | |
| | Left unexplained if this is the real rule | secp224k1’s b = 5, and why no published b exceeds 7 | Why all four primes are ≡ 1 (mod 7) | | |
| Rule B does not depend on its exact set. Any set S with {3, 5, 7} ⊆ S ⊆ {3, …, 10} selects the same four curves. What it needs is that b = 2 was never tried, which secp224k1’s b = 5 supports independently of the secp160k1 skip. | |
| The rules are not exclusive. A search restricted to p ≡ 1 (mod 7) and to b ∈ {3, 5, 7} fits the data equally well. | |
| ## How much is coincidence | |
| Some rule beyond “first candidate” must exist, because secp160k1 skips four. A rule unrelated to the real one, passing each candidate with probability r, matches the eight pass and fail outcomes with probability r^4 × (1 − r)^4. That is 1 in 2,700 for Rule A and 1 in 280 for Rule B. | |
| Neither figure is a p-value. Both rules were found after looking at the data, and the set of rules considered was not fixed in advance. | |
| For Rule A the search can be bounded. The four published primes agree modulo m, for m < 1,000, only when m divides 42, and 2 and 3 are forced. Four random primes share a residue modulo some prime m ≥ 7 about 0.7% of the time, or 2.3% counting m = 5. | |
| Whichever rule is real, the other’s fit is then an accident at the odds in the table. The data cannot say which, and both may hold. | |
| ## A third residue: the generators | |
| Halving each published generator gives a point P with 2P = G whose x-coordinate is short and built around one value shared by all four curves. According to [Khalid Hamadeh’s history](https://khalidhamadeh.com/writing/certicom-generators), Greg Maxwell noticed this on secp224k1 and secp256k1 in 2015 and [Johannes Zweng](https://gist.github.com/johnzweng/863f412689ee383cc41ac7c709ca662c) wrote up all four curves in 2022. Hamadeh extended it to 25 standard curves in 2026. | |
| The four values decompose exactly as x(P) = A × 2^156 + C + B, with C = 0x8ce563f89a0ed9414f5aa28ad0d96d6795f9c63 on every curve. | |
| | Curve | A (prefix) | B (offset) | Is A × 2^156 + C on the curve? | First valid offset counting up | | |
| | --- | --- | --- | --- | --- | | |
| | secp160k1 | 0x4 | −1 | Yes | 0 | | |
| | secp192k1 | 0x554123b7 | +3 | No | +3 | | |
| | secp224k1 | 0x3b7 | 0 | Yes | 0 | | |
| | secp256k1 | 0x3b7 | 0 | Yes | 0 | | |
| **Rule C** says B is the smallest non-negative offset that puts the value on the curve. It holds for secp192k1, secp224k1 and secp256k1. It fails for secp160k1, whose base value is already on the curve yet is published one lower. | |
| The evidence for Rule C is modest. The secp192k1 fit is a 1 in 8 coincidence, and the other two are zero-step cases. Hamadeh proposes the same shape of routine and reports that the final digits “fit no single stopping rule we tried”; we have not seen which rules were tried. | |
| The prefixes are unexplained. Two smaller regularities also hold: secp192k1’s value contains the whole secp256k1 value in its low 168 bits, and y(P) is even on all four curves. | |
| ## One procedure: the working hypothesis | |
| We take Rules A, B and C to be residues of one generation procedure, run by one party, that SEC 2 does not disclose. | |
| What supports it: | |
| - **The shared value C is direct evidence.** Four independent generator choices would not agree on 156 bits. | |
| - **Rules A and B each cover all four curves with one condition.** | |
| - **There is precedent.** [Anthropic’s 2026 reconstruction](https://www.anthropic.com/research/ecdsa-curve-origins) traced the NIST curve seeds to the C library’s lrand48 generator seeded with Unix timestamps. Those runs also passed over viable curves for no stated reason. | |
| What it does not show: | |
| - **A shared mathematical property.** We found no link between C and the residues of p: x(P) mod 7 is 6, 5, 2, 2 across the four curves. | |
| - **A shared larger field.** The greatest common divisor of the differences between the four primes is exactly 42. Each field therefore contains the 42nd roots of unity, and nothing more is common to all four. No prime in a fixed list of 82 standard-curve and Mersenne primes contains the factors of all four c values in its multiplicative group. | |
| - **The source of C.** The value does not come from lrand48, mrand48 or the FreeLIP generator in any byte, word or limb packing we tried. | |
| Two readings remain open. In the first, one uniform rule set produced all four curves. In the second, three curves came from plain “first that works” steps and secp160k1 was produced separately, or by adapting a routine whose 166-bit value does not fit a 160-bit field. | |
| secp160k1 being the exception for both the prime and the generator favours the second reading. Neither explains why all four primes are ≡ 1 (mod 7). | |
| ## What does not explain the skip | |
| - **Other residues.** Modulo 8 the published primes are 3, 7, 5, 7 and the skipped ones 7, 1, 3, 3. No modulus below 1,000 separates them unless it is a multiple of 7. | |
| - **Curve order.** The order n exceeds p for secp160k1 and secp224k1 and is below p for the other two. All four skipped candidates have n < p. The Stack Exchange script stops at the first negative trace (n > p), which matches secp160k1 and secp224k1 only. | |
| - **Embedding degree.** None of the eight curves in the table has embedding degree below 200, so a MOV check rejects none of them. | |
| - **Factorisation of c.** An earlier draft of this note read meaning into 21389 = 73 × 293 with 73 | 292. That property holds for 17% of odd semiprimes between 15,000 and 28,000 (416 of 2,408) and is not evidence. | |
| - **WTLS curves 8 and 9.** These curves (b = 3, base point (1, 2)) use p = 2^112 − 537 and p = 2^160 − 229233. Their primes are ≡ 4 and 5 (mod 7), and neither is the first of its form with prime order at b = 3. Neither rule extends beyond the SEC 2 template. | |
| ## Open questions | |
| - **Why 7?** p ≡ 1 (mod 7) has no stated role. A weaker condition, that −7 is a square mod p (p ≡ 1, 2 or 4 mod 7), also separates skipped from published. It is the condition for a different endomorphism curve in [Gallant, Lambert and Vanstone](https://www.iacr.org/archive/crypto2001/21390189.pdf) (Example 5). Whether that is connected is unknown. | |
| - **Why not b = 2?** Nothing mathematical excludes it: y^2 = x^3 + 2 and y^2 = x^3 + 5 have the same order over secp224k1’s field and are isomorphic there. | |
| - **Is there a record?** Certicom’s generation scripts or notes would settle the matter. We found none. | |
| - **Is this new?** The skip and the b = 2 observation are not; see Prior work. Neither rule turned up in our search, but two ePrint papers, conference talks and non-English sources remain unchecked. | |
| - **Where does C come from?** It is 156 bits and looks random. The obvious generators of the period are ruled out above; hashes of the published SEC 2 seeds have not been tested here. | |
| - **Why is secp160k1 the exception twice?** A separate run, a different operator, or a routine adapted from the larger curves would each explain it. Nothing yet says which. | |
| ## Reproduction | |
| The script needs Python 3.8 or later, has no dependencies, and runs in about a second. It lists every candidate up to the published c for each key size. Raising the bound to 2^16 gives the 41 candidates used for the pass rates above. | |
| ```python | |
| """Candidate primes for the SEC 2 Koblitz curves, p = 2^k - 2^32 - c. | |
| A candidate is a c for which p is prime, p = 1 (mod 3), and some curve | |
| y^2 = x^3 + b over F_p has prime order. Pure Python, no dependencies. | |
| """ | |
| import random | |
| from math import isqrt | |
| BASES = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71] | |
| def is_prime(n): # Miller-Rabin, 20 fixed bases | |
| if n < 2: | |
| return False | |
| for q in BASES: | |
| if n % q == 0: | |
| return n == q | |
| d, s = n - 1, 0 | |
| while d % 2 == 0: | |
| d, s = d // 2, s + 1 | |
| for a in BASES: | |
| x = pow(a, d, n) | |
| if x in (1, n - 1): | |
| continue | |
| for _ in range(s - 1): | |
| x = x * x % n | |
| if x == n - 1: | |
| break | |
| else: | |
| return False | |
| return True | |
| def sqrt_mod(a, p): # Tonelli-Shanks; None for a non-residue | |
| a %= p | |
| if pow(a, (p - 1) // 2, p) != 1: | |
| return None | |
| q, s = p - 1, 0 | |
| while q % 2 == 0: | |
| q, s = q // 2, s + 1 | |
| z = 2 | |
| while pow(z, (p - 1) // 2, p) != p - 1: | |
| z += 1 | |
| m, c, t, r = s, pow(z, q, p), pow(a, q, p), pow(a, (q + 1) // 2, p) | |
| while t != 1: | |
| i, u = 0, t | |
| while u != 1: | |
| u, i = u * u % p, i + 1 | |
| e = pow(c, 1 << (m - i - 1), p) | |
| m, c, t, r = i, e * e % p, t * e * e % p, r * e % p | |
| return r | |
| def odd_orders(p): | |
| """The four odd group orders that y^2 = x^3 + b can have over F_p.""" | |
| r = sqrt_mod(-3, p) | |
| a, x = p, max(r, p - r) | |
| while x * x > p: # Cornacchia: x^2 + 3 y^2 = p | |
| a, x = x, a % x | |
| y = isqrt((p - x * x) // 3) | |
| assert x * x + 3 * y * y == p | |
| return [p + 1 - t for t in (x + 3 * y, -x - 3 * y, x - 3 * y, 3 * y - x)] | |
| def add(P, Q, p): # affine addition on y^2 = x^3 + b | |
| if P is None: | |
| return Q | |
| if Q is None: | |
| return P | |
| (x1, y1), (x2, y2) = P, Q | |
| if x1 == x2: | |
| if (y1 + y2) % p == 0: | |
| return None | |
| lam = 3 * x1 * x1 * pow(2 * y1, -1, p) % p | |
| else: | |
| lam = (y2 - y1) * pow(x2 - x1, -1, p) % p | |
| x3 = (lam * lam - x1 - x2) % p | |
| return x3, (lam * (x1 - x3) - y1) % p | |
| def mul(k, P, p): | |
| R = None | |
| while k: | |
| if k & 1: | |
| R = add(R, P, p) | |
| P, k = add(P, P, p), k >> 1 | |
| return R | |
| def has_prime_order(b, p, prime_orders, rng): | |
| """If n is prime and n*P = O for a point P, the Hasse bound forces #E = n.""" | |
| while True: | |
| x = rng.randrange(p) | |
| y = sqrt_mod(x ** 3 + b, p) | |
| if y: | |
| break | |
| return any(mul(n, (x, y), p) is None for n in prime_orders) | |
| PUBLISHED = {160: (21389, 7), 192: (4553, 3), 224: (6803, 5), 256: (977, 7)} | |
| if __name__ == "__main__": | |
| rng = random.Random(1) | |
| for k, (c_pub, b_pub) in PUBLISHED.items(): | |
| for c in range(1, c_pub + 1, 2): | |
| p = 2 ** k - 2 ** 32 - c | |
| if p % 3 != 1 or not is_prime(p): | |
| continue | |
| prime_orders = [n for n in odd_orders(p) if is_prime(n)] | |
| if not prime_orders: | |
| continue | |
| bs = [b for b in range(1, 21) if has_prime_order(b, p, prime_orders, rng)] | |
| tag = f" <- published, b = {b_pub}" if c == c_pub else "" | |
| print(f"secp{k}k1 c = {c:<6} p mod 7 = {p % 7} prime order for b = {bs}{tag}") | |
| ``` | |
| Output: | |
| ``` | |
| secp160k1 c = 3929 p mod 7 = 3 prime order for b = [12, 15] | |
| secp160k1 c = 16919 p mod 7 = 5 prime order for b = [11] | |
| secp160k1 c = 18293 p mod 7 = 3 prime order for b = [2, 14, 18, 20] | |
| secp160k1 c = 20309 p mod 7 = 3 prime order for b = [17] | |
| secp160k1 c = 21389 p mod 7 = 1 prime order for b = [7, 12, 15, 19] <- published, b = 7 | |
| secp192k1 c = 4553 p mod 7 = 1 prime order for b = [3, 11, 14, 20] <- published, b = 3 | |
| secp224k1 c = 6803 p mod 7 = 1 prime order for b = [2, 5, 6, 15, 18] <- published, b = 5 | |
| secp256k1 c = 977 p mod 7 = 1 prime order for b = [7, 12, 20] <- published, b = 7 | |
| ``` | |
| The half-generator check runs after the script above and reuses its `mul` and `sqrt_mod`. | |
| ```python | |
| # Run after the script above (it uses mul and sqrt_mod): the half-generator check. | |
| H = lambda s: int(s, 16) | |
| GEN = { # k: (c, b, n, Gx, Gy), from SEC 2 | |
| 160: (21389, 7, H("0100000000000000000001B8FA16DFAB9ACA16B6B3"), | |
| H("3B4C382CE37AA192A4019E763036F4F5DD4D7EBB"), | |
| H("938CF935318FDCED6BC28286531733C3F03C4FEE")), | |
| 192: (4553, 3, H("FFFFFFFFFFFFFFFFFFFFFFFE26F2FC170F69466A74DEFD8D"), | |
| H("DB4FF10EC057E9AE26B07D0280B7F4341DA5D1B1EAE06C7D"), | |
| H("9B2F2F6D9C5628A7844163D015BE86344082AA88D95E2F9D")), | |
| 224: (6803, 5, H("010000000000000000000000000001DCE8D2EC6184CAF0A971769FB1F7"), | |
| H("A1455B334DF099DF30FC28A169A467E9E47075A90F7E650EB6B7A45C"), | |
| H("7E089FED7FBA344282CAFBD6F7E319F7C0B0BD59E2CA4BDB556D61A5")), | |
| 256: (977, 7, H("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141"), | |
| H("79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798"), | |
| H("483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8")), | |
| } | |
| C = 0x8CE563F89A0ED9414F5AA28AD0D96D6795F9C63 # the shared 156 bits | |
| for k, (c, b, n, gx, gy) in GEN.items(): | |
| p = 2 ** k - 2 ** 32 - c | |
| assert (gy * gy - gx ** 3 - b) % p == 0 and mul(n, (gx, gy), p) is None | |
| hx, hy = mul((n + 1) // 2, (gx, gy), p) # the point P with 2P = G | |
| A, B = hx >> 156, hx % 2 ** 156 - C # hx = A * 2^156 + C + B | |
| base = A * 2 ** 156 + C | |
| first = next(o for o in range(100) if sqrt_mod((base + o) ** 3 + b, p)) | |
| print(f"secp{k}k1 A = {A:#x} B = {B:+d} first valid offset = +{first} y even: {hy % 2 == 0}") | |
| ``` | |
| Output: | |
| ``` | |
| secp160k1 A = 0x4 B = -1 first valid offset = +0 y even: True | |
| secp192k1 A = 0x554123b7 B = +3 first valid offset = +3 y even: True | |
| secp224k1 A = 0x3b7 B = +0 first valid offset = +0 y even: True | |
| secp256k1 A = 0x3b7 B = +0 first valid offset = +0 y even: True | |
| ``` | |
| ## Sources | |
| - [SEC 2: Recommended Elliptic Curve Domain Parameters, v1.0](https://secg.org/SEC2-Ver-1.0.pdf): the four curves and the §2.1 statement on how Koblitz parameters were chosen. | |
| - [Khalid Hamadeh, “certicom-generators” write-up](https://khalidhamadeh.com/writing/certicom-generators) and [repository](https://github.com/khalid-hamadeh/certicom-generators): the hidden generator values, the proposed routine, and the history of earlier observations. | |
| - [Johannes Zweng, gist on the secpXXXk1 generation points](https://gist.github.com/johnzweng/863f412689ee383cc41ac7c709ca662c): the four-curve half-generator pattern. | |
| - [Anthropic, “Claude reveals ECDSA curve origins”](https://www.anthropic.com/research/ecdsa-curve-origins): the lrand48 and FreeLIP reconstruction of the NIST parameters, and the skipped candidates. | |
| - [Cryptography Stack Exchange, question 104032](https://crypto.stackexchange.com/questions/104032), read through [a mirror](https://cloud.tencent.com/developer/ask/sof/116136092) because the original could not be opened: the recomputation script and the note on secp160k1. | |
| - [Bitcointalk, “Selecting p-values for secp160k1, secp192k1 and secp224k1”](https://bitcointalk.org/index.php?topic=5464362.0): the open question on the three smaller primes. | |
| - [Bitcointalk, “Square roots and cube roots in secp224k1”](https://bitcointalk.org/index.php?topic=5525070.0): the b = 5 versus b = 2 question. | |
| - [Bitcointalk, “Luck when selecting Secp256k1 parameters”](https://bitcointalk.org/index.php?topic=5259486.0): 977 as the first prime with a prime-order curve, b = 7 as the smallest b. | |
| - [Gallant, Lambert, Vanstone, “Faster Point Multiplication on Elliptic Curves with Efficient Endomorphisms”](https://www.iacr.org/archive/crypto2001/21390189.pdf): the endomorphism examples, including the −7 condition. | |
| - [WTLS curve 8](https://std.neuromancer.sk/wtls/wap-wsg-idm-ecid-wtls8/) and [WTLS curve 9](https://std.neuromancer.sk/wtls/wap-wsg-idm-ecid-wtls9/): parameters, from the Standard curve database. | |
| - [DiSSECT paper](https://dissect.crocs.fi.muni.cz/static/dissect.pdf) and [notes on the SECG curves](https://dissect.crocs.fi.muni.cz/standards/secg): context; the paper does not simulate the Koblitz curves. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment