Skip to content
ecdsa.com
C

libsecp256k1 (Bitcoin Core)

Bitcoin Core's libsecp256k1, the reference secp256k1 implementation: opaque signatures serialized as compact or strict DER, RFC 6979 nonces, low-S on both sides, public-key recovery and BIP-340 Schnorr.
License
MIT
Checked version
0.5.0 ·
Source
github.com/bitcoin-core/secp256k1
Documentation
github.com/bitcoin-core/secp256k1/tree/master/include

Compatibility at a glance

ECDSA

ECDSA curves
secp256k1
EdDSA
none
Public key formats
sec1-uncompressedsec1-compressed

libsecp256k1 does one curve and does it carefully: constant-time signing, exhaustive tests and formal verification of parts of the field arithmetic. Bitcoin Core, many wallets and bindings such as coincurve and go-ethereum's cgo backend use it. The facts here were checked on the Alpine 3.24.2 package, version 0.5.0 with the recovery, extrakeys and schnorrsig modules; upstream had reached 0.8.0 at the time.

Signatures live in an opaque struct; the encoding is chosen when serializing — serialize_compact gives 64 bytes of r‖s, serialize_der gives DER — and parsing is equally explicit. parse_der is strict: it accepted minimal DER and refused the same signature with a redundant leading zero in r, following Bitcoin's BIP-66 rules. Lax historical signatures need the lax parser kept in the repository's contrib directory.

Signing takes a 32-byte hash — the library never hashes your message — uses RFC 6979 (the check matched an independent implementation) and always produces low-S. Verification is the strict part: the mirrored high-S form of a valid signature failed, and secp256k1_ecdsa_signature_normalize exists precisely to fix signatures from general-purpose signers. When a signature from OpenSSL, Java or a cloud KMS verifies in roughly half the cases, high-S is the cause; the sample normalizes before verifying.

Public keys are SEC1 points, 65 bytes uncompressed or 33 compressed; a 64-byte x‖y is refused, and there is no SPKI, PEM or JWK support. The recovery module recovers the public key from a signature — the basis of Ethereum addresses — and the schnorrsig module implements BIP-340. Check Bitcoin message signatures with the Bitcoin message verifier and Nostr events with the Nostr event verifier.

Facts and the checks behind them

