Initial commit: 首次建仓,建立目录结构

This commit is contained in:
FXY
2026-06-11 23:49:54 +08:00
commit 4038a476b5
9396 changed files with 2372905 additions and 0 deletions

View File

@ -0,0 +1,13 @@
from coincurve.context import GLOBAL_CONTEXT, Context
from coincurve.keys import PrivateKey, PublicKey, PublicKeyXOnly
from coincurve.utils import verify_signature
__version__ = "21.0.0"
__all__ = [
"GLOBAL_CONTEXT",
"Context",
"PrivateKey",
"PublicKey",
"PublicKeyXOnly",
"verify_signature",
]

View File

@ -0,0 +1,37 @@
from __future__ import annotations
from os import urandom
from threading import Lock
from coincurve._libsecp256k1 import ffi, lib
from coincurve.flags import CONTEXT_FLAGS, CONTEXT_NONE
class Context:
def __init__(self, seed: bytes | None = None, flag=CONTEXT_NONE, name: str = ""):
if flag not in CONTEXT_FLAGS:
msg = f"{flag} is an invalid context flag."
raise ValueError(msg)
self._lock = Lock()
self.ctx = ffi.gc(lib.secp256k1_context_create(flag), lib.secp256k1_context_destroy)
self.reseed(seed)
self.name = name
def reseed(self, seed: bytes | None = None):
"""
Protects against certain possible future side-channel timing attacks.
"""
with self._lock:
seed = urandom(32) if not seed or len(seed) != 32 else seed # noqa: PLR2004
res = lib.secp256k1_context_randomize(self.ctx, ffi.new("unsigned char [32]", seed))
if not res:
msg = "secp256k1_context_randomize"
raise ValueError(msg)
def __repr__(self):
return self.name or super().__repr__()
GLOBAL_CONTEXT = Context(name="GLOBAL_CONTEXT")

View File

