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