File 9382-inet_dns_tsig-Validate-that-a-TSIG-RR-has-NOERROR-in.patch of Package erlang

From d500c6e78e76170ce851f5165ac6b9b58bb23090 Mon Sep 17 00:00:00 2001
From: Raimo Niskanen <raimo@erlang.org>
Date: Wed, 4 Mar 2026 16:29:09 +0000
Subject: [PATCH 2/3] inet_dns_tsig: Validate that a TSIG RR has ?NOERROR in a
 request

Without this check, a server that uses inet_dns_tsig:verify/3
and got a request containing an illegal TSIG record, with
the 'error' field set to ?BADSIG or ?BADKEY, would
misinterpret the request as an error reply and use an
empty MAC when validating the request.  And an empty
MAC could also be in the bogus request.

All that had to be correct in such a malicious request is the
TSIG key name, which often can be guessed.

By this an attacker could manage to do an unauthorized
zone transfer that exposes internal DNS data, or do
unauthorized dynamic updates to manipulate records,
redirect traffic, or disrupt services, etc...

Co-Authored-By: Alexander Clouter <alex@digriz.org.uk>
---
 lib/kernel/src/inet_dns_tsig.erl | 109 +++++++++++++++++++------------
 1 file changed, 66 insertions(+), 43 deletions(-)

