File Coverage

blib/lib/Net/DNS/RR/SIG.pm
Criterion Covered Total %
statement 167 167 100.0
branch 48 48 100.0
condition 14 14 100.0
subroutine 43 43 100.0
pod 14 17 100.0
total 286 289 100.0


line stmt bran cond sub pod time code
1             package Net::DNS::RR::SIG;
2              
3 2     2   17 use strict;
  2         4  
  2         109  
4 2     2   11 use warnings;
  2         6  
  2         245  
5             our $VERSION = (qw$Id: SIG.pm 2042 2025-12-24 10:23:11Z willem $)[2];
6              
7 2     2   16 use base qw(Net::DNS::RR);
  2         5  
  2         232  
8              
9              
10             =head1 NAME
11              
12             Net::DNS::RR::SIG - DNS SIG resource record
13              
14             =cut
15              
16 2     2   13 use integer;
  2         50  
  2         38  
17              
18 2     2   101 use Carp;
  2         3  
  2         185  
19 2     2   650 use Time::Local;
  2         2496  
  2         165  
20              
21 2     2   16 use Net::DNS::Parameters qw(:type);
  2         4  
  2         370  
22              
23 2     2   15 use constant DEBUG => 0;
  2         5  
  2         218  
24              
25 2     2   15 use constant UTIL => defined eval { require Scalar::Util; };
  2         4  
  2         5  
  2         194  
26              
27             eval { require MIME::Base64 };
28              
29             ## IMPORTANT: MUST NOT include crypto packages in metadata (strong crypto prohibited in many territories)
30 2     2   11 use constant DNSSEC => defined $INC{'Net/DNS/SEC.pm'}; ## Discover how we got here, without exposing any crypto
  2         5  
  2         3727  
