System architecture

NIST P-256 seed origin solved: how lrand48 and a 1997 timestamp made the ECDSA curves, and what it means for you

The NIST P-256 seed came from lrand48 and a 1997 timestamp. Rebuild it in Python and C, and see what it means for the ECDSA keys you use.

Kemo
11 min read
Close-up of the brass gear wheels and pinions inside an old watch movement.
On this page
  1. Where did the NIST P-256 seed come from?
  2. How the 1997 seed generator worked, step by step
  3. Reproduce the NIST P-256 seed in Python
  4. Every NIST and ANSI seed, with its timestamp
  5. How Claude recovered the generator: bias, brute force and lattice reduction
  6. Does this prove P-256 has no NSA backdoor?
  7. Should you switch from P-256 to Ed25519 now?
  8. Questions people ask about the NIST curve seeds
  9. Sources and further reading

For 26 years nobody outside the NSA could say where the seeds behind the NIST elliptic curves came from. On 16 October 2026 Anthropic's Frontier Red Team published the answer: the P-256 seed C49D3608 86E70493 6A6678E1 139D26B7 819F7E90 is not a hash of a secret sentence, as the lore said. It is plain output from the C library's lrand48 generator, seeded with a Unix timestamp from 16 August 1997.

This article explains what was found, shows you how to regenerate the P-256 seed yourself in about twenty lines of Python and C, and says what it does and does not change for the ECDSA keys, TLS certificates and signing code you run today. It is for engineers who use P-256 or P-384 (most of us, through TLS and WebAuthn) and want the facts rather than the headlines.

  • Where the NIST P-curve seeds came from, and why anyone cared
  • The exact generator: srand48(timestamp), 20 bytes per candidate, ANSI X9.62 checks
  • Runnable code that rebuilds the P-256 seed and checks it against the published curve
  • What the result says about backdoors, and what is still unexplained
  • Whether you should change curves (short answer: no, not because of this)

Where did the NIST P-256 seed come from?

The seed was generated by the POSIX drand48 family: srand48() was given the current Unix time, and the program read 20 bytes at a time from lrand48(), keeping the top 8 bits of each call. Each 20-byte block was tried as an ANSI X9.62 seed; the first ones that produced an acceptable curve were published. For P-256 that was the 1,842nd candidate drawn after srand48(871696354), which is 1997-08-16 01:52:34 UTC (Anthropic, Valsorda).

The background matters. NIST published P-192, P-224, P-256, P-384 and P-521 in FIPS 186-2 in 2000. Each curve's coefficient b is derived from a 160-bit seed through SHA-1, which lets anyone check that b came from the seed — the curves were called "verifiably random". What nobody could check was where the seeds came from. They were supplied by the NSA; Jerry Solinas, who generated them, reportedly said he had hashed English sentences and lost the exact phrase. Without the inputs, a sceptic could argue the NSA had tried many seeds until it found a curve with a weakness only it knew about.

Koblitz and Menezes examined that argument in 2015. Filippo Valsorda put up a bounty in October 2023 for the pre-images: 12,288, tripled to 36,864 if donated to charity. Anthropic claimed it on 15 October 2026 and donated it to ISRG, the non-profit behind Let's Encrypt (Valsorda).

How the 1997 seed generator worked, step by step

The generator is simple, which is the point. Every published prime-curve seed falls out of the same loop, and the ANSI X9.62 procedure that turns a seed into a curve is public. The diagram shows the pipeline as Anthropic and Valsorda reconstructed it.

The 1997 programUnix time, 1997–98srand48(t), lrand48() >> 2320-byte candidate seedSHA-1 expansion (ANSI X9.62)b from c, curve checksrejected, draw next 20 bytespublished seed in FIPS 186-2seed thegeneratoracceptedinvalid orfiltered
  • Unix time, 1997–98 → srand48(t), lrand48() >> 23: seed the generator
  • srand48(t), lrand48() >> 23 → 20-byte candidate seed
  • 20-byte candidate seed → SHA-1 expansion (ANSI X9.62)
  • SHA-1 expansion (ANSI X9.62) → b from c, curve checks
  • b from c, curve checks → published seed in FIPS 186-2: accepted
  • b from c, curve checks → rejected, draw next 20 bytes: invalid or filtered
  • rejected, draw next 20 bytes → srand48(t), lrand48() >> 23
