Skip to content
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
15 changes: 13 additions & 2 deletions dns/dnssec.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import contextlib
import functools
import hashlib
import math
import struct
import time
from collections.abc import Callable
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
56 changes: 56 additions & 0 deletions tests/test_dnssec.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down