Initial commit: 首次建仓,建立目录结构
This commit is contained in:
@ -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",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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")
|
||||
270
dashboard/venv/lib/python3.12/site-packages/coincurve/der.py
Normal file
270
dashboard/venv/lib/python3.12/site-packages/coincurve/der.py
Normal 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]
|
||||
131
dashboard/venv/lib/python3.12/site-packages/coincurve/ecdsa.py
Normal file
131
dashboard/venv/lib/python3.12/site-packages/coincurve/ecdsa.py
Normal 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
|
||||
@ -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
|
||||
787
dashboard/venv/lib/python3.12/site-packages/coincurve/keys.py
Normal file
787
dashboard/venv/lib/python3.12/site-packages/coincurve/keys.py
Normal 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())
|
||||
@ -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]
|
||||
147
dashboard/venv/lib/python3.12/site-packages/coincurve/utils.py
Normal file
147
dashboard/venv/lib/python3.12/site-packages/coincurve/utils.py
Normal 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
|
||||
Reference in New Issue
Block a user