CAA DNS records

This patchset adds support for DNS CAA (Certification Authority
Authorization) Resource Record which is described in RFC 6844
(https://tools.ietf.org/html/rfc6844)

Change-Id: If9619096f1706d1123895b63b9129b9ffd4fb320
Closes-Bug: 1787552
This commit is contained in:
Tytus Kurek 2018-10-16 15:42:11 +02:00 committed by Graham Hayes
parent 44d9c02c7c
commit c0b54602a4
No known key found for this signature in database
GPG Key ID: 1B263DC59F4AEFD5
10 changed files with 225 additions and 4 deletions

View File

@ -63,7 +63,10 @@ rectype2iparectype = {'A': ('arecord', '%(data)s'),
'PTR': ('ptrrecord', '%(data)s'),
'SPF': ('spfrecord', '%(data)s'),
'SSHFP': ('sshfprecord', '%(data)s'),
'NAPTR': ('naptrrecord', '%(data)s')}
'NAPTR': ('naptrrecord', '%(data)s'),
'CAA': ('caarecord', '%(data)s'),
}
IPA_INVALID_DATA = 3009
IPA_NOT_FOUND = 4001

View File

@ -69,7 +69,7 @@ designate_opts = [
# Supported record types
cfg.ListOpt('supported-record-type', help='Supported record types',
default=['A', 'AAAA', 'CNAME', 'MX', 'SRV', 'TXT', 'SPF', 'NS',
'PTR', 'SSHFP', 'SOA', 'NAPTR']),
'PTR', 'SSHFP', 'SOA', 'NAPTR', 'CAA']),
]
# Set some Oslo Log defaults

View File

@ -49,6 +49,7 @@ from designate.objects.zone_export import ZoneExport, ZoneExportList # noqa
from designate.objects.rrdata_a import A, AList # noqa
from designate.objects.rrdata_aaaa import AAAA, AAAAList # noqa
from designate.objects.rrdata_caa import CAA, CAAList # noqa
from designate.objects.rrdata_cname import CNAME, CNAMEList # noqa
from designate.objects.rrdata_mx import MX, MXList # noqa
from designate.objects.rrdata_naptr import NAPTR, NAPTRList # noqa

View File

