%%% 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]). -include_lib("eunit/include/eunit.hrl"). -record(rr, {name :: list(), % List of name labels. type = 0 :: non_neg_integer(), class = <<>> :: binary(), ttl = 0 :: 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. -spec canonicalize_rr_form(rr(), rr()) -> rr(). 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}. isValidType(#rr{type = Type}) -> case Type of 43 -> true; % DS 46 -> true; % RRSIG 48 -> true; % DNSKEY _ -> false end. %% Sort RR's within the same RRset, remove duplicate RR's and any RR's %% not of the types DNSKEY, DS or RRSIG. canonicalize_rr_ordering(RRs) -> L1 = lists:takewhile(fun isValidType/1, RRs), %%RRsets = splitOnName(L1), %%SortedRRsets = lists:map(fun(L) -> lists:usort(fun cmpRR/2, L) end, L1), %%OneList = lists:append(SortedRRsets), lists:usort(fun cmpRR/2, L1). %% Canonicalise a list of RR's of the types DNSKEY, DS, and RRSIG %% according to RFC4034 section 6. Records of other types are %% removed. Duplicate records are removed. -spec canonicalize(list()) -> list(). canonicalize(RRs) -> %% 6.1 owner name order RRs61 = RRs, % TODO %% 6.2 RR form [DS, RRSIG | Rest] = RRs61, C14N_DS = canonicalize_rr_form(DS, RRSIG), RRs62 = [C14N_DS, RRSIG | Rest], %% 6.3 RR ordering (and dropping duplicates) RRs63 = canonicalize_rr_ordering(RRs62), RRs63. %% cmpRR(A, B) when A#rr.type =< B#rr.type, %% A#rr.class =< B#rr.class, %% A#rr.ttl =< B#rr.ttl, %% A#rr.rdata =< B#rr.rdata -> %% cmpRRname(A#rr.name, B#rr.name); %% cmpRR(_, _) -> %% false. %% @doc -spec cmpRR(rr(), rr()) -> boolean(). cmpRR(A, B) -> case cmpRRname(A#rr.name, B#rr.name) of equal -> ?debugFmt("~p == ~p, next", [A#rr.name, B#rr.name]), case A#rr.type == B#rr.type of false -> A#rr.type =< B#rr.type; true -> case A#rr.class == B#rr.class of false -> A#rr.class =< B#rr.class; true -> case A#rr.ttl == B#rr.ttl of false -> A#rr.ttl =< B#rr.ttl; true -> A#rr.rdata =< B#rr.rdata end end end; NameCmp -> NameCmp end. cmpRRname(A, B) when A == B -> equal; cmpRRname(A, B) when length(A) < length(B) -> true; cmpRRname(A, B) when length(A) > length(B) -> false; cmpRRname(A, B) -> A =< B. %% 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. -define(TV_RR1_inbin, <<7,101,120,97,109,112,108,101,3,99,111,109,0,0,43,0,1,0,0,14,16,0,36,82,106, 13,2,89,208,13,15,120,173,192,134,9,41,169,35,70,122,194,189,203,240,40,210, 4,171,20,30,135,63,107,184,116,61,213,134,7,101,120,97,109,112,108,101,3,99, 111,109,0,0,46,0,1,0,0,14,16,0,87,0,43,13,2,0,0,14,16,87,22,3,11,87,3,142,11, 80,81,3,99,111,109,0,6,228,88,59,0,197,54,50,211,112,165,110,118,14,215,62, 255,210,31,169,117,192,113,47,232,31,111,175,28,118,31,225,190,139,249,250, 244,69,217,9,111,122,75,130,10,159,190,71,241,184,230,58,126,189,225,42,29, 195,7,217,85,233,231,155,0,0,48,0,1,0,0,14,16,0,68,1,0,3,13,209,167,133,117, 137,124,191,163,201,10,151,19,139,232,224,244,203,106,201,233,28,167,30,177, 84,53,125,127,85,116,219,50,35,216,117,50,127,240,195,143,219,193,12,65,95,5, 16,116,0,141,5,83,66,213,40,91,22,196,101,145,127,109,68,210,7,101,120,97, 109,112,108,101,3,99,111,109,0,0,43,0,1,0,0,14,16,0,36,82,106,13,2,89,208,13, 15,120,173,192,134,9,41,169,35,70,122,194,189,203,240,40,210,4,171,20,30,135, 63,107,184,116,61,213,134,0,0,48,0,1,0,0,14,16,0,68,1,0,3,13,209,167,133,117, 137,124,191,163,201,10,151,19,139,232,224,244,203,106,201,233,28,167,30,177, 84,53,125,127,85,116,219,50,35,216,117,50,127,240,195,143,219,193,12,65,95,5, 16,116,0,141,5,83,66,213,40,91,22,196,101,145,127,109,68,210>>). -define(TV_RR1_in, decode_rrset(?TV_RR1_inbin)). -define(TV_RR1_out, []). gen_rrset(Name) -> gen_rrset(Name, 0, 0, <<>>). gen_rrset(Name, Type) -> gen_rrset(Name, Type, 0, <<>>). gen_rrset(Name, Type, TTL, RDATA) -> N = string:tokens(Name, "."), RRs = [#rr{name = N, type = Type, class = <<0, 1>>, ttl = TTL, rdata = RDATA}, #rr{name = N, type = 46, class = <<0, 1>>, ttl = TTL, rdata = <<0,0,0,0, TTL:32/integer>>}], %%?debugFmt("gen_rrset: ~p", [RRs]), RRs. basic_test_() -> {ok, SingleRRbin} = file:read_file("test/testdata/dnssec/testrrsets/single-record"), [SingleRR | _] = decode_rrset(SingleRRbin), [G | _] = gen_rrset("example.com", 43, 3600, <<82,106,13,2,89,208,13,15,120,173,192,134,9,41, 169,35,70,122,194,189,203,240,40,210,4,171,20, 30,135,63,107,184,116,61,213,134>>), [?_assertMatch(#rr{name = ["example", "com"], type = 43, class = <<0,1>>, ttl = 3600}, SingleRR), ?_assertEqual(G, SingleRR)]. -define(TV_R1, gen_rrset("example.com", 43, 3600, <<>>)). -define(TV_R2, gen_rrset("com", 43, 3600, <<>>)). -define(TV_R3, gen_rrset("com", 43, 300, <<>>)). c14n_test_() -> [ %% Reverse order, names. ?_assertEqual(lists:append([?TV_R2, ?TV_R1]), canonicalize(lists:append([?TV_R1, ?TV_R2]))), %% Remove duplicate R1. ?_assertEqual(lists:append([?TV_R2, ?TV_R1]), canonicalize(lists:append([?TV_R1, ?TV_R2, ?TV_R1]))), %% Reverse order, TTL. ?_assertEqual(lists:append([?TV_R3, ?TV_R2]), canonicalize(lists:append([?TV_R2, ?TV_R3]))), %% Sorting and removing. %%?_assertEqual(lists:append([?TV_R3, ?TV_R2, ?TV_R1]), %% canonicalize(lists:append([?TV_R2, ?TV_R3, ?TV_R1, ?TV_R3]))), ?_assert(true)]. sorting_test_() -> [ ?_assertEqual(lists:append([ gen_rrset("example", 43), gen_rrset("a.example", 43), gen_rrset("yljkjljk.a.example", 43), gen_rrset("Z.a.example", 43), gen_rrset("zABC.a.EXAMPLE", 43), gen_rrset("z.example", 43), gen_rrset("\001.z.example", 43), gen_rrset("*.z.example", 43), gen_rrset("\200.z.example", 43)]), canonicalize( lists:append([ gen_rrset("\001.z.example", 43), gen_rrset("a.example", 43), gen_rrset("example", 43), gen_rrset("Z.a.example", 43), gen_rrset("zABC.a.EXAMPLE", 43), gen_rrset("\200.z.example", 43), gen_rrset("z.example", 43), gen_rrset("*.z.example", 43), gen_rrset("yljkjljk.a.example", 43)]))) ].