File Coverage

blib/lib/Net/DNS/RR/RRSIG.pm
Criterion Covered Total %
statement 161 161 100.0
branch 46 46 100.0
path n/a
condition 16 16 100.0
subroutine 42 42 100.0
pod 17 17 100.0
total 282 282 100.0


line stmt bran path cond sub pod time code
1               package Net::DNS::RR::RRSIG;
2                
3 2       2   12 use strict;
  2           2  
  2           60  
4 2       2   7 use warnings;
  2           3  
  2           120  
5               our $VERSION = (qw$Id: RRSIG.pm 2042 2025-12-24 10:23:11Z willem $)[2];
6                
7 2       2   9 use base qw(Net::DNS::RR);
  2           17  
  2           210  
8                
9                
10               =head1 NAME
11                
12               Net::DNS::RR::RRSIG - DNS RRSIG resource record
13                
14               =cut
15                
16 2       2   11 use integer;
  2           3  
  2           11  
17                
18 2       2   36 use Carp;
  2           3  
  2           126  
19 2       2   439 use Time::Local;
  2           1439  
  2           96  
20                
21 2       2   10 use Net::DNS::Parameters qw(:type);
  2           3  
  2           234  
22                
23 2       2   11 use constant DEBUG => 0;
  2           2  
  2           202  