@ -101,6 +101,9 @@ class StringFields(ovoo_fields.StringField):
RE_NAPTR_FLAGS = r'^(?!.*(.).*\1)[APSU]+$'
RE_NAPTR_SERVICE = r'^([A-Za-z]([A-Za-z0-9]*)(\+[A-Za-z]([A-Za-z0-9]{0,31}))*)?' # noqa
RE_NAPTR_REGEXP = r'^([^0-9i\\])(.*)\1((.+)|(\\[1-9]))\1(i?)'
RE_KVP = r'^\s[A-Za-z0-9]+=[A-Za-z0-9]+'
RE_URL_MAIL = r'^mailto:[A-Za-z0-9_\-]+@.*'
RE_URL_HTTP = r'^http(s)?://.*/'
def __init__(self, nullable=False, read_only=False,
default=ovoo_fields.UnspecifiedDefault, description='',
@ -337,6 +340,57 @@ class NaptrRegexpField(StringFields):
return value
class CaaPropertyField(StringFields):
def __init__(self, **kwargs):
super(CaaPropertyField, self).__init__(**kwargs)
def coerce(self, obj, attr, value):
value = super(CaaPropertyField, self).coerce(obj, attr, value)
prpt = value.split(' ', 1)
tag = prpt[0]
val = prpt[1]
if (tag == 'issue' or tag == 'issuewild'):
entries = val.split(';')
idn = entries.pop(0)
domain = idn.split('.')
for host in domain:
if len(host) > 63:
raise ValueError("Host %s is too long" % host)
idn_with_dot = idn + '.'
if not re.match(self.RE_ZONENAME, idn_with_dot):
raise ValueError("Domain %s does not match" % idn)
for entry in entries:
if not re.match(self.RE_KVP, entry):
raise ValueError("%s is not valid key-value pair" % entry)
elif tag == 'iodef':
if re.match(self.RE_URL_MAIL, val):
parts = val.split('@')
idn = parts[1]
domain = idn.split('.')
for host in domain:
if len(host) > 63:
raise ValueError("Host %s is too long" % host)
idn_with_dot = idn + '.'
if not re.match(self.RE_ZONENAME, idn_with_dot):
raise ValueError("Domain %s does not match" % idn)
elif re.match(self.RE_URL_HTTP, val):
parts = val.split('/')
idn = parts[2]
domain = idn.split('.')
for host in domain:
if len(host) > 63:
raise ValueError("Host %s is too long" % host)
idn_with_dot = idn + '.'
if not re.match(self.RE_ZONENAME, idn_with_dot):
raise ValueError("Domain %s does not match" % idn)
else:
raise ValueError("%s is not valid URL" % val)
else:
raise ValueError("Property tag %s must be 'issue', 'issuewild'"
" or 'iodef'" % value)
return value
class Any(ovoo_fields.FieldType):
@staticmethod
def coerce(obj, attr, value):

View File

@ -0,0 +1,54 @@
# Copyright 2018 Canonical Ltd.
#
# Author: Tytus Kurek <tytus.kurek@canonical.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from designate.objects.record import Record
from designate.objects.record import RecordList
from designate.objects import base
from designate.objects import fields
@base.DesignateRegistry.register
class CAA(Record):
"""
CAA Resource Record Type
Defined in: RFC6844
"""
fields = {
'flags': fields.IntegerFields(minimum=0, maximum=1),
'prpt': fields.CaaPropertyField()
}
def _to_string(self):
return ("%(flag)s %(prpt)s" % self)
def _from_string(self, v):
flags, prpt = v.split(' ', 1)
self.flags = int(flags)
self.prpt = prpt
# The record type is defined in the RFC. This will be used when the record
# is sent by mini-dns.
RECORD_TYPE = 257
@base.DesignateRegistry.register
class CAAList(RecordList):
LIST_ITEM_TYPE = CAA
fields = {
'objects': fields.ListOfObjectsField('CAA'),
}

View File

@ -0,0 +1,44 @@
# Copyright 2018 Canonical Ltd.
#
# Author: Tytus Kurek <tytus.kurek@canonical.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from sqlalchemy import MetaData, Table, Enum
meta = MetaData()
def upgrade(migrate_engine):
meta.bind = migrate_engine
RECORD_TYPES = ['A', 'AAAA', 'CNAME', 'MX', 'SRV', 'TXT', 'SPF', 'NS',
'PTR', 'SSHFP', 'SOA', 'CAA']
records_table = Table('recordsets', meta, autoload=True)
records_table.columns.type.alter(name='type', type=Enum(*RECORD_TYPES))
def downgrade(migrate_engine):
meta.bind = migrate_engine
RECORD_TYPES = ['A', 'AAAA', 'CNAME', 'MX', 'SRV', 'TXT', 'SPF', 'NS',
'PTR', 'SSHFP', 'SOA']
records_table = Table('recordsets', meta, autoload=True)
# Delete all CAA records
records_table.filter_by(name='type', type='CAA').delete()
# Remove CAA from the ENUM
records_table.columns.type.alter(type=Enum(*RECORD_TYPES))

View File

@ -29,7 +29,8 @@ CONF = cfg.CONF
RESOURCE_STATUSES = ['ACTIVE', 'PENDING', 'DELETED', 'ERROR']
RECORD_TYPES = ['A', 'AAAA', 'CNAME', 'MX', 'SRV', 'TXT', 'SPF', 'NS', 'PTR',
'SSHFP', 'SOA', 'NAPTR']
'SSHFP', 'SOA', 'NAPTR', 'CAA']
TASK_STATUSES = ['ACTIVE', 'PENDING', 'DELETED', 'ERROR', 'COMPLETE']
TSIG_ALGORITHMS = ['hmac-md5', 'hmac-sha1', 'hmac-sha224', 'hmac-sha256',
'hmac-sha384', 'hmac-sha512']

View File

@ -1823,7 +1823,7 @@ class CentralServiceTest(CentralTestCase):
def test_update_recordset_immutable_type(self):
zone = self.create_zone()
# ['A', 'AAAA', 'CNAME', 'MX', 'SRV', 'TXT', 'SPF', 'NS', 'PTR',
# 'SSHFP', 'SOA', 'NAPTR']
# 'SSHFP', 'SOA', 'NAPTR', 'CAA']
# Create a recordset
recordset = self.create_recordset(zone)
cname_recordset = self.create_recordset(zone, type='CNAME')

View File

@ -0,0 +1,55 @@
# Copyright 2018 Canonical Ltd.
#
# Author: Tytus Kurek <tytus.kurek@canonical.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_log import log as logging
import oslotest.base
from designate import objects
LOG = logging.getLogger(__name__)
def debug(*a, **kw):
for v in a:
LOG.debug(repr(v))
for k in sorted(kw):
LOG.debug("%s: %s", k, repr(kw[k]))
class CAARecordTest(oslotest.base.BaseTestCase):
def test_parse_caa_issue(self):
caa_record = objects.CAA()
caa_record._from_string('0 issue ca.example.net')
self.assertEqual(0, caa_record.flags)
self.assertEqual('issue ca.example.net', caa_record.prpt)
def test_parse_caa_issuewild(self):
caa_record = objects.CAA()
caa_record._from_string('1 issuewild ca.example.net; policy=ev')
self.assertEqual(1, caa_record.flags)
self.assertEqual('issuewild ca.example.net; policy=ev',
caa_record.prpt)
def test_parse_caa_iodef(self):
caa_record = objects.CAA()
caa_record._from_string('0 iodef https://example.net/')
self.assertEqual(0, caa_record.flags)
self.assertEqual('iodef https://example.net/', caa_record.prpt)

View File

@ -186,3 +186,12 @@ Objects NAPTR Record
:members:
:undoc-members:
:show-inheritance:
Objects CAA Record
====================
.. automodule:: designate.objects.rrdata_caa
:members:
:undoc-members:
:show-inheritance: