Claude odtwarza pochodzenie krzywych ECDSA — co to znaczy dla zaufania do parametrów kryptograficznych
Badanie Anthropic o pochodzeniu krzywych ECDSA to pretekst, by sprawdzić, które stałe kryptograficzne w Twoim stacku potrafisz zweryfikować, a które tylko „się zgadzają”.

Większość zespołów webowych ma w głowie prosty model: kryptografia to biblioteka, którą się instaluje, a nie zbiór decyzji, które ktoś kiedyś podjął. Tymczasem każda krzywa eliptyczna w Twoim stacku to konkretny zestaw liczb — współczynniki a i b, punkt bazowy G, rząd grupy n — wybranych przez kogoś w konkretnym momencie i z konkretnego powodu. Albo i bez powodu, który ktokolwiek zapisał.
Anthropic opublikowało badanie, w którym prześledzono pochodzenie parametrów krzywych ECDSA i to, jak (nie)kompletna jest dokumentacja tych wyborów po latach (Claude reveals ECDSA curve origins). Nie będę streszczał szczegółów matematycznych — po pierwsze nie są do niczego potrzebne w codziennej pracy, po drugie warto je przeczytać u źródła, a nie w cudzym omówieniu. Interesuje mnie warstwa inżynierska: czy potrafisz w swoim projekcie odpowiedzieć na pytanie „skąd się wzięła ta stała i kto za nią stoi”.
Bo to pytanie zadajemy dziś bardzo rzadko. Instalujemy jose, wpisujemy algorithms: ['ES256'], konfigurujemy TLS i idziemy dalej. Parametry są niewidoczne — schowane w stdlibie, w pakiecie, w certyfikacie wystawionym przez CA. Ten tekst jest o tym, jak zrobić z nich coś widocznego i policzalnego.
Co właściwie jest problemem: provenance, nie matematyka
W kryptografii istnieje pojęcie nothing up my sleeve — liczby, która „nic nie kryje w rękawie”. Klasyczny przykład: jeśli potrzebujesz stałej w algorytmie hashującym, bierzesz pierwiastki sześcienne z pierwszych liczb pierwszych. Każdy może to odtworzyć w dwóch linijkach Pythona, więc autor nie mógł ukryć w tej liczbie żadnej furtki. Reguła wyboru jest publiczna i trywialna do sprawdzenia.
Problem pojawia się, gdy parametr wygląda losowo, a nikt nie potrafi powiedzieć, jak powstał. Historia zna już taki przypadek: Dual_EC_DRBG, generator liczb losowych promowany w standardach NIST w połowie lat 2000. Zawierał dwie stałe, których pochodzenia nikt poza autorem nie umiał uzasadnić. Kiedy w 2013 roku pojawiły się dokumenty sugerujące, że ktoś mógł znać relację między nimi, okazało się, że nie ma sposobu, by tę obawę rozstrzygnąć — bo nie było publicznego procesu, który by ją wykluczał. Nie chodzi o to, że dowiedziono backdoora. Chodzi o to, że nikt nie potrafił go wykluczyć, a standard już zdążył się rozprzestrzenić.
Badanie o krzywych ECDSA dotyka dokładnie tej samej warstwy: nie tego, czy dana krzywa jest złamana, tylko tego, jak dobrze udokumentowana jest droga od „ktoś wybrał te liczby” do „te liczby są w Twoim JWT”. To problem inżynierski, bo dotyczy dokumentacji, procesu i możliwości audytu — a nie tego, czy potrafisz policzyć logarytm dyskretny.
Gdzie te stałe siedzą w Twoim stacku
Zanim zaczniesz audyt, musisz wiedzieć, czego szukać. W typowej aplikacji webowej krzywe eliptyczne pojawiają się w co najmniej pięciu miejscach:
- JWT z ES256 — to ECDSA na krzywej P-256 z SHA-256, zgodnie z RFC 7518. ES384 to P-384, EdDSA to Ed25519 (RFC 8037). Jeśli Twoja aplikacja weryfikuje tokeny, to właśnie tam siedzi krzywa.
- TLS — uzgadnianie klucza (X25519, secp256r1) oraz podpisy certyfikatów wystawianych przez CA.
- Passkeys / WebAuthn — specyfikacja wymaga, by authenticator i relying party obsługiwały co najmniej ES256, czyli znowu P-256.
- Podpisy artefaktów — Sigstore, podpisywanie commitów w gicie (
ssh-ed25519,ecdsa-sha2-nistp256), sumy kontrolne paczek. - Biblioteki w zależnościach —
node:crypto,jose,elliptic,@noble/curves,node-forge. Każda z nich ma własną listę dozwolonych krzywych i własne decyzje o domyślnych wartościach.
Warto zauważyć, co z tego wynika: P-256 jest w Twoim projekcie nie dlatego, że ją wybrałeś, ale dlatego, że WebAuthn jej wymaga, a JWT ES256 jest najpopularniejszym algorytmem podpisów w ekosystemie. To nie jest zły wybór — to wybór, którego nie podjąłeś.
Ćwiczenie: inwentaryzacja w 20 minut
Zacznij od tego, co faktycznie masz zainstalowane. Poniższe komendy nie są audytem bezpieczeństwa, tylko rozpoznaniem terenu.
# 1. Co realnie ciągnie kryptografię w projekcie
npm ls --all 2>/dev/null \
| grep -Ei 'crypto|tls|jwt|jose|elliptic|noble|forge|sodium|bcrypt' \
| sort -u
# 2. Jaka wersja OpenSSL stoi pod Node
node -p 'process.versions.openssl'
openssl version -a
# 3. Jakie krzywe są w ogóle dostępne w Twoim środowisku
openssl ecparam -list_curves \
| grep -Ei 'secp256r1|prime256v1|secp384r1|secp521r1|secp256k1'
# 4. Czym podpisane są Twoje certyfikaty
openssl x509 -in cert.pem -noout -text | grep -A2 'Public Key Algorithm'
# 5. SBOM, jeśli chcesz mieć to w formie maszynowej
npm sbom --sbom-format=cyclonedx > sbom.json
Drugi krok to sprawdzenie, czy w kodzie nie ma miejsca, w którym algorytm jest przyjmowany „z zewnątrz”. Najczęstszy błąd to brak białej listy przy weryfikacji JWT:
import { jwtVerify, createRemoteJWKSet } from 'jose';
const jwks = createRemoteJWKSet(
new URL('https://auth.example.com/.well-known/jwks.json')
);
export async function verify(token) {
const { payload, protectedHeader } = await jwtVerify(token, jwks, {
issuer: 'https://auth.example.com/',
audience: 'api',
algorithms: ['ES256'], // biała lista, nie „cokolwiek przyszło w nagłówku”
clockTolerance: 5, // sekundy
});
return { payload, kid: protectedHeader.kid };
}
Trzeci krok jest najbardziej niewygodny: dla każdej znalezionej krzywej zapisz w repo plik docs/crypto-inventory.md z odpowiedziami na cztery pytania — gdzie jest użyta, jaki standard ją definiuje, kto ją wybrał i kiedy ostatnio ktoś to sprawdził. Przykład wpisu:
## P-256 (secp256r1) — ES256 w JWT
- Gdzie: weryfikacja tokenów w api-gateway, logowanie passkey
- Źródło: FIPS 186-4 (Appendix D), RFC 7518 §3.4
- Kto wybrał: NIST, wybór potwierdzony przez IETF w standardzie
- Poziom zaufania: L2 — procedura opisana, ale nieodtwarzalna samodzielnie
- Ostatni przegląd: 2026-10-17, @krzysiek
- Ryzyko: parametry nie są „nic-up-my-sleeve”; zaufanie opiera się na procesie NIST
Taki plik ma jedną niezaprzeczalną zaletę: przy zmianie biblioteki widać w diffie, że coś się zmieniło w warstwie, której normalnie nikt nie czyta.
Trzy poziomy zaufania do parametrów
Nie każdy parametr wymaga tego samego wysiłku. Warto wprowadzić prostą klasyfikację i używać jej konsekwentnie w dokumentacji zespołu:
| Poziom | Definicja | Przykład | Co robisz |
|---|---|---|---|
| L1 | regułę wyboru da się odtworzyć w kilka minut | secp256k1: a = 0, b = 7 | akceptujesz, możesz zweryfikować sam |
| L2 | procedura opisana w standardzie, ale nieodtwarzalna samodzielnie | P-256, FIPS 186-4 | akceptujesz i dokumentujesz źródło |
| L3 | brak publicznego uzasadnienia wyboru | dowolne „magiczne stałe” w bibliotece | pytasz, eskalujesz, unikasz |
Kluczowa jest tu konsekwencja: jeśli parametr jest L3, nie znaczy od razu, że jest zły. Znaczy, że nie masz podstaw, by go ocenić — a to inna sytuacja niż „sprawdzony”. Poziom L2 też nie jest gwarancją: standard może być stary, procedura może być opisana ogólnikowo, a instytucja, która go wydała, może mieć swoje interesy. Różnica polega na tym, że w L2 masz przynajmniej dokument, do którego możesz się odwołać i który ktoś kiedyś recenzował.
Checklist do polityki zależności kryptograficznych
To, co realnie da się wdrożyć bez przepisywania połowy aplikacji:
- Biała lista algorytmów wszędzie. JWT, TLS, WebAuthn — żadne miejsce nie powinno akceptować algorytmu wskazanego przez drugą stronę bez sprawdzenia go z listą.
- Tylko nazwane krzywe. Jeśli biblioteka pozwala podać dowolne parametry krzywej (explicit parameters), traktuj to jako czerwoną flagę — to wektor ataków na niepoprawną walidację punktów.
- Alert przy zmianie domyślnych wartości. Bump major w
jose,elliptic,node-forgealbo zmiana domyślnej listy krzywych wnode:cryptoto sygnał do przeglądu, nie do automatycznego merge’a dependabota. - Rejestr w repo, nie w Confluence.
docs/crypto-inventory.mdwersjonowany razem z kodem, z właścicielem i datą przeglądu. Parametry mają termin przydatności tak samo jak zależności. - SBOM w CI.
npm sbom --sbom-format=cyclonedxjako artefakt builda — tanieje audyt przy następnym badaniu, bo nie musisz rekonstruować drzewa zależności z pamięci. - Reakcja na nowe badania. Nie panic rollback, nie natychmiastowa migracja. Najpierw: czy dotyczy to parametru z Twojego rejestru i na jakim poziomie zaufania on jest. Dopiero potem decyzja.
Czego z tego NIE wyciągać
Trzy pokusy, którym warto się oprzeć.
Po pierwsze: nie pisz własnej kryptografii. ECDSA jest poprawna matematycznie i katastrofalna w implementacji — wystarczy jeden powtórzony nonce, żeby wyciekł klucz prywatny. Historia zna to z konsoli Sony z 2010 roku. Audyt provenance parametrów to nie to samo co audyt implementacji i nie zastępuje korzystania z dojrzałej biblioteki.
Po drugie: nie migruj z P-256 na secp256k1 tylko dlatego, że ma „ładniejsze” stałe. WebAuthn wymaga P-256, ekosystem JWT jest zbudowany wokół P-256, a secp256k1 ma własne problemy praktyczne i znacznie mniejsze wsparcie. Niższy poziom zaufania do provenance nie jest sam w sobie powodem do zmiany, jeśli alternatywa jest gorsza pod każdym innym względem.
Po trzecie: brak dowodu to nie dowód. To, że nie wiemy dokładnie, jak NIST wybrał b dla P-256, nie znaczy, że krzywa jest skompromitowana. Znaczy, że poziom zaufania to L2, a nie L1 — i tak należy to opisać w dokumentacji, bez dramatyzmu i bez ogłaszania końca świata.
Wnioski
Badanie o pochodzeniu krzywych ECDSA jest ciekawe głównie dlatego, że pokazuje lukę, która nie jest luką w matematyce, tylko w naszym procesie: nie mamy nawyku pytania „skąd się to wzięło”. W praktyce wystarczy kilka godzin, żeby zrobić inwentaryzację stałych kryptograficznych w projekcie, sklasyfikować je na trzech poziomach zaufania i wpisać do repo dokument, który za pół roku ktoś przeczyta przed merge’em zależności. To nie czyni nikogo bezpiecznym. Czyni go natomiast zdolnym do odpowiedzi na pytanie, które w końcu ktoś zada — i to jest różnica między „zaufaj nam” a „wiemy, komu ufamy i dlaczego”.