diff --git a/dns/dnssec.py b/dns/dnssec.py index 7304ccc9..e989c502 100644 --- a/dns/dnssec.py +++ b/dns/dnssec.py @@ -23,6 +23,7 @@ import contextlib import functools import hashlib +import math import struct import time from collections.abc import Callable @@ -37,6 +38,7 @@ import dns.rdataset import dns.rdatatype import dns.rrset +import dns.serial import dns.transaction import dns.zone from dns.dnssectypes import Algorithm, DSDigest, NSEC3Hash @@ -369,9 +371,18 @@ def _validate_rrsig( if now is None: now = time.time() - if rrsig.expiration < now: + try: + now_seconds = math.floor(now) + except (OverflowError, ValueError) as exc: + raise ValidationFailure("invalid validation time") from exc + + # RFC 4034 section 3.1.5: compare RRSIG times as 32-bit serial numbers. + current = dns.serial.Serial(now_seconds) + expiration = dns.serial.Serial(rrsig.expiration) + # The exact expiration instant is inclusive, even when now is fractional. + if not (current <= expiration) or (current == expiration and now > now_seconds): raise ValidationFailure("expired") - if rrsig.inception > now: + if not (dns.serial.Serial(rrsig.inception) <= current): raise ValidationFailure("not yet valid") data = _make_rrsig_signature_data(rrset, rrsig, origin) diff --git a/tests/test_dnssec.py b/tests/test_dnssec.py index e69bebcd..e744d093 100644 --- a/tests/test_dnssec.py +++ b/tests/test_dnssec.py @@ -1089,6 +1089,62 @@ def testTimeBounds(self): abs_ed448_mx, abs_ed448_mx_rrsig_1, abs_ed448_keys_1, None ) + def testTimeBoundsAcrossRollover(self) -> None: + key = ed25519.Ed25519PrivateKey.generate() + signer = abs_soa.name + dnskey = dns.dnssec.make_dnskey(key.public_key(), dns.dnssec.Algorithm.ED25519) + keys = {signer: dns.rrset.from_rdata(signer, 300, dnskey)} + rollover = 2**32 + rrsig = dns.dnssec.sign( + abs_soa, key, signer, dnskey, inception=rollover - 16, expiration=32 + ) + + for now in (rollover - 16, rollover - 1, rollover + 1, rollover + 32): + with self.subTest(now=now): + dns.dnssec.validate_rrsig(abs_soa, rrsig, keys, now=now) + + for now, message in ( + (rollover - 16.5, "not yet valid"), + (rollover + 32.5, "expired"), + (float("nan"), "invalid validation time"), + (float("inf"), "invalid validation time"), + ): + with self.subTest(now=now): + with self.assertRaisesRegex(dns.dnssec.ValidationFailure, message): + dns.dnssec.validate_rrsig(abs_soa, rrsig, keys, now=now) + + def testTimeBoundsAtSerialHalfRange(self) -> None: + key = ed25519.Ed25519PrivateKey.generate() + signer = abs_soa.name + dnskey = dns.dnssec.make_dnskey(key.public_key(), dns.dnssec.Algorithm.ED25519) + keys = {signer: dns.rrset.from_rdata(signer, 300, dnskey)} + half_range = 2**31 + serial_space = 2**32 + now = half_range + 100 + + # RFC 1982 leaves ordering undefined at exactly half the serial space. + # Keep one bound equal to now while testing the other bound. + for inception, expiration, message in ( + (now - half_range + 1, now, None), + (now - half_range, now, "not yet valid"), + (now, now + half_range - 1, None), + (now, now + half_range, "expired"), + ): + with self.subTest(inception=inception, expiration=expiration): + rrsig = dns.dnssec.sign( + abs_soa, + key, + signer, + dnskey, + inception=inception % serial_space, + expiration=expiration % serial_space, + ) + if message is None: + dns.dnssec.validate_rrsig(abs_soa, rrsig, keys, now=now) + else: + with self.assertRaisesRegex(dns.dnssec.ValidationFailure, message): + dns.dnssec.validate_rrsig(abs_soa, rrsig, keys, now=now) + def testOwnerNameMismatch(self): bogus = dns.name.from_text("example.bogus") with self.assertRaises(dns.dnssec.ValidationFailure):