From cfc4de75e34e83aa94a70621ed19608afb86142b Mon Sep 17 00:00:00 2001 From: Kasumi Hanazuki Date: Wed, 28 Feb 2024 06:48:40 +0000 Subject: [PATCH] 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 --- lib/resolv.rb | 64 ++++++++++++++++++++++++++++++++- test/resolv/test_resource.rb | 68 ++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 1 deletion(-) diff --git a/lib/resolv.rb b/lib/resolv.rb index 1363d49..b585b10 100644 --- a/lib/resolv.rb +++ b/lib/resolv.rb @@ -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 ] ## diff --git a/test/resolv/test_resource.rb b/test/resolv/test_resource.rb index b688155..4343802 100644 --- a/test/resolv/test_resource.rb +++ b/test/resolv/test_resource.rb @@ -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