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