File Coverage

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


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