diff --git a/lib/kernel/src/inet_dns_tsig.erl b/lib/kernel/src/inet_dns_tsig.erl
index e87f77fd2e..06b26c48e1 100644
--- a/lib/kernel/src/inet_dns_tsig.erl
+++ b/lib/kernel/src/inet_dns_tsig.erl
@@ -110,7 +110,8 @@ sign(
     case Hdr of
         %% client uses this
         <<Id:16, QR:1, _:15, _/binary>>
-          when QR == 0, TSId == undefined ->
+          when TSId == undefined ->
+            QR = 0, % ASSERT that the client is not signing a response
             sign(Pkt, TS#tsig_state{ id = Id }, Error, Hdr, Rest);
         %%
         %% client and server use this
@@ -152,7 +153,7 @@ sign(Pkt, TS, Error, Hdr, <<ARCount:16, Content/binary>>) ->
 %% name compression algorithm
 -spec verify(binary(), #dns_rec{}, tsig_state()) ->
                  {ok,tsig_state()} |
-                 {error,formerr | {notauth,badkey | badsig | badtime}}.
+                 {error,formerr | {notauth,badkey | badsig | badtime} | {tsig,tsig_error()}}.
 verify(Pkt, Response, TS) ->
     try do_verify(Pkt, Response, TS) of
         R ->
@@ -167,46 +168,53 @@ do_verify(Pkt,
           Response = #dns_rec{
                          header = #dns_header{ qr = QR },
                          arlist = ARList },
-          TS0 = #tsig_state{ id = TSId })
-              when QR == false, TSId == undefined ->
-    ARList == [] andalso throw({notauth,badsig}),
-    #dns_rr_tsig{
-        domain = Name,
-        algname = AlgName,
-        mac = MAC,
-        original_id = OriginalId,
-        error = Error
-    } = lists:last(ARList),
-    Key = lists:keyfind(Name, 1, TS0#tsig_state.key),
-    Key == false andalso throw({notauth,badkey}),
-    {Alg,AlgSize} = case inet_dns:decode_algname(AlgName) of
-        {A,S} ->
-            {A,S};
-        A ->
-            {A,maps:get(size, crypto:hash_info(A))}
-    end,
-    %% RFC8945, section 5.2.2.1: MAC Truncation
-    MACSize = if
-        Error == ?BADSIG; Error == ?BADKEY ->
-            AlgSize;
-        true ->
-            byte_size(MAC)
-    end,
-    lists:member(Alg, ?ALGS_SUPPORTED) orelse throw({notauth,badkey}),
-    MACValid = MACSize >= ?MAC_SIZE_MIN
-                andalso
-               MACSize >= AlgSize div 2
-                andalso
-               MACSize =< AlgSize,
-    MACValid orelse throw(formerr),
-    TS = TS0#tsig_state{ alg = {Alg,AlgSize}, key = Key, id = OriginalId },
-    do_verify(Pkt, Response, TS);
+          TS0 = #tsig_state{ id = undefined }) ->
+    QR = false, % ASSERT that the caller has not passed a response
+    case ARList =/= [] andalso lists:last(ARList) of
+        false ->
+            {error,{notauth,badsig}};
+        #dns_rr_tsig{
+           domain = Name,
+           algname = AlgName,
+           original_id = OriginalId,
+           error = ?NOERROR
+          } = TSigRR ->
+            {Alg,AlgSize} =
+                case inet_dns:decode_algname(AlgName) of
+                    {A,S} ->
+                        {A,S};
+                    A ->
+                        {A,maps:get(size, crypto:hash_info(A))}
+                end,
+            lists:member(Alg, ?ALGS_SUPPORTED) orelse throw({notauth,badkey}),
+            Key = lists:keyfind(Name, 1, TS0#tsig_state.key),
+            Key == false andalso throw({notauth,badkey}),
+            TS = TS0#tsig_state{
+                   alg = {Alg,AlgSize}, key = Key, id = OriginalId },
+            do_verify(Pkt, Response, TS, TSigRR);
+        _OtherRR ->
+            %% RFC8945, section 5.2:
+            %%   If an incoming message contains a TSIG record, it MUST be
+            %%   the last record in the additional section.  Multiple TSIG
+            %%   records are not allowed.  If multiple TSIG records are
+            %%   detected or a TSIG record is present in any other position,
+            %%   the DNS message is dropped and a response with RCODE 1
+            %%   (FORMERR) MUST be returned.  ...
+            %%   If the TSIG RR cannot be  interpreted, the server MUST regard
+            %%   the message as corrupt and return a FORMERR to the server.  ...
+            %% And, section 4.2:
+            %%   TSIG RR, field Error:
+            %%     ... In requests, this MUST be zero.
+            {error,formerr}
+    end;
 %%
 %% client and server use this
-do_verify(Pkt = <<_Id:16, QR:1, _:15, _/binary>>,
-          Response = #dns_rec{ arlist = ARList },
+do_verify(Pkt,
+          Response = #dns_rec{
+                        header = #dns_header{ qr = QR },
+                        arlist = ARList },
           TS = #tsig_state{ qr = TSQR })
-              when QR == TSQR; QR == 1 andalso TSQR >= 2 ->
+  when QR =:= (TSQR > 0) -> % Query/response status the same in header and state
     case ARList =/= [] andalso lists:last(ARList) of
         %% RFC8945, section 5.3.1: TSIG on TCP Connections
         false when TSQR == 3 -> % not 2 as we must start with a TSIG RR
@@ -218,13 +226,20 @@ do_verify(Pkt = <<_Id:16, QR:1, _:15, _/binary>>,
         false when TSQR >= 4, TSQR =< 99 ->
             MACN = macN(Pkt, TS),
             {ok,TS#tsig_state{ qr = TSQR + 1, mac = MACN }};
+        false ->
+            {error,{notauth,badsig}};
+        % RFC8945, section 5.3: Generation of TSIG on Answers
+        _TSigRR = #dns_rr_tsig{ error = Error }
+          when Error == ?BADSIG; Error == ?BADKEY ->
+            {error,{tsig,Error}};
         TSigRR = #dns_rr_tsig{} ->
             do_verify(Pkt, Response, TS, TSigRR);
-        false ->
-            {error,{notauth,badsig}}
+        _OtherRR ->
+            %% The last RR is not a TSIG RR, see the function clause above
+            {error,formerr}
     end.
 
-do_verify(Pkt, _Response, TS, TSigRR) ->
+do_verify(Pkt, _Response, TS = #tsig_state{ alg = {_Alg,AlgSize} }, TSigRR) ->
     Now = ?NOW,
     #dns_rr_tsig{
         offset = Offset,
@@ -234,6 +249,14 @@ do_verify(Pkt, _Response, TS, TSigRR) ->
         error = Error,
         other_data = OtherData
     } = TSigRR,
+    %% RFC8945, section 5.2.2.1: MAC Truncation
+    MACSize = byte_size(MAC),
+    MACValid = MACSize >= ?MAC_SIZE_MIN
+                andalso
+               MACSize >= AlgSize div 2
+                andalso
+               MACSize =< AlgSize,
+    MACValid orelse throw(formerr),
     PktS = iolist_to_binary([
         <<(TS#tsig_state.id):16>>,
         binary:part(Pkt, {2,8}),
-- 
2.51.0

openSUSE Build Service is sponsored by