@ -0,0 +1,270 @@
"""
Minimal, dependency-free ASN.1/DER encoder & decoder for secp256k1 EC private keys.
This module implements just enough DER encoding/decoding to support:
1. Outputting a DER-encoded PKCS#8 EC private key (with an embedded ECPrivateKey per RFC 5915)
2. Reading such a DER-encoded EC private key
Only the following ASN.1 types are supported:
- INTEGER
- BIT STRING
- OCTET STRING
- OBJECT IDENTIFIER
- SEQUENCE
- Context-specific EXPLICIT tags (for the optional public key)
The expected DER structure is as follows:
PrivateKeyInfo ::= SEQUENCE {
version INTEGER, -- must be 0
privateKeyAlgorithm SEQUENCE {
algorithm OBJECT IDENTIFIER, -- id-ecPublicKey (1.2.840.10045.2.1)
parameters OBJECT IDENTIFIER -- secp256k1 (1.3.132.0.10)
},
privateKey OCTET STRING -- DER encoding of ECPrivateKey
}
ECPrivateKey ::= SEQUENCE {
version INTEGER, -- must be 1
privateKey OCTET STRING, -- the secret bytes
publicKey [1] EXPLICIT BIT STRING OPTIONAL -- uncompressed public key
}
"""
from __future__ import annotations
from coincurve.utils import int_to_bytes
# ASN.1 DER tag bytes
INTEGER_TAG = 0x02
BIT_STRING_TAG = 0x03
OCTET_STRING_TAG = 0x04
OBJECT_IDENTIFIER_TAG = 0x06
SEQUENCE_TAG = 0x30
# OIDs
EC_PUBKEY_OID = bytes([0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01]) # 1.2.840.10045.2.1 (ecPublicKey)
SECP256K1_OID = bytes([0x2B, 0x81, 0x04, 0x00, 0x0A]) # 1.3.132.0.10 (secp256k1)
# Pre-computed structures
VERSION_INTEGER_ZERO = bytes([INTEGER_TAG, 0x01, 0x00]) # INTEGER 0
VERSION_INTEGER_ONE = bytes([INTEGER_TAG, 0x01, 0x01]) # INTEGER 1
EC_ALGORITHM_IDENTIFIER = bytes([
SEQUENCE_TAG,
16,
OBJECT_IDENTIFIER_TAG,
len(EC_PUBKEY_OID),
*EC_PUBKEY_OID,
OBJECT_IDENTIFIER_TAG,
len(SECP256K1_OID),
*SECP256K1_OID,
])
def encode_length(length: int) -> bytes:
"""Encode a length in DER format."""
# Short form
if length < 128: # noqa: PLR2004
return bytes([length])
# Long form
length_bytes = int_to_bytes(length)
return bytes([0x80 | len(length_bytes)]) + length_bytes
def encode_octet_string(value: bytes) -> bytes:
"""Encode an OCTET STRING in DER format."""
length_bytes = encode_length(len(value))
length_bytes_len = len(length_bytes)
result = bytearray(1 + length_bytes_len + len(value))
result[0] = OCTET_STRING_TAG
result[1 : 1 + length_bytes_len] = length_bytes
result[1 + length_bytes_len :] = value
return bytes(result)
def encode_bit_string(value: bytes, unused_bits: int = 0) -> bytes:
"""Encode a BIT STRING in DER format."""
length_bytes = encode_length(len(value) + 1)
length_bytes_len = len(length_bytes)
result = bytearray(1 + length_bytes_len + 1 + len(value))
result[0] = BIT_STRING_TAG
result[1 : 1 + length_bytes_len] = length_bytes
result[1 + length_bytes_len] = unused_bits
result[1 + length_bytes_len + 1 :] = value
return bytes(result)
def encode_der(private_key: bytes, public_key: bytes | None = None) -> bytes:
"""
Encode an EC private key in DER format (PKCS#8/RFC 5208).
Optimized for secp256k1 keys.
Parameters:
private_key: The private key as bytes (32 bytes for secp256k1)
public_key: The public key as bytes (65 bytes uncompressed for secp256k1, starting with 0x04)
Returns:
The DER-encoded private key
"""
# EC private key contains version(1) + octet string + optional pubkey
ec_key_buffer = bytearray(VERSION_INTEGER_ONE)
# Add private key as octet string
private_key_os = encode_octet_string(private_key)
ec_key_buffer.extend(private_key_os)
# Add public key if provided (optional)
if public_key is not None:
public_key_bs = encode_bit_string(public_key)
pubkey_len = len(public_key_bs)
ec_key_buffer.append(0xA1) # context-specific [1] constructed
ec_key_buffer.extend(encode_length(pubkey_len))
ec_key_buffer.extend(public_key_bs)
# Wrap EC private key in sequence
ec_key_seq = bytearray([SEQUENCE_TAG])
ec_key_seq.extend(encode_length(len(ec_key_buffer)))
ec_key_seq.extend(ec_key_buffer)
# Wrap in octet string for outer structure
ec_key_os = encode_octet_string(ec_key_seq)
# Build the outer PKCS#8 structure
result = bytearray([SEQUENCE_TAG])
# Calculate total length: version(3) + alg_id(18) + octet_string(len)
outer_len = 3 + len(EC_ALGORITHM_IDENTIFIER) + len(ec_key_os)
result.extend(encode_length(outer_len))
# Version 0
result.extend(VERSION_INTEGER_ZERO)
# Algorithm identifier (pre-computed)
result.extend(EC_ALGORITHM_IDENTIFIER)
# EC key wrapped in octet string
result.extend(ec_key_os)
return bytes(result)
def decode_length(data: bytes, offset: int) -> tuple[int, int]:
"""
Decode a DER length field.
Parameters:
data: The DER-encoded data
offset: The current offset in the data
Returns:
Tuple of (length, new_offset)
"""
length_byte = data[offset]
offset += 1
# Short form
if length_byte < 128: # noqa: PLR2004
return length_byte, offset
# Long form
num_length_bytes = length_byte & 0x7F
length = 0
for _ in range(num_length_bytes):
length = (length << 8) | data[offset]
offset += 1
return length, offset
def decode_der(der_data: bytes) -> bytes:
"""
Decode a DER-encoded EC private key to extract the private key secret.
Optimized for secp256k1 keys.
Parameters:
der_data: The DER-encoded private key in PKCS#8 format
Returns:
The private key secret as bytes
"""
# Quick validation for performance
if len(der_data) < 34 or der_data[0] != SEQUENCE_TAG: # noqa: PLR2004
msg = "Invalid DER: not a valid PKCS#8 structure"
raise ValueError(msg)
# Skip outer sequence tag and length
offset = 1
_, offset = decode_length(der_data, offset)
# Skip version INTEGER (should be 0)
if der_data[offset] != INTEGER_TAG:
msg = "Invalid DER: expected INTEGER tag for version"
raise ValueError(msg)
offset += 1
version_len, offset = decode_length(der_data, offset)
offset += version_len # Skip version value
# Validate algorithm identifier is for EC
if der_data[offset] != SEQUENCE_TAG:
msg = "Invalid DER: expected SEQUENCE tag for algorithm"
raise ValueError(msg)
offset += 1
alg_len, offset = decode_length(der_data, offset)
alg_end = offset + alg_len # Store the end position of algorithm identifier
# Check if first OID is EC
if der_data[offset] != OBJECT_IDENTIFIER_TAG:
msg = "Invalid DER: expected OBJECT IDENTIFIER tag"
raise ValueError(msg)
offset += 1
oid_len, offset = decode_length(der_data, offset)
algorithm_oid = der_data[offset : offset + oid_len]
# Check if it's an EC key
if oid_len != len(EC_PUBKEY_OID) or algorithm_oid != EC_PUBKEY_OID:
msg = "Not an EC private key"
raise ValueError(msg)
# Skip to the end of algorithm identifier section
offset = alg_end
# Extract private key octet string
if der_data[offset] != OCTET_STRING_TAG:
msg = "Invalid DER: expected OCTET STRING for private key"
raise ValueError(msg)
offset += 1
priv_len, offset = decode_length(der_data, offset)
# Parse EC private key structure
ec_data = der_data[offset : offset + priv_len]
# Verify EC structure starts with sequence
if len(ec_data) < 2 or ec_data[0] != SEQUENCE_TAG: # noqa: PLR2004
msg = "Invalid EC key format: missing sequence"
raise ValueError(msg)
# Skip sequence tag and length
ec_offset = 1
_, ec_offset = decode_length(ec_data, ec_offset)
# Skip version INTEGER (should be 1)
if ec_data[ec_offset] != INTEGER_TAG:
msg = "Invalid EC key format: missing version"
raise ValueError(msg)
ec_offset += 1
ec_ver_len, ec_offset = decode_length(ec_data, ec_offset)
ec_offset += ec_ver_len # Skip version value
# Get private key octet string
if ec_data[ec_offset] != OCTET_STRING_TAG:
msg = "Invalid DER: expected OCTET STRING for EC private key"
raise ValueError(msg)
ec_offset += 1
key_len, ec_offset = decode_length(ec_data, ec_offset)
# Extract private key
return ec_data[ec_offset : ec_offset + key_len]

View File

