File Coverage

blib/lib/Net/DNS/RR/TSIG.pm
Criterion Covered Total %
statement 284 284 100.0
branch 98 98 100.0
condition 25 25 100.0
subroutine 45 45 100.0
pod 21 23 100.0
total 473 475 100.0


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