cheatah
Module

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))   # true

What's inside

  • Field/scalar arithmetic in Montgomery form over both P-256 moduli (the field prime p and the group order n); 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), raw r||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

BM_P256_Sign

58.34 µs

±770.07 ns IQR

17.1 k/s

BM_P256_Verify

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

fn 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).

Parameters
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.

Returns

true iff valid.

Complexity

O(1) — two scalar multiplications, computed as one Strauss-Shamir double chain.

Allocation

none.

fn bool verify_der(const std::string &pubkey_xy, const std::string &msg_hash, const std::string &sig_der) source#

Verify an ECDSA/P-256 signature.

Parameters
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}.

Returns

true iff the signature is valid for pubkey_xy over msg_hash.

Complexity

O(1) — two scalar multiplications, computed as one Strauss-Shamir double chain.

Allocation

a temporary raw r||s signature string.

fn std::string rs_to_der(const std::string &sig_raw) source#

Encode 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.

Parameters
sig_raw

the 64-byte r||s signature.

Returns

the DER bytes, or "" if sig_raw has the wrong length or a zero integer.

Complexity

O(1).

Allocation

the returned string.

fn std::string sign_raw(const std::string &privkey, const std::string &msg_hash) source#

Deterministic ECDSA/P-256 signing (RFC 6979 nonce, HMAC-SHA256).

Returns the raw 64-byte r||s signature — the form an ES256 JWT carries.

Parameters
privkey

the private scalar d as 32 big-endian bytes.

msg_hash

the digest to sign (truncated to 256 bits if longer).

Returns

the signature as 64 bytes r||s, or "" if privkey is not a valid scalar (wrong length, zero, or >= the group order n).

Complexity

O(1) — one fixed-base scalar multiplication per RFC 6979 candidate (almost always one).

Allocation

the returned signature plus RFC 6979 HMAC scratch strings.

fn std::string public_from_private(const std::string &privkey) source#

Derive the public key point from a private scalar: Q = d·G.

Parameters
privkey

the private scalar d as 32 big-endian bytes.

Returns

the public key as 64 bytes (X||Y), or "" if d is out of range.

Complexity

O(1) — one fixed-base scalar multiplication.

Allocation

the returned point.

fn std::string spki_ec_point(std::string_view spki_or_cert_der) source#

Extract 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.

Parameters
spki_or_cert_der

the certificate or SPKI DER bytes.

Returns

the 64-byte X||Y point, or "" if not a P-256 EC key.

Complexity

O(der length).

Allocation

the returned point.