File Coverage

blib/lib/Net/DNS/RR/TSIG.pm
Criterion Covered Total %
statement 285 285 100.0
branch 98 98 100.0
path n/a
condition 25 25 100.0
subroutine 45 45 100.0
pod 21 23 100.0
total 474 476 100.0


line stmt bran path cond sub pod time code
1               package Net::DNS::RR::TSIG;
2                
3 7       7   41 use strict;
  7           12  
  7           263  
4 7       7   26 use warnings;
  7           12  
  7           521  
5               our $VERSION = (qw$Id: TSIG.pm 2003 2025-01-21 12:06:06Z willem $)[2];
6                
7 7       7   41 use base qw(Net::DNS::RR);
  7           11  
  7           673  
8                
9                
10               =head1 NAME
11                
12               Net::DNS::RR::TSIG - DNS TSIG resource record
13                
14               =cut
15                
16 7       7   35 use integer;
  7           8  
  7           33  
17                
18 7       7   171 use Carp;
  7           9  
  7           410  
19                
20 7       7   28 use Net::DNS::DomainName;
  7           8  
  7           231  
21 7       7   26 use Net::DNS::Parameters qw(:class :type :rcode);
  7           10  
  7           1212  
22                
23 7       7   40 use constant SYMLINK => defined(&CORE::readlink); # Except Win32, VMS, RISC OS
  7           10  
  7           527  
24                
25 7       7   32 use constant ANY => classbyname q(ANY);
  7           9  
  7           31  
26 7       7   28 use constant TSIG => typebyname q(TSIG);
  7           23  
  7           24  
