SevyDevy

Guide · Backend

NIST Curve Seeds Explained: How P-256 Was Traced to a 1997 Timestamp

19 min readUpdated By Saket Bhatnagar

Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.

In short

  • The NIST curve seeds, the 160-bit numbers hashed (scrambled by a fixed function) to build the P-256 signature curve and its sisters, came from rand48. That is an old C random number generator started from the clock. Only one of the ten seeds, for a rarely used curve called B-163, was still unexplained as of 19 October 2026.
  • For P-256, the start was srand48(871696354), the call that sets rand48's starting state. 871696354 is a Unix timestamp, seconds since 1970: 01:52:34 UTC on 16 August 1997. The published seed is the 1,842nd chunk of 20 bytes it produced.
  • This is not a break (a way to defeat the curve), and it makes a backdoor (a weakness planted by the designer) implausible. Filippo Valsorda, who offered a bounty for the answer, estimates the designers had at most about 30 bits of choice. That is roughly a billion options, against the roughly 70 bits a 2017 technique for hiding a weak curve relied on.
  • The lesson for your own systems: make every public constant reproducible from a published script, use standard curves and never seed secrets such as voucher codes from the clock.
  • Beginners can stop after the nothing-up-my-sleeve section. Engineers then get Python checks that rebuild P-256's seed and coefficient, seeds of the binary-field curves that read as 1984 dates, the open questions and a clock-seeded voucher attack.

What goes wrong when nobody can explain where a constant came from?

People stop trusting it, even when it is safe, because nobody can tell an honest choice from a planted one. The NIST curve seeds stayed unexplained from 2000 until October 2026 and fed backdoor rumours for years.

Picture a common setup. A payments team signs every API request with ECDSA, a digital signature method, on the P-256 curve. A partner bank's security questionnaire asks why anyone should trust a curve whose seed was never explained. The setup is made up, but the question is a fair one.

The same week, the team finds a cashback voucher script that seeds Python's random module with the current time. Both problems come down to one question: where did a number come from, and who could have chosen it?

Which words do you need before reading about NIST curve seeds?

You need eight terms, each with a plain meaning here.

  • Elliptic curve: the points that fit a short equation. For P-256 it is y squared = x cubed - 3x + b, with all arithmetic done modulo a 256-bit prime p.
  • Modulo p: keep only the remainder after dividing by p, like a clock that wraps at 12.
  • P-256: one of five prime-field curves NIST published in 2000, from P-192 to P-521. The number is the size of p in bits. NIST also published curves over binary fields, a different kind of arithmetic, called B- and K-curves.
  • Seed: a starting value fed into a process. For the P- and B-curves, a published 160-bit seed was hashed to make the coefficient b.
  • Hash: a function such as SHA-1 that turns any input into a fixed-size scrambled output. The same input always gives the same output.
  • Pseudo-random number generator (PRNG): a formula that turns one starting number into a random-looking stream. The same start always gives the same stream.
  • Unix timestamp: the number of seconds since 00:00 UTC on 1 January 1970, the usual way C programs read the clock.
  • Base point: the fixed starting point on a curve that every key on that curve is built from.

What did researchers find about the NIST curve seeds in October 2026?

They found that the NIST curve seeds were not hashes of forgotten sentences. Nine of the ten were plain output of rand48, a random number generator in the C library, started from a Unix timestamp or half of one.

The old story centred on Jerry Solinas, the US National Security Agency (NSA) cryptographer who reportedly generated the seeds in 1997. In 2023, researcher Steve Weis reported an anonymous colleague's account: Solinas said he had hashed a sentence about deserving a raise, then lost the exact phrase. Solinas died in early 2023. The new timestamps put the runs between May 1997 and April 1998.

Valsorda announced the bounty on 5 October 2023: $12,288, tripled to $36,864 if the winner gave it to a US charity. The researchers donated it to ISRG, the nonprofit that runs Let's Encrypt.

The five prime-curve runs, rebuilt by our Python script on 19 October 2026 and matching the output in Valsorda's post (times in UTC)
Curvesrand48 inputDate and time (UTC)Published seed is block
P-19286396673418 May 1997, 14:45:34139
P-5218710027248 August 1997, 01:12:041,136
P-25687169635416 August 1997, 01:52:341,842
P-38487295260830 August 1997, 14:50:082,688
P-2248916079273 April 1998, 12:52:077,026