24                
25 2       2   9 use constant UTIL => defined eval { require Scalar::Util; };
  2           4  
  2           2  
  2           103  
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   6 use constant DNSSEC => defined $INC{'Net/DNS/SEC.pm'}; ## Discover how we got here, without exposing any crypto
  2           2  
  2           1902  
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               delete @DNSSEC_verify{@deprecated}; ## DNSSEC status per RFC9904
47               delete @DNSSEC_siggen{map { abs($_) } @deprecated};
48                
49               my @field = qw(typecovered algorithm labels orgttl sigexpiration siginception keytag);
50                
51                
52               sub _decode_rdata { ## decode rdata from wire-format octet string
53 334       334   542 my ( $self, $data, $offset, @opaque ) = @_;
54                
55 334           417 my $limit = $offset + $self->{rdlength};
56 334           703 @{$self}{@field} = unpack "\@$offset n C2 N3 n", $$data;
  334           1293  
57 334           771 ( $self->{signame}, $offset ) = Net::DNS::DomainName->decode( $data, $offset + 18, @opaque );
58 334           678 $self->{sigbin} = substr $$data, $offset, $limit - $offset;
59 334           497 return;
60               }
61                
62                
63               sub _encode_rdata { ## encode rdata as wire-format octet string
64 5       5   7 my $self = shift;
65                
66 5           11 my $signame = $self->{signame};
67 5           9 return pack 'n C2 N3 n a* a*', @{$self}{@field}, $signame->canonical, $self->sigbin;
  5           27  
68               }
69                
70                
71               sub _format_rdata { ## format rdata portion of RR string.
72 3       3   5 my $self = shift;
73                
74 3           4 my $signame = $self->{signame};
75 3           5 my @sig64 = split /\s+/, MIME::Base64::encode( $self->sigbin );
76 3           19 my @rdata = ( map( { $self->$_ } @field ), $signame->string, @sig64 );
  21           31  
77 3           67 $rdata[3] .= "\n";
78 3           16 return @rdata;
79               }
80                
81                
82               sub _parse_rdata { ## populate RR from rdata in argument list
83 3       3   10 my ( $self, @argument ) = @_;
84                
85 3           5 foreach ( @field, qw(signame) ) { $self->$_( shift @argument ) }
  24           54  
86 3           8 $self->signature(@argument);
87 3           7 return;
88               }
89                
90                
91               sub _defaults { ## specify RR attribute default values
92 2       2   4 my $self = shift;
93                
94 2           4 $self->sigval(30);
95 2           4 return;
96               }
97                
98                
99               sub typecovered {
100 9       9 1 25 my ( $self, @value ) = @_;
101 9           11 for (@value) { $self->{typecovered} = typebyname($_) }
  4           11  
102 9           14 my $typecode = $self->{typecovered};
103 9 100         29 return defined $typecode ? typebyval($typecode) : undef;
104               }
105                
106                
107               sub algorithm {
108 19       19 1 1657 my ( $self, $arg ) = @_;
109                
110 19 100         48 unless ( ref($self) ) { ## class method or simple function
111 3           8 my $argn = pop;
112 3 100         19 return $argn =~ /[^0-9]/ ? _algbyname($argn) : _algbyval($argn);
113               }
114                
115 16 100         57 return $self->{algorithm} unless defined $arg;
116 8 100         22 return _algbyval( $self->{algorithm} ) if $arg =~ /MNEMONIC/i;
117 7           17 return $self->{algorithm} = _algbyname($arg);
118               }
119                
120                
121               sub labels {
122 9       9 1 1146 my ( $self, @value ) = @_;
123 9           13 for (@value) { $self->{labels} = 0 + $_ }
  4           11  
124 9     100     28 return $self->{labels} || 0;
125               }
126                
127                
128               sub orgttl {
129 9       9 1 1170 my ( $self, @value ) = @_;
130 9           15 for (@value) { $self->{orgttl} = 0 + $_ }
  4           12  
131 9     100     28 return $self->{orgttl} || 0;
132               }
133                
134                
135               sub sigexpiration {
136 11       11 1 1160 my ( $self, @value ) = @_;
137 11           17 for (@value) { $self->{sigexpiration} = _string2time($_) }
  4           8  
138 11           140 my $time = $self->{sigexpiration};
139 11 100   100     42 return unless defined wantarray && defined $time;
140 6           18 return UTIL ? Scalar::Util::dualvar( $time, _time2string($time) ) : _time2string($time);
141               }
142                
143               sub siginception {
144 11       11 1 1172 my ( $self, @value ) = @_;
145 11           18 for (@value) { $self->{siginception} = _string2time($_) }
  4           8  
146 11           156 my $time = $self->{siginception};
147 11 100   100     43 return unless defined wantarray && defined $time;
148 6           9 return UTIL ? Scalar::Util::dualvar( $time, _time2string($time) ) : _time2string($time);
149               }
150                
151 2       2 1 786 sub sigex { return &sigexpiration; } ## historical
152                
153 2       2 1 747 sub sigin { return &siginception; } ## historical
154                
155               sub sigval {
156 2       2 1 5 my ( $self, @value ) = @_;
157 2       2   12 no integer;
  2           3  
  2           9  
158 2           4 return ( $self->{sigval} ) = map { int( 86400 * $_ ) } @value;
  2           15  
159               }
160                
161                
162               sub keytag {
163 9       9 1 1121 my ( $self, @value ) = @_;
164 9           16 for (@value) { $self->{keytag} = 0 + $_ }
  4           10  
165 9     100     35 return $self->{keytag} || 0;
166               }
167                
168                
169               sub signame {
170 6       6 1 1164 my ( $self, @value ) = @_;
171 6           11 for (@value) { $self->{signame} = Net::DNS::DomainName->new($_) }
  4           15  
172 6 100         36 return $self->{signame} ? $self->{signame}->name : undef;
173               }
174                
175                
176               sub sig {
177 8       8 1 757 my ( $self, @value ) = @_;
178 8 100         23 return MIME::Base64::encode( $self->sigbin(), "" ) unless scalar @value;
179 4           19 return $self->sigbin( MIME::Base64::decode( join "", @value ) );
180               }
181                
182                
183               sub sigbin {
184 16       16 1 28 my ( $self, @value ) = @_;
185 16           24 for (@value) { $self->{sigbin} = $_ }
  4           7  
186 16     100     103 return $self->{sigbin} || "";
187               }
188                
189                
190 6       6 1 1146 sub signature { return &sig; }
191                
192                
193               sub create {
194 1       1 1 449 unless (DNSSEC) {
195 1           189 croak qq[No "use Net::DNS::SEC" declaration in application code];
196               } else {
197               my ( $class, $rrsetref, $priv_key, %etc ) = @_;
198                
199               $rrsetref = [$rrsetref] unless ref($rrsetref) eq 'ARRAY';
200               my $RR = $rrsetref->[0];
201               croak '$rrsetref is not reference to RR array' unless ref($RR) =~ /^Net::DNS::RR/;
202                
203               # All the TTLs need to be the same in the data RRset.
204               my $ttl = $RR->ttl;
205               croak 'RRs in RRset do not have same TTL' if grep { $_->ttl != $ttl } @$rrsetref;
206                
207               my $private = ref($priv_key) ? $priv_key : Net::DNS::SEC::Private->new($priv_key);
208               croak 'unable to parse private key' unless ref($private) eq 'Net::DNS::SEC::Private';
209                
210               my @label = grep { $_ ne chr(42) } $RR->{owner}->_wire; # count labels
211                
212               my $self = Net::DNS::RR->new(
213               name => $RR->name,
214               type => 'RRSIG',
215               class => 'IN',
216               ttl => $ttl,
217               typecovered => $RR->type,
218               labels => scalar @label,
219               orgttl => $ttl,
220               siginception => time(),
221               algorithm => $private->algorithm,
222               keytag => $private->keytag,
223               signame => $private->signame,
224               );
225                
226               while ( my ( $attribute, $value ) = each %etc ) {
227               $self->$attribute($value);
228               }
229                
230               $self->{sigexpiration} = $self->{siginception} + $self->{sigval}
231               unless $self->{sigexpiration};
232                
233               my $sigdata = $self->_CreateSigData($rrsetref);
234               $self->_CreateSig( $sigdata, $private );
235               return $self;
236               }
237               }
238                
239                
240               sub verify {
241                
242               # Reminder...
243                
244               # $rrsetref must be a reference to an array of RR objects.
245                
246               # $keyref is either a key object or a reference to an array of key objects.
247                
248 1       1 1 459 unless (DNSSEC) {
249 1           145 croak qq[No "use Net::DNS::SEC" declaration in application code];
250               } else {
251               my ( $self, $rrsetref, $keyref ) = @_;
252                
253               croak '$keyref argument is scalar or undefined' unless ref($keyref);
254                
255               print '$keyref argument is ', ref($keyref), "\n" if DEBUG;
256               if ( ref($keyref) eq "ARRAY" ) {
257                
258               # We will iterate over the supplied key list and
259               # return when there is a successful verification.
260               # If not, continue so that we survive key-id collision.
261                
262               print "Iterating over ", scalar(@$keyref), " keys\n" if DEBUG;
263               my @error;
264               foreach my $keyrr (@$keyref) {
265               my $result = $self->verify( $rrsetref, $keyrr );
266               return $result if $result;
267               my $error = $self->{vrfyerrstr};
268               my $keyid = $keyrr->keytag;
269               push @error, "key $keyid: $error";
270               print "key $keyid: $error\n" if DEBUG;
271               next;
272               }
273                
274               $self->{vrfyerrstr} = join "\n", @error;
275               return 0;
276                
277               } elsif ( $keyref->isa('Net::DNS::RR::DNSKEY') ) {
278                
279               print "Validating using key with keytag: ", $keyref->keytag, "\n" if DEBUG;
280                
281               } else {
282               croak join ' ', ref($keyref), 'can not be used as DNSSEC key';
283               }
284                
285                
286               $rrsetref = [$rrsetref] unless ref($rrsetref) eq 'ARRAY';
287               my $RR = $rrsetref->[0];
288               croak '$rrsetref not a reference to array of RRs' unless ref($RR) =~ /^Net::DNS::RR/;
289                
290               if (DEBUG) {
291               print "\n ---------------------- RRSIG DEBUG --------------------";
292               print "\n SIG:\t", $self->string;
293               print "\n KEY:\t", $keyref->string;
294               print "\n -------------------------------------------------------\n";
295               }
296                
297               $self->{vrfyerrstr} = '';
298               unless ( $self->algorithm == $keyref->algorithm ) {
299               $self->{vrfyerrstr} = 'algorithm does not match';
300               return 0;
301               }
302                
303               unless ( $self->keytag == $keyref->keytag ) {
304               $self->{vrfyerrstr} = 'keytag does not match';
305               return 0;
306               }
307                
308               my $sigdata = $self->_CreateSigData($rrsetref);
309               $self->_VerifySig( $sigdata, $keyref ) || return 0;
310                
311               # time to do some time checking.
312               my $t = time;
313                
314               if ( _ordered( $self->{sigexpiration}, $t ) ) {
315               $self->{vrfyerrstr} = join ' ', 'Signature expired at', $self->sigexpiration;
316               return 0;
317               } elsif ( _ordered( $t, $self->{siginception} ) ) {
318               $self->{vrfyerrstr} = join ' ', 'Signature valid from', $self->siginception;
319               return 0;
320               }
321                
322               return 1;
323               }
324               } #END verify
325                
326                
327               sub vrfyerrstr {
328 2       2 1 924 my $self = shift;
329 2           8 return $self->{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 8       8   14 my $arg = shift;
371 8           17 my $key = uc $arg; # synthetic key
372 8           20 $key =~ s/[\W_]//g; # strip non-alphanumerics
373 8           18 my $val = $algbyname{$key};
374 8 100         31 return $val if defined $val;
375 2 100         303 return $key =~ /^\d/ ? $arg : croak qq[unknown algorithm $arg];
376               }
377                
378               sub _algbyval {
379 3       3   7 my $value = shift;
380 3     100     26 return $algbyval{$value} || return $value;
381               }
382               }
383                
384                
385               sub _CreateSigData {
386                
387               # This method creates the data string that will be signed.
388               # See RFC4034(6) and RFC6840(5.1) on how this string is constructed
389                
390               # This method is called by the method that creates a signature
391               # and by the method that verifies the signature. It is assumed
392               # that the creation method has checked that all the TTLs are
393               # the same for the rrsetref and that sig->orgttl has been set
394               # to the TTL of the data. This method will set the datarr->ttl
395               # to the sig->orgttl for all the RR in the rrsetref.
396                
397 1       1   490 if (DNSSEC) {
398               my ( $self, $rrsetref ) = @_;
399                
400               print "_CreateSigData\n" if DEBUG;
401                
402               my $sigdata = pack 'n C2 N3 n a*', @{$self}{@field}, $self->{signame}->canonical;
403               print "\npreamble\t", unpack( 'H*', $sigdata ), "\n" if DEBUG;
404                
405               my $owner = $self->{owner}; # create wildcard domain name
406               my $limit = $self->{labels};
407               my @label = $owner->_wire;
408               shift @label while scalar @label > $limit;
409               my $wild = bless {label => \@label}, ref($owner); # DIY to avoid wrecking name cache
410               my $suffix = $wild->canonical;
411               unshift @label, chr(42); # asterisk
412                
413               my @RR = map { bless( {%$_}, ref($_) ) } @$rrsetref; # shallow RR clone
414               my $rr = $RR[0];
415               my $class = $rr->class;
416               my $type = $rr->type;
417               my $ttl = $self->orgttl;
418                
419               my %table;
420               foreach my $RR (@RR) {
421               my $ident = $RR->{owner}->canonical;
422               my $match = substr $ident, -length($suffix);
423               croak 'RRs in RRset have different NAMEs' if $match ne $suffix;
424               croak 'RRs in RRset have different TYPEs' if $type ne $RR->type;
425               croak 'RRs in RRset have different CLASS' if $class ne $RR->class;
426               $RR->ttl($ttl); # reset TTL
427                
428               my $offset = 10 + length($suffix); # RDATA offset
429               if ( $ident ne $match ) {
430               $RR->{owner} = $wild;
431               $offset += 2;
432               print "\nsubstituting wildcard name: ", $RR->name if DEBUG;
433               }
434                
435               # For sorting we create a hash table of canonical data keyed on RDATA
436               my $canonical = $RR->canonical;
437               $table{substr $canonical, $offset} = $canonical;
438               }
439                
440               $sigdata = join '', $sigdata, map { $table{$_} } sort keys %table;
441                
442               if (DEBUG) {
443               my $i = 0;
444               foreach my $rdata ( sort keys %table ) {
445               print "\n>>> ", $i++, "\tRDATA:\t", unpack 'H*', $rdata;
446               print "\nRR: ", unpack( 'H*', $table{$rdata} ), "\n";
447               }
448               print "\n sigdata:\t", unpack( 'H*', $sigdata ), "\n";
449               }
450                
451               return $sigdata;
452               }
453               }
454                
455                
456               sub _CreateSig {
457 1       1   43 if (DNSSEC) {
458               my ( $self, @argument ) = @_;
459                
460               my $algorithm = $self->algorithm;
461               return eval {
462               my $class = $DNSSEC_siggen{$algorithm};
463               die "algorithm $algorithm not supported\n" unless $class;
464               $self->sigbin( $class->sign(@argument) );
465               } || return croak "${@}signature generation failed";
466               }
467               }
468                
469                
470               sub _VerifySig {
471 1       1   422 if (DNSSEC) {
472               my ( $self, @argument ) = @_;
473                
474               my $algorithm = $self->algorithm;
475               my $returnval = eval {
476               my $class = $DNSSEC_verify{$algorithm};
477               die "algorithm $algorithm not supported\n" unless $class;
478               $class->verify( @argument, $self->sigbin );
479               };
480                
481               unless ($returnval) {
482               $self->{vrfyerrstr} = "${@}signature verification failed";
483               print "\n", $self->{vrfyerrstr}, "\n" if DEBUG;
484               return 0;
485               }
486                
487               # uncoverable branch true # unexpected return value from EVP_DigestVerify
488               croak "internal error in algorithm $algorithm verification" unless $returnval == 1;
489               print "\nalgorithm $algorithm verification successful\n" if DEBUG;
490               return $returnval;
491               }
492               }
493                
494                
495               sub _ordered() { ## irreflexive 32-bit partial ordering
496 12       12   1607 my ( $n1, $n2 ) = @_;
497                
498 12 100         27 return 0 unless defined $n2; # ( any, undef )
499 11 100         20 return 1 unless defined $n1; # ( undef, any )
500                
501               # unwise to assume 64-bit arithmetic, or that 32-bit integer overflow goes unpunished
502 2       2   3696 use integer; # fold, leaving $n2 non-negative
  2           5  
  2           12  
503 10           13 $n1 = ( $n1 & 0xFFFFFFFF ) ^ ( $n2 & 0x80000000 ); # -2**31 <= $n1 < 2**32
504 10           12 $n2 = ( $n2 & 0x7FFFFFFF ); # 0 <= $n2 < 2**31
505                
506 10 100         40 return $n1 < $n2 ? ( $n1 > ( $n2 - 0x80000000 ) ) : ( $n2 < ( $n1 - 0x80000000 ) );
507               }
508                
509                
510               my $y1998 = timegm( 0, 0, 0, 1, 0, 1998 );
511               my $y2026 = timegm( 0, 0, 0, 1, 0, 2026 );
512               my $y2082 = $y2026 << 1;
513               my $y2054 = $y2082 - $y1998;
514               my $m2026 = int( 0x80000000 - $y2026 );
515               my $m2054 = int( 0x80000000 - $y2054 );
516               my $t2082 = int( $y2082 & 0x7FFFFFFF );
517               my $t2100 = 1960058752;
518                
519               sub _string2time { ## parse time specification string
520 16       16   4738 my $arg = shift;
521 16 100         32 return int($arg) if length($arg) < 12;
522 15           56 my ( $y, $m, @dhms ) = unpack 'a4 a2 a2 a2 a2 a2', $arg . '00';
523 15 100         37 if ( $arg lt '20380119031408' ) { # calendar folding
    100            
524 10 100         34 return timegm( reverse(@dhms), $m - 1, $y ) if $y < 2026;
525 1           5 return timegm( reverse(@dhms), $m - 1, $y - 56 ) + $y2026;
526               } elsif ( $y > 2082 ) {
527 4           16 my $z = timegm( reverse(@dhms), $m - 1, $y - 84 ); # expunge 29 Feb 2100
528 4 100         133 return $z < 1456790400 ? $z + $y2054 : $z + $y2054 - 86400;
529               }
530 1           5 return ( timegm( reverse(@dhms), $m - 1, $y - 56 ) + $y2054 ) - $y1998;
531               }
532                
533                
534               sub _time2string { ## format time specification string
535 26       26   3889 my $arg = shift;
536 26           47 my $ls31 = int( $arg & 0x7FFFFFFF );
537 26 100         89 if ( $arg & 0x80000000 ) {
    100            
538                
539 10 100         20 if ( $ls31 > $t2082 ) {
540 8 100         28 $ls31 += 86400 unless $ls31 < $t2100; # expunge 29 Feb 2100
541 8           30 my ( $yy, $mm, @dhms ) = reverse( ( gmtime( $ls31 + $m2054 ) )[0 .. 5] );
542 8           43 return sprintf '%d%02d%02d%02d%02d%02d', $yy + 1984, $mm + 1, @dhms;
543               }
544                
545 2           8 my ( $yy, $mm, @dhms ) = reverse( ( gmtime( $ls31 + $m2026 ) )[0 .. 5] );
546 2           11 return sprintf '%d%02d%02d%02d%02d%02d', $yy + 1956, $mm + 1, @dhms;
547                
548                
549               } elsif ( $ls31 > $y2026 ) {
550 2           9 my ( $yy, $mm, @dhms ) = reverse( ( gmtime( $ls31 - $y2026 ) )[0 .. 5] );
551 2           10 return sprintf '%d%02d%02d%02d%02d%02d', $yy + 1956, $mm + 1, @dhms;
552               }
553                
554 14           59 my ( $yy, $mm, @dhms ) = reverse( ( gmtime $ls31 )[0 .. 5] );
555 14           127 return sprintf '%d%02d%02d%02d%02d%02d', $yy + 1900, $mm + 1, @dhms;
556               }
557                
558               ########################################
559                
560                
561               1;
562               __END__