One timestamp seeds the generator; 20-byte candidates are tried until a curve passes the checks. Reconstructed from Anthropic's write-up and Valsorda's Go reimplementation.

Two details explain why this stayed hidden. First, lrand48() returns the top 31 bits of a 48-bit internal state (man page), and the program shifted that right by another 23 bits, so each output byte exposes only 8 of the 48 state bits. Second, the seed's job is to feed SHA-1, so nobody looked at it as the output of a weak generator; everyone assumed it was itself a hash.

Valsorda notes two undocumented extra rejection rules in the original program. One is that the bit length of the group order n must not exceed that of the field prime p; the other is still unexplained. Together they leave roughly twice as many valid seeds as were published (Valsorda).

Reproduce the NIST P-256 seed in Python

You need nothing but Python 3's standard library. The script below re-implements one lrand48 step (the 48-bit linear congruential generator with multiplier 0x5DEECE66D and increment 0xB, as documented in the man page), draws candidate 1,842 after seeding with the 1997 timestamp, and then does what X9.62 does: expands the seed with SHA-1 into an integer c and checks that the published b satisfies b² · c ≡ −27 (mod p). The generator part follows Anthropic's CC0 reproduction script; the SHA-1 check is the standard X9.62 test.

p256_seed.pyPython
import hashlibfrom datetime import datetime, timezone # P-256 parameters from FIPS 186-4 / SP 800-186P = 2**256 - 2**224 + 2**192 + 2**96 - 1B = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604BPUBLISHED_SEED = "C49D360886E704936A6678E1139D26B7819F7E90" def lrand48_bytes(t, n):    """Candidate n (1-based): 20 bytes, each lrand48() >> 23, after srand48(t)."""    x = (t << 16) | 0x330E                      # state that srand48(t) sets    out = b""    for i in range(20 * n):        x = (0x5DEECE66D * x + 0xB) % 2**48     # one lrand48 step        if i >= 20 * (n - 1):            out += bytes([x >> 40])              # top 8 of 48 bits == lrand48() >> 23    return out def c_from_seed(seed, p):    """ANSI X9.62 / FIPS 186-2: build c from SHA-1 of the seed."""    L = p.bit_length()    v, w = (L - 1) // 160, L - 160 * ((L - 1) // 160) - 1    h = int.from_bytes(hashlib.sha1(seed).digest(), "big") & ((1 << w) - 1)    z = int.from_bytes(seed, "big")    for i in range(1, v + 1):        si = ((z + i) % 2**160).to_bytes(20, "big")        h = (h << 160) | int.from_bytes(hashlib.sha1(si).digest(), "big")    return h t = int(datetime(1997, 8, 16, 1, 52, 34, tzinfo=timezone.utc).timestamp())seed = lrand48_bytes(t, 1842)print("srand48 seed  :", t)print("candidate 1842:", seed.hex().upper())print("matches FIPS  :", seed.hex().upper() == PUBLISHED_SEED)c = c_from_seed(seed, P)print("b^2 * c == -27 (mod p):", (B * B * c) % P == (-27) % P)
Python 3.13 on Linux, run on 18 October 2026Shell
$ python3 p256_seed.pysrand48 seed  : 871696354candidate 1842: C49D360886E704936A6678E1139D26B7819F7E90matches FIPS  : Trueb^2 * c == -27 (mod p): True

The last line is the important one. It shows the chain end to end: a 1997 timestamp produces the 20-byte seed, and that seed produces the exact b coefficient in every P-256 implementation you use. Change the timestamp by one second, or the candidate number by one, and both checks print False.

The same seed from the real lrand48 in C

If you would rather not trust a re-implementation, ask glibc directly. This program calls the actual srand48 and lrand48 from the C library:

seed.cText
/* Regenerate the NIST P-256 seed with the C library's own lrand48. */#include <stdio.h>#include <stdlib.h> int main(void) {    srand48(871696354L);                  /* 1997-08-16 01:52:34 UTC */    unsigned char seed[20];    for (int n = 1; n <= 1842; n++)       /* candidate 1842 is P-256's */        for (int i = 0; i < 20; i++)            seed[i] = (unsigned char)(lrand48() >> 23);    for (int i = 0; i < 20; i++) printf("%02X", seed[i]);    printf("\n");    return 0;}
gcc 13.3, glibc 2.39Shell
$ gcc -O2 -o seed seed.c && ./seedC49D360886E704936A6678E1139D26B7819F7E90$ date -u -d @871696354Sat Aug 16 01:52:34 UTC 1997

