Skip to content

Commit 15c3e15

Browse files
committed
feat(dnssec)!: Implement parser for DNSSEC RRSIG records (#72)
1 parent 3944181 commit 15c3e15

File tree

4 files changed

+242
-9
lines changed

4 files changed

+242
-9
lines changed

cryptoparser/common/parse.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -457,11 +457,11 @@ def parse_time_delta(self, name):
457457
class ParserBinary(ParserBase):
458458
byte_order = attr.ib(default=ByteOrder.NETWORK, validator=attr.validators.in_(ByteOrder))
459459

460-
def parse_timestamp(self, name, milliseconds=False):
461-
value, parsed_length = self._parse_numeric_array(name, 1, 8, int)
460+
def parse_timestamp(self, name, milliseconds=False, item_size=8):
461+
value, parsed_length = self._parse_numeric_array(name, 1, item_size, int)
462462

463463
self._parsed_length += parsed_length
464-
if value[0] == 0xffffffffffffffff:
464+
if value[0] == (2 ** (8 * item_size) - 1):
465465
self._parsed_values[name] = None
466466
else:
467467
value = value[0]
@@ -798,7 +798,7 @@ def compose_time_delta(self, value):
798798
class ComposerBinary(ComposerBase):
799799
byte_order = attr.ib(default=ByteOrder.NETWORK, validator=attr.validators.in_(ByteOrder))
800800

801-
def compose_timestamp(self, value, milliseconds=False):
801+
def compose_timestamp(self, value, milliseconds=False, item_size=8):
802802
if value is None:
803803
timestamp = 0xffffffffffffffff
804804
else:
@@ -808,7 +808,7 @@ def compose_timestamp(self, value, milliseconds=False):
808808
timestamp *= 1000
809809
timestamp += value.microsecond // 1000
810810

811-
return self._compose_numeric_array([timestamp, ], 8)
811+
return self._compose_numeric_array([timestamp, ], item_size)
812812

813813
def _compose_numeric_array(self, values, item_size):
814814
composed_bytes = bytearray()

cryptoparser/dnsrec/record.py

Lines changed: 119 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import abc
44
import collections
5+
import datetime
56
import enum
67

78
import attr
@@ -17,9 +18,9 @@
1718
PublicKeyParamsRsa,
1819
)
1920

20-
from cryptodatahub.dnssec.algorithm import DnsSecAlgorithm, DnsSecDigestType
21+
from cryptodatahub.dnssec.algorithm import DnsRrType, DnsSecAlgorithm, DnsSecDigestType
2122

22-
from cryptoparser.common.base import OneByteEnumParsable, Serializable
23+
from cryptoparser.common.base import OneByteEnumParsable, Serializable, TwoByteEnumParsable
2324
from cryptoparser.common.exception import NotEnoughData
2425
from cryptoparser.common.parse import ByteOrder, ComposerBinary, ParsableBase, ParserBinary
2526

@@ -303,3 +304,119 @@ def compose(self):
303304
composer.compose_raw(self.digest)
304305