31              
32             my @algorithms;
33             my @deprecated;
34             if (DNSSEC) {
35             foreach my $class ( map {"Net::DNS::SEC::$_"} qw(Private DSA RSA ECDSA EdDSA Digest SM2) ) {
36             my @index = eval join '', qw(r e q u i r e), " $class; ${class}::_index()"; ## no critic
37             push @algorithms, map { ( $_ => $class ) } @index;
38             push @deprecated, eval "${class}::_deprecate()"; ## no critic
39             }
40             croak 'Net::DNS::SEC version not supported' unless scalar(@algorithms);
41             }
42              
43             my %DNSSEC_verify = @algorithms;
44             my %DNSSEC_siggen = @algorithms;
45              
46             my @field = qw(typecovered algorithm labels orgttl sigexpiration siginception keytag);
47              
48              
49             sub _decode_rdata { ## decode rdata from wire-format octet string
50 2     2   5 my ( $self, $data, $offset, @opaque ) = @_;
51              
52 2         5 my $limit = $offset + $self->{rdlength};
53 2         9 @{$self}{@field} = unpack "\@$offset n C2 N3 n", $$data;
  2         40  
54 2         11 ( $self->{signame}, $offset ) = Net::DNS::DomainName->decode( $data, $offset + 18, @opaque );
55 2         11 $self->{sigbin} = substr $$data, $offset, $limit - $offset;
56              
57 2 100       273 croak('misplaced or corrupt SIG') unless $limit == length $$data;
58 1         5 my $raw = substr $$data, 0, $self->{offset}++;
59 1         3 $self->{rawref} = \$raw;
60 1         5 return;
61             }
62              
63              
64             sub _encode_rdata { ## encode rdata as wire-format octet string
65 7     7   20 my ( $self, $offset, @opaque ) = @_;
66              
67 7         15 my $signame = $self->{signame};
68              
69 7         10 if ( DNSSEC && !$self->{sigbin} ) {
70             my ( undef, $packet ) = @opaque;
71             my $private = delete $self->{private}; # one shot is all you get
72             my $sigdata = $self->_CreateSigData($packet);
73             $self->_CreateSig( $sigdata, $private || die 'missing key reference' );
74             }
75              
76 7         14 return pack 'n C2 N3 n a* a*', @{$self}{@field}, $signame->canonical, $self->sigbin;
  7         47  
77             }
78              
79              
80             sub _format_rdata { ## format rdata portion of RR string.
81 4     4   8 my $self = shift;
82              
83 4   100     17 my $sname = $self->{signame} || return '';
84 3         9 my @sig64 = split /\s+/, MIME::Base64::encode( $self->sigbin );
85 3         10 my @rdata = ( map( { $self->$_ } @field ), $sname->string, @sig64 );
  21         57  
86 3         43 return @rdata;
87             }
88              
89              
90             sub _parse_rdata { ## populate RR from rdata in argument list
91 2     2   12 my ( $self, @argument ) = @_;
92              
93 2         8 foreach ( @field, qw(signame) ) { $self->$_( shift @argument ) }
  16         57  
94 2         8 $self->signature(@argument);
95 2         8 return;
96             }
97              
98              
99             sub _defaults { ## specify RR attribute default values
100 2     2   7 my $self = shift;
101              
102 2         25 $self->class('ANY');
103 2         10 $self->typecovered('TYPE0');
104 2         8 $self->algorithm(1);
105 2         8 $self->labels(0);
106 2         7 $self->orgttl(0);
107 2         8 $self->sigval(10);
108 2         5 return;
109             }
110              
111              
112             sub typecovered {
113 10     10 0 38 my ( $self, @value ) = @_; # uncoverable pod
114 10         25 for (@value) { $self->{typecovered} = typebyname($_) }
  5         19  
115 10         20 my $typecode = $self->{typecovered};
116 10 100       82 return defined $typecode ? typebyval($typecode) : undef;
117             }
118              
119              
120             sub algorithm {
121 22     22 1 2312 my ( $self, $arg ) = @_;
122              
123 22 100       68 unless ( ref($self) ) { ## class method or simple function
124 3         7 my $argn = pop;
125 3 100       20 return $argn =~ /[^0-9]/ ? _algbyname($argn) : _algbyval($argn);
126             }
127              
128 19 100       84 return $self->{algorithm} unless defined $arg;
129 9 100       35 return _algbyval( $self->{algorithm} ) if $arg =~ /MNEMONIC/i;
130 8         21 return $self->{algorithm} = _algbyname($arg);
131             }
132              
133              
134             sub labels {
135 10     10 0 1379 return shift->{labels} = 0; # uncoverable pod
136             }
137              
138              
139             sub orgttl {
140 10     10 0 1333 return shift->{orgttl} = 0; # uncoverable pod
141             }
142              
143              
144             sub sigexpiration {
145 10     10 1 1333 my ( $self, @value ) = @_;
146 10         25 for (@value) { $self->{sigexpiration} = _string2time($_) }
  3         8  
147 10         154 my $time = $self->{sigexpiration};
148 10 100 100     56 return unless defined wantarray && defined $time;
149 6         17 return UTIL ? Scalar::Util::dualvar( $time, _time2string($time) ) : _time2string($time);
150             }
151              
152             sub siginception {
153 10     10 1 2929 my ( $self, @value ) = @_;
154 10         23 for (@value) { $self->{siginception} = _string2time($_) }
  3         7  
155 10         128 my $time = $self->{siginception};
156 10 100 100     52 return unless defined wantarray && defined $time;
157 6         30 return UTIL ? Scalar::Util::dualvar( $time, _time2string($time) ) : _time2string($time);
158             }
159              
160 2     2 1 949 sub sigex { return &sigexpiration; } ## historical
161              
162 2     2 1 873 sub sigin { return &siginception; } ## historical
163              
164             sub sigval {
165 2     2 1 5 my ( $self, @value ) = @_;
166 2     2   20 no integer;
  2         5  
  2         13  
167 2         6 ( $self->{sigval} ) = map { int( 60.0 * $_ ) } @value;
  2         11  
168 2         5 return;
169             }
170              
171              
172             sub keytag {
173 8     8 1 2166 my ( $self, @value ) = @_;
174 8         21 for (@value) { $self->{keytag} = 0 + $_ }
  3         8  
175 8   100     90 return $self->{keytag} || 0;
176             }
177              
178              
179             sub signame {
180 5     5 1 1709 my ( $self, @value ) = @_;
181 5         14 for (@value) { $self->{signame} = Net::DNS::DomainName2535->new($_) }
  3         25  
182 5 100       38 return $self->{signame} ? $self->{signame}->name : undef;
183             }
184              
185              
186             sub sig {
187 7     7 1 1003 my ( $self, @value ) = @_;
188 7 100       47 return MIME::Base64::encode( $self->sigbin(), "" ) unless scalar @value;
189 3         24 return $self->sigbin( MIME::Base64::decode( join "", @value ) );
190             }
191              
192              
193             sub sigbin {
194 19     19 1 158 my ( $self, @value ) = @_;
195 19         47 for (@value) { $self->{sigbin} = $_ }
  5         18  
196 19   100     169 return $self->{sigbin} || "";
197             }
198              
199              
200 5     5 1 1791 sub signature { return &sig; }
201              
202              
203             sub create {
204 2     2 1 432 unless (DNSSEC) {
205 2         639 croak qq[No "use Net::DNS::SEC" declaration in application code];
206             } else {
207             my ( $class, $data, $priv_key, %etc ) = @_;
208              
209             my $private = ref($priv_key) ? $priv_key : ( Net::DNS::SEC::Private->new($priv_key) );
210             croak 'Unable to parse private key' unless ref($private) eq 'Net::DNS::SEC::Private';
211              
212             my $self = Net::DNS::RR->new(
213             type => 'SIG',
214             typecovered => 'TYPE0',
215             siginception => time(),
216             algorithm => $private->algorithm,
217             keytag => $private->keytag,
218             signame => $private->signame,
219             );
220              
221             while ( my ( $attribute, $value ) = each %etc ) {
222             $self->$attribute($value);
223             }
224              
225             $self->{sigexpiration} = $self->{siginception} + $self->{sigval}
226             unless $self->{sigexpiration};
227              
228             $self->_CreateSig( $self->_CreateSigData($data), $private ) if $data;
229              
230             $self->{private} = $private unless $data; # mark packet for SIG0 generation
231             return $self;
232             }
233             }
234              
235              
236             sub verify {
237              
238             # Reminder...
239              
240             # $dataref may be either a data string or a reference to a
241             # Net::DNS::Packet object.
242             #
243             # $keyref is either a key object or a reference to an array
244             # of keys.
245              
246 2     2 1 474 unless (DNSSEC) {
247 2         306 croak qq[No "use Net::DNS::SEC" declaration in application code];
248             } else {
249             my ( $self, $dataref, $keyref ) = @_;
250              
251             if ( my $isa = ref($dataref) ) {
252             print '$dataref argument is ', $isa, "\n" if DEBUG;
253             croak '$dataref must be scalar or a Net::DNS::Packet'
254             unless $isa =~ /Net::DNS/ && $dataref->isa('Net::DNS::Packet');
255             }
256              
257             print '$keyref argument is of class ', ref($keyref), "\n" if DEBUG;
258             if ( ref($keyref) eq "ARRAY" ) {
259              
260             # We will iterate over the supplied key list and
261             # return when there is a successful verification.
262             # If not, continue so that we survive key-id collision.
263              
264             print "Iterating over ", scalar(@$keyref), " keys\n" if DEBUG;
265             my @error;
266             foreach my $keyrr (@$keyref) {
267             my $result = $self->verify( $dataref, $keyrr );
268             return $result if $result;
269             my $error = $self->{vrfyerrstr};
270             my $keyid = $keyrr->keytag;
271             push @error, "key $keyid: $error";
272             print "key $keyid: $error\n" if DEBUG;
273             next;
274             }
275              
276             $self->{vrfyerrstr} = join "\n", @error;
277             return 0;
278              
279             } elsif ( $keyref->isa('Net::DNS::RR::DNSKEY') ) {
280              
281             print "Validating using key with keytag: ", $keyref->keytag, "\n" if DEBUG;
282              
283             } else {
284             croak join ' ', ref($keyref), 'can not be used as SIG0 key';
285             }
286              
287             croak "SIG typecovered is TYPE$self->{typecovered}" if $self->{typecovered};
288              
289             if (DEBUG) {
290             print "\n ---------------------- SIG DEBUG ----------------------";
291             print "\n SIG:\t", $self->string;
292             print "\n KEY:\t", $keyref->string;
293             print "\n -------------------------------------------------------\n";
294             }
295              
296             $self->{vrfyerrstr} = '';
297             unless ( $self->algorithm == $keyref->algorithm ) {
298             $self->{vrfyerrstr} = 'algorithm does not match';
299             return 0;
300             }
301              
302             unless ( $self->keytag == $keyref->keytag ) {
303             $self->{vrfyerrstr} = 'keytag does not match';
304             return 0;
305             }
306              
307             # The data that is to be verified
308             my $sigdata = $self->_CreateSigData($dataref);
309              
310             my $verified = $self->_VerifySig( $sigdata, $keyref ) || return 0;
311              
312             # time to do some time checking.
313             my $t = time;
314              
315             if ( _ordered( $self->{sigexpiration}, $t ) ) {
316             $self->{vrfyerrstr} = join ' ', 'Signature expired at', $self->sigexpiration;
317             return 0;
318             } elsif ( _ordered( $t, $self->{siginception} ) ) {
319             $self->{vrfyerrstr} = join ' ', 'Signature valid from', $self->siginception;
320             return 0;
321             }
322              
323             return 1;
324             }
325             } #END verify
326              
327              
328             sub vrfyerrstr {
329 2     2 1 2208 return shift->{vrfyerrstr};
330             }
331              
332              
333             ########################################
334              
335             {
336             my @algbyname = (
337             'DELETE' => 0, # [RFC4034][RFC4398][RFC8078]
338             'RSAMD5' => 1, # [RFC3110][RFC4034]
339             'DH' => 2, # [RFC2539]
340             'DSA' => 3, # [RFC3755][RFC2536]
341             ## Reserved => 4, # [RFC6725]
342             'RSASHA1' => 5, # [RFC3110][RFC4034]
343             'DSA-NSEC3-SHA1' => 6, # [RFC5155]
344             'RSASHA1-NSEC3-SHA1' => 7, # [RFC5155]
345             'RSASHA256' => 8, # [RFC5702]
346             ## Reserved => 9, # [RFC6725]
347             'RSASHA512' => 10, # [RFC5702]
348             ## Reserved => 11, # [RFC6725]
349             'ECC-GOST' => 12, # [RFC5933]
350             'ECDSAP256SHA256' => 13, # [RFC6605]
351             'ECDSAP384SHA384' => 14, # [RFC6605]
352             'ED25519' => 15, # [RFC8080]
353             'ED448' => 16, # [RFC8080]
354             'SM2SM3' => 17, # [RFC9563]
355             'ECC-GOST12' => 23, # [RFC9558]
356              
357             'INDIRECT' => 252, # [RFC4034]
358             'PRIVATEDNS' => 253, # [RFC4034]
359             'PRIVATEOID' => 254, # [RFC4034]
360             ## Reserved => 255, # [RFC4034]
361             );
362              
363             my %algbyval = reverse @algbyname;
364              
365             foreach (@algbyname) { s/[\W_]//g; } # strip non-alphanumerics
366             my @algrehash = map { /^\d/ ? ($_) x 3 : uc($_) } @algbyname;
367             my %algbyname = @algrehash; # work around broken cperl
368              
369             sub _algbyname {
370 9     9   21 my $arg = shift;
371 9         23 my $key = uc $arg; # synthetic key
372 9         33 $key =~ s/[\W_]//g; # strip non-alphanumerics
373 9         24 my $val = $algbyname{$key};
374 9 100       46 return $val if defined $val;
375 2 100       298 return $key =~ /^\d/ ? $arg : croak qq[unknown algorithm $arg];
376             }
377              
378             sub _algbyval {
379 3     3   8 my $value = shift;
380 3   100     29 return $algbyval{$value} || return $value;
381             }
382             }
383              
384              
385             {
386             my %siglen = (
387             1 => 128,
388             3 => 41,
389             5 => 256,
390             6 => 41,
391             7 => 256,
392             8 => 256,
393             10 => 256,
394             12 => 64,
395             13 => 64,
396             14 => 96,
397             15 => 64,
398             16 => 114,
399             );
400              
401             sub _size { ## estimate encoded size
402 2     2   934 my $self = shift;
403 2         30 my $clone = bless {%$self}, ref($self); # shallow clone
404 2         10 $clone->sigbin( 'x' x $siglen{$self->algorithm} );
405 2         12 return length $clone->encode();
406             }
407             }
408              
409              
410             sub _CreateSigData {
411 1     1   1985 if (DNSSEC) {
412             my ( $self, $message ) = @_;
413              
414             if ( ref($message) ) {
415             die 'missing packet reference' unless $message->isa('Net::DNS::Packet');
416             my @unsigned = grep { ref($_) ne ref($self) } @{$message->{additional}};
417             local $message->{additional} = \@unsigned; # remake header image
418             my @part = qw(question answer authority additional);
419             my @size = map { scalar @{$message->{$_}} } @part;
420             my $rref = delete $self->{rawref};
421             my $data = $rref ? $$rref : $message->encode;
422             my ( $id, $status ) = unpack 'n2', $data;
423             my $hbin = pack 'n6 a*', $id, $status, @size;
424             $message = $hbin . substr $data, length $hbin;
425             }
426              
427             my $sigdata = pack 'n C2 N3 n a*', @{$self}{@field}, $self->{signame}->encode;
428             print "\npreamble\t", unpack( 'H*', $sigdata ), "\nrawdata\t", unpack( 'H100', $message ), " ...\n"
429             if DEBUG;
430             return join '', $sigdata, $message;
431             }
432             }
433              
434              
435             sub _CreateSig {
436 1     1   45 if (DNSSEC) {
437             my ( $self, @argument ) = @_;
438              
439             my $algorithm = $self->algorithm;
440             return eval {
441             my $class = $DNSSEC_siggen{$algorithm};
442             die "algorithm $algorithm not supported\n" unless $class;
443             $self->sigbin( $class->sign(@argument) );
444             } || return croak "${@}signature generation failed";
445             }
446             }
447              
448              
449             sub _VerifySig {
450 1     1   469 if (DNSSEC) {
451             my ( $self, @argument ) = @_;
452              
453             my $algorithm = $self->algorithm;
454             my $returnval = eval {
455             my $class = $DNSSEC_verify{$algorithm};
456             die "algorithm $algorithm not supported\n" unless $class;
457             $class->verify( @argument, $self->sigbin );
458             };
459              
460             unless ($returnval) {
461             $self->{vrfyerrstr} = "${@}signature verification failed";
462             print "\n", $self->{vrfyerrstr}, "\n" if DEBUG;
463             return 0;
464             }
465              
466             # uncoverable branch true # unexpected return value from EVP_DigestVerify
467             croak "internal error in algorithm $algorithm verification" unless $returnval == 1;
468             print "\nalgorithm $algorithm verification successful\n" if DEBUG;
469             return $returnval;
470             }
471             }
472              
473              
474             sub _ordered() { ## irreflexive 32-bit partial ordering
475 12     12   4766 my ( $n1, $n2 ) = @_;
476              
477 12 100       42 return 0 unless defined $n2; # ( any, undef )
478 11 100       33 return 1 unless defined $n1; # ( undef, any )
479              
480             # unwise to assume 64-bit arithmetic, or that 32-bit integer overflow goes unpunished
481 2     2   7047 use integer; # fold, leaving $n2 non-negative
  2         5  
  2         12  
482 10         24 $n1 = ( $n1 & 0xFFFFFFFF ) ^ ( $n2 & 0x80000000 ); # -2**31 <= $n1 < 2**32
483 10         16 $n2 = ( $n2 & 0x7FFFFFFF ); # 0 <= $n2 < 2**31
484              
485 10 100       72 return $n1 < $n2 ? ( $n1 > ( $n2 - 0x80000000 ) ) : ( $n2 < ( $n1 - 0x80000000 ) );
486             }
487              
488              
489             my $y1998 = timegm( 0, 0, 0, 1, 0, 1998 );
490             my $y2026 = timegm( 0, 0, 0, 1, 0, 2026 );
491             my $y2082 = $y2026 << 1;
492             my $y2054 = $y2082 - $y1998;
493             my $m2026 = int( 0x80000000 - $y2026 );
494             my $m2054 = int( 0x80000000 - $y2054 );
495             my $t2082 = int( $y2082 & 0x7FFFFFFF );
496             my $t2100 = 1960058752;
497              
498             sub _string2time { ## parse time specification string
499 14     14   10702 my $arg = shift;
500 14 100       48 return int($arg) if length($arg) < 12;
501 13         95 my ( $y, $m, @dhms ) = unpack 'a4 a2 a2 a2 a2 a2', $arg . '00';
502 13 100       61 if ( $arg lt '20380119031408' ) { # calendar folding
    100          
503 8 100       64 return timegm( reverse(@dhms), $m - 1, $y ) if $y < 2026;
504 1         8 return timegm( reverse(@dhms), $m - 1, $y - 56 ) + $y2026;
505             } elsif ( $y > 2082 ) {
506 4         26 my $z = timegm( reverse(@dhms), $m - 1, $y - 84 ); # expunge 29 Feb 2100
507 4 100       291 return $z < 1456790400 ? $z + $y2054 : $z + $y2054 - 86400;
508             }
509 1         8 return ( timegm( reverse(@dhms), $m - 1, $y - 56 ) + $y2054 ) - $y1998;
510             }
511              
512              
513             sub _time2string { ## format time specification string
514 26     26   10505 my $arg = shift;
515 26         58 my $ls31 = int( $arg & 0x7FFFFFFF );
516 26 100       110 if ( $arg & 0x80000000 ) {
    100          
517              
518 10 100       33 if ( $ls31 > $t2082 ) {
519 8 100       23 $ls31 += 86400 unless $ls31 < $t2100; # expunge 29 Feb 2100
520 8         75 my ( $yy, $mm, @dhms ) = reverse( ( gmtime( $ls31 + $m2054 ) )[0 .. 5] );
521 8         69 return sprintf '%d%02d%02d%02d%02d%02d', $yy + 1984, $mm + 1, @dhms;
522             }
523              
524 2         15 my ( $yy, $mm, @dhms ) = reverse( ( gmtime( $ls31 + $m2026 ) )[0 .. 5] );
525 2         44 return sprintf '%d%02d%02d%02d%02d%02d', $yy + 1956, $mm + 1, @dhms;
526              
527              
528             } elsif ( $ls31 > $y2026 ) {
529 2         14 my ( $yy, $mm, @dhms ) = reverse( ( gmtime( $ls31 - $y2026 ) )[0 .. 5] );
530 2         19 return sprintf '%d%02d%02d%02d%02d%02d', $yy + 1956, $mm + 1, @dhms;
531             }
532              
533 14         96 my ( $yy, $mm, @dhms ) = reverse( ( gmtime $ls31 )[0 .. 5] );
534 14         175 return sprintf '%d%02d%02d%02d%02d%02d', $yy + 1900, $mm + 1, @dhms;
535             }
536              
537             ########################################
538              
539              
540             1;
541             __END__