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($$) {