305306
return composer.composed_bytes
307+
308+
309+
class DnsRrTypeFactory(TwoByteEnumParsable):
310+
@classmethod
311+
def get_enum_class(cls):
312+
return DnsRrType
313+
314+
@abc.abstractmethod
315+
def compose(self):
316+
raise NotImplementedError()
317+
318+
319+
@attr.s
320+
class DnsNameUncompressed(ParsableBase, Serializable):
321+
labels = attr.ib(
322+
validator=attr.validators.deep_iterable(member_validator=attr.validators.instance_of(six.string_types))
323+
)
324+
325+
def __str__(self):
326+
return six.u('.').join(self.labels)
327+
328+
def _as_markdown(self, level):
329+
return self._markdown_result(str(self), level)
330+
331+
@classmethod
332+
def convert(cls, value):
333+
if isinstance(value, cls):
334+
return value
335+
if isinstance(value, six.string_types):
336+
if not value:
337+
return cls([])
338+
339+
return cls(value.split('.'))
340+
341+
raise InvalidValue(value, cls, 'labels')
342+
343+
@classmethod
344+
def _parse(cls, parsable):
345+
parser = ParserBinary(parsable)
346+
347+
labels = []
348+
while True:
349+
parser.parse_string('label', 1, encoding='idna')
350+
label = parser['label']
351+
352+
if not label:
353+
break
354+
355+
labels.append(label)
356+
357+
return cls(labels), parser.parsed_length
358+
359+
def compose(self):
360+
composer = ComposerBinary()
361+
362+
for label in self.labels:
363+
composer.compose_string(label, 'idna', 1)
364+
365+
composer.compose_numeric(0, 1)
366+
367+
return composer.composed_bytes
368+
369+
370+
@attr.s
371+
class DnsRecordRrsig(ParsableBase): # pylint: disable=too-many-instance-attributes
372+
HEADER_SIZE = 24
373+
374+
type_covered = attr.ib(validator=attr.validators.instance_of(DnsRrType))
375+
algorithm = attr.ib(validator=attr.validators.instance_of(DnsSecAlgorithm))
376+
labels = attr.ib(validator=attr.validators.instance_of(six.integer_types))
377+
original_ttl = attr.ib(
378+
validator=attr.validators.instance_of(six.integer_types),
379+
metadata={'human_readable_name': 'Original TTL'}
380+
)
381+
signature_expiration = attr.ib(validator=attr.validators.instance_of(datetime.datetime))
382+
signature_inception = attr.ib(validator=attr.validators.instance_of(datetime.datetime))
383+
key_tag = attr.ib(validator=attr.validators.instance_of(six.integer_types))
384+
signers_name = attr.ib(
385+
converter=DnsNameUncompressed.convert,
386+
validator=attr.validators.instance_of(DnsNameUncompressed)
387+
)
388+
signature = attr.ib(validator=attr.validators.instance_of((bytes, bytearray)))
389+
390+
@classmethod
391+
def _parse(cls, parsable):
392+
if len(parsable) < cls.HEADER_SIZE:
393+
raise NotEnoughData(cls.HEADER_SIZE - len(parsable))
394+
395+
parser = ParserBinary(parsable)
396+
397+
parser.parse_parsable('type_covered', DnsRrTypeFactory)
398+
parser.parse_parsable('algorithm', DnsSecAlgorithmFactory)
399+
parser.parse_numeric('labels', 1)
400+
parser.parse_numeric('original_ttl', 4)
401+
parser.parse_timestamp('signature_expiration', item_size=4)
402+
parser.parse_timestamp('signature_inception', item_size=4)
403+
parser.parse_numeric('key_tag', 2)
404+
parser.parse_parsable('signers_name', DnsNameUncompressed)
405+
parser.parse_raw('signature', parser.unparsed_length)
406+
407+
return cls(**parser), parser.parsed_length
408+
409+
def compose(self):
410+
composer = ComposerBinary()
411+
412+
composer.compose_numeric_enum_coded(self.type_covered)
413+
composer.compose_numeric_enum_coded(self.algorithm)
414+
composer.compose_numeric(self.labels, 1)
415+
composer.compose_numeric(self.original_ttl, 4)
416+
composer.compose_timestamp(self.signature_expiration, item_size=4)
417+
composer.compose_timestamp(self.signature_inception, item_size=4)
418+
composer.compose_numeric(self.key_tag, 2)
419+
composer.compose_parsable(self.signers_name)
420+
composer.compose_raw(self.signature)
421+
422+
return composer.composed_bytes

test/common/test_parse.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,21 +400,40 @@ def test_parse_timestamp(self):
400400
parser.parse_timestamp('timestamp')
401401
self.assertEqual(parser['timestamp'], datetime.datetime.fromtimestamp(0, dateutil.tz.UTC))
402402