@ -0,0 +1,131 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from coincurve._libsecp256k1 import ffi, lib
from coincurve.context import GLOBAL_CONTEXT, Context
from coincurve.utils import bytes_to_int, int_to_bytes, sha256
if TYPE_CHECKING:
from coincurve.types import Hasher
MAX_SIG_LENGTH = 72
CDATA_SIG_LENGTH = 64
def cdata_to_der(cdata, context: Context = GLOBAL_CONTEXT) -> bytes:
der = ffi.new("unsigned char[72]")
der_length = ffi.new("size_t *", MAX_SIG_LENGTH)
lib.secp256k1_ecdsa_signature_serialize_der(context.ctx, der, der_length, cdata)
return bytes(ffi.buffer(der, der_length[0]))
def der_to_cdata(der: bytes, context: Context = GLOBAL_CONTEXT):
cdata = ffi.new("secp256k1_ecdsa_signature *")
parsed = lib.secp256k1_ecdsa_signature_parse_der(context.ctx, cdata, der, len(der))
if not parsed:
msg = "The DER-encoded signature could not be parsed."
raise ValueError(msg)
return cdata
def recover(message: bytes, recover_sig, hasher: Hasher = sha256, context: Context = GLOBAL_CONTEXT):
msg_hash = hasher(message) if hasher is not None else message
if len(msg_hash) != 32: # noqa: PLR2004
msg = "Message hash must be 32 bytes long."
raise ValueError(msg)
pubkey = ffi.new("secp256k1_pubkey *")
recovered = lib.secp256k1_ecdsa_recover(context.ctx, pubkey, recover_sig, msg_hash)
if recovered:
return pubkey
msg = "failed to recover ECDSA public key"
raise ValueError(msg)
def serialize_recoverable(recover_sig, context: Context = GLOBAL_CONTEXT) -> bytes:
output = ffi.new("unsigned char[64]")
recid = ffi.new("int *")
lib.secp256k1_ecdsa_recoverable_signature_serialize_compact(context.ctx, output, recid, recover_sig)
return bytes(ffi.buffer(output, CDATA_SIG_LENGTH)) + int_to_bytes(recid[0])
def deserialize_recoverable(serialized: bytes, context: Context = GLOBAL_CONTEXT):
if len(serialized) != 65: # noqa: PLR2004
msg = "Serialized signature must be 65 bytes long."
raise ValueError(msg)
ser_sig, rec_id = serialized[:64], bytes_to_int(serialized[64:])
if not 0 <= rec_id <= 3: # noqa: PLR2004
msg = "Invalid recovery id."
raise ValueError(msg)
recover_sig = ffi.new("secp256k1_ecdsa_recoverable_signature *")
parsed = lib.secp256k1_ecdsa_recoverable_signature_parse_compact(context.ctx, recover_sig, ser_sig, rec_id)
if not parsed:
msg = "Failed to parse recoverable signature."
raise ValueError(msg)
return recover_sig
"""
Warning:
The functions below may change and are not tested!
"""
def serialize_compact(raw_sig, context: Context = GLOBAL_CONTEXT): # no cov
output = ffi.new("unsigned char[64]")
res = lib.secp256k1_ecdsa_signature_serialize_compact(context.ctx, output, raw_sig)
if not res:
msg = "secp256k1_ecdsa_signature_serialize_compact"
raise ValueError(msg)
return bytes(ffi.buffer(output, CDATA_SIG_LENGTH))
def deserialize_compact(ser_sig: bytes, context: Context = GLOBAL_CONTEXT): # no cov
if len(ser_sig) != 64: # noqa: PLR2004
msg = "invalid signature length"
raise ValueError(msg)
raw_sig = ffi.new("secp256k1_ecdsa_signature *")
res = lib.secp256k1_ecdsa_signature_parse_compact(context.ctx, raw_sig, ser_sig)
if not res:
msg = "secp256k1_ecdsa_signature_parse_compact"
raise ValueError(msg)
return raw_sig
def signature_normalize(raw_sig, context: Context = GLOBAL_CONTEXT): # no cov
"""
Check and optionally convert a signature to a normalized lower-S form.
This function always return a tuple containing a boolean (True if
not previously normalized or False if signature was already
normalized), and the normalized signature.
"""
sigout = ffi.new("secp256k1_ecdsa_signature *")
res = lib.secp256k1_ecdsa_signature_normalize(context.ctx, sigout, raw_sig)
return not not res, sigout # noqa: SIM208
def recoverable_convert(recover_sig, context: Context = GLOBAL_CONTEXT): # no cov
normal_sig = ffi.new("secp256k1_ecdsa_signature *")
lib.secp256k1_ecdsa_recoverable_signature_convert(context.ctx, normal_sig, recover_sig)
return normal_sig

View File

@ -0,0 +1,18 @@
from __future__ import annotations
from coincurve._libsecp256k1 import lib
CONTEXT_NONE = lib.SECP256K1_CONTEXT_NONE
CONTEXT_FLAGS = {
CONTEXT_NONE,
}
EC_COMPRESSED = lib.SECP256K1_EC_COMPRESSED
EC_UNCOMPRESSED = lib.SECP256K1_EC_UNCOMPRESSED
# Additional flags available from libsecp256k1
# lib.SECP256K1_TAG_PUBKEY_EVEN
# lib.SECP256K1_TAG_PUBKEY_ODD
# lib.SECP256K1_TAG_PUBKEY_UNCOMPRESSED
# lib.SECP256K1_TAG_PUBKEY_HYBRID_EVEN
# lib.SECP256K1_TAG_PUBKEY_HYBRID_ODD

View File