Read the table in time order: P-192 came first and P-224 last, 320 days later. Each run drew blocks and tested them with the curve recipe. Block numbers count from the very first draw.

One run could yield several curves. Valsorda's program shows that ANSI X9.62's sample curves prime192v3 and prime192v2 are blocks 5,228 and 6,013 of the same P-192 run.

How were the NIST P-curves generated from a seed?

Each P-curve came from a public recipe in ANSI X9.62, a 1998 financial-industry standard. You hash a 160-bit seed with SHA-1, turn the output into the coefficient b and keep the curve only if it passes safety checks.

For P-256, the recipe takes the rightmost 95 bits of SHA-1(seed) and all 160 bits of SHA-1(seed + 1). Here seed + 1 means the seed read as a number, plus one. Joined, they make a 255-bit number c. Then b is set so that b squared times c equals -27, modulo p.

The -27 is not magic. Every NIST P-curve fixes the other coefficient, a, at -3 for speed, and the recipe ties c to b through a cubed, which is -27. NIST's current curve document, SP 800-186, states this relation.

For the NIST curves, a candidate was kept only if its number of points was prime and it passed the other checks. NIST published the curves with their seeds in FIPS 186-2 in 2000. Anyone could rerun SHA-1 on the seed, so the method was described as choosing curves verifiably at random.

The weak spot was the seed itself. The hash proves b came from the seed, not where the seed came from. A designer could, in theory, try many seeds until one gave a curve with a hidden weakness.

How a 1997 timestamp became P-256

Notice the one step that is still not public maths: why about half of the passing seeds, such as block 876, were skipped. Everything else can now be rerun.

Diagram connections:

  • Clock reading to srand48 and rand48
  • srand48 and rand48 to 20-byte candidate seed
  • 20-byte candidate seed to SHA-1 hashes
  • SHA-1 hashes to Coefficient b
  • Coefficient b to Passes the checks?
  • Passes the checks? to Unknown skip rule (Yes)
  • Unknown skip rule to P-256 published (Kept)
  • Unknown skip rule to 20-byte candidate seed (Skipped: next block)
  • Passes the checks? to 20-byte candidate seed (No: draw the next block)
  • Clock reading: 871696354 seconds after 1 January 1970
  • srand48 and rand48: Turns the timestamp into a byte stream
  • 20-byte candidate seed: Each next 20 bytes is one try
  • SHA-1 hashes: SHA-1 of seed and seed + 1 give the number c
  • Coefficient b: b squared times c = -27, modulo p
  • Passes the checks?: Prime number of points, plus other rules
  • Unknown skip rule: About half of passing seeds were skipped
  • P-256 published: Block 1,842, printed in FIPS 186-2

Does the 1997 timestamp mean P-256 had a backdoor?

No. Valsorda writes that the findings completely rule out the attack technique people feared, described below, and that practitioners had no serious concerns even before. Nothing in your code needs patching.

The researchers' write-up adds that this is not a cryptographic break, new attack or weakness in the curves. They say they do not believe weakened curves were picked.

The worry was freedom of choice. In 2017, Daniel Bernstein and Tanja Lange described to fellow cryptographers Neal Koblitz and Alfred Menezes a technique that could use 70 bits of seed freedom. It aimed at a hypothetical class of 2^61 weak prime-order curves, curves whose number of points is prime.

Valsorda generously assumes the designers could pick any second in 1997 or 1998 and any of the first 16 candidates with a prime number of points. That is 63,072,000 seconds times 16, about a billion choices, or barely 30 bits.

Seventy bits is about 1.2 trillion times more room than that. He concludes the recipe leaves too little room to target a weak class or plant a NOBUS backdoor, one that nobody but its maker could use.

So the team's answer to the bank is short. The seed is traced to a 1997 clock reading, the leftover choice is about 30 bits, and the team keeps P-256.

What are nothing-up-my-sleeve numbers in cryptography?

They are constants built from something public and hard to tweak, so the designer had no room to cheat. The name borrows a magician's line before a trick.

A common pattern is hashing a plain, documented string. The best designs explain every choice. SafeCurves, a curve review site by Bernstein and Lange, calls this rigidity: the process limits how many curves it can produce.