403+
parser = ParserBinary(b'\x00\x00\x00\x00')
404+
parser.parse_timestamp('timestamp', item_size=4)
405+
self.assertEqual(parser['timestamp'], datetime.datetime.fromtimestamp(0, dateutil.tz.UTC))
406+
403407
parser = ParserBinary(b'\x00\x00\x00\x00\x00\x00\x00\xff')
404408
parser.parse_timestamp('timestamp', milliseconds=True)
405409
self.assertEqual(
406410
parser['timestamp'],
407411
datetime.datetime.fromtimestamp(0, dateutil.tz.UTC) + datetime.timedelta(microseconds=255000)
408412
)
409413

414+
parser = ParserBinary(b'\x00\x00\x00\xff')
415+
parser.parse_timestamp('timestamp', milliseconds=True, item_size=4)
416+
self.assertEqual(
417+
parser['timestamp'],
418+
datetime.datetime.fromtimestamp(0, dateutil.tz.UTC) + datetime.timedelta(microseconds=255000)
419+
)
420+
410421
parser = ParserBinary(b'\xff\xff\xff\xff\xff\xff\xff\xff')
411422
parser.parse_timestamp('timestamp')
412423
self.assertEqual(parser['timestamp'], None)
413424

425+
parser = ParserBinary(b'\xff\xff\xff\xff')
426+
parser.parse_timestamp('timestamp', item_size=4)
427+
self.assertEqual(parser['timestamp'], None)
428+
414429
parser = ParserBinary(b'\x00\x00\x00\x00\xff\xff\xff\xff')
415430
parser.parse_timestamp('timestamp')
416431
self.assertEqual(parser['timestamp'], datetime.datetime.fromtimestamp(0xffffffff, dateutil.tz.UTC))
417432

433+
parser = ParserBinary(b'\x00\x00\xff\xff')
434+
parser.parse_timestamp('timestamp', item_size=4)
435+
self.assertEqual(parser['timestamp'], datetime.datetime.fromtimestamp(0x0000ffff, dateutil.tz.UTC))
436+
418437

419438
class TestParserText(TestParsableBase):
420439
def test_error(self):

test/dnsrec/test_record.py

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33

44
import base64
55
import collections
6+
import datetime
67
import unittest
78

9+
import dateutil
10+
811
from cryptodatahub.common.algorithm import Authentication, KeyExchange, NamedGroup
912
from cryptodatahub.common.exception import InvalidValue
1013
from cryptodatahub.common.key import (
@@ -15,10 +18,17 @@
1518
PublicKeyParamsRsa,
1619
)
1720

18-
from cryptodatahub.dnssec.algorithm import DnsSecAlgorithm, DnsSecDigestType
21+
from cryptodatahub.dnssec.algorithm import DnsSecAlgorithm, DnsSecDigestType, DnsRrType
1922

2023
from cryptoparser.common.exception import NotEnoughData
21-
from cryptoparser.dnsrec.record import DnsRecordDnskey, DnsRecordDs, DnsSecFlag, DnsSecProtocol
24+
from cryptoparser.dnsrec.record import (
25+
DnsNameUncompressed,
26+
DnsRecordDnskey,
27+
DnsRecordDs,
28+
DnsRecordRrsig,
29+
DnsSecFlag,
30+
DnsSecProtocol,
31+
)
2232

2333

2434
class TestDnsRecordDnskey(unittest.TestCase):
@@ -362,3 +372,90 @@ def test_parse(self):
362372

