Skip to content

Commit

Permalink
Add local asn1 encoding tools
Browse files Browse the repository at this point in the history
  • Loading branch information
carver committed Jun 3, 2019
1 parent 0047363 commit 2883b12
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,6 @@ fabric.properties

# test fixtures
fixtures/*

# editors
*.swp
118 changes: 118 additions & 0 deletions eth_keys/utils/der.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Non-recoverable signatures are encoded using a DER sequence of two integers
# We locally implement serialization and deserialization for this specific spec
# with constrained inputs.
# This is done locally to avoid importing a 3rd-party library, in this very sensitive project.
# asn1tools and pyasn1 were used as reference APIs, see how in tests/core/test_utils_asn1.py
#
# See more about DER encodings, and ASN.1 in general, here:
# http://luca.ntop.org/Teaching/Appunti/asn1.html
#
# These methods are NOT intended for external use outside of this project. They do not
# fully validate inputs and make assumptions that are not *generally* true.

from typing import (
Iterator,
Tuple,
)

from eth_utils import (
apply_to_return_value,
big_endian_to_int,
int_to_big_endian,
)


@apply_to_return_value(bytes)
def two_int_sequence_encoder(signature_r: int, signature_s: int) -> Iterator[int]:
"""
Encode two integers using DER, defined as:
::
ECDSASpec DEFINITIONS ::= BEGIN
ECDSASignature ::= SEQUENCE {
r INTEGER,
s INTEGER
}
END
Only a subset of integers are supported: positive, 32-byte ints.
See: https://docs.microsoft.com/en-us/windows/desktop/seccertenroll/about-sequence
"""
# Sequence tag
yield 0x30

encoded1 = _encode_int(signature_r)
encoded2 = _encode_int(signature_s)

# Sequence length
yield len(encoded1) + len(encoded2)

yield from encoded1
yield from encoded2


def two_int_sequence_decoder(encoded: bytes) -> Tuple[int, int]:
"""
Decode bytes to two integers using DER, defined as:
::
ECDSASpec DEFINITIONS ::= BEGIN
ECDSASignature ::= SEQUENCE {
r INTEGER,
s INTEGER
}
END
Only a subset of integers are supported: positive, 32-byte ints.
r is returned first, and s is returned second
See: https://docs.microsoft.com/en-us/windows/desktop/seccertenroll/about-sequence
"""
if encoded[0] != 0x30:
raise ValueError("Encoded sequence must start with 0x30 byte, but got %s" % encoded[0])

# skip sequence length
int1, rest = _decode_int(encoded[2:])
int2, empty = _decode_int(rest)

if len(empty) != 0:
raise ValueError("Encoded sequence must not contain any trailing data, but had %r" % empty)

return int1, int2


@apply_to_return_value(bytes)
def _encode_int(primitive: int) -> Iterator[int]:
# See: https://docs.microsoft.com/en-us/windows/desktop/seccertenroll/about-integer

# Integer tag
yield 0x02

encoded = int_to_big_endian(primitive)
if encoded[0] >= 128:
# Indicate that integer is positive (it always is, but doesn't always need the flag)
yield len(encoded) + 1
yield 0x00
else:
yield len(encoded)

yield from encoded


def _decode_int(encoded: bytes) -> Tuple[int, bytes]:
# See: https://docs.microsoft.com/en-us/windows/desktop/seccertenroll/about-integer

if encoded[0] != 0x02:
raise ValueError(
"Encoded value must be an integer, starting with on 0x02 byte, but got %s" % encoded[0]
)

length = encoded[1]
# to_int can handle leading zeros
decoded_int = big_endian_to_int(encoded[2:2 + length])

return decoded_int, encoded[2 + length:]
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
"eth-utils>=1.3.0,<2.0.0",
],
'test': [
"asn1tools>=0.146.2,<0.147",
"pyasn1>=0.4.5,<0.5",
'pytest==3.2.2',
'hypothesis==3.30.0',
"eth-hash[pysha3];implementation_name=='cpython'",
Expand Down
96 changes: 96 additions & 0 deletions tests/core/test_utils_der.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import itertools
import pytest

import asn1tools
from hypothesis import (
example,
settings,
strategies as st,
given,
)
from pyasn1.codec.der import (
encoder as pyasn1_encoder,
decoder as pyasn1_decoder,
)
from pyasn1.type import (
univ,
namedtype,
)

from eth_keys.utils.der import (
two_int_sequence_decoder,
two_int_sequence_encoder,
)

ASN1_ECDSA_SPEC_STRING = """\
ECDSASpec DEFINITIONS ::= BEGIN
ECDSASignature ::= SEQUENCE {
r INTEGER,
s INTEGER
}
END
"""
ASN1_SPEC = asn1tools.compile_string(ASN1_ECDSA_SPEC_STRING, "der")


def asn1tools_encode(r, s):
return ASN1_SPEC.encode("ECDSASignature", {"r": r, "s": s})


def asn1tools_decode(encoded):
decoded = ASN1_SPEC.decode("ECDSASignature", encoded)
return decoded["r"], decoded["s"]


class TwoInts(univ.Sequence):
componentType = namedtype.NamedTypes(
namedtype.NamedType('r', univ.Integer()),
namedtype.NamedType('s', univ.Integer()),
)


def pyasn1_encode(r, s):
structured = TwoInts()
structured["r"] = r
structured["s"] = s
return pyasn1_encoder.encode(structured)


def pyasn1_decode(encoded):
decoded = pyasn1_decoder.decode(encoded, asn1Spec=TwoInts())
return decoded[0]["r"], decoded[0]["s"]


MAX_32_BYTE_INT = 256 ** 32 - 1
uint32strategy = st.integers(min_value=0, max_value=MAX_32_BYTE_INT)

@pytest.mark.parametrize(
'encoder, decoder',
(
(two_int_sequence_encoder, asn1tools_decode),
(two_int_sequence_encoder, pyasn1_decode),
(two_int_sequence_encoder, two_int_sequence_decoder),
(asn1tools_encode, two_int_sequence_decoder),
(pyasn1_encode, two_int_sequence_decoder),
),
ids=(
'local_encode=>asn1tools_decode',
'local_encode=>pyasn1_decode',
'local_encode=>local_decode',
'asn1tools_encode=>local_decode',
'pyasn1_encode=>local_decode',
),
)
@given(
uint32strategy,
uint32strategy,
)
@example(0, 0)
@example(MAX_32_BYTE_INT, MAX_32_BYTE_INT)
@example(MAX_32_BYTE_INT // 2, MAX_32_BYTE_INT // 2)
@example(MAX_32_BYTE_INT // 2 + 1, MAX_32_BYTE_INT // 2 + 1)
@settings(max_examples=2000)
def test_encode_decode_pairings(encoder, decoder, r, s):
encoded = encoder(r, s)
end_r, end_s = decoder(encoded)
assert (end_r, end_s) == (r, s)

0 comments on commit 2883b12

Please sign in to comment.