package Net::DNS::RR::TSIG;

# $Id: TSIG.pm,v 1.1 2002/02/13 04:07:50 ctriv Exp $

use strict;
use vars qw(@ISA);

use Net::DNS::Packet;
use Digest::HMAC_MD5;
use MIME::Base64;

use constant DEFAULT_ALGORITHM => "HMAC-MD5.SIG-ALG.REG.INT";
use constant DEFAULT_FUDGE     => 300;

@ISA = qw(Net::DNS::RR);

sub new {
	my ($class, $self, $data, $offset) = @_;

	if ($self->{"rdlength"} > 0) {
		my $alg;
		($alg, $offset) = Net::DNS::Packet::dn_expand($data, $offset);
		$self->{"algorithm"} = $alg;

		my ($time_high, $time_low) = unpack("\@$offset nN", $$data);
		$self->{"time_signed"} = $time_low;	# bug
		$offset += &Net::DNS::INT16SZ + &Net::DNS::INT32SZ;

		my ($fudge, $macsize) = unpack("\@$offset nn", $$data);
		$self->{"fudge"} = $fudge;
		$self->{"mac_size"} = $macsize;
		$offset += &Net::DNS::INT16SZ + &Net::DNS::INT16SZ;

		my $mac = substr($$data, $offset, $macsize);
		$self->{"mac"} = $mac;
		$offset += $macsize;

		my ($oid, $error, $olen) = unpack("\@$offset nnn", $$data);
		$self->{"original_id"} = $oid;
		$self->{"error"} = $error;
		$self->{"other_len"} = $olen;
		$offset += &Net::DNS::INT16SZ + &Net::DNS::INT16SZ
                        +  &Net::DNS::INT16SZ;

		my $odata = substr($$data, $offset, $olen);
		my ($odata_high, $odata_low) = unpack("nN", $odata);
		$self->{"other_data"} = $odata_low;
	}

	return bless $self, $class;
}

sub new_from_string {
	my ($class, $self, $string) = @_;

	if ($string && ($string =~ /^(.*)$/)) {
		$self->{"key"}     = $1;
	}

	$self->{"algorithm"}   = DEFAULT_ALGORITHM;
	$self->{"time_signed"} = time;
	$self->{"fudge"}       = DEFAULT_FUDGE;
	$self->{"mac_size"}    = 0;
	$self->{"mac"}         = "";
	$self->{"original_id"} = 0;
	$self->{"error"}       = 0;
	$self->{"other_len"}   = 0;
	$self->{"other_data"}  = "";

	# RFC 2845 Section 2.3
	$self->{"class"} = "ANY";

	return bless $self, $class;
}

sub error {
	my $self = shift;

	my $rcode;
	my $error = $self->{"error"};

	if (defined($error)) {
		$rcode = $Net::DNS::rcodesbyval{$error} || $error;
	}

	return $rcode;
}

sub mac_size {
	my $self = shift;
	return length(defined($self->{"mac"}) ? $self->{"mac"} : "");
}

sub mac {
	my $self = shift;
	my $mac = unpack("H*", $self->{"mac"}) if defined($self->{"mac"});
	return $mac;
}

sub rdatastr {
	my $self = shift;

	my $error = $self->error;
	$error = "UNDEFINED" unless defined $error;

	my $rdatastr;

	if (exists $self->{"algorithm"}) {
		$rdatastr = "$self->{algorithm}. $error";
		if ($self->{"other_len"} && defined($self->{"other_data"})) {
			$rdatastr .= " $self->{other_data}";
		}
	}
	else {
		$rdatastr = "; no data";
	}

	return $rdatastr;
}