363373
def test_compose(self):
364374
self.assertEqual(self.record.compose(), self.record_bytes)
375+
376+
377+
class TestDnsNameUncompressed(unittest.TestCase):
378+
def setUp(self):
379+
self.label_empty_bytes = b'\x00'
380+
self.label_empty_name = DnsNameUncompressed([])
381+
self.label_single_bytes = b'\x01a\x00'
382+
self.label_single_name = DnsNameUncompressed(['a'])
383+
self.label_multiple_bytes = b'\x01a\x02bb\x03ccc\x00'
384+
self.label_multiple_name = DnsNameUncompressed(['a', 'bb', 'ccc'])
385+
386+
def test_error_convert_invalid_value(self):
387+
with self.assertRaises(InvalidValue) as context_manager:
388+
DnsNameUncompressed.convert(None)
389+
390+
self.assertEqual(context_manager.exception.value, None)
391+
392+
def test_parse(self):
393+
self.assertEqual(DnsNameUncompressed.parse_exact_size(self.label_empty_bytes), self.label_empty_name)
394+
self.assertEqual(DnsNameUncompressed.parse_exact_size(self.label_single_bytes), self.label_single_name)
395+
self.assertEqual(DnsNameUncompressed.parse_exact_size(self.label_multiple_bytes), self.label_multiple_name)
396+
397+
def test_compose(self):
398+
self.assertEqual(self.label_empty_name.compose(), self.label_empty_bytes)
399+
self.assertEqual(self.label_single_name.compose(), self.label_single_bytes)
400+
self.assertEqual(self.label_multiple_name.compose(), self.label_multiple_bytes)
401+
402+
def test_convert(self):
403+
self.assertEqual(DnsNameUncompressed.convert(self.label_empty_name), self.label_empty_name)
404+
self.assertEqual(DnsNameUncompressed.convert(self.label_single_name), self.label_single_name)
405+
self.assertEqual(DnsNameUncompressed.convert(self.label_multiple_name), self.label_multiple_name)
406+
407+
self.assertEqual(DnsNameUncompressed.convert(''), self.label_empty_name)
408+
self.assertEqual(DnsNameUncompressed.convert('a'), self.label_single_name)
409+
self.assertEqual(DnsNameUncompressed.convert('a.bb.ccc'), self.label_multiple_name)
410+
411+
def test_str(self):
412+
self.assertEqual(str(self.label_empty_name), '')
413+
self.assertEqual(str(self.label_single_name), 'a')
414+
self.assertEqual(str(self.label_multiple_name), 'a.bb.ccc')
415+
416+
def test_as_markdown(self):
417+
self.assertEqual(self.label_empty_name.as_markdown(), '')
418+
self.assertEqual(self.label_single_name.as_markdown(), 'a')
419+
self.assertEqual(self.label_multiple_name.as_markdown(), 'a.bb.ccc')
420+
421+
422+
class TestDnsRecordRrsig(unittest.TestCase):
423+
def setUp(self):
424+
self.record_bytes = bytes(
425+
b'\x00\x01' + # type_covered: A
426+
b'\x01' + # algorithm: RSAMD5
427+
b'\x03' + # labels
428+
b'\x00\x00\x0e\x10' + # original_ttl: 3600
429+
b'\x00\x00\x00\x01' + # signature_expiration
430+
b'\x00\x00\x00\x02' + # signature_inception
431+
b'\xab\xcd' + # key_tag
432+
b'\x06signer\x00' + # signers_name
433+
32 * b'\xff' + # signature
434+
b''
435+
)
436+
self.record = DnsRecordRrsig(
437+
type_covered=DnsRrType.A,
438+
algorithm=DnsSecAlgorithm.RSAMD5,
439+
labels=3,
440+
original_ttl=3600,
441+
signature_expiration=datetime.datetime(1970, 1, 1, 0, 0, 1, tzinfo=dateutil.tz.UTC),
442+
signature_inception=datetime.datetime(1970, 1, 1, 0, 0, 2, tzinfo=dateutil.tz.UTC),
443+
key_tag=0xabcd,
444+
signers_name='signer',
445+
signature=32 * b'\xff',
446+
)
447+
448+
def test_error_not_enough_data(self):
449+
with self.assertRaises(NotEnoughData) as context_manager:
450+
DnsRecordRrsig.parse_exact_size(b'\x00')
451+
452+
self.assertEqual(
453+
context_manager.exception.bytes_needed,
454+
DnsRecordRrsig.HEADER_SIZE - 1
455+
)
456+
457+
def test_parse(self):
458+
self.assertEqual(DnsRecordRrsig.parse_exact_size(self.record_bytes), self.record)
459+
460+
def test_compose(self):
461+
self.assertEqual(self.record.compose(), self.record_bytes)

0 commit comments

Comments
 (0)