Skip Menu |

This queue is for tickets about the Net-DNS CPAN distribution.

Report information
The Basics
Id: 53610
Status: resolved
Priority: 0/
Queue: Net-DNS

People
Owner: OLAF [...] cpan.org
Requestors: bitcard [...] antibozo.net
Cc:
AdminCc:

Bug Information
Severity: Important
Broken in: (no value)
Fixed in: (no value)



Subject: TSIG validation support
The design of Net::DNS::Packet makes it impossible to validate TSIG signatures. A patch to fix this and to add validation support to Net::DNS is attached. Rationale: with people deploying DNSSEC, the ability to perform validated zone transfers via TSIG is useful since some people are using this as a secure method to ship zone files to a central signing repository. Net::DNS would be a useful part of this process, but, although it can currently sign AXFR requests, it cannot validate the responses. An attacker thus may substitute alternate zone data as a response to the AXFR. This patch was also provided via email to the listed package maintainers MFURH and OLAF. The patch is a unified diff against current trunk in SVN, Net-DNS 0.66, SVN revision 846. A summary of the changes in this patch: - If a parsed DNS message contains a TSIG RR, save the segment of the original DNS message data that will later be used to validate the TSIG. This is everything from the beginning of the header (with the arcount decremented) up to but not including the last RR (the TSIG). - Add a sig_data() method that is used by Net::DNS::RR::TSIG to acquire the data from a packet to be used for computing a signature. For a parsed message, this returns the data saved as described above. For a constructed packet, this performs the fake clone and serialization that used to be coded in Net::DNS::RR::TSIG->rr_rdata(). - Add a validate_tsig() method that takes a reference Net::DNS::RR::TSIG (from which the key and algorithm are used), and a boolean indicating whether the message is an answer (if so, the request MAC, also drawn from the supplied Net::DNS::RR::TSIG is prepended to the data before signing). validate_tsig() returns an RCODE that may include one of the extended values added to %Net::DNS::rcodesbyvalue earlier. In DNS/RR/TSIG.pm: - Add a mac_binary() method that returns the packed length+MAC structure used for signing. - Modify the sig_data() method to rely on Net::DNS::Packet->sig_data() instead of cloning and serializing the packet data. - Remove the stub implementation that adds a member {"request_mac"} to the data before signing; this does not appear to be supported by any other code or documented in the API. Hopefully no one is relying on it. - Add a sign_data() method to invoke the signing function. This is invoked by Net::DNS::Packet->validate_tsig() to generate the final signature for comparison. - Modify rr_rdata() to use sign_data() instead of directly invoking the signing function.
Subject: Net-DNS-tsig-validation.patch
Index: lib/Net/DNS.pm =================================================================== --- lib/Net/DNS.pm (revision 846) +++ lib/Net/DNS.pm (working copy) @@ -330,6 +330,9 @@ 'NXRRSET' => 8, # RFC 2136 'NOTAUTH' => 9, # RFC 2136 'NOTZONE' => 10, # RFC 2136 + 'BADSIG' => 16, # RFC 2845 (only valid in TSIG error) + 'BADKEY' => 17, # RFC 2845 (only valid in TSIG error) + 'BADTIME' => 18, # RFC 2845 (only valid in TSIG error) ); %rcodesbyval = reverse %rcodesbyname; Index: lib/Net/DNS/Packet.pm =================================================================== --- lib/Net/DNS/Packet.pm (revision 846) +++ lib/Net/DNS/Packet.pm (working copy) @@ -311,14 +311,71 @@ undef $self->{offset}; my $arcount = $self->{header}->arcount; my $rr; + my $lastoffset; while ( $arcount-- ) { + $lastoffset = $offset; ($rr, $offset) = Net::DNS::RR->parse($data, $offset); push(@rr, $rr); } + # If last RR is a TSIG, save the portion of the original data + # that would be needed for validation. It is not generally + # possible to reconstruct this from the parsed packet because + # different DNS implementations do DNS label compression + # differently. + if (defined ($rr) && ($rr->type eq 'TSIG')) { + my ($fakeheader, $headeroffset) = Net::DNS::Header->parse($data); + $fakeheader->arcount($fakeheader->arcount - 1); + my $sigdata = $fakeheader->data; + $sigdata .= substr ($$data, $headeroffset, $lastoffset - $headeroffset); + $self->{sig_data} = $sigdata; + $self->{tsig_rr} = $rr; + } else { + undef $self->{sig_data}; + undef $self->{tsig_rr}; + } + @{$self->{additional}} = @rr; } +=head2 sigdata + + $sigdata = $packet->sig_data; + +Returns the data that would be used to compute a TSIG signature for this +packet. For a query packet, this is simply the serialized data, without any +associated TSIG RR. For an answer we received from a nameserver, this is the +original packet data, with the TSIG RR removed. + +=cut + +sub sig_data { + my ($self) = @_; + my $newpacket; + + # If this was an answer with TSIG, we already have it. + return $self->{sig_data} if (exists ($self->{tsig_rr}) && exists ($self->{sig_data})); + + # If there's no TSIG on this packet, easy. + my @additional = @{$self->{additional}}; + return $self->data unless (scalar (@additional) && ($additional[$#additional]->type eq 'TSIG')); + + # Fake up a copy of the packet without the TSIG and serialize that. + # Note: this code was originally in RR/TSIG.pm. It belonged here. + bless($newpacket = {},"Net::DNS::Packet"); + %{$newpacket} = %{$self}; + bless($newpacket->{"header"} = {},"Net::DNS::Header"); + $newpacket->{"additional"} = []; + %{$newpacket->{"header"}} = %{$self->{"header"}}; + @{$newpacket->{"additional"}} = @{$self->{"additional"}}; + shift(@{$newpacket->{"additional"}}); + $newpacket->{"header"}{"arcount"}--; + $newpacket->{"compnames"} = {}; + + return $newpacket->data; +} + + =head2 print $packet->print; @@ -672,6 +729,56 @@ +=head2 validate_tsig + + $tsig_rcode = $packet->validate_tsig($tsig_rr, $isanswer); + +Validates the TSIG RR on a packet, using the provided TSIG. The provided +TSIG name and algorithm must match the TSIG on the packet being validated. +The validity period of TSIG on the packet must include the current time. +If the packet is an answer packet, the validating TSIG must be from the +corresponding query, since the query TSIG MAC must be included in the +validation stream. The second argument is a boolean indicating whether the +packet to be validated is an answer. + +Returns a TSIG RCODE indicating validation result. For a successful +validation, this will be NOERROR. Otherwise, the result will be FORMERR, +BADKEY, BADSIG, or BADTIME. + +=cut + +sub validate_tsig { + my ($self, $tsig, $isanswer) = @_; + + # Force parsing of the packet. + $self->additional; + + return $Net::DNS::rcodesbyname{FORMERR} unless (exists ($self->{tsig_rr}) && defined ($tsig)); + + my $a = $self->{tsig_rr}; + + return $Net::DNS::rcodesbyname{BADKEY} unless (lc($a->name) eq lc($tsig->name)); + return $Net::DNS::rcodesbyname{BADKEY} unless (lc($a->algorithm) eq lc($tsig->algorithm)); + + my $now = time; + return $Net::DNS::rcodesbyname{BADTIME} unless (($a->time_signed + $a->fudge >= $now) && ($a->time_signed - $a->fudge <= $now)); + + my $sigdata = ''; + + # If this is an answer, include the MAC from the original TSIG. + $sigdata .= $tsig->mac_binary if ($isanswer); + + $sigdata .= $a->sig_data ($self); + + my $sig = $tsig->sign_data($sigdata); + + return $Net::DNS::rcodesbyname{BADSIG} if ($sig ne pack('H*', $a->mac)); + + return $Net::DNS::rcodesbyname{NOERROR}; +} + + + =head2 sign_sig0 SIG0 support is provided through the Net::DNS::RR::SIG class. This class is not part Index: lib/Net/DNS/RR/TSIG.pm =================================================================== --- lib/Net/DNS/RR/TSIG.pm (revision 846) +++ lib/Net/DNS/RR/TSIG.pm (working copy) @@ -105,6 +105,12 @@ my $mac = unpack("H*", $self->{"mac"}) if defined($self->{"mac"}); return $mac; } + +sub mac_binary { + my $self = shift; + return undef unless defined($self->{"mac"}); + return pack ('n', length($self->{"mac"})) . $self->{"mac"}; +} sub rdatastr { my $self = shift; @@ -127,28 +133,14 @@ } # return the data that needs to be signed/verified. This is useful for -# external TSIG verification routines +# external TSIG verification routines. This does not include any request +# MAC prefix. sub sig_data { my ($self, $packet) = @_; - my ($newpacket, $sigdata); - # XXX this is horrible. $pkt = Net::DNS::Packet->clone($packet); maybe? - bless($newpacket = {},"Net::DNS::Packet"); - %{$newpacket} = %{$packet}; - bless($newpacket->{"header"} = {},"Net::DNS::Header"); - $newpacket->{"additional"} = []; - %{$newpacket->{"header"}} = %{$packet->{"header"}}; - @{$newpacket->{"additional"}} = @{$packet->{"additional"}}; - shift(@{$newpacket->{"additional"}}); - $newpacket->{"header"}{"arcount"}--; - $newpacket->{"compnames"} = {}; + my $sigdata = ''; + $sigdata .= $packet->sig_data; - # Add the request MAC if present (used to validate responses). - $sigdata .= pack("H*", $self->{"request_mac"}) - if $self->{"request_mac"}; - - $sigdata .= $newpacket->data; - # Don't compress the record (key) name. my $tmppacket = Net::DNS::Packet->new(""); $sigdata .= $tmppacket->dn_comp(lc($self->{"name"}), 0); @@ -169,7 +161,14 @@ return $sigdata; } + +sub sign_data { + my ($self, $data) = @_; + return &{$self->{"sign_func"}}($self->{"key"}, $data) if (exists $self->{"key"}); + return ''; +} + sub rr_rdata { my ($self, $packet, $offset) = @_; my $rdata = ""; @@ -179,7 +178,7 @@ my $sigdata = $self->sig_data($packet); # and call the signing function - $self->{"mac"} = &{$self->{"sign_func"}}($self->{"key"}, $sigdata); + $self->{"mac"} = $self->sign_data ($sigdata); $self->{"mac_size"} = length($self->{"mac"}); # construct the signed TSIG record @@ -261,7 +260,14 @@ Returns the message authentication code (MAC) as a string of hex characters. The programmer must call a Net::DNS::Packet object's data method before this will return anything meaningful. + +=head2 mac_binary + ($length, $mac) = unpack ('nH*', $rr->mac_binary); + +Returns the serialized MAC length and data, as would be used for +encoding the MAC in a signature computation. + =head2 original_id $rr->original_id(12345); @@ -298,7 +304,13 @@ Returns the packet packed according to RFC2845 in a form for signing. This is only needed if you want to supply an external signing function, such as is needed for TSIG-GSS. + +=head2 sign_data + my $mac = $tsig->sign_data($bytes); + +Returns the MAC computed using the TSIG's signing algorithm and key. + =head2 sign_func sub my_sign_fn($$) {
From: bitcard [...] antibozo.net
On Mon Jan 11 18:36:09 2010, antibozo wrote: Show quoted text
> - Add a validate_tsig() method that takes a reference Net::DNS::RR::TSIG > (from which the key and algorithm are used), and a boolean indicating > whether the message is an answer (if so, the request MAC, also drawn > from the supplied Net::DNS::RR::TSIG is prepended to the data before > signing). validate_tsig() returns an RCODE that may include one of the > extended values added to %Net::DNS::rcodesbyvalue earlier.
It occurs to me now that the answer boolean is unnecessary. The answer status can be retrieved from $packet->header->qr.
From: bitcard [...] antibozo.net
I am working on improving the patch to incorporate the previous observation and to include automatic TSIG validation within Resolver/Base.pm. This adds additional complexity within Resolver->axfr() because this method parses multiple envelopes. The TSIG validation algorithm on multiple envelopes is somewhat more complex.
Thank you for working on this. Glancing over the patch it seems to be more than useful. I am looking forward to your improvements and will then assess the patches in more detail.
From: rwfranks [...] acm.org
Implementation on SVN trunk and will appear in 0.73. Assuming your local BIND is configured for the shared key, you should be able to do this: $query = new Net::DNS::Packet( 'foo.example', 'A' ); $query->sign_tsig( 'Khmac-sha512.example.+165+01018.key' ); $reply = $resolver->send( $query ); $verified = $reply->verify( $query ); die $reply->verifyerr unless $verified; On Mon Jan 11 18:36:09 2010, antibozo wrote: Show quoted text
> The design of Net::DNS::Packet makes it impossible to validate TSIG > signatures.
Fixed in 0.73