RFC 7748, published in January 2016, documents this for Curve25519, a popular modern curve. Given the prime, it takes the smallest curve parameter and the smallest base point that pass published checks, so anyone can rerun the search.

SafeCurves rigidity levels, plus our reading of the NIST seeds after October 2026
LevelWhat it meansExampleCan a designer steer it?
Fully rigidThe whole generation process is explainedCurve25519Almost no room left
Somewhat rigidA few unexplained parts give few bits of controlbrainpoolP256t1, secp256k1A few bits of control
ManipulatableA large unexplained input existsP-256 as SafeCurves lists itMany curves to pick from
Our reading, not a SafeCurves levelThe seed source is known, why some seeds were skipped is notP-256 after the 2026 findingAbout 30 bits, per Valsorda

SafeCurves still listed P-256 as manipulatable when we checked on 19 October 2026.

Beginners can stop here. The rest is for engineers: the generator itself, how the timestamps were recovered, what is still open, and what clock-seeded randomness does to real secrets.

How does the rand48 generator turn a timestamp into a seed?

rand48 keeps one 48-bit number as its state. Each step multiplies the state by 25214903917 (0x5DEECE66D in hexadecimal), adds 11 and keeps the lowest 48 bits.

Calling srand48(t) puts the low 32 bits of t into the top of the state and fixes the bottom 16 bits at 0x330E. So the whole stream depends only on t, the clock reading.

Think of a deck that a machine shuffles by one fixed routine, starting from an order set by the clock. Know the second it started and you know every card that follows. Where the picture breaks: rand48 can also be run backwards, step by step.

Each new state gives one byte: its top 8 bits. Valsorda's code writes this as drand48() times 256, the researchers as lrand48() shifted right by 23. Both read the same 8 bits, and 20 bytes make one candidate seed.

We checked this against the real C library. On glibc 2.39, srand48, drand48 and lrand48 matched our Python formula for 5,000 steps from each of four different starts.

Pause and predict before you run the script below. If the researchers are right, the 1,842nd block from this 1997 clock reading should equal the published seed, all 160 bits.

python
# Rebuild the published P-256 seed from a 1997 timestamp
from datetime import datetime, timezone

t = 871696354  # the srand48 input the researchers found
state = (t << 16) | 0x330E  # what srand48(t) does to the 48-bit state


def next_byte():
    global state
    state = (state * 0x5DEECE66D + 0xB) % 2**48  # one rand48 step
    return state >> 40  # top 8 bits, the same as int(drand48() * 256)


for block in range(1842):  # draw 1,842 candidate seeds of 20 bytes each
    seed = bytes(next_byte() for _ in range(20))

print(datetime.fromtimestamp(t, timezone.utc))  # 1997-08-16 01:52:34+00:00
print(seed.hex())  # c49d360886e704936a6678e1139d26b7819f7e90

What just happened? The script set the state from the clock reading, drew 1,842 blocks of 20 bytes and printed the last one. It matches the seed in SP 800-186 exactly, and took about 20 milliseconds on our test server.

The second check closes the chain from seed to curve. It applies the recipe from the previous section to the published seed and tests the relation with b.

python
# Check that P-256's coefficient b really comes from its seed (FIPS 186 recipe)
import hashlib

seed = bytes.fromhex("c49d360886e704936a6678e1139d26b7819f7e90")
p = 2**256 - 2**224 + 2**192 + 2**96 - 1  # the P-256 prime
b = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B

h0 = int.from_bytes(hashlib.sha1(seed).digest(), "big") % 2**95  # rightmost 95 bits
s1 = (int.from_bytes(seed, "big") + 1).to_bytes(20, "big")  # the seed plus one
h1 = int.from_bytes(hashlib.sha1(s1).digest(), "big")  # 160 more bits
c = (h0 << 160) | h1  # a 255-bit number

print(hex(c))  # 0x7efba1662985be9403cb055c75d4f7e0ce8d84a9c5114abcaf3177680104fa0d
print(c * b * b % p == p - 27)  # True: b squared times c equals -27 mod p

It prints the same c that SP 800-186 lists for P-256, then True. Together, the two scripts take you from a 1997 clock reading to P-256's coefficient b. The standard wraps seed + 1 around at 160 bits, which only matters for a seed of all ones, so the script skips that step.