sub rr_rdata {
	my ($self, $packet, $offset) = @_;
	my ($hmac, $newpacket, $newoffset, $key, $sigdata);
	my $rdata = "";

	if (exists $self->{"key"}) {
		$key = $self->{"key"};
		$key =~ s/ //g;
		$key = decode_base64($key);

		$hmac = Digest::HMAC_MD5->new($key);
		bless($newpacket = {},"Net::DNS::Packet");
		$newoffset = $offset;
		%{$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;

		# 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);

        	$sigdata .= pack("n", $Net::DNS::classesbyname{uc($self->{"class"})});
        	$sigdata .= pack("N", $self->{"ttl"});

		# Don't compress the algorithm name.
		$tmppacket->{"compnames"} = {};
		$sigdata .= $tmppacket->dn_comp(lc($self->{"algorithm"}), 0);

		$sigdata .= pack("nN", 0, $self->{"time_signed"});	# bug
		$sigdata .= pack("n", $self->{"fudge"});
		$sigdata .= pack("nn", $self->{"error"}, $self->{"other_len"});

		$sigdata .= pack("nN", 0, $self->{"other_data"})
		    if $self->{"other_data"};

		$hmac->add($sigdata);

		$self->{"mac"} = $hmac->digest;
		$self->{"mac_size"} = length($self->{"mac"});

		# Don't compress the algorithm name.
		$tmppacket->{"compnames"} = {};
		$rdata .= $tmppacket->dn_comp($self->{"algorithm"}, 0);

		$rdata .= pack("nN", 0, $self->{"time_signed"});	# bug
		$rdata .= pack("nn", $self->{"fudge"}, $self->{"mac_size"});
		$rdata .= $self->{"mac"};

		$rdata .= pack("nnn",($packet->{"header"}->{"id"},
		                      $self->{"error"},
		                      $self->{"other_len"}));

		$rdata .= pack("nN", 0, $self->{"other_data"})
		    if $self->{"other_data"};
	}

	return $rdata;
}

1;
__END__

=head1 NAME

Net::DNS::RR::TSIG - DNS TSIG resource record

=head1 SYNOPSIS

C<use Net::DNS::RR>;

=head1 DESCRIPTION

Class for DNS Transaction Signature (TSIG) resource records.

=head1 METHODS

=head2 algorithm

    $rr->algorithm($algorithm_name);
    print "algorithm = ", $rr->algorithm, "\n";

Gets or sets the domain name that specifies the name of the algorithm.
The only algorithm currently supported is HMAC-MD5.SIG-ALG.REG.INT.

=head2 time_signed

    $rr->time_signed(time);
    print "time signed = ", $rr->time_signed, "\n";

Gets or sets the signing time as the number of seconds since 1 Jan 1970
00:00:00 UTC.

The default signing time is the current time.

=head2 fudge

    $rr->fudge(60);
    print "fudge = ", $rr->fudge, "\n";

Gets or sets the "fudge", i.e., the seconds of error permitted in the
signing time.

The default fudge is 300 seconds.

=head2 mac_size

    print "MAC size = ", $rr->mac_size, "\n";

Returns the number of octets in the message authentication code (MAC).
The programmer must call a Net::DNS::Packet object's data method
before this will return anything meaningful.

=head2 mac

    print "MAC = ", $rr->mac, "\n";

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 original_id

    $rr->original_id(12345);
    print "original ID = ", $rr->original_id, "\n";

Gets or sets the original message ID.

=head2 error

    print "error = ", $rr->error, "\n";

Returns the RCODE covering TSIG processing.  Common values are
NOERROR, BADSIG, BADKEY, and BADTIME.  See RFC 2845 for details.

=head2 other_len

    print "other len = ", $rr->other_len, "\n";

Returns the length of the Other Data.  Should be zero unless the
error is BADTIME.

=head2 other_data

    print "other data = ", $rr->other_data, "\n";

Returns the Other Data.  This field should be empty unless the
error is BADTIME, in which case it will contain the server's
time as the number of seconds since 1 Jan 1970 00:00:00 UTC.

=head1 BUGS

This code is still under development.  Use with caution on production
systems.

The time_signed and other_data fields should be 48-bit unsigned
integers (RFC 2845, Sections 2.3 and 4.5.2).  The current implementation
ignores the upper 16 bits; this will cause problems for times later
than 19 Jan 2038 03:14:07 UTC.

The only algorithm currently supported is HMAC-MD5.SIG-ALG.REG.INT.

=head1 COPYRIGHT

Copyright (c) 2000 Michael Fuhr.  All rights reserved.  This program
is free software; you can redistribute it and/or modify it under
the same terms as Perl itself.

=head1 ACKNOWLEDGMENT

Most of the code in the Net::DNS::RR::TSIG module was contributed
by Chris Turbeville.

=head1 SEE ALSO

L<perl(1)>, L<Net::DNS>, L<Net::DNS::Resolver>, L<Net::DNS::Packet>,
L<Net::DNS::Header>, L<Net::DNS::Question>, L<Net::DNS::RR>,
RFC 2845

=cut