The drand48 algorithm is fixed by POSIX, so any conforming C library should print the same line; the 1997 machine and today's glibc agree to the bit.

Every NIST and ANSI seed, with its timestamp

The same method reproduces all five NIST prime-curve seeds, five ANSI X9.62 prime curves, and four of the NIST binary curves. The table lists Anthropic's published values. Several curves share a timestamp: one run of the program produced more than one curve.

Curvesrand48 time (UTC)CandidateSeed
P-1921997-05-18 14:45:341393045AE6FC8422F64ED579528D38120EAE12196D5
P-5211997-08-08 01:12:041,136D09E8800291CB85396CC6717393284AAA0DA64BA
P-2561997-08-16 01:52:341,842C49D360886E704936A6678E1139D26B7819F7E90
P-3841997-08-30 14:50:082,688A335926AA319A27A1D00896A6773A4827ACDAC73
P-2241998-04-03 12:52:077,026BD71344799D5C7FCDC45B59FA3B9AB8F6A948BC5
prime239v11997-05-15 15:49:032,185E43BB460F0B80CC0C0B075798E948060F8321B7D
prime192v21997-05-18 14:45:346,01331A92EE2029FD10D901B113E990710F0D21AC6B6
B-2331998-04-17 15:24:18 (time ÷ 2)15574D59FF07F6B413D0EA14B344B20A2DB049B50C3
B-5711998-04-17 15:32:08 (time ÷ 2)732AA058F73A0E33AB486B0F610410C53A7F132310
Selected curve seeds with the UTC timestamp passed to srand48 and the candidate number that was published. Source: [Anthropic's reproduction script](https://www.anthropic.com/research/ecdsa-curve-origins).
How many 20-byte candidates each NIST prime curve tookCandidate number of the published seed after srand48(timestamp)
P-192139 candidatesP-5211,136 candidatesP-2561,842 candidatesP-3842,688 candidatesP-2247,026 candidates

Source: Anthropic, Claude reveals ECDSA curve origins (16 October 2026), reproduction script output: https://www.anthropic.com/research/ecdsa-curve-origins

How many 20-byte candidates each NIST prime curve took
Candidate number
P-192139 candidates
P-5211,136 candidates
P-2561,842 candidates
P-3842,688 candidates
P-2247,026 candidates

The binary curves are odder. Read directly, their timestamps land in February 1984; doubled, they land on 17 April 1998, two weeks after the P-224 run. Anthropic suggests the program "may have halved the clock". B-163's seed remains unexplained.

How Claude recovered the generator: bias, brute force and lattice reduction

Anthropic says an internal research model worked out the generator in under half an hour, without the old "hashed sentence" hint. The write-up describes three steps (Anthropic):

  1. Look for a 1990s generator's fingerprint. A scanner tested 34 combinations of generator and output slice against the published seeds.
  2. Spot the bias. In four binary-curve seeds the top bit of all twenty 32-bit words was zero, which happens by chance with probability about 1 in 5,810. That matches raw lrand48(), which returns 31-bit values. The 17 hidden state bits were then brute-forced over 2^17 values.
  3. Use LLL for the harder case. The prime-curve seeds keep only 8 bits per call (lrand48() >> 23), too few for brute force. The state was recovered with the LLL lattice basis reduction algorithm, the standard tool for recovering truncated LCG output.

None of these techniques is new; recovering a truncated linear congruential generator with lattices is textbook cryptanalysis. What was missing was the hypothesis that the seeds were generator output at all. Steve Weis, a co-author, had published an amateur investigation of the seeds earlier; Valsorda independently reimplemented the algorithm in Go before confirming the bounty (Valsorda).

Does this prove P-256 has no NSA backdoor?

No, but it removes the main argument for one. Anthropic is explicit that this "is not a cryptographic break, new attack, or weakness in the ECDSA curves", and says the findings make a backdoor "much less plausible" without proving its absence (Anthropic).

The reasoning is about freedom. To plant a weakness, someone would have to search many candidate curves for a rare weak property. If the seeds had been arbitrary hashes, there were up to 160 bits of choice. Now the only choices are which timestamp to use and which valid candidate to keep. Valsorda estimates that even trying every 1997 timestamp and choosing among the first few viable candidates gives "barely 30 bits of freedom", which rules out the 2017 Bernstein–Lange approach that needed about 70 bits (Valsorda).

The seeds were never secret sentences. They were a timestamp and a 1990s random number generator.

Some loose ends remain, and they are worth stating plainly:

  • Skipped candidates. Some valid candidates were passed over without a known reason. P-256's candidate 876 also gives a prime-order curve of the right size; P-224 passed over three. Anthropic says the number skipped is too small for a weakened-curve search to be plausible (Anthropic).
  • How many runs? Anthropic notes that "nothing in the numbers shows how many runs there were". A searched timestamp would look the same as an honest one.
  • Base points. The base points of P-192 and five ANSI curves came from the random number generator in Arjen Lenstra's FreeLIP library, but the base points of P-224, P-256, P-384 and P-521 are still unexplained. The base point does not affect the security of the discrete-log problem on a prime-order curve, so this is a matter of provenance rather than safety.
  • B-163. Its seed does not fit the pattern.

On Hacker News, where the story drew more than 900 points, cryptographers in the thread made the same case from the other side: a class of weak curves would have to be enormous to be found with a search that small.

Should you switch from P-256 to Ed25519 now?

Not because of this. Nothing about P-256, P-384 or P-521 changed this week: the curve parameters are identical, no attack was published, and your keys are exactly as strong as they were. If anything, the result removes a reason people gave for avoiding the NIST curves. Valsorda goes further and writes that he would now personally recommend them over Curve25519, while noting they arrive "just in time" to be replaced by post-quantum signatures (Valsorda).

You useChanges?What to keep in mind
P-256 / P-384 in TLS, WebAuthn, JWT ES256NoSame parameters, same security; no action needed
Ed25519 / X25519NoStill a good choice; FIPS 186-5 added EdDSA in 2023
NIST binary curves (B-, K-)NoAlready deprecated in SP 800-186
P-192NoToo small for new systems regardless of its seed
Any classical ECCLong termPlan the move to post-quantum signatures and key exchange
What the seed discovery changes for common uses of the NIST curves, and what it does not

For context, NIST's FIPS 186-5 and SP 800-186, published in February 2023, already deprecated the binary-field curves, added EdDSA with two Edwards curves, and kept DSA only for verifying existing signatures. If you are choosing a curve for something new, the deciding factors are still library support, compliance (P-256 and P-384 are central to web PKI and to the NSA's CNSA suite, as Valsorda notes), and how easy each is to implement safely — not the 1997 seeds.

