[ruby/resolv] Implement CAA resource record

This patch implements handling of CAA resource records defined by [RFC8659].

- There are no known deployment of CAA records outside of IN (Internet),
  but the RFC does not state that CAA records are class-specific.
  Thus `CAA` class is defined as a class-independent RRType.
- `CAA` class stores `flags` field (a 1-octet bitset) as an Integer.
  In this way it's easier to ensure the encoded RR is in the valid wire format.

[RFC8659]: https://datatracker.ietf.org/doc/html/rfc8659

https://github.com/ruby/resolv/commit/cfc4de75e3

Co-authored-by: aeris <aeris@imirhil.fr>
This commit is contained in:
Kasumi Hanazuki 2024-02-28 06:48:40 +00:00 коммит произвёл git
Родитель d3ae5808bb
Коммит 2508a79699
2 изменённых файлов: 131 добавлений и 1 удалений

Просмотреть файл

@ -2537,8 +2537,70 @@ class Resolv
TypeValue = 255 # :nodoc:
end
##
# CAA resource record defined in RFC 8659
#
# These records identify certificate authority allowed to issue
# certificates for the given domain.
class CAA < Resource
TypeValue = 257
##
# Creates a new CAA for +flags+, +tag+ and +value+.
def initialize(flags, tag, value)
unless (0..255) === flags
raise ArgumentError.new('flags must be an Integer between 0 and 255')
end
unless (1..15) === tag.bytesize
raise ArgumentError.new('length of tag must be between 1 and 15')
end
@flags = flags
@tag = tag
@value = value
end
##
# Flags for this proprty:
# - Bit 0 : 0 = not critical, 1 = critical
attr_reader :flags
##
# Property tag ("issue", "issuewild", "iodef"...).
attr_reader :tag
##
# Property value.
attr_reader :value
##
# Whether the critical flag is set on this property.
def critical?
flags & 0x80 != 0
end
def encode_rdata(msg) # :nodoc:
msg.put_pack('C', @flags)
msg.put_string(@tag)
msg.put_bytes(@value)
end
def self.decode_rdata(msg) # :nodoc:
flags, = msg.get_unpack('C')
tag = msg.get_string
value = msg.get_bytes
self.new flags, tag, value
end
end
ClassInsensitiveTypes = [ # :nodoc:
NS, CNAME, SOA, PTR, HINFO, MINFO, MX, TXT, LOC, ANY
NS, CNAME, SOA, PTR, HINFO, MINFO, MX, TXT, LOC, ANY, CAA
]
##

Просмотреть файл