@ -0,0 +1,787 @@
from __future__ import annotations
import os
from typing import TYPE_CHECKING
from coincurve._libsecp256k1 import ffi, lib
from coincurve.context import GLOBAL_CONTEXT, Context
from coincurve.der import decode_der, encode_der
from coincurve.ecdsa import cdata_to_der, der_to_cdata, deserialize_recoverable, recover, serialize_recoverable
from coincurve.flags import EC_COMPRESSED, EC_UNCOMPRESSED
from coincurve.utils import (
DEFAULT_NONCE,
bytes_to_int,
der_to_pem,
get_valid_secret,
hex_to_bytes,
int_to_bytes_padded,
pad_scalar,
pem_to_der,
sha256,
validate_secret,
)
if TYPE_CHECKING:
from coincurve.types import Hasher, Nonce
class PrivateKey:
def __init__(self, secret: bytes | None = None, context: Context = GLOBAL_CONTEXT):
"""
Initializes a private key.
Parameters:
secret: The secret used to initialize the private key.
If not provided, a new key will be generated.
context: The context to use.
"""
self.secret: bytes = validate_secret(secret) if secret is not None else get_valid_secret()
self.context = context
self.public_key: PublicKey = PublicKey.from_valid_secret(self.secret, self.context)
self.public_key_xonly: PublicKeyXOnly = PublicKeyXOnly.from_valid_secret(self.secret, self.context)
def sign(self, message: bytes, hasher: Hasher = sha256, custom_nonce: Nonce = DEFAULT_NONCE) -> bytes:
"""
Creates an ECDSA signature.
Parameters:
message: The message to sign.
hasher (collections.abc.Callable[[bytes], bytes] | None): The hash function to use, which must
return 32 bytes. By default, the `sha256` algorithm is used. If `None`, no hashing occurs.
custom_nonce (tuple[ffi.CData, ffi.CData]): Custom nonce data in the form `(nonce_function, input_data)`.
For more information, refer to the `libsecp256k1` documentation
[here](https://github.com/bitcoin-core/secp256k1/blob/v0.6.0/include/secp256k1.h#L637-L642).
Returns:
The ECDSA signature.
Raises:
ValueError: If the message hash was not 32 bytes long, the nonce generation
function failed, or the private key was invalid.
"""
msg_hash = hasher(message) if hasher is not None else message
if len(msg_hash) != 32: # noqa: PLR2004
msg = "Message hash must be 32 bytes long."
raise ValueError(msg)
signature = ffi.new("secp256k1_ecdsa_signature *")
nonce_fn, nonce_data = custom_nonce
signed = lib.secp256k1_ecdsa_sign(self.context.ctx, signature, msg_hash, self.secret, nonce_fn, nonce_data)
if not signed:
msg = "The nonce generation function failed, or the private key was invalid."
raise ValueError(msg)
return cdata_to_der(signature, self.context)
def sign_schnorr(self, message: bytes, aux_randomness: bytes = b"") -> bytes:
"""
Creates a Schnorr signature.
Parameters:
message: The message to sign.
aux_randomness: 32 bytes of fresh randomness, empty bytestring (auto-generated),
or None (no randomness).
Returns:
The Schnorr signature.
Raises:
ValueError: If the message was not 32 bytes long, the optional auxiliary
random data was not 32 bytes long, signing failed, or the signature was invalid.
"""
if len(message) != 32: # noqa: PLR2004
msg = "Message must be 32 bytes long."
raise ValueError(msg)
if aux_randomness == b"":
aux_randomness = os.urandom(32)
elif aux_randomness is None:
aux_randomness = ffi.NULL
elif len(aux_randomness) != 32: # noqa: PLR2004
msg = "Auxiliary random data must be 32 bytes long."
raise ValueError(msg)
keypair = ffi.new("secp256k1_keypair *")
res = lib.secp256k1_keypair_create(self.context.ctx, keypair, self.secret)
if not res:
msg = "Secret was invalid"
raise ValueError(msg)
signature = ffi.new("unsigned char[64]")
res = lib.secp256k1_schnorrsig_sign32(self.context.ctx, signature, message, keypair, aux_randomness)
if not res:
msg = "Signing failed"
raise ValueError(msg)
res = lib.secp256k1_schnorrsig_verify(
self.context.ctx, signature, message, len(message), self.public_key_xonly.public_key
)
if not res:
msg = "Invalid signature"
raise ValueError(msg)
return bytes(ffi.buffer(signature))
def sign_recoverable(self, message: bytes, hasher: Hasher = sha256, custom_nonce: Nonce = DEFAULT_NONCE) -> bytes:
"""
Creates a recoverable ECDSA signature.
Parameters:
message: The message to sign.
hasher (collections.abc.Callable[[bytes], bytes] | None): The hash function to use, which must
return 32 bytes. By default, the `sha256` algorithm is used. If `None`, no hashing occurs.
custom_nonce (tuple[ffi.CData, ffi.CData]): Custom nonce data in the form `(nonce_function, input_data)`.
For more information, refer to the `libsecp256k1` documentation
[here](https://github.com/bitcoin-core/secp256k1/blob/v0.6.0/include/secp256k1.h#L637-L642).
Returns:
The recoverable ECDSA signature.
Raises:
ValueError: If the message hash was not 32 bytes long, the nonce generation
function failed, or the private key was invalid.
"""
msg_hash = hasher(message) if hasher is not None else message
if len(msg_hash) != 32: # noqa: PLR2004
msg = "Message hash must be 32 bytes long."
raise ValueError(msg)
signature = ffi.new("secp256k1_ecdsa_recoverable_signature *")
nonce_fn, nonce_data = custom_nonce
signed = lib.secp256k1_ecdsa_sign_recoverable(
self.context.ctx, signature, msg_hash, self.secret, nonce_fn, nonce_data
)
if not signed:
msg = "The nonce generation function failed, or the private key was invalid."
raise ValueError(msg)
return serialize_recoverable(signature, self.context)
def ecdh(self, public_key: bytes) -> bytes:
"""
Computes an EC Diffie-Hellman secret in constant time.
!!! note
This prevents malleability by returning `sha256(compressed_public_key)` instead of the `x` coordinate
directly.
Parameters:
public_key: The formatted public key.
Returns:
The 32-byte shared secret.
Raises:
ValueError: If the public key could not be parsed or was invalid.
"""
secret = ffi.new("unsigned char [32]")
lib.secp256k1_ecdh(self.context.ctx, secret, PublicKey(public_key).public_key, self.secret, ffi.NULL, ffi.NULL)
return bytes(ffi.buffer(secret, 32))
def add(self, scalar: bytes, update: bool = False) -> PrivateKey: # noqa: FBT001, FBT002
"""
Adds a scalar to the private key.
Parameters:
scalar: The scalar with which to add.
update: Whether to update the private key in-place.
Returns:
The new private key, or the modified private key if `update` is `True`.
Raises:
ValueError: If the tweak was out of range or the resulting private key was invalid.
"""
scalar = pad_scalar(scalar)
secret = ffi.new("unsigned char [32]", self.secret)
success = lib.secp256k1_ec_seckey_tweak_add(self.context.ctx, secret, scalar)
if not success:
msg = "The tweak was out of range, or the resulting private key is invalid."
raise ValueError(msg)
secret = bytes(ffi.buffer(secret, 32))
if update:
self.secret = secret
self._update_public_key()
return self
return PrivateKey(secret, self.context)
def multiply(self, scalar: bytes, update: bool = False) -> PrivateKey: # noqa: FBT001, FBT002
"""
Multiplies the private key by a scalar.
Parameters:
scalar: The scalar with which to multiply.
update: Whether to update the private key in-place.
Returns:
The new private key, or the modified private key if `update` is `True`.
"""
scalar = validate_secret(scalar)
secret = ffi.new("unsigned char [32]", self.secret)
lib.secp256k1_ec_seckey_tweak_mul(self.context.ctx, secret, scalar)
secret = bytes(ffi.buffer(secret, 32))
if update:
self.secret = secret
self._update_public_key()
return self
return PrivateKey(secret, self.context)
def to_hex(self) -> str:
"""
Returns the private key encoded as a hex string.
"""
return self.secret.hex()
def to_int(self) -> int:
"""
Returns the private key as an integer.
"""
return bytes_to_int(self.secret)
def to_pem(self) -> bytes:
"""
Returns the private key encoded in PEM format.
"""
return der_to_pem(self.to_der())
def to_der(self) -> bytes:
"""
Returns the private key encoded in DER format.
"""
return encode_der(self.secret, self.public_key.format(compressed=False))
@classmethod
def from_hex(cls, hexed: str, context: Context = GLOBAL_CONTEXT) -> PrivateKey:
"""
Creates a private key from a hex string.
Parameters:
hexed: The private key encoded as a hex string.
context: The context to use.
Returns:
The private key.
"""
return PrivateKey(hex_to_bytes(hexed), context)
@classmethod
def from_int(cls, num: int, context: Context = GLOBAL_CONTEXT) -> PrivateKey:
"""
Creates a private key from an integer.
Parameters:
num: The private key as an integer.
context: The context to use.
Returns:
The private key.
"""
return PrivateKey(int_to_bytes_padded(num), context)
@classmethod
def from_pem(cls, pem: bytes, context: Context = GLOBAL_CONTEXT) -> PrivateKey:
"""
Creates a private key from PEM format.
Parameters:
pem: The private key encoded in PEM format.
context: The context to use.
Returns:
The private key.
"""
return PrivateKey(decode_der(pem_to_der(pem)), context)
@classmethod
def from_der(cls, der: bytes, context: Context = GLOBAL_CONTEXT) -> PrivateKey:
"""
Creates a private key from DER format.
Parameters:
der: The private key encoded in DER format.
context: The context to use.
Returns:
The private key.
"""
return PrivateKey(decode_der(der), context)
def _update_public_key(self):
created = lib.secp256k1_ec_pubkey_create(self.context.ctx, self.public_key.public_key, self.secret)
if not created:
msg = "Invalid secret."
raise ValueError(msg)
def __eq__(self, other) -> bool:
return self.secret == other.secret
def __hash__(self) -> int:
return hash(self.secret)
class PublicKey:
def __init__(self, data: bytes | ffi.CData, context: Context = GLOBAL_CONTEXT):
"""
Initializes a public key.
Parameters:
data (bytes): The formatted public key. This class supports parsing
compressed (33 bytes, header byte `0x02` or `0x03`),
uncompressed (65 bytes, header byte `0x04`), or
hybrid (65 bytes, header byte `0x06` or `0x07`) format public keys.
context: The context to use.
Raises:
ValueError: If the public key could not be parsed or was invalid.
"""
if not isinstance(data, bytes):
self.public_key = data
else:
public_key = ffi.new("secp256k1_pubkey *")
parsed = lib.secp256k1_ec_pubkey_parse(context.ctx, public_key, data, len(data))
if not parsed:
msg = "The public key could not be parsed or is invalid."
raise ValueError(msg)
self.public_key = public_key
self.context = context
@classmethod
def from_secret(cls, secret: bytes, context: Context = GLOBAL_CONTEXT) -> PublicKey:
"""
Derives a public key from a private key secret.
Parameters:
secret: The private key secret.
context: The context to use.
Returns:
The public key.
Raises:
ValueError: If an invalid secret was used.
"""
public_key = ffi.new("secp256k1_pubkey *")
created = lib.secp256k1_ec_pubkey_create(context.ctx, public_key, validate_secret(secret))
if not created: # no cov
msg = (
"Somehow an invalid secret was used. Please "
"submit this as an issue here: "
"https://github.com/ofek/coincurve/issues/new"
)
raise ValueError(msg)
return PublicKey(public_key, context)
@classmethod
def from_valid_secret(cls, secret: bytes, context: Context = GLOBAL_CONTEXT) -> PublicKey:
"""
Derives a public key from a valid private key secret, avoiding input checks.
Parameters:
secret: The private key secret.
context: The context to use.
Returns:
The public key.
Raises:
ValueError: If the secret was invalid.
"""
public_key = ffi.new("secp256k1_pubkey *")
created = lib.secp256k1_ec_pubkey_create(context.ctx, public_key, secret)
if not created:
msg = "Invalid secret."
raise ValueError(msg)
return PublicKey(public_key, context)
@classmethod
def from_point(cls, x: int, y: int, context: Context = GLOBAL_CONTEXT) -> PublicKey:
"""
Derives a public key from a coordinate point.
Parameters:
x: The x coordinate.
y: The y coordinate.
context: The context to use.
Returns:
The public key.
"""
return PublicKey(b"\x04" + int_to_bytes_padded(x) + int_to_bytes_padded(y), context)
@classmethod
def from_signature_and_message(
cls, signature: bytes, message: bytes, hasher: Hasher = sha256, context: Context = GLOBAL_CONTEXT
) -> PublicKey:
"""
Recovers an ECDSA public key from a recoverable signature.
Parameters:
signature: The recoverable ECDSA signature.
message: The message that was supposedly signed.
hasher (collections.abc.Callable[[bytes], bytes] | None): The hash function to use, which must
return 32 bytes. By default, the `sha256` algorithm is used. If `None`, no hashing occurs.
context: The context to use.
Returns:
The public key that signed the message.
Raises:
ValueError: If the message hash was not 32 bytes long or recovery of the
ECDSA public key failed.
"""
return PublicKey(
recover(message, deserialize_recoverable(signature, context=context), hasher=hasher, context=context)
)
@classmethod
def combine_keys(cls, public_keys: list[PublicKey], context: Context = GLOBAL_CONTEXT) -> PublicKey:
"""
Adds a number of public keys together.
Parameters:
public_keys: A sequence of public keys.
context: The context to use.
Returns:
The combined public key.
Raises:
ValueError: If the sum of the public keys was invalid.
"""
public_key = ffi.new("secp256k1_pubkey *")
combined = lib.secp256k1_ec_pubkey_combine(
context.ctx, public_key, [pk.public_key for pk in public_keys], len(public_keys)
)
if not combined:
msg = "The sum of the public keys is invalid."
raise ValueError(msg)
return PublicKey(public_key, context)
def format(self, compressed: bool = True) -> bytes: # noqa: FBT001, FBT002
"""
Formats the public key.
Parameters:
compressed: Whether to use the compressed format.
Returns:
The 33 byte formatted public key, or the 65 byte formatted public key
if `compressed` is `False`.
"""
length = 33 if compressed else 65
serialized = ffi.new("unsigned char [%d]" % length) # noqa: UP031
output_len = ffi.new("size_t *", length)
lib.secp256k1_ec_pubkey_serialize(
self.context.ctx, serialized, output_len, self.public_key, EC_COMPRESSED if compressed else EC_UNCOMPRESSED
)
return bytes(ffi.buffer(serialized, length))
def point(self) -> tuple[int, int]:
"""
Returns the public key as a coordinate point.
"""
public_key = self.format(compressed=False)
return bytes_to_int(public_key[1:33]), bytes_to_int(public_key[33:])
def verify(self, signature: bytes, message: bytes, hasher: Hasher = sha256) -> bool:
"""
Verifies an ECDSA signature.
Parameters:
signature: The ECDSA signature.
message: The message that was supposedly signed.
hasher (collections.abc.Callable[[bytes], bytes] | None): The hash function to use, which must
return 32 bytes. By default, the `sha256` algorithm is used. If `None`, no hashing occurs.
Returns:
A boolean indicating whether the signature is correct.
Raises:
ValueError: If the message hash was not 32 bytes long or the
DER-encoded signature could not be parsed.
"""
msg_hash = hasher(message) if hasher is not None else message
if len(msg_hash) != 32: # noqa: PLR2004
msg = "Message hash must be 32 bytes long."
raise ValueError(msg)
verified = lib.secp256k1_ecdsa_verify(self.context.ctx, der_to_cdata(signature), msg_hash, self.public_key)
# A performance hack to avoid global bool() lookup.
return not not verified # noqa: SIM208
def add(self, scalar: bytes, update: bool = False) -> PublicKey: # noqa: FBT001, FBT002
"""
Adds a scalar to the public key.
Parameters:
scalar: The scalar with which to add.
update: Whether to update the public key in-place.
Returns:
The new public key, or the modified public key if `update` is `True`.
Raises:
ValueError: If the tweak was out of range or the resulting public key was invalid.
"""
scalar = pad_scalar(scalar)
new_key = ffi.new("secp256k1_pubkey *", self.public_key[0])
success = lib.secp256k1_ec_pubkey_tweak_add(self.context.ctx, new_key, scalar)
if not success:
msg = "The tweak was out of range, or the resulting public key is invalid."
raise ValueError(msg)
if update:
self.public_key = new_key
return self
return PublicKey(new_key, self.context)
def multiply(self, scalar: bytes, update: bool = False) -> PublicKey: # noqa: FBT001, FBT002
"""
Multiplies the public key by a scalar.
Parameters:
scalar: The scalar with which to multiply.
update: Whether to update the public key in-place.
Returns:
The new public key, or the modified public key if `update` is `True`.
"""
scalar = validate_secret(scalar)
new_key = ffi.new("secp256k1_pubkey *", self.public_key[0])
lib.secp256k1_ec_pubkey_tweak_mul(self.context.ctx, new_key, scalar)
if update:
self.public_key = new_key
return self
return PublicKey(new_key, self.context)
def combine(self, public_keys: list[PublicKey], update: bool = False) -> PublicKey: # noqa: FBT001, FBT002
"""
Adds a number of public keys together.
Parameters:
public_keys: A sequence of public keys.
update: Whether to update the public key in-place.
Returns:
The combined public key, or the modified public key if `update` is `True`.
Raises:
ValueError: If the sum of the public keys was invalid.
"""
new_key = ffi.new("secp256k1_pubkey *")
combined = lib.secp256k1_ec_pubkey_combine(
self.context.ctx, new_key, [pk.public_key for pk in [self, *public_keys]], len(public_keys) + 1
)
if not combined:
msg = "The sum of the public keys is invalid."
raise ValueError(msg)
if update:
self.public_key = new_key
return self
return PublicKey(new_key, self.context)
def __eq__(self, other) -> bool:
return self.format(compressed=False) == other.format(compressed=False)
def __hash__(self) -> int:
return hash(self.format(compressed=False))
class PublicKeyXOnly:
def __init__(self, data: bytes | ffi.CData, parity: bool = False, context: Context = GLOBAL_CONTEXT): # noqa: FBT001, FBT002
"""
Initializes a BIP340 `x-only` public key.
Parameters:
data (bytes): The formatted public key.
parity: Whether the encoded point is the negation of the public key.
context: The context to use.
Raises:
ValueError: If the public key could not be parsed or is invalid.
"""
if not isinstance(data, bytes):
self.public_key = data
else:
public_key = ffi.new("secp256k1_xonly_pubkey *")
parsed = lib.secp256k1_xonly_pubkey_parse(context.ctx, public_key, data)
if not parsed:
msg = "The public key could not be parsed or is invalid."
raise ValueError(msg)
self.public_key = public_key
self.parity = parity
self.context = context
@classmethod
def from_secret(cls, secret: bytes, context: Context = GLOBAL_CONTEXT) -> PublicKeyXOnly:
"""
Derives an x-only public key from a private key secret.
Parameters:
secret: The private key secret.
context: The context to use.
Returns:
The x-only public key.
Raises:
ValueError: If the secret was invalid.
"""
keypair = ffi.new("secp256k1_keypair *")
res = lib.secp256k1_keypair_create(context.ctx, keypair, validate_secret(secret))
if not res:
msg = "Secret was invalid"
raise ValueError(msg)
xonly_pubkey = ffi.new("secp256k1_xonly_pubkey *")
pk_parity = ffi.new("int *")
res = lib.secp256k1_keypair_xonly_pub(context.ctx, xonly_pubkey, pk_parity, keypair)
return cls(xonly_pubkey, parity=not not pk_parity[0], context=context) # noqa: SIM208
@classmethod
def from_valid_secret(cls, secret: bytes, context: Context = GLOBAL_CONTEXT) -> PublicKeyXOnly:
"""
Derives an x-only public key from a valid private key secret, avoiding input checks.
Parameters:
secret: The private key secret.
context: The context to use.
Returns:
The x-only public key.
Raises:
ValueError: If the secret was invalid.
"""
keypair = ffi.new("secp256k1_keypair *")
res = lib.secp256k1_keypair_create(context.ctx, keypair, secret)
if not res:
msg = "Secret was invalid"
raise ValueError(msg)
xonly_pubkey = ffi.new("secp256k1_xonly_pubkey *")
pk_parity = ffi.new("int *")
res = lib.secp256k1_keypair_xonly_pub(context.ctx, xonly_pubkey, pk_parity, keypair)
return cls(xonly_pubkey, parity=not not pk_parity[0], context=context) # noqa: SIM208
def format(self) -> bytes:
"""
Serializes the public key.
Returns:
The public key serialized as 32 bytes.
Raises:
ValueError: If the public key in `self.public_key` is invalid.
"""
output32 = ffi.new("unsigned char [32]")
res = lib.secp256k1_xonly_pubkey_serialize(self.context.ctx, output32, self.public_key)
if not res:
msg = "Public key in self.public_key must be valid"
raise ValueError(msg)
return bytes(ffi.buffer(output32, 32))
def verify(self, signature: bytes, message: bytes) -> bool:
"""
Verifies a Schnorr signature over a given message.
Parameters:
signature: The 64-byte Schnorr signature to verify.
message: The message to be verified.
Returns:
A boolean indicating whether the signature is correct.
Raises:
ValueError: If the signature is not 64 bytes long.
"""
if len(signature) != 64: # noqa: PLR2004
msg = "Signature must be 64 bytes long."
raise ValueError(msg)
return not not lib.secp256k1_schnorrsig_verify( # noqa: SIM208
self.context.ctx, signature, message, len(message), self.public_key
)
def tweak_add(self, scalar: bytes) -> None:
"""
Adds a scalar to the public key.
Parameters:
scalar: The scalar with which to add.
Returns:
The modified public key.
Raises:
ValueError: If the tweak was out of range or the resulting public key would be invalid.
"""
scalar = pad_scalar(scalar)
out_pubkey = ffi.new("secp256k1_pubkey *")
res = lib.secp256k1_xonly_pubkey_tweak_add(self.context.ctx, out_pubkey, self.public_key, scalar)
if not res:
msg = "The tweak was out of range, or the resulting public key would be invalid"
raise ValueError(msg)
pk_parity = ffi.new("int *")
lib.secp256k1_xonly_pubkey_from_pubkey(self.context.ctx, self.public_key, pk_parity, out_pubkey)
self.parity = not not pk_parity[0] # noqa: SIM208
def __eq__(self, other) -> bool:
res = lib.secp256k1_xonly_pubkey_cmp(self.context.ctx, self.public_key, other.public_key)
return res == 0
def __hash__(self) -> int:
return hash(self.format())