Questions people ask about the NIST curve seeds

Where did the NIST P-256 seed come from?

It came from the C library's lrand48 generator seeded with the Unix timestamp 871696354 (16 August 1997, 01:52:34 UTC). The program drew 20 bytes per candidate, and the 1,842nd candidate became the published seed.

Was the P-256 seed a hash of a sentence?

No. The long-standing story that Jerry Solinas hashed a humorous English sentence was wrong. The seed is raw pseudo-random output, which is why no one ever found the sentence.

Does this mean P-256 has an NSA backdoor?

No. The finding makes a deliberately weakened curve much less plausible, because the generator left only about 30 bits of freedom to choose from. It does not prove a negative, and a few skipped candidates are still unexplained.

Is P-256 still safe to use?

Yes. The curve parameters have not changed and no new attack was published. P-256 is as strong as it was before the announcement.

Should I switch from ECDSA P-256 to Ed25519?

Not because of this news. Both are sound; choose on library support, compliance needs and implementation safety. Plan for post-quantum algorithms either way.

How did Claude find the ECDSA curve seeds?

It looked for statistical fingerprints of 1990s generators, found a bias matching lrand48, brute-forced 17 hidden state bits for the binary curves, and used LLL lattice reduction to recover the truncated generator state for the prime curves.

Can I verify the P-256 seed myself?

Yes. Anthropic published CC0 reproduction scripts, and the short Python and C programs in this article regenerate the seed and check it against the published b coefficient.

Are the NIST base points explained too?

Only some. The base points of P-192 and several ANSI X9.62 curves came from FreeLIP's generator; those of P-224, P-256, P-384 and P-521 remain unexplained.

Sources and further reading

Discussion

Join the discussion

Sign in with Google, Apple or an email code to comment and reply.

One practical article a week

Follow k13 in any RSS reader. No account needed.