From 3c0a1e56b046426cc457f430d004e6f3f76a219d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Romain=20Tarti=C3=A8re?= Date: Thu, 1 Oct 2026 14:41:02 -1000 Subject: [PATCH] Implement TLSA resource record Add support for TLSA resource records as defined in [RFC6698]. [RFC6698]: https://www.rfc-editor.org/info/rfc6698 --- lib/resolv.rb | 55 +++++++++++++++++++++++++++++++++++- test/resolv/test_resource.rb | 26 +++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/lib/resolv.rb b/lib/resolv.rb index 36e2d28..8eecdc8 100644 --- a/lib/resolv.rb +++ b/lib/resolv.rb @@ -499,6 +499,7 @@ def each_name(address) # * Resolv::DNS::Resource::IN::SOA # * Resolv::DNS::Resource::IN::SRV # * Resolv::DNS::Resource::IN::SVCB + # * Resolv::DNS::Resource::IN::TLSA # * Resolv::DNS::Resource::IN::TXT # * Resolv::DNS::Resource::IN::WKS # @@ -2636,6 +2637,58 @@ def self.decode_rdata(msg) # :nodoc: end end + ## + # TLSA resource record defined in RFC 6698 + # + # These records are used to associate a TLS server certificate or public + # key with the domain name where the record is found. + + class TLSA < Resource + + TypeValue = 52 # :nodoc: + + def initialize(certificate_usage, selector, matching_type, certificate_association_data) + @certificate_usage = certificate_usage.to_int + @selector = selector.to_int + @matching_type = matching_type.to_int + @certificate_association_data = certificate_association_data + end + + ## + # The Certificate Usage for this TLSA record. + + attr_reader :certificate_usage + + ## + # The Selector for this TLSA record. + + attr_reader :selector + + ## + # The Matching Type for this TLSA record. + + attr_reader :matching_type + + ## + # The Certificate Association Data for this TLSA record. + + attr_reader :certificate_association_data + + def encode_rdata(msg) # :nodoc: + msg.put_bytes(@certificate_usage) + msg.put_bytes(@selector) + msg.put_bytes(@matching_type) + msg.put_pack('H*', @certificate_association_data) + end + + def self.decode_rdata(msg) # :nodoc: + certificate_usage, selector, matching_type = msg.get_unpack('ccc') + certificate_association_data = msg.get_bytes.unpack1('H*') + + return self.new(certificate_usage, selector, matching_type, certificate_association_data) + end + end + ## # Unstructured text resource. @@ -2829,7 +2882,7 @@ def self.decode_rdata(msg) # :nodoc: end ClassInsensitiveTypes = [ # :nodoc: - NS, CNAME, SOA, PTR, HINFO, MINFO, MX, TXT, LOC, ANY, CAA + NS, CNAME, SOA, PTR, HINFO, MINFO, MX, TXT, LOC, ANY, CAA, TLSA ] ## diff --git a/test/resolv/test_resource.rb b/test/resolv/test_resource.rb index c5d22ec..f04b2d4 100644 --- a/test/resolv/test_resource.rb +++ b/test/resolv/test_resource.rb @@ -227,3 +227,29 @@ def test_caa_tag end end end + +class TestResolvResourceTLSA < Test::Unit::TestCase + def test_tlsa_roundtrip + # gathered in the wild, trimed from transation id and additional RRs, reformatted for clarity + raw_msg = "\x00\x00\x00\x00\x00\x01\x00\x02\x00\x00\x00\x00\x04_443\x04_tcp\x07freebsd\x03org\x00\x00\x34\x00\x01\xc0\x0c\x00\x34\x00\x01\x00\x00\x0d\x22\x00\x23\x03\x01\x01\x24\x04\x9a\xa6\xe0\x30\x51\xa0\xdf\x3a\xef\xbb\xfa\xd4\x68\x6e\x62\x07\xdd\x4e\x60\x18\x58\xee\x40\xc3\x1c\x8b\x0b\xd6\xbc\x03\xc0\x0c\x00\x34\x00\x01\x00\x00\x0d\x22\x00\x23\x03\x01\x01\x31\xef\x2a\x4d\x6e\x28\x5c\xc2\x9a\x63\x6c\x51\x71\xf7\xda\x0a\xc6\x9c\xc4\x4c\xeb\xaf\x5c\xd0\x39\xda\x8c\xc8\x11\x87\x48\x2a".b + + m = Resolv::DNS::Message.new(0) + m.add_question('_443._tcp.freebsd.org.', Resolv::DNS::Resource::IN::TLSA) + m.add_answer('_443._tcp.freebsd.org.', 3362, Resolv::DNS::Resource::IN::TLSA.new(3, 1, 1, '24049aa6e03051a0df3aefbbfad4686e6207dd4e601858ee40c31c8b0bd6bc03')) + m.add_answer('_443._tcp.freebsd.org.', 3362, Resolv::DNS::Resource::IN::TLSA.new(3, 1, 1, '31ef2a4d6e285cc29a636c5171f7da0ac69cc44cebaf5cd039da8cc81187482a')) + assert_equal raw_msg, m.encode + + m = Resolv::DNS::Message.decode(raw_msg) + assert_equal 2, m.answer.size + _, _, tlsa0 = m.answer[0] + assert_equal 3, tlsa0.certificate_usage + assert_equal 1, tlsa0.selector + assert_equal 1, tlsa0.matching_type + assert_equal '24049aa6e03051a0df3aefbbfad4686e6207dd4e601858ee40c31c8b0bd6bc03', tlsa0.certificate_association_data + _, _, tlsa1 = m.answer[1] + assert_equal 3, tlsa1.certificate_usage + assert_equal 1, tlsa1.selector + assert_equal 1, tlsa1.matching_type + assert_equal '31ef2a4d6e285cc29a636c5171f7da0ac69cc44cebaf5cd039da8cc81187482a', tlsa1.certificate_association_data + end +end