Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement CAA resource record #48

Merged
merged 1 commit into from
Feb 29, 2024
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
64 changes: 63 additions & 1 deletion lib/resolv.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2537,8 +2537,70 @@ class ANY < Query
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
]

##
Expand Down
68 changes: 68 additions & 0 deletions test/resolv/test_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,71 @@ def test_srv_no_compress
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