Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add local asn1 encoding tools #60

Merged
merged 1 commit into from
Jun 3, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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=500)
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)