Source code for octodns.record.tlsa

#
#
#

import re

from ..equality import EqualityTupleMixin
from .base import Record, ValuesMixin, unquote
from .rr import RrParseError
from .validator import ValueValidator


[docs] class TlsaValueValidator(ValueValidator): ''' Validates TLSA rdata: ``certificate_usage`` in [0, 3], ``selector`` in [0, 1], ``matching_type`` in [0, 2], and ``certificate_association_data`` is present. '''
[docs] def validate(self, value_cls, data, _type): reasons = [] for value in data: try: certificate_usage = int(value.get('certificate_usage', 0)) if certificate_usage < 0 or certificate_usage > 3: reasons.append( f'invalid certificate_usage ' f'"{certificate_usage}"' ) except ValueError: reasons.append( f'invalid certificate_usage ' f'"{value["certificate_usage"]}"' ) try: selector = int(value.get('selector', 0)) if selector < 0 or selector > 1: reasons.append(f'invalid selector "{selector}"') except ValueError: reasons.append(f'invalid selector "{value["selector"]}"') try: matching_type = int(value.get('matching_type', 0)) if matching_type < 0 or matching_type > 2: reasons.append(f'invalid matching_type "{matching_type}"') except ValueError: reasons.append( f'invalid matching_type ' f'"{value["matching_type"]}"' ) if 'certificate_usage' not in value: reasons.append('missing certificate_usage') if 'selector' not in value: reasons.append('missing selector') if 'matching_type' not in value: reasons.append('missing matching_type') if 'certificate_association_data' not in value: reasons.append('missing certificate_association_data') return reasons
[docs] class TlsaValueRfcValidator(ValueValidator): ''' Strict TLSA rdata validator per RFC 6698. - ``certificate_usage``, ``selector``, and ``matching_type`` must each be integers in [0, 255] (uint8 fields). - ``certificate_association_data`` must be a valid hexadecimal string. - When ``matching_type`` is 1 (SHA-256), the data must be exactly 64 hex characters (32 bytes). - When ``matching_type`` is 2 (SHA-512), the data must be exactly 128 hex characters (64 bytes). Enabled as part of the ``strict`` validator set:: manager: enabled: - strict ''' _hex_re = re.compile(r'^[0-9a-fA-F]+$') _matching_type_lengths = {1: 64, 2: 128}
[docs] def validate(self, value_cls, data, _type): reasons = [] for value in data: matching_type = None for field in ('certificate_usage', 'selector', 'matching_type'): if field not in value: reasons.append(f'missing {field}') else: try: int_val = int(value[field]) if not 0 <= int_val <= 255: reasons.append( f'invalid {field} "{int_val}"; must be 0-255' ) elif field == 'matching_type': matching_type = int_val except (ValueError, TypeError): reasons.append(f'invalid {field} "{value[field]}"') if 'certificate_association_data' not in value: reasons.append('missing certificate_association_data') else: cad = value['certificate_association_data'] if not cad or not self._hex_re.match(str(cad)): reasons.append( f'invalid certificate_association_data "{cad}"; must be hex' ) elif matching_type in self._matching_type_lengths: expected = self._matching_type_lengths[matching_type] if len(str(cad)) != expected: reasons.append( f'certificate_association_data must be {expected} hex characters for matching_type {matching_type}' ) return reasons
[docs] class TlsaValueBestPracticeValidator(ValueValidator): ''' Checks that TLSA records do not use matching_type 0 (full DER-encoded certificate or public key stored verbatim). RFC 7671 §4.1 advises against matching_type 0 in production: any certificate renewal requires a DNS update before the new certificate can be used. Use matching_type 1 (SHA-256) or 2 (SHA-512) instead. Enabled as part of the ``best-practice`` validator set:: manager: enabled: - best-practice '''
[docs] def validate(self, value_cls, data, _type): reasons = [] for value in data: try: matching_type = int(value['matching_type']) except (KeyError, ValueError, TypeError): continue if matching_type == 0: reasons.append( 'TLSA matching_type 0 (full data) is not recommended; ' 'use matching_type 1 (SHA-256) or 2 (SHA-512)' ) return reasons
[docs] class TlsaValue(EqualityTupleMixin, dict): VALIDATORS = [ TlsaValueValidator('tlsa-value', sets={'legacy'}), TlsaValueRfcValidator('tlsa-value-rfc', sets={'strict'}), TlsaValueBestPracticeValidator( 'tlsa-value-best-practice', sets={'best-practice'} ), ]
[docs] @classmethod def _schema(cls): return { 'type': 'object', 'required': [ 'certificate_usage', 'selector', 'matching_type', 'certificate_association_data', ], 'properties': { 'certificate_usage': { 'type': 'integer', 'minimum': 0, 'maximum': 3, }, 'selector': {'type': 'integer', 'minimum': 0, 'maximum': 1}, 'matching_type': { 'type': 'integer', 'minimum': 0, 'maximum': 2, }, 'certificate_association_data': {'type': 'string'}, }, }
[docs] @classmethod def parse_rdata_text(self, value): try: ( certificate_usage, selector, matching_type, certificate_association_data, ) = value.split(' ') except ValueError: raise RrParseError() try: certificate_usage = int(certificate_usage) except ValueError: pass try: selector = int(selector) except ValueError: pass try: matching_type = int(matching_type) except ValueError: pass certificate_association_data = unquote(certificate_association_data) return { 'certificate_usage': certificate_usage, 'selector': selector, 'matching_type': matching_type, 'certificate_association_data': certificate_association_data, }
[docs] @classmethod def process(cls, values): return [cls(v) for v in values]
[docs] def __init__(self, value): super().__init__( { 'certificate_usage': int(value.get('certificate_usage', 0)), 'selector': int(value.get('selector', 0)), 'matching_type': int(value.get('matching_type', 0)), # force to str (hex-only values may be coerced to int) and # normalize to lowercase for case-insensitive comparison 'certificate_association_data': str( value['certificate_association_data'] ).lower(), } )
@property def certificate_usage(self): return self['certificate_usage'] @certificate_usage.setter def certificate_usage(self, value): self['certificate_usage'] = value @property def selector(self): return self['selector'] @selector.setter def selector(self, value): self['selector'] = value @property def matching_type(self): return self['matching_type'] @matching_type.setter def matching_type(self, value): self['matching_type'] = value @property def certificate_association_data(self): return self['certificate_association_data'] @certificate_association_data.setter def certificate_association_data(self, value): self['certificate_association_data'] = value @property def rdata_text(self): return f'{self.certificate_usage} {self.selector} {self.matching_type} {self.certificate_association_data}'
[docs] def template(self, params): if '{' not in self.certificate_association_data: return self new = self.__class__(self) new.certificate_association_data = ( new.certificate_association_data.format(**params) ) return new
[docs] def _equality_tuple(self): return ( self.certificate_usage, self.selector, self.matching_type, self.certificate_association_data, )
[docs] def __repr__(self): return f"'{self.certificate_usage} {self.selector} {self.matching_type} {self.certificate_association_data}'"
[docs] class TlsaRecord(ValuesMixin, Record): REFERENCES = ( 'https://datatracker.ietf.org/doc/html/rfc6698', 'https://datatracker.ietf.org/doc/html/rfc7671', 'https://datatracker.ietf.org/doc/html/rfc7672', 'https://datatracker.ietf.org/doc/html/rfc7673', ) _type = 'TLSA' _value_type = TlsaValue
Record.register_type(TlsaRecord)