Skip to content

Commit cfc4de7

Browse files
hanazukiaeris
andcommitted
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 Co-authored-by: aeris <aeris@imirhil.fr>
1 parent 081b8df commit cfc4de7

File tree

2 files changed

+131
-1
lines changed

2 files changed

+131
-1
lines changed

lib/resolv.rb

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2537,8 +2537,70 @@ class ANY < Query
25372537
TypeValue = 255 # :nodoc:
25382538
end
25392539

2540+
##
2541+
# CAA resource record defined in RFC 8659
2542+
#
2543+
# These records identify certificate authority allowed to issue
2544+
# certificates for the given domain.
2545+
2546+
class CAA < Resource
2547+
TypeValue = 257
2548+
2549+
##
2550+
# Creates a new CAA for +flags+, +tag+ and +value+.
2551+
2552+
def initialize(flags, tag, value)
2553+
unless (0..255) === flags
2554+
raise ArgumentError.new('flags must be an Integer between 0 and 255')
2555+
end
2556+
unless (1..15) === tag.bytesize
2557+
raise ArgumentError.new('length of tag must be between 1 and 15')
2558+
end
2559+
2560+
@flags = flags
2561+
@tag = tag
2562+
@value = value
2563+
end
2564+
2565+
##
2566+
# Flags for this proprty:
2567+
# - Bit 0 : 0 = not critical, 1 = critical
2568+
2569+
attr_reader :flags
2570+
2571+
##
2572+
# Property tag ("issue", "issuewild", "iodef"...).
2573+
2574+
attr_reader :tag
2575+
2576+
##
2577+
# Property value.
2578+
2579+
attr_reader :value
2580+
2581+
##
2582+
# Whether the critical flag is set on this property.
2583+
2584+
def critical?
2585+
flags & 0x80 != 0
2586+
end
2587+
2588+
def encode_rdata(msg) # :nodoc:
2589+
msg.put_pack('C', @flags)
2590+
msg.put_string(@tag)
2591+
msg.put_bytes(@value)
2592+
end
2593+
2594+
def self.decode_rdata(msg) # :nodoc:
2595+
flags, = msg.get_unpack('C')
2596+
tag = msg.get_string
2597+
value = msg.get_bytes
2598+
self.new flags, tag, value
2599+
end
2600+
end
2601+
25402602
ClassInsensitiveTypes = [ # :nodoc:
2541-
NS, CNAME, SOA, PTR, HINFO, MINFO, MX, TXT, LOC, ANY
2603+
NS, CNAME, SOA, PTR, HINFO, MINFO, MX, TXT, LOC, ANY, CAA
25422604
]
25432605

25442606
##

test/resolv/test_resource.rb

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,71 @@ def test_srv_no_compress
3232
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
3333
end
3434
end
35+
36+
class TestResolvResourceCAA < Test::Unit::TestCase
37+
def test_caa_roundtrip
38+
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
39+
40+
m = Resolv::DNS::Message.new(0)
41+
m.add_answer('new.example.com', 0, Resolv::DNS::Resource::IN::CAA.new(0, 'issue', 'ca1.example.net'))
42+
m.add_answer('new.example.com', 0, Resolv::DNS::Resource::IN::CAA.new(128, 'tbs', 'Unknown'))
43+
assert_equal raw_msg, m.encode
44+
45+
m = Resolv::DNS::Message.decode(raw_msg)
46+
assert_equal 2, m.answer.size
47+
_, _, caa0 = m.answer[0]
48+
assert_equal 0, caa0.flags
49+
assert_equal false, caa0.critical?
50+
assert_equal 'issue', caa0.tag
51+
assert_equal 'ca1.example.net', caa0.value
52+
_, _, caa1 = m.answer[1]
53+
assert_equal true, caa1.critical?
54+
assert_equal 128, caa1.flags
55+
assert_equal 'tbs', caa1.tag
56+
assert_equal 'Unknown', caa1.value
57+
end
58+
59+
def test_caa_stackoverflow
60+
# gathered in the wild
61+
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
62+
63+
m = Resolv::DNS::Message.decode(raw_msg)
64+
assert_equal 11, m.answer.size
65+
_, _, caa3 = m.answer[3]
66+
assert_equal 0, caa3.flags
67+
assert_equal 'issue', caa3.tag
68+
assert_equal 'pki.goog; cansignhttpexchanges=yes', caa3.value
69+
_, _, caa8 = m.answer[8]
70+
assert_equal 0, caa8.flags
71+
assert_equal 'issuewild', caa8.tag
72+
assert_equal 'pki.goog; cansignhttpexchanges=yes', caa8.value
73+
_, _, caa10 = m.answer[10]
74+
assert_equal 128, caa10.flags
75+
assert_equal 'iodef', caa10.tag
76+
assert_equal 'mailto:sysadmin-team@stackoverflow.com', caa10.value
77+
end
78+
79+
def test_caa_flags
80+
assert_equal 255,
81+
Resolv::DNS::Resource::IN::CAA.new(255, 'issue', 'ca1.example.net').flags
82+
assert_raise(ArgumentError) do
83+
Resolv::DNS::Resource::IN::CAA.new(256, 'issue', 'ca1.example.net')
84+
end
85+
86+
assert_raise(ArgumentError) do
87+
Resolv::DNS::Resource::IN::CAA.new(-1, 'issue', 'ca1.example.net')
88+
end
89+
end
90+
91+
def test_caa_tag
92+
assert_raise(ArgumentError, 'Empty tag should be rejected') do
93+
Resolv::DNS::Resource::IN::CAA.new(0, '', 'ca1.example.net')
94+
end
95+
96+
assert_equal '123456789012345',
97+
Resolv::DNS::Resource::IN::CAA.new(0, '123456789012345', 'ca1.example.net').tag
98+
assert_raise(ArgumentError, 'Tag longer than 15 bytes should be rejected') do
99+
Resolv::DNS::Resource::IN::CAA.new(0, '1234567890123456', 'ca1.example.net')
100+
end
101+
end
102+
end

0 commit comments

Comments
 (0)