Skip to content

Commit 84b7bc7

Browse files
authored
Remove asn1crypto dependency (#181)
1 parent 6bf3721 commit 84b7bc7

File tree

4 files changed

+275
-24
lines changed

4 files changed

+275
-24
lines changed

docs/history.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Important changes are emphasized.
88

99
- **Breaking:** Drop support for Python 3.8
1010
- Add support for Python 3.13
11-
- Remove `cffi` as a runtime dependency
11+
- Remove all runtime dependencies (`cffi` & `asn1crypto`)
1212
- Add `COINCURVE_VENDOR_CFFI` environment variable to control vendoring of the `_cffi_backend` module
1313
- Minor performance improvement by removing use of formatted string constants
1414
- Upgrade [libsecp256k1][] to version 0.6.0

pyproject.toml

-3
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,6 @@ keywords = [
2727
readme = "README.md"
2828
license = "MIT OR Apache-2.0"
2929
requires-python = ">=3.9"
30-
dependencies = [
31-
"asn1crypto",
32-
]
3330
classifiers = [
3431
"Development Status :: 5 - Production/Stable",
3532
"Intended Audience :: Developers",

src/coincurve/der.py

+270
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
"""
2+
Minimal, dependency-free ASN.1/DER encoder & decoder for secp256k1 EC private keys.
3+
4+
This module implements just enough DER encoding/decoding to support:
5+
6+
1. Outputting a DER-encoded PKCS#8 EC private key (with an embedded ECPrivateKey per RFC 5915)
7+
2. Reading such a DER-encoded EC private key
8+
9+
Only the following ASN.1 types are supported:
10+
11+
- INTEGER
12+
- BIT STRING
13+
- OCTET STRING
14+
- OBJECT IDENTIFIER
15+
- SEQUENCE
16+
- Context-specific EXPLICIT tags (for the optional public key)
17+
18+
The expected DER structure is as follows:
19+
20+
PrivateKeyInfo ::= SEQUENCE {
21+
version INTEGER, -- must be 0
22+
privateKeyAlgorithm SEQUENCE {
23+
algorithm OBJECT IDENTIFIER, -- id-ecPublicKey (1.2.840.10045.2.1)
24+
parameters OBJECT IDENTIFIER -- secp256k1 (1.3.132.0.10)
25+
},
26+
privateKey OCTET STRING -- DER encoding of ECPrivateKey
27+
}
28+
29+
ECPrivateKey ::= SEQUENCE {
30+
version INTEGER, -- must be 1
31+
privateKey OCTET STRING, -- the secret bytes
32+
publicKey [1] EXPLICIT BIT STRING OPTIONAL -- uncompressed public key
33+
}
34+
"""
35+
36+
from __future__ import annotations
37+
38+
from coincurve.utils import int_to_bytes
39+
40+
# ASN.1 DER tag bytes
41+
INTEGER_TAG = 0x02
42+
BIT_STRING_TAG = 0x03
43+
OCTET_STRING_TAG = 0x04
44+
OBJECT_IDENTIFIER_TAG = 0x06
45+
SEQUENCE_TAG = 0x30
46+
47+
# OIDs
48+
EC_PUBKEY_OID = bytes([0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01]) # 1.2.840.10045.2.1 (ecPublicKey)
49+
SECP256K1_OID = bytes([0x2B, 0x81, 0x04, 0x00, 0x0A]) # 1.3.132.0.10 (secp256k1)
50+
51+
# Pre-computed structures
52+
VERSION_INTEGER_ZERO = bytes([INTEGER_TAG, 0x01, 0x00]) # INTEGER 0
53+
VERSION_INTEGER_ONE = bytes([INTEGER_TAG, 0x01, 0x01]) # INTEGER 1
54+
EC_ALGORITHM_IDENTIFIER = bytes([
55+
SEQUENCE_TAG,
56+
16,
57+
OBJECT_IDENTIFIER_TAG,
58+
len(EC_PUBKEY_OID),
59+
*EC_PUBKEY_OID,
60+
OBJECT_IDENTIFIER_TAG,
61+
len(SECP256K1_OID),
62+
*SECP256K1_OID,
63+
])
64+
65+
66+
def encode_length(length: int) -> bytes:
67+
"""Encode a length in DER format."""
68+
# Short form
69+
if length < 128: # noqa: PLR2004
70+
return bytes([length])
71+
72+
# Long form
73+
length_bytes = int_to_bytes(length)
74+
return bytes([0x80 | len(length_bytes)]) + length_bytes
75+
76+
77+
def encode_octet_string(value: bytes) -> bytes:
78+
"""Encode an OCTET STRING in DER format."""
79+
length_bytes = encode_length(len(value))
80+
length_bytes_len = len(length_bytes)
81+
result = bytearray(1 + length_bytes_len + len(value))
82+
result[0] = OCTET_STRING_TAG
83+
result[1 : 1 + length_bytes_len] = length_bytes
84+
result[1 + length_bytes_len :] = value
85+
return bytes(result)
86+
87+
88+
def encode_bit_string(value: bytes, unused_bits: int = 0) -> bytes:
89+
"""Encode a BIT STRING in DER format."""
90+
length_bytes = encode_length(len(value) + 1)
91+
length_bytes_len = len(length_bytes)
92+
result = bytearray(1 + length_bytes_len + 1 + len(value))
93+
result[0] = BIT_STRING_TAG
94+
result[1 : 1 + length_bytes_len] = length_bytes
95+
result[1 + length_bytes_len] = unused_bits
96+
result[1 + length_bytes_len + 1 :] = value
97+
return bytes(result)
98+
99+
100+
def encode_der(private_key: bytes, public_key: bytes | None = None) -> bytes:
101+
"""
102+
Encode an EC private key in DER format (PKCS#8/RFC 5208).
103+
Optimized for secp256k1 keys.
104+
105+
Parameters:
106+
private_key: The private key as bytes (32 bytes for secp256k1)
107+
public_key: The public key as bytes (65 bytes uncompressed for secp256k1, starting with 0x04)
108+
109+
Returns:
110+
The DER-encoded private key
111+
"""
112+
# EC private key contains version(1) + octet string + optional pubkey
113+
ec_key_buffer = bytearray(VERSION_INTEGER_ONE)
114+
115+
# Add private key as octet string
116+
private_key_os = encode_octet_string(private_key)
117+
ec_key_buffer.extend(private_key_os)
118+
119+
# Add public key if provided (optional)
120+
if public_key is not None:
121+
public_key_bs = encode_bit_string(public_key)
122+
pubkey_len = len(public_key_bs)
123+
ec_key_buffer.append(0xA1) # context-specific [1] constructed
124+
ec_key_buffer.extend(encode_length(pubkey_len))
125+
ec_key_buffer.extend(public_key_bs)
126+
127+
# Wrap EC private key in sequence
128+
ec_key_seq = bytearray([SEQUENCE_TAG])
129+
ec_key_seq.extend(encode_length(len(ec_key_buffer)))
130+
ec_key_seq.extend(ec_key_buffer)
131+
132+
# Wrap in octet string for outer structure
133+
ec_key_os = encode_octet_string(ec_key_seq)
134+
135+
# Build the outer PKCS#8 structure
136+
result = bytearray([SEQUENCE_TAG])
137+
138+
# Calculate total length: version(3) + alg_id(18) + octet_string(len)
139+
outer_len = 3 + len(EC_ALGORITHM_IDENTIFIER) + len(ec_key_os)
140+
result.extend(encode_length(outer_len))
141+
142+
# Version 0
143+
result.extend(VERSION_INTEGER_ZERO)
144+
145+
# Algorithm identifier (pre-computed)
146+
result.extend(EC_ALGORITHM_IDENTIFIER)
147+
148+
# EC key wrapped in octet string
149+
result.extend(ec_key_os)
150+
151+
return bytes(result)
152+
153+
154+
def decode_length(data: bytes, offset: int) -> tuple[int, int]:
155+
"""
156+
Decode a DER length field.
157+
158+
Parameters:
159+
data: The DER-encoded data
160+
offset: The current offset in the data
161+
162+
Returns:
163+
Tuple of (length, new_offset)
164+
"""
165+
length_byte = data[offset]
166+
offset += 1
167+
168+
# Short form
169+
if length_byte < 128: # noqa: PLR2004
170+
return length_byte, offset
171+
172+
# Long form
173+
num_length_bytes = length_byte & 0x7F
174+
length = 0
175+
for _ in range(num_length_bytes):
176+
length = (length << 8) | data[offset]
177+
offset += 1
178+
return length, offset
179+
180+
181+
def decode_der(der_data: bytes) -> bytes:
182+
"""
183+
Decode a DER-encoded EC private key to extract the private key secret.
184+
Optimized for secp256k1 keys.
185+
186+
Parameters:
187+
der_data: The DER-encoded private key in PKCS#8 format
188+
189+
Returns:
190+
The private key secret as bytes
191+
"""
192+
# Quick validation for performance
193+
if len(der_data) < 34 or der_data[0] != SEQUENCE_TAG: # noqa: PLR2004
194+
msg = "Invalid DER: not a valid PKCS#8 structure"
195+
raise ValueError(msg)
196+
197+
# Skip outer sequence tag and length
198+
offset = 1
199+
_, offset = decode_length(der_data, offset)
200+
201+
# Skip version INTEGER (should be 0)
202+
if der_data[offset] != INTEGER_TAG:
203+
msg = "Invalid DER: expected INTEGER tag for version"
204+
raise ValueError(msg)
205+
offset += 1
206+
version_len, offset = decode_length(der_data, offset)
207+
offset += version_len # Skip version value
208+
209+
# Validate algorithm identifier is for EC
210+
if der_data[offset] != SEQUENCE_TAG:
211+
msg = "Invalid DER: expected SEQUENCE tag for algorithm"
212+
raise ValueError(msg)
213+
offset += 1
214+
215+
alg_len, offset = decode_length(der_data, offset)
216+
alg_end = offset + alg_len # Store the end position of algorithm identifier
217+
218+
# Check if first OID is EC
219+
if der_data[offset] != OBJECT_IDENTIFIER_TAG:
220+
msg = "Invalid DER: expected OBJECT IDENTIFIER tag"
221+
raise ValueError(msg)
222+
offset += 1
223+
oid_len, offset = decode_length(der_data, offset)
224+
algorithm_oid = der_data[offset : offset + oid_len]
225+
226+
# Check if it's an EC key
227+
if oid_len != len(EC_PUBKEY_OID) or algorithm_oid != EC_PUBKEY_OID:
228+
msg = "Not an EC private key"
229+
raise ValueError(msg)
230+
231+
# Skip to the end of algorithm identifier section
232+
offset = alg_end
233+
234+
# Extract private key octet string
235+
if der_data[offset] != OCTET_STRING_TAG:
236+
msg = "Invalid DER: expected OCTET STRING for private key"
237+
raise ValueError(msg)
238+
offset += 1
239+
priv_len, offset = decode_length(der_data, offset)
240+
241+
# Parse EC private key structure
242+
ec_data = der_data[offset : offset + priv_len]
243+
244+
# Verify EC structure starts with sequence
245+
if len(ec_data) < 2 or ec_data[0] != SEQUENCE_TAG: # noqa: PLR2004
246+
msg = "Invalid EC key format: missing sequence"
247+
raise ValueError(msg)
248+
249+
# Skip sequence tag and length
250+
ec_offset = 1
251+
_, ec_offset = decode_length(ec_data, ec_offset)
252+
253+
# Skip version INTEGER (should be 1)
254+
if ec_data[ec_offset] != INTEGER_TAG:
255+
msg = "Invalid EC key format: missing version"
256+
raise ValueError(msg)
257+
ec_offset += 1
258+
ec_ver_len, ec_offset = decode_length(ec_data, ec_offset)
259+
ec_offset += ec_ver_len # Skip version value
260+
261+
# Get private key octet string
262+
if ec_data[ec_offset] != OCTET_STRING_TAG:
263+
msg = "Invalid DER: expected OCTET STRING for EC private key"
264+
raise ValueError(msg)
265+
ec_offset += 1
266+
267+
key_len, ec_offset = decode_length(ec_data, ec_offset)
268+
269+
# Extract private key
270+
return ec_data[ec_offset : ec_offset + key_len]

src/coincurve/keys.py

+4-20
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@
33
import os
44
from typing import TYPE_CHECKING
55

6-
from asn1crypto.keys import ECDomainParameters, ECPointBitString, ECPrivateKey, PrivateKeyAlgorithm, PrivateKeyInfo
7-
86
from coincurve._libsecp256k1 import ffi, lib
97
from coincurve.context import GLOBAL_CONTEXT, Context
8+
from coincurve.der import decode_der, encode_der
109
from coincurve.ecdsa import cdata_to_der, der_to_cdata, deserialize_recoverable, recover, serialize_recoverable
1110
from coincurve.flags import EC_COMPRESSED, EC_UNCOMPRESSED
1211
from coincurve.utils import (
@@ -265,20 +264,7 @@ def to_der(self) -> bytes:
265264
"""
266265
Returns the private key encoded in DER format.
267266
"""
268-
pk = ECPrivateKey({
269-
"version": "ecPrivkeyVer1",
270-
"private_key": self.to_int(),
271-
"public_key": ECPointBitString(self.public_key.format(compressed=False)),
272-
})
273-
274-
return PrivateKeyInfo({
275-
"version": 0,
276-
"private_key_algorithm": PrivateKeyAlgorithm({
277-
"algorithm": "ec",
278-
"parameters": ECDomainParameters(name="named", value="1.3.132.0.10"),
279-
}),
280-
"private_key": pk,
281-
}).dump()
267+
return encode_der(self.secret, self.public_key.format(compressed=False))
282268

283269
@classmethod
284270
def from_hex(cls, hexed: str, context: Context = GLOBAL_CONTEXT) -> PrivateKey:
@@ -320,9 +306,7 @@ def from_pem(cls, pem: bytes, context: Context = GLOBAL_CONTEXT) -> PrivateKey:
320306
Returns:
321307
The private key.
322308
"""
323-
return PrivateKey(
324-
int_to_bytes_padded(PrivateKeyInfo.load(pem_to_der(pem)).native["private_key"]["private_key"]), context
325-
)
309+
return PrivateKey(decode_der(pem_to_der(pem)), context)
326310

327311
@classmethod
328312
def from_der(cls, der: bytes, context: Context = GLOBAL_CONTEXT) -> PrivateKey:
@@ -336,7 +320,7 @@ def from_der(cls, der: bytes, context: Context = GLOBAL_CONTEXT) -> PrivateKey:
336320
Returns:
337321
The private key.
338322
"""
339-
return PrivateKey(int_to_bytes_padded(PrivateKeyInfo.load(der).native["private_key"]["private_key"]), context)
323+
return PrivateKey(decode_der(der), context)
340324

341325
def _update_public_key(self):
342326
created = lib.secp256k1_ec_pubkey_create(self.context.ctx, self.public_key.public_key, self.secret)

0 commit comments

Comments
 (0)