Skip to content
Open
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
55 changes: 54 additions & 1 deletion lib/resolv.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
#
Expand Down Expand Up @@ -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
Comment on lines +2651 to +2653

@smortex smortex Oct 2, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if adding bound checks here is desirable (RFC6698 list acceptable values in section 2.1). Advices are welcome!

@certificate_association_data = certificate_association_data

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am also not sure about the internal representation to use for this piece of information.

I chose a hex string representation to match what dig(1) outputs, but will be happy to adjust it on request.

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.

Expand Down Expand Up @@ -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
]

##
Expand Down
26 changes: 26 additions & 0 deletions test/resolv/test_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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