27                
28               eval { require Digest::HMAC };
29               eval { require Digest::MD5 };
30               eval { require Digest::SHA };
31               eval { require MIME::Base64 };
32                
33                
34               sub _decode_rdata { ## decode rdata from wire-format octet string
35 27       27   73 my ( $self, $data, $offset ) = @_;
36                
37 27           93 my $limit = $offset + $self->{rdlength};
38 27           132 ( $self->{algorithm}, $offset ) = Net::DNS::DomainName->decode( $data, $offset );
39                
40               # Design decision: Use 32 bits, which will work until the end of time()!
41 27           99 @{$self}{qw(time_signed fudge)} = unpack "\@$offset xxN n", $$data;
  27           84  
42 27           50 $offset += 8;
43                
44 27           97 my $mac_size = unpack "\@$offset n", $$data;
45 27           113 $self->{macbin} = unpack "\@$offset xx a$mac_size", $$data;
46 27           54 $offset += $mac_size + 2;
47                
48 27           92 @{$self}{qw(original_id error)} = unpack "\@$offset nn", $$data;
  27           122  
49 27           57 $offset += 4;
50                
51 27           75 my $other_size = unpack "\@$offset n", $$data;
52 27           97 $self->{other} = unpack "\@$offset xx a$other_size", $$data;
53 27           45 $offset += $other_size + 2;
54                
55 27 100         287 croak('misplaced or corrupt TSIG') unless $limit == length $$data;
56 26           185 my $raw = substr $$data, 0, $self->{offset}++;
57 26           61 $self->{rawref} = \$raw;
58 26           96 return;
59               }
60                
61                
62               sub _encode_rdata { ## encode rdata as wire-format octet string
63 36       36   55 my $self = shift;
64                
65 36           48 my $offset = shift;
66 36           48 my $undef = shift;
67 36           41 my $message = shift;
68 36           79 my $macbin = $self->macbin;
69 36 100         72 unless ($macbin) {
70 31           78 my $sigdata = $self->sig_data($message); # form data to be signed
71 30           85 $macbin = $self->macbin( $self->_mac_function($sigdata) );
72               }
73                
74 35           82 my $rdata = $self->{algorithm}->canonical;
75                
76               # Design decision: Use 32 bits, which will work until the end of time()!
77 35           79 $rdata .= pack 'xxN n', $self->time_signed, $self->fudge;
78                
79 35           79 $rdata .= pack 'na*', length($macbin), $macbin;
80                
81 35           74 $rdata .= pack 'nn', $self->original_id, $self->{error};
82                
83 35           61 my $other = $self->other;
84 35           94 $rdata .= pack 'na*', length($other), $other;
85                
86 35           104 return $rdata;
87               }
88                
89                
90               sub _defaults { ## specify RR attribute default values
91 6       6   10 my $self = shift;
92                
93 6           20 $self->algorithm(157);
94 6           38 $self->class('ANY');
95 6           17 $self->error(0);
96 6           15 $self->fudge(300);
97 6           14 $self->other('');
98 6           12 return;
99               }
100                
101                
102               sub _size { ## estimate encoded size
103 2       2   346 my $self = shift;
104 2           16 my $clone = bless {%$self}, ref($self); # shallow clone
105 2           19 return length $clone->encode( 0, undef, Net::DNS::Packet->new() );
106               }
107                
108                
109               sub encode { ## override RR method
110 35       35 1 72 my ( $self, @argument ) = @_;
111 35           103 my $kname = $self->{owner}->encode(); # uncompressed key name
112 35     100     57 my $rdata = eval { $self->_encode_rdata(@argument) } || '';
113 35           176 return pack 'a* n2 N n a*', $kname, TSIG, ANY, 0, length $rdata, $rdata;
114               }
115                
116                
117               sub string { ## override RR method
118 2       2 1 7 my $self = shift;
119 2           13 my $owner = $self->{owner}->string;
120 2           7 my $type = $self->type;
121 2           4 my $algorithm = $self->algorithm;
122 2           3 my $time_signed = $self->time_signed;
123 2           3 my $fudge = $self->fudge;
124 2           3 my $signature = $self->mac;
125 2           4 my $original_id = $self->original_id;
126 2           4 my $error = $self->error;
127 2           4 my $other = $self->other;
128                
129 2           11 return <<"QQ";
130               ; $owner $type
131               ; algorithm: $algorithm
132               ; time signed: $time_signed fudge: $fudge
133               ; signature: $signature
134               ; original id: $original_id
135               ; $error $other
136               QQ
137               }
138                
139                
140 120       120 1 250 sub algorithm { return &_algorithm; }
141                
142                
143               sub key {
144 26       26 1 64 my ( $self, @argument ) = @_;
145 26 100         62 return MIME::Base64::encode( $self->keybin(), "" ) unless scalar @argument;
146 25           127 return $self->keybin( MIME::Base64::decode( join "", @argument ) );
147               }
148                
149                
150 27       27 1 45 sub keybin { return &_keybin; }
151                
152                
153               sub time_signed {
154 129       129 1 739 my ( $self, @value ) = @_;
155 129           181 for (@value) { $self->{time_signed} = 0 + $_ }
  1           3  
156 129 100         423 return $self->{time_signed} ? $self->{time_signed} : ( $self->{time_signed} = time() );
157               }
158                
159                
160               sub fudge {
161 137       137 1 957 my ( $self, @value ) = @_;
162 137           185 for (@value) { $self->{fudge} = 0 + $_ }
  10           19  
163 137     100     501 return $self->{fudge} || 0;
164               }
165                
166                
167               sub mac {
168 5       5 1 521 my ( $self, @value ) = @_;
169 5 100         11 return MIME::Base64::encode( $self->macbin(), "" ) unless scalar @value;
170 1           6 return $self->macbin( MIME::Base64::decode( join "", @value ) );
171               }
172                
173                
174               sub macbin {
175 152       152 1 832 my ( $self, @value ) = @_;
176 152           223 for (@value) { $self->{macbin} = $_ }
  33           63  
177 152     100     534 return $self->{macbin} || "";
178               }
179                
180                
181               sub prior_mac {
182 3       3 1 244 my ( $self, @value ) = @_;
183 3 100         18 return MIME::Base64::encode( $self->prior_macbin(), "" ) unless scalar @value;
184 1           6 return $self->prior_macbin( MIME::Base64::decode( join "", @value ) );
185               }
186                
187                
188               sub prior_macbin {
189 94       94 1 140 my ( $self, @value ) = @_;
190 94           127 for (@value) { $self->{prior_macbin} = $_ }
  32           68  
191 94     100     235 return $self->{prior_macbin} || "";
192               }
193                
194                
195               sub request_mac {
196 3       3 1 208 my ( $self, @value ) = @_;
197 3 100         9 return MIME::Base64::encode( $self->request_macbin(), "" ) unless scalar @value;
198 1           5 return $self->request_macbin( MIME::Base64::decode( join "", @value ) );
199               }
200                
201                
202               sub request_macbin {
203 61       61 1 99 my ( $self, @value ) = @_;
204 61           84 for (@value) { $self->{request_macbin} = $_ }
  17           41  
205 61     100     188 return $self->{request_macbin} || "";
206               }
207                
208                
209               sub original_id {
210 95       95 1 171 my ( $self, @value ) = @_;
211 95           139 for (@value) { $self->{original_id} = 0 + $_ }
  42           102  
212 95     100     336 return $self->{original_id} || 0;
213               }
214                
215                
216               sub error {
217 48       48 1 274 my ( $self, @value ) = @_;
218 48           67 for (@value) {
219 20           170 my $error = $self->{error} = rcodebyname($_);
220 20 100         46 $self->other( time() ) if $error == 18;
221               }
222 48     100     142 return rcodebyval( $self->{error} || '' );
223               }
224                
225                
226               sub other {
227 90       90 1 130 my ( $self, @value ) = @_;
228 90 100         129 for (@value) { $self->{other} = $_ ? pack( 'xxN', $_ ) : '' }
  9           75  
229 90 100         216 return $self->{other} ? unpack( 'N', $self->{other} ) : '';
230               }
231                
232                
233 1       1 0 225 sub other_data { return &other; } # uncoverable pod
234                
235                
236               sub sig_function {
237 51       51 1 676 my ( $self, @value ) = @_;
238 51           80 for (@value) { $self->{sig_function} = $_ }
  42           77  
239 51           90 return $self->{sig_function};
240               }
241                
242 1       1 0 232 sub sign_func { return &sig_function; } # uncoverable pod
243                
244                
245               sub sig_data {
246 61       61 1 106 my ( $self, $message ) = @_;
247                
248 61 100         215 if ( ref($message) ) {
249 59 100         233 die 'missing packet reference' unless $message->isa('Net::DNS::Packet');
250 58           72 my @unsigned = grep { ref($_) ne ref($self) } @{$message->{additional}};
  80           211  
  58           147  
251 58           137 local $message->{additional} = \@unsigned; # remake header image
252 58           152 my @part = qw(question answer authority additional);
253 58           88 my @size = map { scalar @{$message->{$_}} } @part;
  232           236  
  232           370  
254 58 100         131 if ( my $rawref = $self->{rawref} ) {
255 16           29 delete $self->{rawref};
256 16           47 my $hbin = pack 'n6', $self->original_id, $message->{status}, @size;
257 16           364 $message = join '', $hbin, substr $$rawref, length $hbin;
258               } else {
259 42           116 my $data = $message->encode;
260 42           96 my $id = $message->header->id;
261 42           138 my $hbin = pack 'n6', $id, $message->{status}, @size;
262 42           129 $message = join '', $hbin, substr $data, length $hbin;
263 42           109 $self->original_id($id);
264               }
265               }
266                
267               # Design decision: Use 32 bits, which will work until the end of time()!
268 60           150 my $time = pack 'xxN n', $self->time_signed, $self->fudge;
269                
270               # Insert the prior MAC if present (multi-packet message).
271 60 100         154 $self->prior_macbin( $self->{link}->macbin ) if $self->{link};
272 60           124 my $prior_macbin = $self->prior_macbin;
273 60 100         209 return pack 'na* a* a*', length($prior_macbin), $prior_macbin, $message, $time if $prior_macbin;
274                
275               # Insert the request MAC if present (used to validate responses).
276 42           78 my $req_mac = $self->request_macbin;
277 42 100         95 my $sigdata = $req_mac ? pack( 'na*', length($req_mac), $req_mac ) : '';
278                
279 42     100     125 $sigdata .= $message || '';
280                
281 42           110 my $kname = $self->{owner}->canonical; # canonical key name
282 42           178 $sigdata .= pack 'a* n N', $kname, ANY, 0;
283                
284 42           125 $sigdata .= $self->{algorithm}->canonical; # canonical algorithm name
285                
286 41           442 $sigdata .= $time;
287                
288 41           84 $sigdata .= pack 'n', $self->{error};
289                
290 41           94 my $other = $self->other;
291 41           88 $sigdata .= pack 'na*', length($other), $other;
292                
293 41           116 return $sigdata;
294               }
295                
296                
297               sub create {
298 54       54 1 5370 my ( $class, $karg, @argument ) = @_;
299 54 100         493 croak 'argument undefined' unless defined $karg;
300                
301 52 100         159 if ( ref($karg) ) {
    100            
302 32 100         232 if ( $karg->isa('Net::DNS::Packet') ) {
    100            
    100            
303 5           18 my $sigrr = $karg->sigrr;
304 5 100         340 croak 'no TSIG in request packet' unless defined $sigrr;
305 3           41 return Net::DNS::RR->new( # ( request, options )
306               name => $sigrr->name,
307               type => 'TSIG',
308               algorithm => $sigrr->algorithm,
309               request_macbin => $sigrr->macbin,
310               @argument
311               );
312                
313               } elsif ( ref($karg) eq __PACKAGE__ ) {
314 16           53 my $tsig = $karg->_chain;
315 16           38 $tsig->{macbin} = undef;
316 16           45 return $tsig;
317                
318               } elsif ( ref($karg) eq 'Net::DNS::RR::KEY' ) {
319 10           53 return Net::DNS::RR->new(
320               name => $karg->name,
321               type => 'TSIG',
322               algorithm => $karg->algorithm,
323               key => $karg->key,
324               @argument
325               );
326               }
327                
328               } elsif ( ( scalar(@argument) % 2 ) == 0 ) {
329 19           80 require File::Spec; # ( keyfile, options )
330 19           1827 require Net::DNS::ZoneFile;
331 19           272 my ($keypath) = SYMLINK ? grep( {$_} readlink($karg), $karg ) : $karg;
  38           95  
332 19           409 my ( $vol, $dir, $name ) = File::Spec->splitpath($keypath);
333 19           69 $name =~ m/^K([^+]+)\+\d+\+(\d+)\./; # BIND dnssec-keygen
334 19           41 my ( $keyname, $keytag ) = ( $1, $2 );
335                
336 19           107 my $keyfile = Net::DNS::ZoneFile->new($karg);
337 18           28 my ( $algorithm, $secret );
338 18           51 while ( $keyfile->_getline ) {
339 65 100         151 /^key "([^"]+)"/ and $keyname = $1; # BIND tsig key
340 65 100         170 /algorithm ([^;]+);/ and $algorithm = $1;
341 65 100         152 /secret "([^"]+)";/ and $secret = $1;
342                
343 65 100         101 /^Algorithm:/ and ( undef, $algorithm ) = split; # BIND dnssec private key
344 65 100         137 /^Key:/ and ( undef, $secret ) = split;
345                
346 65 100         216 next unless /\bIN\s+KEY\b/; # BIND dnssec public key
347 3           25 my $keyrr = Net::DNS::RR->new($_);
348 3 100   100     10 carp "$karg does not appear to be a BIND dnssec public key"
349               unless $keyrr->keytag == ( $keytag || 0 );
350 3           35 return $class->create( $keyrr, @argument );
351               }
352                
353 15           71 foreach ( $keyname, $algorithm, $secret ) {
354 44 100         206 croak 'key file incompatible with TSIG' unless $_;
355               }
356                
357 14           99 return Net::DNS::RR->new(
358               name => $keyname,
359               type => 'TSIG',
360               algorithm => $algorithm,
361               key => $secret,
362               @argument
363               );
364               }
365                
366 2           366 croak "Usage: $class->create( \$keyfile, \@options )";
367               }
368                
369                
370               sub verify {
371 40       40 1 98 my ( $self, $data, @link ) = @_;
372 40           58 my $fail = undef;
373                
374 40 100         81 if ( scalar @link ) {
375                
376 31           52 my $link = shift @link;
377 31 100         84 unless ( ref($link) ) {
378 1           2003 $self->error('BADSIG'); # (multi-packet)
379 1           4 return $fail;
380               }
381                
382 30           106 my $signerkey = lc( join '+', $self->name, $self->algorithm );
383 30 100         231 if ( $link->isa('Net::DNS::Packet') ) {
    100            
384 13           68 my $request = $link->sigrr; # request TSIG
385 13           49 my $rqstkey = lc( join '+', $request->name, $request->algorithm );
386 13 100         38 $self->error('BADKEY') unless $signerkey eq $rqstkey;
387 13           39 $self->request_macbin( $request->macbin );
388                
389               } elsif ( $link->isa(__PACKAGE__) ) {
390 16           45 my $priorkey = lc( join '+', $link->name, $link->algorithm );
391 16 100         381 $self->error('BADKEY') unless $signerkey eq $priorkey;
392 16           61 $self->prior_macbin( $link->macbin );
393                
394               } else {
395 1           183 croak 'Usage: $tsig->verify( $reply, $query )';
396               }
397               }
398                
399 38 100         126 return $fail if $self->{error};
400                
401 30           87 my $sigdata = $self->sig_data($data); # form data to be verified
402 29           81 my $tsigmac = $self->_mac_function($sigdata);
403 29           526 my $tsig = $self->_chain;
404                
405 29           57 my $macbin = $self->macbin;
406 29           46 my $maclen = length $macbin;
407 29 100         92 $self->error('BADSIG') if $macbin ne substr $tsigmac, 0, $maclen;
408                
409 29           58 my $minlen = length($tsigmac) >> 1; # per RFC4635, 3.1
410 29 100   100     124 $self->error('BADTRUNC') if $maclen < $minlen or $maclen > length $tsigmac;
411 29 100         62 $self->error('BADTRUNC') if $maclen < 10;
412                
413 29           55 my $time_signed = $self->time_signed;
414 29 100         82 if ( abs( time() - $time_signed ) > $self->fudge ) {
415 1           3 $self->error('BADTIME');
416 1           2 $self->other($time_signed);
417               }
418                
419 29 100         162 return $self->{error} ? $fail : $tsig;
420               }
421                
422 25       25 1 55 sub vrfyerrstr { return shift->error; }
423                
424                
425               ########################################
426                
427               {
428               my @algbyname = (
429               'HMAC-MD5.SIG-ALG.REG.INT' => 157, # numbers are from ISC BIND keygen
430               'HMAC-SHA1' => 161, # and not blessed by IANA
431               'HMAC-SHA224' => 162,
432               'HMAC-SHA256' => 163,
433               'HMAC-SHA384' => 164,
434               'HMAC-SHA512' => 165,
435               );
436                
437               my @algalias = (
438               'HMAC-MD5' => 157,
439               'HMAC-SHA' => 161,
440               );
441                
442               my %algbyval = reverse @algbyname;
443                
444               my @algrehash = map { /^\d/ ? ($_) x 3 : uc($_) } @algbyname, @algalias;
445               foreach (@algrehash) { s/[\W_]//g; } # strip non-alphanumerics
446               my %algbyname = @algrehash; # work around broken cperl
447                
448               sub _algbyname {
449 43       43   94 my $key = uc shift; # synthetic key
450 43           188 $key =~ s/[\W_]//g; # strip non-alphanumerics
451 43           180 return $algbyname{$key};
452               }
453                
454               sub _algbyval {
455 41       41   60 my $value = shift;
456 41           87 return $algbyval{$value};
457               }
458               }
459                
460                
461               {
462               my %digest = (
463               '157' => ['Digest::MD5'],
464               '161' => ['Digest::SHA'],
465               '162' => ['Digest::SHA', 224, 64],
466               '163' => ['Digest::SHA', 256, 64],
467               '164' => ['Digest::SHA', 384, 128],
468               '165' => ['Digest::SHA', 512, 128],
469               );
470                
471                
472               my %keytable;
473                
474               sub _algorithm { ## install sig function in key table
475 120       120   168 my $self = shift;
476                
477 120 100         254 if ( my $algname = shift ) {
478                
479 43 100         82 unless ( my $digtype = _algbyname($algname) ) {
480 2           8 $self->{algorithm} = Net::DNS::DomainName->new($algname);
481                
482               } else {
483 41           112 $algname = _algbyval($digtype);
484 41           193 $self->{algorithm} = Net::DNS::DomainName->new($algname);
485                
486 41           63 my ( $hash, @param ) = @{$digest{$digtype}};
  41           106  
487 41           75 my ( undef, @block ) = @param;
488 41           211 my $digest = $hash->new(@param);
489               my $function = sub {
490 93       93   23857 my $hmac = Digest::HMAC->new( shift, $digest, @block );
491 93           2861 $hmac->add(shift);
492 93           991 return $hmac->digest;
493 41           494 };
494                
495 41           133 $self->sig_function($function);
496                
497 41     100     177 my $keyname = ( $self->{owner} || return )->canonical;
498 35           254 $keytable{$keyname}{digest} = $function;
499               }
500               }
501                
502 114 100         394 return defined wantarray ? $self->{algorithm}->name : undef;
503               }
504                
505                
506               sub _keybin { ## install key in key table
507 27       27   52 my ( $self, @argument ) = @_;
508 27 100         154 croak 'access to TSIG key material denied' unless scalar @argument;
509 26     100     64 my $keyref = $keytable{$self->{owner}->canonical} ||= {};
510 26           43 my $private = shift @argument; # closure keeps private key private
511               $keyref->{key} = sub {
512 59       59   72 my $function = $keyref->{digest};
513 59           129 return &$function( $private, @_ );
514 26           143 };
515 26           77 return;
516               }
517                
518                
519               sub _mac_function { ## apply keyed hash function to argument
520 59       59   99 my ( $self, @argument ) = @_;
521 59           142 my $owner = $self->{owner}->canonical;
522 59 100         190 $self->algorithm( $self->algorithm ) unless $keytable{$owner}{digest};
523 59           94 my $keyref = $keytable{$owner};
524 59 100         114 $keyref->{digest} = $self->sig_function unless $keyref->{digest};
525 59           88 my $function = $keyref->{key};
526 59           116 return &$function(@argument);
527               }
528               }
529                
530                
531               # _chain() creates a new TSIG object linked to the original
532               # RR, for the purpose of signing multi-message transfers.
533                
534               sub _chain {
535 45       45   73 my $self = shift;
536 45           102 $self->{link} = undef;
537 45           430 return bless {%$self, link => $self}, ref($self);
538               }
539                
540               ########################################
541                
542                
543               1;
544               __END__