Skip to content

Commit 1bca8cf

Browse files
authored
fix: DER encoding for tag and length in TLV (#282)
1 parent f124ce5 commit 1bca8cf

File tree

2 files changed

+68
-21
lines changed

2 files changed

+68
-21
lines changed

src/erc7730/common/binary.py

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,43 +13,53 @@ def from_hex(value: str) -> bytes:
1313
return bytes.fromhex(value.removeprefix("0x"))
1414

1515

16-
def tlv(tag: int | IntEnum, *value: bytes | str | None) -> bytes:
16+
def tlv(tag: int | IntEnum, value: bytes | str | None = None) -> bytes:
1717
"""
1818
Encode a value in TLV format (Tag-Length-Value)
1919
20-
If value is not encoded, it will be encoded as ASCII.
21-
If input string is not ASCII, and UnicodeEncodeError is raised.
20+
Tag and length are DER encoded. If tag value or length exceed 255, an OverflowError is raised.
2221
23-
If encoded value is longer than 255 bytes, an OverflowError is raised.
22+
If value is not encoded, it will be encoded as ASCII.
23+
If input string is not ASCII, a UnicodeEncodeError is raised.
2424
2525
@param tag: the tag (can be an enum)
26-
@param value: the value (can be already encoded, or a string)
26+
@param value: the value (can be already encoded, a string or None)
2727
@return: encoded TLV
2828
"""
29-
values_encoded = bytearray()
30-
for v in value:
31-
if v is not None:
32-
values_encoded.extend(v.encode("ascii", errors="strict") if isinstance(v, str) else v)
33-
return (
34-
(tag.value if isinstance(tag, IntEnum) else tag).to_bytes(1, "big")
35-
+ len(values_encoded).to_bytes(1, "big")
36-
+ values_encoded
37-
)
3829

30+
return der_encode_int(tag.value if isinstance(tag, IntEnum) else tag) + length_value(value)
3931

40-
def length_value(value: bytes | str | None) -> bytes:
32+
33+
def length_value(
34+
value: bytes | str | None,
35+
) -> bytes:
4136
"""
42-
Prepend the length of the value encoded on 1 byte to the value itself.
37+
Prepend the length (DER encoded) of the value encoded to the value itself.
38+
If length exceeds 255 bytes, an OverflowError is raised.
4339
4440
If value is not encoded, it will be encoded as ASCII.
45-
If input string is not ASCII, and UnicodeEncodeError is raised.
46-
47-
If encoded value is longer than 255 bytes, an OverflowError is raised.
41+
If input string is not ASCII, a UnicodeEncodeError is raised.
4842
4943
@param value: the value (can be already encoded, or a string)
5044
@return: encoded TLV
5145
"""
5246
if value is None:
5347
return (0).to_bytes(1, "big")
54-
value_encoded = value.encode("ascii", errors="strict") if isinstance(value, str) else value
55-
return len(value_encoded).to_bytes(1, "big") + value_encoded
48+
match value:
49+
case bytes():
50+
value_encoded = value
51+
case str():
52+
value_encoded = value.encode("ascii", errors="strict")
53+
return der_encode_int(len(value_encoded)) + value_encoded
54+
55+
56+
def der_encode_int(value: int) -> bytes:
57+
"""
58+
Encode an integer in DER format.
59+
If value exceeds 255, an OverflowError is raised.
60+
61+
@param value: the integer to encode
62+
@return: DER encoded byte array
63+
"""
64+
value_bytes = value.to_bytes(1, "big") # raises OverflowError if value >= 256
65+
return (0x81).to_bytes(1, "big") + value_bytes if value >= 0x80 else value_bytes

tests/common/test_binary.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from enum import IntEnum
2+
3+
import pytest
4+
5+
from erc7730.common.binary import tlv
6+
7+
8+
class _Tag(IntEnum):
9+
FIELD = 5
10+
11+
12+
@pytest.mark.parametrize(
13+
"tag, value, expected",
14+
[
15+
(1, None, b"\x01\x00"),
16+
(1, b"\xab", b"\x01\x01\xab"),
17+
(1, "hi", b"\x01\x02hi"),
18+
(_Tag.FIELD, b"\xff", b"\x05\x01\xff"),
19+
(0x80, None, b"\x81\x80\x00"),
20+
(1, b"\x00" * 128, b"\x01\x81\x80" + b"\x00" * 128),
21+
],
22+
)
23+
def test_tlv(tag: int | IntEnum, value: bytes | str | None, expected: bytes) -> None:
24+
assert tlv(tag, value) == expected
25+
26+
27+
@pytest.mark.parametrize(
28+
"tag, value, exc",
29+
[
30+
(256, None, OverflowError),
31+
(1, b"\x00" * 256, OverflowError),
32+
(1, "là-haut", UnicodeEncodeError),
33+
],
34+
)
35+
def test_tlv_errors(tag: int, value: bytes | str | None, exc: type[Exception]) -> None:
36+
with pytest.raises(exc):
37+
tlv(tag, value)

0 commit comments

Comments
 (0)