p256
NIST P-256 (secp256r1) elliptic curve — ECDSA signature verification and deterministic (RFC 6979) signing, plus the DER/SPKI parsing needed to use it with TLS certificates and JWTs. From scratch, no external libraries.
This is the curve the rest of the world's TLS certificates and OAuth/JWT (ES256) are signed with. Adding it lets cheatah's tls client verify a real server's ecdsa_secp256r1_sha256 certificate (most of the public web), and lets applications sign an ES256 JWT.
import io
import hashlib
import p256
let h = hashlib.sha256_digest("the message")
let sig = p256.sign_raw(privkey, h) # 64-byte r||s (RFC 6979, deterministic)
io.print(p256.verify_raw(pubkey_xy, h, sig)) # trueWhat's inside
Field/scalar arithmetic in Montgomery form over both P-256 moduli (the field prime
pand the group ordern); the Montgomery constants are derived from the modulus at startup, so there are no hand-transcribed magic numbers.Points in Jacobian coordinates (
a = -3).Verify uses Strauss-Shamir (one doubling chain for
u1·G + u2·Q).Sign uses an RFC 6979 deterministic nonce (HMAC-SHA256 — no entropy source needed, never repeats a nonce) and a fixed-base comb for
k·G.Parsing: DER
SEQUENCE{r,s}(the TLS/X.509 form), rawr||s(the JWT ES256 form), and the uncompressed EC point out of a certificate's SPKI.
Byte conventions: scalars/coordinates are 32 big-endian bytes; a public key point is the 64 bytes X||Y; a raw signature is the 64 bytes r||s.
Correctness & speed
Verified against the RFC 6979 Appendix A.2.5 P-256/SHA-256 test vector — the deterministic signature matches bit-for-bit, and verification round-trips it (stdlib/tests/p256_test.cpp). Micro-benchmarks (tests/benchmarks/p256_bench.cpp, release build, one core):
Op | median | spread | throughput |
|---|---|---|---|
| 58.34 µs | ±770.07 ns IQR | 17.1 k/s |
| 88.69 µs | ±588.83 ns IQR | 11.3 k/s |
Verification runs once per TLS handshake (negligible next to a network round trip); signing is the per-message JWT path.
Security notes
ECDSA verification here handles public data. Signing uses a deterministic nonce (RFC 6979), which removes the catastrophic "repeated/biased `k`" failure mode. The scalar routines are written straightforwardly for correctness; they are not yet hardened to be fully constant-time against a local timing attacker, which matters only when signing with a long-lived private key on a shared host.
Functions
bool verify_raw(const std::string &pubkey_xy, const std::string &msg_hash, const std::string &sig_raw)
source#
Verify an ECDSA/P-256 signature given the raw 64-byte r||s form (the JWT ES256 layout).
pubkey_xy | the public key (64 bytes X||Y). |
msg_hash | the digest (truncated to 256 bits if longer). |
sig_raw | the signature as 64 bytes r||s. |
true iff valid.
O(1) — two scalar multiplications, computed as one Strauss-Shamir double chain.
none.
CheatahP256.VerifyKnownVectorbool verify_der(const std::string &pubkey_xy, const std::string &msg_hash, const std::string &sig_der)
source#
Verify an ECDSA/P-256 signature.
pubkey_xy | the public key as 64 bytes (X||Y), big-endian. |
msg_hash | the message digest (e.g. 32 bytes of SHA-256); if longer than 32 bytes it is truncated to the leftmost 256 bits (FIPS 186-4). |
sig_der | the signature, DER-encoded SEQUENCE{INTEGER r, INTEGER s}. |
true iff the signature is valid for pubkey_xy over msg_hash.
O(1) — two scalar multiplications, computed as one Strauss-Shamir double chain.
a temporary raw r||s signature string.
CheatahP256.VerifyKnownVectorEncode a raw 64-byte r||s signature as the DER SEQUENCE{INTEGER r, INTEGER s} that TLS CertificateVerify and X.509 carry — the inverse of the parse inside verify_der, with minimal-form integers (stripped leading zeros, 0x00 sign byte when the top bit is set).
Composes with sign_raw to produce the wire form a TLS 1.3 server sends for ecdsa_secp256r1_sha256.
sig_raw | the 64-byte r||s signature. |
the DER bytes, or "" if sig_raw has the wrong length or a zero integer.
O(1).
the returned string.
Deterministic ECDSA/P-256 signing (RFC 6979 nonce, HMAC-SHA256).
Returns the raw 64-byte r||s signature — the form an ES256 JWT carries.
privkey | the private scalar d as 32 big-endian bytes. |
msg_hash | the digest to sign (truncated to 256 bits if longer). |
the signature as 64 bytes r||s, or "" if privkey is not a valid scalar (wrong length, zero, or >= the group order n).
O(1) — one fixed-base scalar multiplication per RFC 6979 candidate (almost always one).
the returned signature plus RFC 6979 HMAC scratch strings.
CheatahP256.SignKnownVectorDerive the public key point from a private scalar: Q = d·G.
privkey | the private scalar d as 32 big-endian bytes. |
the public key as 64 bytes (X||Y), or "" if d is out of range.
O(1) — one fixed-base scalar multiplication.
the returned point.
CheatahP256.PublicFromPrivateExtract the P-256 public key point from a certificate / SubjectPublicKeyInfo DER: finds the id-ecPublicKey + prime256v1 AlgorithmIdentifier and returns the uncompressed point's 64 bytes (X||Y).
Returns "" if the SPKI is not a P-256 uncompressed EC key.
spki_or_cert_der | the certificate or SPKI DER bytes. |
the 64-byte X||Y point, or "" if not a P-256 EC key.
O(der length).
the returned point.
CheatahP256.SpkiExtractsPoint