Nothing below is copied from documentation. The check content/libraries/verify/libsecp256k1/run.sh ran libsecp256k1 (Bitcoin Core) 0.5.0 in the container image alpine:3.24.2 on October 20, 2026; the page must match what it printed, and each fact lists those lines.

  • ECDSA signature encoding

    structured

    Signatures are an opaque struct; serialize_compact writes 64 bytes of r‖s and serialize_der writes DER.

    Recorded by the check
    fact ecdsa-signature-encoding = structured
    evidence ecdsa-signature-encoding: secp256k1_ecdsa_signature is opaque: serialize_compact gives 64 bytes, serialize_der 70 bytes
  • ECDSA nonce

    rfc6979

    RFC 6979: the signature equalled an independent RFC 6979 implementation's with s normalized.

    Recorded by the check
    fact ecdsa-nonce = rfc6979
    evidence ecdsa-nonce: two signatures identical: yes; equal to the reference RFC 6979 signature with s normalized: yes
  • Low-S when signing

    low-s

    No high-S value among 64 signatures.

    Recorded by the check
    fact ecdsa-low-s-sign = low-s
  • High-S when verifying

    rejects-high-s

    secp256k1_ecdsa_verify rejected (r, n − s); after secp256k1_ecdsa_signature_normalize it verified.

    Recorded by the check
    fact ecdsa-low-s-verify = rejects-high-s
    evidence ecdsa-low-s-verify: verify on (r, n - s): 0; signature_normalize reports it was high: 1; normalized verifies: 1
  • What the signing call hashes

    takes-digest

    secp256k1_ecdsa_sign takes a 32-byte message hash.

    Recorded by the check
    fact ecdsa-prehash = takes-digest
    evidence ecdsa-prehash: secp256k1_ecdsa_sign takes a 32-byte message hash (msghash32)
  • Default hash

    none

    No hashing happens inside signing or verification.

    Recorded by the check
    fact ecdsa-default-hash = none
  • DER strictness

    rejects-non-minimal

    parse_der accepted minimal DER and refused an encoding with a redundant leading zero byte in r.

    Recorded by the check
    fact der-strictness = rejects-non-minimal
    evidence der-strictness: parse_der on minimal DER: 1; with a redundant leading zero in r: 0
  • Malformed ECDSA signature

    returns-error

    Malformed input fails at the parsing step (parse_der returns 0); verify only receives parsed signatures.

    Recorded by the check
    fact ecdsa-malformed-signature = returns-error
    evidence ecdsa-malformed-signature: parsing is a separate call: parse_der on 3 bytes returned 0; verify only ever sees parsed signatures
  • ECDSA curves

    secp256k1

    secp256k1 only.

    Recorded by the check
    fact ecdsa-curves = secp256k1
  • Public key recovery

    supported

    The recovery module recovered the signer's public key from a recoverable signature.

    Recorded by the check
    fact public-key-recovery = supported
  • Schnorr signatures

    BIP-340

    The schnorrsig module produced and verified a BIP-340 signature against the x-only public key.

    Recorded by the check
    fact schnorr = BIP-340
  • EdDSA

    none

    No EdDSA.

    Recorded by the check
    fact eddsa = none
  • Post-quantum algorithms

    none

    No post-quantum algorithms.

    Recorded by the check
    fact pqc = none
  • Public key formats

    sec1-uncompressedsec1-compressed

    ec_pubkey_parse accepts 65-byte uncompressed and 33-byte compressed SEC1 points; a 64-byte x‖y was refused.

    Recorded by the check
    fact public-key-formats = sec1-uncompressed, sec1-compressed
    evidence public-key-formats: ec_pubkey_parse: 65-byte uncompressed 1, 33-byte compressed 1, 64-byte x||y 0
  • Private key formats

    raw-scalar

    Secret keys are raw 32-byte scalars.

    Recorded by the check
    fact private-key-formats = raw-scalar

Usage

Sign a 32-byte hash, parse a signature as it would arrive from elsewhere, normalize s and verify.
#include <secp256k1.h>
#include <stdio.h>
#include <string.h>

int main(void) {
    secp256k1_context *ctx = secp256k1_context_create(SECP256K1_CONTEXT_NONE);
    unsigned char seckey[32], msghash[32];
    memset(seckey, 0x11, 32);   /* use a random 32-byte key in real code */
    memset(msghash, 0x22, 32);  /* the caller hashes: libsecp256k1 signs a 32-byte digest */

    secp256k1_pubkey pub;
    secp256k1_ecdsa_signature sig;
    if (!secp256k1_ec_pubkey_create(ctx, &pub, seckey) || !secp256k1_ecdsa_sign(ctx, &sig, msghash, seckey, NULL, NULL)) return 1;

    /* A signature from elsewhere: parse (strict DER or 64-byte compact), then normalize s,
       because verify rejects high-S signatures that OpenSSL or Java produce half the time. */
    unsigned char compact[64];
    secp256k1_ecdsa_signature_serialize_compact(ctx, compact, &sig);
    secp256k1_ecdsa_signature parsed;
    if (!secp256k1_ecdsa_signature_parse_compact(ctx, &parsed, compact)) return 1;
    secp256k1_ecdsa_signature_normalize(ctx, &parsed, &parsed);

    printf("verified: %d\n", secp256k1_ecdsa_verify(ctx, &parsed, msghash, &pub));
    secp256k1_context_destroy(ctx);
    return 0;
}

The snippet harness runs every sample verbatim in the container image alpine:3.24.2, and the run fails unless the output matches.

Setup
apk add --no-cache -q build-base libsecp256k1-dev=0.5.0-r1 >/dev/null
Command
cc -o usage usage.c -lsecp256k1 && ./usage
Output must match (regular expression)
verified: 1

Libraries