ECDSA cryptographic signature library (pure python)
—
Comprehensive encoding and decoding utilities for digital signatures, cryptographic keys, and ASN.1 structures. The ecdsa library provides multiple encoding formats to ensure compatibility with different systems and standards including DER, PEM, SSH, and raw binary formats.
Functions to encode ECDSA signature pairs (r, s) into various standard formats.
def sigencode_string(r, s, order):
"""
Encode signature as raw concatenated byte string.
Parameters:
- r: int, signature r component
- s: int, signature s component
- order: int, curve order for length calculation
Returns:
bytes, concatenated r||s as fixed-length byte string
"""
def sigencode_strings(r, s, order):
"""
Encode signature components as separate byte strings.
Parameters:
- r: int, signature r component
- s: int, signature s component
- order: int, curve order for length calculation
Returns:
tuple[bytes, bytes], (r_bytes, s_bytes) as separate strings
"""
def sigencode_der(r, s, order):
"""
Encode signature in DER ASN.1 format.
Parameters:
- r: int, signature r component
- s: int, signature s component
- order: int, curve order (unused but kept for API consistency)
Returns:
bytes, DER-encoded ASN.1 SEQUENCE containing r and s INTEGERs
"""
def sigencode_string_canonize(r, s, order):
"""
Encode signature as canonical raw byte string (low-s form).
Parameters:
- r: int, signature r component
- s: int, signature s component
- order: int, curve order for canonicalization
Returns:
bytes, canonical concatenated r||s with s in low form
"""
def sigencode_strings_canonize(r, s, order):
"""
Encode signature components as canonical separate byte strings.
Parameters:
- r: int, signature r component
- s: int, signature s component
- order: int, curve order for canonicalization
Returns:
tuple[bytes, bytes], canonical (r_bytes, s_bytes) with low-s
"""
def sigencode_der_canonize(r, s, order):
"""
Encode signature in canonical DER format (low-s form).
Parameters:
- r: int, signature r component
- s: int, signature s component
- order: int, curve order for canonicalization
Returns:
bytes, canonical DER-encoded signature with low-s
"""Functions to decode signatures from various formats back to (r, s) integer pairs.
def sigdecode_string(signature, order):
"""
Decode signature from raw concatenated byte string.
Parameters:
- signature: bytes, concatenated r||s byte string
- order: int, curve order for length calculation
Returns:
tuple[int, int], (r, s) signature components
Raises:
ValueError: if signature length is incorrect
"""
def sigdecode_strings(rs_strings, order):
"""
Decode signature from separate byte string tuple.
Parameters:
- rs_strings: tuple[bytes, bytes], (r_bytes, s_bytes)
- order: int, curve order for validation
Returns:
tuple[int, int], (r, s) signature components
"""
def sigdecode_der(sig_der, order):
"""
Decode signature from DER ASN.1 format.
Parameters:
- sig_der: bytes, DER-encoded ASN.1 signature
- order: int, curve order for validation
Returns:
tuple[int, int], (r, s) signature components
Raises:
UnexpectedDER: if DER structure is invalid
"""Low-level utilities for converting between integers and byte strings with proper length handling.
def number_to_string(num, order):
"""
Convert integer to fixed-length byte string based on curve order.
Parameters:
- num: int, number to convert
- order: int, curve order determining output length
Returns:
bytes, big-endian byte string with length based on order
"""
def number_to_string_crop(num, order):
"""
Convert integer to minimal-length byte string.
Parameters:
- num: int, number to convert
- order: int, curve order for validation
Returns:
bytes, minimal big-endian byte string (no leading zeros)
"""
def string_to_number(string):
"""
Convert byte string to integer.
Parameters:
- string: bytes, big-endian byte string
Returns:
int, converted number
"""
def string_to_number_fixedlen(string, order):
"""
Convert byte string to integer with length validation.
Parameters:
- string: bytes, big-endian byte string
- order: int, curve order for length validation
Returns:
int, converted number
Raises:
ValueError: if string length doesn't match expected length
"""Functions for encoding data structures in Distinguished Encoding Rules (DER) format.
def encode_sequence(*encoded_pieces):
"""
Encode ASN.1 SEQUENCE containing multiple elements.
Parameters:
- encoded_pieces: bytes, already DER-encoded elements
Returns:
bytes, DER-encoded SEQUENCE
"""
def encode_integer(r):
"""
Encode integer as ASN.1 INTEGER.
Parameters:
- r: int, integer to encode
Returns:
bytes, DER-encoded INTEGER
"""
def encode_octet_string(s):
"""
Encode byte string as ASN.1 OCTET STRING.
Parameters:
- s: bytes, byte string to encode
Returns:
bytes, DER-encoded OCTET STRING
"""
def encode_bitstring(s, unused=0):
"""
Encode byte string as ASN.1 BIT STRING.
Parameters:
- s: bytes, byte string to encode
- unused: int, number of unused bits in last byte (0-7)
Returns:
bytes, DER-encoded BIT STRING
"""
def encode_oid(first, second, *pieces):
"""
Encode ASN.1 OBJECT IDENTIFIER from components.
Parameters:
- first: int, first OID component
- second: int, second OID component
- pieces: int, additional OID components
Returns:
bytes, DER-encoded OBJECT IDENTIFIER
"""
def encode_constructed(tag, value):
"""
Encode constructed ASN.1 type with custom tag.
Parameters:
- tag: int, ASN.1 tag number
- value: bytes, content to encode
Returns:
bytes, DER-encoded constructed type
"""
def encode_implicit(tag, value, cls="context-specific"):
"""
Encode implicit ASN.1 tag.
Parameters:
- tag: int, tag number
- value: bytes, content to encode
- cls: str, tag class ("context-specific", "application", etc.)
Returns:
bytes, DER-encoded implicit tag
"""Functions for decoding DER-encoded ASN.1 structures.
def remove_sequence(string):
"""
Decode ASN.1 SEQUENCE and return content.
Parameters:
- string: bytes, DER-encoded data starting with SEQUENCE
Returns:
tuple[bytes, bytes], (sequence_content, remaining_data)
Raises:
UnexpectedDER: if not a valid SEQUENCE
"""
def remove_integer(string):
"""
Decode ASN.1 INTEGER and return value.
Parameters:
- string: bytes, DER-encoded data starting with INTEGER
Returns:
tuple[int, bytes], (integer_value, remaining_data)
Raises:
UnexpectedDER: if not a valid INTEGER
"""
def remove_octet_string(string):
"""
Decode ASN.1 OCTET STRING and return content.
Parameters:
- string: bytes, DER-encoded data starting with OCTET STRING
Returns:
tuple[bytes, bytes], (octet_string_content, remaining_data)
"""
def remove_bitstring(string, expect_unused=0):
"""
Decode ASN.1 BIT STRING and return content.
Parameters:
- string: bytes, DER-encoded data starting with BIT STRING
- expect_unused: int, expected number of unused bits
Returns:
tuple[bytes, bytes], (bitstring_content, remaining_data)
Raises:
UnexpectedDER: if unused bits don't match expected
"""
def remove_object(string):
"""
Decode ASN.1 OBJECT IDENTIFIER and return OID tuple.
Parameters:
- string: bytes, DER-encoded data starting with OBJECT IDENTIFIER
Returns:
tuple[tuple[int, ...], bytes], (oid_tuple, remaining_data)
"""
def remove_constructed(string):
"""
Decode constructed ASN.1 type.
Parameters:
- string: bytes, DER-encoded constructed type
Returns:
tuple[int, bytes, bytes], (tag, content, remaining_data)
"""Functions for converting between DER binary format and PEM text format.
def encode_length(l):
"""
Encode ASN.1 length field.
Parameters:
- l: int, length to encode
Returns:
bytes, DER-encoded length
"""
def read_length(string):
"""
Decode ASN.1 length field.
Parameters:
- string: bytes, DER data starting with length field
Returns:
tuple[int, bytes], (length, remaining_data)
"""
def is_sequence(string):
"""
Check if data starts with ASN.1 SEQUENCE.
Parameters:
- string: bytes, DER-encoded data
Returns:
bool, True if starts with SEQUENCE tag
"""
def topem(der, name):
"""
Convert DER to PEM format.
Parameters:
- der: bytes, DER-encoded data
- name: str, PEM label (e.g., "PRIVATE KEY", "PUBLIC KEY")
Returns:
str, PEM-formatted string with headers and base64 encoding
"""
def unpem(pem):
"""
Convert PEM to DER format.
Parameters:
- pem: str or bytes, PEM-formatted data
Returns:
bytes, DER-encoded data (base64 decoded, headers removed)
"""Standard ASN.1 Object Identifiers used in elliptic curve cryptography.
oid_ecPublicKey: tuple # (1, 2, 840, 10045, 2, 1) - EC public key algorithm
encoded_oid_ecPublicKey: bytes # DER-encoded version of above
oid_ecDH: tuple # (1, 3, 132, 1, 12) - ECDH algorithm
oid_ecMQV: tuple # (1, 3, 132, 1, 13) - ECMQV algorithmUtilities for cryptographically secure random number generation.
class PRNG:
"""Pseudorandom number generator for deterministic signatures."""
def randrange(order, entropy=None):
"""
Generate cryptographically secure random number in range [1, order-1].
Parameters:
- order: int, upper bound (exclusive)
- entropy: callable or None, entropy source (default: os.urandom)
Returns:
int, random number in specified range
"""class UnexpectedDER(Exception):
"""Raised when DER encoding or decoding encounters invalid structure."""from ecdsa import SigningKey, NIST256p
from ecdsa.util import sigencode_der, sigdecode_der, sigencode_string, sigdecode_string
# Generate key and sign data
sk = SigningKey.generate(curve=NIST256p)
vk = sk.verifying_key
message = b"Test message for encoding"
# Sign with different encodings
sig_raw = sk.sign(message, sigencode=sigencode_string)
sig_der = sk.sign(message, sigencode=sigencode_der)
print(f"Raw signature length: {len(sig_raw)} bytes")
print(f"DER signature length: {len(sig_der)} bytes")
# Verify with corresponding decodings
vk.verify(sig_raw, message, sigdecode=sigdecode_string)
vk.verify(sig_der, message, sigdecode=sigdecode_der)
# Decode signatures to (r, s) pairs
r_raw, s_raw = sigdecode_string(sig_raw, sk.curve.order)
r_der, s_der = sigdecode_der(sig_der, sk.curve.order)
print(f"Raw signature r: {r_raw}")
print(f"DER signature r: {r_der}")
print(f"Signatures match: {r_raw == r_der and s_raw == s_der}")from ecdsa.der import encode_sequence, encode_integer, remove_sequence, remove_integer
# Create DER-encoded sequence containing two integers
r, s = 12345, 67890
der_r = encode_integer(r)
der_s = encode_integer(s)
der_sequence = encode_sequence(der_r, der_s)
print(f"DER sequence: {der_sequence.hex()}")
# Decode the sequence
sequence_content, remaining = remove_sequence(der_sequence)
decoded_r, content_after_r = remove_integer(sequence_content)
decoded_s, final_remaining = remove_integer(content_after_r)
print(f"Decoded r: {decoded_r}, s: {decoded_s}")
print(f"Original r: {r}, s: {s}")
print(f"Match: {decoded_r == r and decoded_s == s}")from ecdsa import SigningKey, NIST256p
from ecdsa.der import topem, unpem
# Generate key and export as DER
sk = SigningKey.generate(curve=NIST256p)
der_key = sk.to_der()
# Convert DER to PEM
pem_key = topem(der_key, "EC PRIVATE KEY")
print("PEM format:")
print(pem_key.decode())
# Convert PEM back to DER
der_from_pem = unpem(pem_key)
print(f"DER roundtrip successful: {der_key == der_from_pem}")
# Also works with public keys
vk = sk.verifying_key
der_pubkey = vk.to_der()
pem_pubkey = topem(der_pubkey, "PUBLIC KEY")
der_pubkey_roundtrip = unpem(pem_pubkey)
print(f"Public key roundtrip successful: {der_pubkey == der_pubkey_roundtrip}")from ecdsa.util import number_to_string, string_to_number, number_to_string_crop
from ecdsa import NIST256p
# Work with curve order for proper length calculation
order = NIST256p.order
test_number = 0x1234567890abcdef
# Convert number to fixed-length string
fixed_bytes = number_to_string(test_number, order)
minimal_bytes = number_to_string_crop(test_number, order)
print(f"Original number: 0x{test_number:x}")
print(f"Fixed length: {len(fixed_bytes)} bytes - {fixed_bytes.hex()}")
print(f"Minimal length: {len(minimal_bytes)} bytes - {minimal_bytes.hex()}")
# Convert back to numbers
recovered_fixed = string_to_number(fixed_bytes)
recovered_minimal = string_to_number(minimal_bytes)
print(f"Recovered from fixed: 0x{recovered_fixed:x}")
print(f"Recovered from minimal: 0x{recovered_minimal:x}")
print(f"All match: {test_number == recovered_fixed == recovered_minimal}")from ecdsa import SigningKey, SECP256k1
from ecdsa.util import sigencode_der_canonize, sigdecode_der
# Bitcoin uses canonical signatures (low-s form)
sk = SigningKey.generate(curve=SECP256k1)
message = b"Bitcoin transaction data"
# Generate canonical signature
canonical_sig = sk.sign(message, sigencode=sigencode_der_canonize)
# Decode and verify s is in low form
r, s = sigdecode_der(canonical_sig, sk.curve.order)
print(f"Signature r: {r}")
print(f"Signature s: {s}")
print(f"s is canonical (low): {s <= sk.curve.order // 2}")
# Verify signature
vk = sk.verifying_key
vk.verify(canonical_sig, message, sigdecode=sigdecode_der)
print("Canonical signature verified successfully")from ecdsa.der import (
encode_oid, remove_object, encode_octet_string, remove_octet_string,
encode_bitstring, remove_bitstring
)
# Work with Object Identifiers
secp256k1_oid = (1, 3, 132, 0, 10) # secp256k1 curve OID
encoded_oid = encode_oid(*secp256k1_oid)
decoded_oid, remaining = remove_object(encoded_oid)
print(f"Original OID: {secp256k1_oid}")
print(f"Decoded OID: {decoded_oid}")
print(f"OID match: {secp256k1_oid == decoded_oid}")
# Work with octet strings
test_data = b"Secret key material"
encoded_octets = encode_octet_string(test_data)
decoded_octets, remaining = remove_octet_string(encoded_octets)
print(f"Octet string roundtrip: {test_data == decoded_octets}")
# Work with bit strings (for public key encoding)
pubkey_data = b"\x04" + b"x" * 64 # Uncompressed public key format
encoded_bits = encode_bitstring(pubkey_data, unused=0)
decoded_bits, remaining = remove_bitstring(encoded_bits, expect_unused=0)
print(f"Bit string roundtrip: {pubkey_data == decoded_bits}")Install with Tessl CLI
npx tessl i tessl/pypi-ecdsa