TSIG RFC 8945 section 5.2.3 (Time Check and Error Handling) mentions this: > The server SHOULD also cache the most recent Time Signed value in a > message generated by a key and SHOULD return BADTIME if a message > received later has an earlier Time Signed value.
If a TSIG implementation does not implement this clause, there is the
possibilty of transaction replay by an on-path party, which is why this
clause exists. Such a replay is usually considered a minor concern, but
depending on the circumstances, it could be problematic. This issue has
been communicated directly or indirectly to the popular open source DNS
implementations that don't implement this requirement, and 3 months to a
few years have passed. Some have decided not to implement it; I received
no response from one implementation. I think Knot implements this
clause, and so does Loop.
Some operators use shared TSIG keys where multiple pairs of peers use
the same shared keys. Implementing this clause can cause problems with
such use-cases. Please implement the clause if you are not leaving it
out on purpose.
From our system tests, a sample Python program is attached that would
test for the behavior against a suitably configured nameserver.
Mukund
#
# Copyright (C) 2025 Banu Systems Private Limited. All rights reserved.
#
import unittest
import socket
import time
import dns.update
import dns.query
import dns.tsigkeyring
import dns.rcode
class TestTsigReplay(unittest.TestCase):
def test_update_replay(self):
keyring = dns.tsigkeyring.from_text({
'update-key.' : ('hmac-sha256.',
'R16NojROxtxH/xbDl//ehDsHm5DjWTQ2YXV+hGC2iBY=')
})
qname = dns.name.from_text('www.example.com.')
q = dns.message.make_query(qname, dns.rdatatype.A)
response = dns.query.udp(q, '10.53.0.1', port=5300, timeout=2)
self.assertEqual(response.rcode(), dns.rcode.NXDOMAIN)
# Transaction 1: www.example.com. A 1.1.1.1
update = dns.update.Update('example.com.', keyring=keyring)
update.replace('www', 300, 'A', '1.1.1.1')
# msg is the wire format message that an MITM can observe
msg = update.to_wire()
# Send the wire format message to named over UDP
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.sendto(msg, ('10.53.0.1', 5300))
reply = s.recvfrom(4096)
# Assume the update message was intercepted by a MITM party when
# transaction 1 was performed
intercepted_msg = msg
# Sleep for 2 seconds to increment the timesigned value for the
# next update.
time.sleep(2)
qname = dns.name.from_text('www.example.com.')
q = dns.message.make_query(qname, dns.rdatatype.A)
response = dns.query.udp(q, '10.53.0.1', port=5300, timeout=2)
self.assertEqual(response.rcode(), dns.rcode.NOERROR)
rrs = response.get_rrset(response.answer, qname,
dns.rdataclass.IN, dns.rdatatype.A)
self.assertTrue(rrs is not None)
seen = set([rdata.address for rdata in rrs])
self.assertIn('1.1.1.1', seen)
self.assertNotIn('2.2.2.2', seen)
# Transaction 2: www.example.com. A 2.2.2.2
update = dns.update.Update('example.com.', keyring=keyring)
update.replace('www', 300, 'A', '2.2.2.2')
response = dns.query.udp(update, '10.53.0.1', port=5300, timeout=2)
self.assertEqual(response.rcode(), dns.rcode.NOERROR)
qname = dns.name.from_text('www.example.com.')
q = dns.message.make_query(qname, dns.rdatatype.A)
response = dns.query.udp(q, '10.53.0.1', port=5300, timeout=2)
self.assertEqual(response.rcode(), dns.rcode.NOERROR)
rrs = response.get_rrset(response.answer, qname,
dns.rdataclass.IN, dns.rdatatype.A)
self.assertTrue(rrs is not None)
seen = set([rdata.address for rdata in rrs])
self.assertIn('2.2.2.2', seen)
self.assertNotIn('1.1.1.1', seen)
# Replay of transaction 1 by MITM
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.sendto(intercepted_msg, ('10.53.0.1', 5300))
reply = s.recvfrom(4096)
# Check that replay must not have succeeded
qname = dns.name.from_text('www.example.com.')
q = dns.message.make_query(qname, dns.rdatatype.A)
response = dns.query.udp(q, '10.53.0.1', port=5300, timeout=2)
self.assertEqual(response.rcode(), dns.rcode.NOERROR)
rrs = response.get_rrset(response.answer, qname,
dns.rdataclass.IN, dns.rdatatype.A)
self.assertTrue(rrs is not None)
seen = set([rdata.address for rdata in rrs])
self.assertIn('2.2.2.2', seen)
self.assertNotIn('1.1.1.1', seen)
if __name__ == '__main__':
unittest.main()
signature.asc
Description: PGP signature
_______________________________________________ DNSOP mailing list -- [email protected] To unsubscribe send an email to [email protected]
