%%% Copyright (c) 2016, NORDUnet A/S. %%% See LICENSE for licensing information. -module(dns). -export([decode_rrset/1, decode_rr/1, encode_rrset/1, encode_rr/1, canonicalize/1, validate/1]). -record(rr, {name :: list(), % List of name labels. type :: non_neg_integer(), class :: binary(), ttl :: integer(), rdata :: binary()}). -type rr() :: #rr{}. -spec decode_name_label(binary()) -> tuple(). decode_name_label(RRbin) -> <> = RRbin, case IsPtr of 0 -> <<_:2/integer, Len:6/integer, Label:Len/binary, Rest/binary>> = RRbin, {binary_to_list(Label), Rest}; _ -> <<_:2/integer, _Ptr:14/integer, Rest/binary>> = RRbin, {'*compressed*', Rest} end. -spec encode_name_label(string()) -> binary(). encode_name_label(Label) -> LabelBin = list_to_binary(Label), Len = byte_size(LabelBin), <>. decode_name(RRbin) -> decode_name(RRbin, []). decode_name(<<0, Rest/binary>>, Acc) -> {lists:reverse(Acc), Rest}; decode_name(RRbin, Acc) -> {Label, Rest} = decode_name_label(RRbin), decode_name(Rest, [Label | Acc]). -spec encode_name(list()) -> binary(). encode_name(Name) -> encode_name(Name, []). encode_name([], Acc) -> Bin = list_to_binary(lists:reverse(Acc)), <>; encode_name([H|T], Acc) -> encode_name(T, [encode_name_label(H) | Acc]). has_compressed_name_p(Name) -> lists:any(fun(Label) -> case Label of '*compressed' -> true; _ -> false end end, Name). -spec decode_rr(binary()) -> {rr(), binary()}. decode_rr(RRBin) -> {Name, RestRR} = decode_name(RRBin), <> = RestRR, {#rr{name = Name, type = Type, class = Class, ttl = TTL, rdata = RDATA}, Rest}. -spec decode_rrset(binary()) -> [rr()]. decode_rrset(RRSet) -> decode_rrset(RRSet, []). decode_rrset(<<>>, Acc) -> lists:reverse(Acc); decode_rrset(RRSet, Acc) -> {RR, Rest} = decode_rr(RRSet), decode_rrset(Rest, [RR | Acc]). -spec encode_rr(rr()) -> binary(). encode_rr(#rr{name = Name, type = Type, class = Class, ttl = TTL, rdata = RDATA}) -> EncodedName = encode_name(Name), RDLength = byte_size(RDATA), <>. -spec encode_rrset(list()) -> binary(). encode_rrset(RRSet) -> encode_rrset(RRSet, []). encode_rrset([], Acc) -> list_to_binary(lists:reverse(Acc)); encode_rrset([H|T], Acc) -> encode_rrset(T, [encode_rr(H) | Acc]). %% Canonicalise a single RR according to RFC4034 section 6.2. canonicalize_rr_form(RR, RRSIG) -> %% 1. Expand domain name -- a label with a length field >= 0xC0 is %% a two octet pointer, which we can't expand (since we don't have %% the full message): Do nothing. %% 2. Owner name casing: Lowercase. LCName = lists:map(fun string:to_lower/1, RR#rr.name), %% 3. DNS names in RDATA casing -- N/A for DS and DNSKEY but %% FIXME: needs to be done for RRSIG? %% 4. FIXME: unexpanded owner name %% 5. Set TTL to "Original TTL" of the corresponding RRSIG. <<_:4/binary, OrigTTL:32/integer, _/binary>> = RRSIG#rr.rdata, RR#rr{name = LCName, ttl = OrigTTL}. %% Canonicalise an RRset with DNSKEY, DS, and RRSIG records according %% to RFC4034 section 6. Records of other types are removed. Duplicate %% records are removed. canonicalize(RRset) -> %% 6.1 owner name order RRset61 = RRset, % TODO %% 6.2 RR form [DS, RRSIG | Rest] = RRset61, C14N_DS = canonicalize_rr_form(DS, RRSIG), RRset62 = [C14N_DS, RRSIG | Rest], %% 6.3 RR ordering (and dropping duplicates) RRset63 = RRset62, RRset63. %% Is the RR set valid for our needs from a DNS point of view? If so, %% return the signature inception time of the RRSIG covering the DS %% RR, to be used as the validation time for the DNSSEC validation. -spec validate(binary()) -> {valid, integer()} | {invalid, atom()}. validate(RRsetBin) -> [DS, RRSIG | _Rest] = decode_rrset(RRsetBin), case has_compressed_name_p(DS#rr.name) of false when DS#rr.type == 43, RRSIG#rr.type == 46 -> <<_:12/binary, SigInceptionTime:32/integer, _/binary>> = RRSIG#rr.rdata, {valid, SigInceptionTime}; false -> {invalid, badtype}; true -> {invalid, compressed_name} end. %% TODO: Add unit tests.