How did researchers work backwards from the seeds to the timestamps?

They matched the seeds to a known generator, then solved for its hidden state. Stepping that state back to the start revealed srand48's input, the timestamp.

Finding the generator

According to their write-up, the researchers' scanner tested 34 combinations of old generators and ways of slicing output into bytes.

One clue stood out. Each seed splits into five 32-bit words. NIST published ten seeds: five for the P-curves and five for the B-curves, since K-curves have none. Four of the ten have a zero top bit in every word, 20 words in all.

The researchers put the odds of that by chance at 1 in 5,810. We reproduced it: a random seed has a 1 in 32 chance of five zero top bits, and at least 4 of 10 happens once in 5,810 tries. The four are B-233, B-283, B-409 and B-571.

Recovering the hidden bits

Each byte shows only 8 of the 48 state bits. Their write-up says they used LLL lattice reduction, a standard method for equations with partly hidden values. The recovered start states ended in 0x330E, as srand48 requires.

Working backwards from a published seed

Notice the last check. A recovered start counts only if rerunning it rebuilds all 160 bits of the published seed.

Diagram connections:

  • Published 160-bit seed to Generator scanner
  • Generator scanner to LLL lattice reduction
  • LLL lattice reduction to Ends in 0x330E?
  • Ends in 0x330E? to Unix timestamp (Yes: read the top 32 bits)
  • Ends in 0x330E? to Generator scanner (No: try another guess)
  • Unix timestamp to Rerun and compare
  • Published 160-bit seed: Printed in the standard since 2000
  • Generator scanner: Tried 34 generator and slicing guesses
  • LLL lattice reduction: Solves for the 40 hidden bits per step
  • Ends in 0x330E?: The fixed low bits that srand48 sets
  • Unix timestamp: For P-256: 16 August 1997
  • Rerun and compare: Rebuilds the full seed exactly

The binary curves used a halved clock

The four binary seeds come from the same generator, sliced differently. Each 32-bit word is one whole lrand48() output, which has only 31 bits, so its top bit is always zero. That explains the clue.

It also leaves only 17 hidden bits per step, so we recovered the starts by brute force in under a second. The four srand48 inputs, 446413329 to 446413564, read as 23 February 1984.

Doubled, they land between 15:24 and 15:32 UTC on 17 April 1998. So their write-up suggests the program passed half the clock reading to srand48. That is an inference from the dates, not a documented fact.

What is still unexplained

Valsorda found roughly twice as many valid seeds as were published, so about half were skipped by a rule nobody has identified. In P-256's stream, block 876 passed every known check but was not used.

His code also applies one rule the standards never wrote down: the number of points may not have more bits than p. He calls it undocumented, so treat it as a finding, not a spec.

Their write-up also says the base points of P-224, P-256, P-384 and P-521 are unexplained. The B-163 seed fits nothing they tried, as of 19 October 2026.

Why is a time-seeded random generator dangerous for secrets?

Because an attacker can guess the time. A day has only 86,400 seconds, so trying every one is a small loop, not a hard attack.

The 1997 seeds were recoverable for the same reason. That was harmless, because a curve seed is meant to be public. For a voucher code, password reset token or key, it is a real hole.

The team's voucher script gives Rs 100 cashback per code (made up). The snippet below copies its bug, then attacks it by trying every second of the issue day.

python
import random
import secrets
import time

LETTERS = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"


def weak_voucher(issued_at):
    random.seed(issued_at)  # the bug: the clock is the only secret
    return "".join(random.choice(LETTERS) for _ in range(8))


issued_at = 1792310400  # made up: 18 October 2026, 13:30 IST
code = weak_voucher(issued_at)

start = time.perf_counter()
day_start = issued_at - issued_at % 86400  # midnight UTC that day
for guess in range(day_start, day_start + 86400):  # every second of the day
    if weak_voucher(guess) == code:
        print("Recovered", code, "from timestamp", guess)
print(f"Tried 86,400 seconds in {time.perf_counter() - start:.1f} s")

print("Safe voucher:", secrets.token_urlsafe(8))  # 8 bytes from the OS's secure source