View File

@ -0,0 +1,8 @@
from __future__ import annotations
from collections.abc import Callable
from coincurve._libsecp256k1 import ffi
Hasher = Callable[[bytes], bytes] | None
Nonce = tuple[ffi.CData, ffi.CData]

View File

@ -0,0 +1,147 @@
from __future__ import annotations
from base64 import b64decode, b64encode
from hashlib import sha256 as _sha256
from os import environ, urandom
from typing import TYPE_CHECKING
from coincurve._libsecp256k1 import ffi, lib
from coincurve.context import GLOBAL_CONTEXT, Context
if TYPE_CHECKING:
from collections.abc import Generator
from coincurve.types import Hasher
GROUP_ORDER = (
b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xba\xae\xdc\xe6\xafH\xa0;\xbf\xd2^\x8c\xd06AA"
)
GROUP_ORDER_INT = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
KEY_SIZE = 32
MSG_HASH_SIZE = 32
ZERO = b"\x00"
PEM_HEADER = b"-----BEGIN PRIVATE KEY-----\n"
PEM_FOOTER = b"-----END PRIVATE KEY-----\n"
if environ.get("COINCURVE_BUILDING_DOCS") != "true":
DEFAULT_NONCE = (ffi.NULL, ffi.NULL)
def sha256(bytestr: bytes) -> bytes:
return _sha256(bytestr).digest()
else: # no cov
class __Nonce(tuple): # noqa: SLOT001
def __repr__(self) -> str:
return "(ffi.NULL, ffi.NULL)"
class __HasherSHA256:
def __call__(self, bytestr: bytes) -> bytes:
return _sha256(bytestr).digest()
def __repr__(self) -> str:
return "sha256"
DEFAULT_NONCE = __Nonce((ffi.NULL, ffi.NULL))
sha256 = __HasherSHA256()
def pad_hex(hexed: str) -> str:
# Pad odd-length hex strings.
return hexed if not len(hexed) & 1 else f"0{hexed}"
def bytes_to_int(bytestr: bytes) -> int:
return int.from_bytes(bytestr, "big")
def int_to_bytes(num: int) -> bytes:
return num.to_bytes((num.bit_length() + 7) // 8 or 1, "big")
def int_to_bytes_padded(num: int) -> bytes:
return pad_scalar(num.to_bytes((num.bit_length() + 7) // 8 or 1, "big"))
def hex_to_bytes(hexed: str) -> bytes:
return pad_scalar(bytes.fromhex(pad_hex(hexed)))
def chunk_data(data: bytes, size: int) -> Generator[bytes, None, None]:
return (data[i : i + size] for i in range(0, len(data), size))
def der_to_pem(der: bytes) -> bytes:
return b"".join([PEM_HEADER, b"\n".join(chunk_data(b64encode(der), 64)), b"\n", PEM_FOOTER])
def pem_to_der(pem: bytes) -> bytes:
return b64decode(b"".join(pem.strip().splitlines()[1:-1]))
def get_valid_secret() -> bytes:
while True:
secret = urandom(KEY_SIZE)
if ZERO < secret < GROUP_ORDER:
return secret
def pad_scalar(scalar: bytes) -> bytes:
return (ZERO * (KEY_SIZE - len(scalar))) + scalar
def validate_secret(secret: bytes) -> bytes:
if not 0 < bytes_to_int(secret) < GROUP_ORDER_INT:
msg = f"Secret scalar must be greater than 0 and less than {GROUP_ORDER_INT}."
raise ValueError(msg)
return pad_scalar(secret)
def verify_signature(
signature: bytes, message: bytes, public_key: bytes, hasher: Hasher = sha256, context: Context = GLOBAL_CONTEXT
) -> bool:
"""
Verify an ECDSA signature.
Parameters:
signature: The ECDSA signature.
message: The message that was supposedly signed.
public_key: The formatted public key.
hasher (collections.abc.Callable[[bytes], bytes] | None): The hash function to use, which must return 32 bytes.
By default, the `sha256` algorithm is used. If `None`, no hashing occurs.
context: The secp256k1 context.
Returns:
A boolean indicating whether or not the signature is correct.
Raises:
ValueError: If the public key could not be parsed or was invalid, the
message hash was not 32 bytes long, or the DER-encoded signature
could not be parsed.
"""
pubkey = ffi.new("secp256k1_pubkey *")
pubkey_parsed = lib.secp256k1_ec_pubkey_parse(context.ctx, pubkey, public_key, len(public_key))
if not pubkey_parsed:
msg = "The public key could not be parsed or is invalid."
raise ValueError(msg)
msg_hash = hasher(message) if hasher is not None else message
if len(msg_hash) != MSG_HASH_SIZE:
msg = "Message hash must be 32 bytes long."
raise ValueError(msg)
sig = ffi.new("secp256k1_ecdsa_signature *")
sig_parsed = lib.secp256k1_ecdsa_signature_parse_der(context.ctx, sig, signature, len(signature))
if not sig_parsed:
msg = "The DER-encoded signature could not be parsed."
raise ValueError(msg)
verified = lib.secp256k1_ecdsa_verify(context.ctx, sig, msg_hash, pubkey)
# A performance hack to avoid global bool() lookup.
return not not verified # noqa: SIM208