@ -32,3 +32,71 @@ class TestResolvResource < Test::Unit::TestCase
assert_equal "\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x07example\x03com\x00\x00\x21\x00\x01\x00\x00\x00\x00\x00\x17\x00\x00\x00\x00\x00\x00\x03www\x07example\x03com\x00", m.encode, issue29
end
end
class TestResolvResourceCAA < Test::Unit::TestCase
def test_caa_roundtrip
raw_msg = "\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x03new\x07example\x03com\x00\x01\x01\x00\x01\x00\x00\x00\x00\x00\x16\x00\x05issueca1.example.net\xC0\x0C\x01\x01\x00\x01\x00\x00\x00\x00\x00\x0C\x80\x03tbsUnknown".b
m = Resolv::DNS::Message.new(0)
m.add_answer('new.example.com', 0, Resolv::DNS::Resource::IN::CAA.new(0, 'issue', 'ca1.example.net'))
m.add_answer('new.example.com', 0, Resolv::DNS::Resource::IN::CAA.new(128, 'tbs', 'Unknown'))
assert_equal raw_msg, m.encode
m = Resolv::DNS::Message.decode(raw_msg)
assert_equal 2, m.answer.size
_, _, caa0 = m.answer[0]
assert_equal 0, caa0.flags
assert_equal false, caa0.critical?
assert_equal 'issue', caa0.tag
assert_equal 'ca1.example.net', caa0.value
_, _, caa1 = m.answer[1]
assert_equal true, caa1.critical?
assert_equal 128, caa1.flags
assert_equal 'tbs', caa1.tag
assert_equal 'Unknown', caa1.value
end
def test_caa_stackoverflow
# gathered in the wild
raw_msg = "\x8D\x32\x81\x80\x00\x01\x00\x0B\x00\x00\x00\x00\x0Dstackoverflow\x03com\x00\x01\x01\x00\x01\xC0\x0C\x01\x01\x00\x01\x00\x00\x01\x2C\x00\x13\x00\x05issuecomodoca.com\xC0\x0C\x01\x01\x00\x01\x00\x00\x01\x2C\x00\x2D\x00\x05issuedigicert.com; cansignhttpexchanges=yes\xC0\x0C\x01\x01\x00\x01\x00\x00\x01\x2C\x00\x16\x00\x05issueletsencrypt.org\xC0\x0C\x01\x01\x00\x01\x00\x00\x01\x2C\x00\x29\x00\x05issuepki.goog; cansignhttpexchanges=yes\xC0\x0C\x01\x01\x00\x01\x00\x00\x01\x2C\x00\x12\x00\x05issuesectigo.com\xC0\x0C\x01\x01\x00\x01\x00\x00\x01\x2C\x00\x17\x00\x09issuewildcomodoca.com\xC0\x0C\x01\x01\x00\x01\x00\x00\x01\x2C\x00\x31\x00\x09issuewilddigicert.com; cansignhttpexchanges=yes\xC0\x0C\x01\x01\x00\x01\x00\x00\x01\x2C\x00\x1A\x00\x09issuewildletsencrypt.org\xC0\x0C\x01\x01\x00\x01\x00\x00\x01\x2C\x00\x2D\x00\x09issuewildpki.goog; cansignhttpexchanges=yes\xC0\x0C\x01\x01\x00\x01\x00\x00\x01\x2C\x00\x16\x00\x09issuewildsectigo.com\xC0\x0C\x01\x01\x00\x01\x00\x00\x01\x2C\x00\x2D\x80\x05iodefmailto:sysadmin-team@stackoverflow.com".b
m = Resolv::DNS::Message.decode(raw_msg)
assert_equal 11, m.answer.size
_, _, caa3 = m.answer[3]
assert_equal 0, caa3.flags
assert_equal 'issue', caa3.tag
assert_equal 'pki.goog; cansignhttpexchanges=yes', caa3.value
_, _, caa8 = m.answer[8]
assert_equal 0, caa8.flags
assert_equal 'issuewild', caa8.tag
assert_equal 'pki.goog; cansignhttpexchanges=yes', caa8.value
_, _, caa10 = m.answer[10]
assert_equal 128, caa10.flags
assert_equal 'iodef', caa10.tag
assert_equal 'mailto:sysadmin-team@stackoverflow.com', caa10.value
end
def test_caa_flags
assert_equal 255,
Resolv::DNS::Resource::IN::CAA.new(255, 'issue', 'ca1.example.net').flags
assert_raise(ArgumentError) do
Resolv::DNS::Resource::IN::CAA.new(256, 'issue', 'ca1.example.net')
end
assert_raise(ArgumentError) do
Resolv::DNS::Resource::IN::CAA.new(-1, 'issue', 'ca1.example.net')
end
end
def test_caa_tag
assert_raise(ArgumentError, 'Empty tag should be rejected') do
Resolv::DNS::Resource::IN::CAA.new(0, '', 'ca1.example.net')
end
assert_equal '123456789012345',
Resolv::DNS::Resource::IN::CAA.new(0, '123456789012345', 'ca1.example.net').tag
assert_raise(ArgumentError, 'Tag longer than 15 bytes should be rejected') do
Resolv::DNS::Resource::IN::CAA.new(0, '1234567890123456', 'ca1.example.net')
end
end
end