Expect one Recovered line with the timestamp, then the time taken. On one core of our 2.1 GHz test server with Python 3.13, the full day took 0.8 to 2.2 seconds across our runs.

The fix is the last line. The secrets module reads the operating system's secure random source. token_urlsafe(8) uses 8 random bytes, 64 bits, and prints 11 characters.

How do you make your own constants and parameters verifiable?

Derive each constant from a public input with a short script, commit the script and let anyone rerun it. Use standard curves, never your own.

The team has its own constants, such as a fixed label mixed into its webhook signatures. It agrees on this checklist.

  1. Start from a plain input

    Hash a readable string, such as the product name and the constant's purpose, with SHA-256.

  2. Commit the script

    Keep the script that makes the constant next to the code that uses it.

  3. Write the retry rule

    If an output fails a check, the script tries the next counter value. Never pick by hand, and record every skip.

  4. Let someone else rerun it

    A reviewer reruns the script and confirms the output matches, byte for byte.

  5. Prefer standards

    Use P-256 or Curve25519 from a well-tested library. Never invent your own curve.

The retry rule is where the NIST story still has a gap. A recipe that skips candidates without saying why is the part reviewers cannot check.

How do you explain NIST curve seeds in a system design interview?

Treat it as a trust question about where a system's constants came from. Use this two-minute frame.

  1. Name the worry

    P-256's seed was unexplained for more than 25 years, so people feared a hidden weak curve.

  2. Explain the recipe

    A 160-bit seed is hashed with SHA-1 to make b, then safety checks decide whether the curve is kept.

  3. Give the October 2026 finding

    The seed came from rand48 seeded with a 1997 timestamp, rebuilt by the researchers' code and Valsorda's Go program.

  4. Size the leftover freedom

    Valsorda estimates at most about 30 bits of choice remain, against the roughly 70 bits the 2017 technique relied on.

  5. Contrast rigid designs

    Curve25519 takes the smallest values that pass published checks, so there is almost nothing to explain.

  6. Apply it to your design

    Make public constants reproducible, use standard curves and use secure randomness for secrets.

Questions people ask

Were the NIST elliptic curves backdoored?
There is no evidence of a backdoor, and the October 2026 finding makes one implausible. Nine of the ten seeds came from a clock-seeded generator run in 1997 and 1998. Valsorda estimates the designers had about 30 bits of choice, far too few for the roughly 70-bit technique Bernstein and Lange described in 2017.
Should I stop using P-256 after this news?
No. Nothing about the curve's maths changed, and the researchers call it no break, new attack or weakness. Your keys, certificates and signatures are as strong as before. Valsorda now writes that he would personally recommend the NIST curves over Curve25519. He adds, half-joking, that this comes just in time for them to be retired in favour of post-quantum signatures, which are designed to resist quantum computers.
What is srand48 and how is it different from drand48 and lrand48?
srand48 is the C library function that seeds the rand48 family. It puts the low 32 bits of its argument into the top of a 48-bit state and sets the low 16 bits to 0x330E. drand48 and lrand48 then step that same state. drand48 returns a fraction between 0 and 1, and lrand48 returns the top 31 bits as a whole number.
Can I reproduce the NIST curve seeds myself?
Yes, for the five prime curves. Seed rand48 with the timestamp from the table, draw 20-byte blocks and compare the listed block with the published seed. Our Python script does P-256 in well under a second. Valsorda's Go program also reruns the full curve checks, which needs the PARI/GP maths tool to count curve points.
What about the NIST curve seeds is still unexplained?
Three things remain open as of 19 October 2026. Roughly half of the valid candidates were skipped by an unknown rule. The base points of P-224, P-256, P-384 and P-521 are unexplained. The seed of the binary curve B-163 also fits nothing tried so far.
Has NIST responded to the curve seed discovery?
We found no NIST statement as of 19 October 2026. Confirmation comes from the researchers' published code and Filippo Valsorda's independent Go program. Both rebuild the published seeds, so you can check the result yourself without trusting either of them.
Are the NIST binary curves still recommended?
No. NIST's SP 800-186, published in February 2023, deprecates elliptic curves over binary fields, which covers the B- and K-curves. New systems should use prime-field curves such as P-256. The binary-curve seed finding matters for history, not for new designs.

Sources and further reading

Practise this with Lala

Practise