File Coverage

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


line stmt bran cond sub pod time code
1             package Net::DNS::RR::RRSIG;
2              
3 2     2   16 use strict;
  2         5  
  2         104  
4 2     2   12 use warnings;
  2         6  
  2         302  
5             our $VERSION = (qw$Id: RRSIG.pm 2042 2025-12-24 10:23:11Z willem $)[2];
6              
7 2     2   26 use base qw(Net::DNS::RR);
  2         4  
  2         240  
8              
9              
10             =head1 NAME
11              
12             Net::DNS::RR::RRSIG - DNS RRSIG resource record
13              
14             =cut
15              
16 2     2   15 use integer;
  2         5  
  2         13  
17              
18 2     2   124 use Carp;
  2         4  
  2         216  
19 2     2   703 use Time::Local;
  2         2661  
  2         184  
20              
21 2     2   15 use Net::DNS::Parameters qw(:type);
  2         5  
  2         318  
22              
23 2     2   14 use constant DEBUG => 0;
  2         5  
  2         249  
24              
25 2     2   15 use constant UTIL => defined eval { require Scalar::Util; };
  2         5  
  2         4  
  2         236  
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   31 use constant DNSSEC => defined $INC{'Net/DNS/SEC.pm'}; ## Discover how we got here, without exposing any crypto
  2         5  
  2         5546  
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 339     339   790 my ( $self, $data, $offset, @opaque ) = @_;
54              
55 339         642 my $limit = $offset + $self->{rdlength};
56 339         1256 @{$self}{@field} = unpack "\@$offset n C2 N3 n", $$data;
  339         1910  
57 339         1294 ( $self->{signame}, $offset ) = Net::DNS::DomainName->decode( $data, $offset + 18, @opaque );
58 339         1159 $self->{sigbin} = substr $$data, $offset, $limit - $offset;
59 339         877 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         5 my $signame = $self->{signame};
67 5         5 return pack 'n C2 N3 n a* a*', @{$self}{@field}, $signame->canonical, $self->sigbin;
  5         14  
68             }
69              
70              
71             sub _format_rdata { ## format rdata portion of RR string.
72 3     3   5 my $self = shift;
73              
74 3         3 my $signame = $self->{signame};
75 3         7 my @sig64 = split /\s+/, MIME::Base64::encode( $self->sigbin );
76 3         6 my @rdata = ( map( { $self->$_ } @field ), $signame->string, @sig64 );
  21         35  
77 3         7 $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   13 my ( $self, @argument ) = @_;
84              
85 3         5 foreach ( @field, qw(signame) ) { $self->$_( shift @argument ) }
  24         45  
86 3         9 $self->signature(@argument);
87 3         5 return;
88             }
89              
90              
91             sub _defaults { ## specify RR attribute default values
92 2     2   11 my $self = shift;
93              
94 2         12 $self->sigval(30);
95 2         6 return;
96             }
97              
98              
99             sub typecovered {
100 9     9 1 22 my ( $self, @value ) = @_;
101 9         12 for (@value) { $self->{typecovered} = typebyname($_) }
  4         12  
102 9         12 my $typecode = $self->{typecovered};
103 9 100       31 return defined $typecode ? typebyval($typecode) : undef;
104             }
105              
106              
107             sub algorithm {
108 19     19 1 1673 my ( $self, $arg ) = @_;
109              
110 19 100       36 unless ( ref($self) ) { ## class method or simple function
111 3         5 my $argn = pop;
112 3 100       15 return $argn =~ /[^0-9]/ ? _algbyname($argn) : _algbyval($argn);
113             }
114              
115 16 100       32 return $self->{algorithm} unless defined $arg;
116 8 100       23 return _algbyval( $self->{algorithm} ) if $arg =~ /MNEMONIC/i;
117 7         12 return $self->{algorithm} = _algbyname($arg);
118             }
119              
120              
121             sub labels {
122 9     9 1 801 my ( $self, @value ) = @_;
123 9         10 for (@value) { $self->{labels} = 0 + $_ }
  4         7  
124 9   100     24 return $self->{labels} || 0;
125             }
126              
127              
128             sub orgttl {
129 9     9 1 705 my ( $self, @value ) = @_;
130 9         15 for (@value) { $self->{orgttl} = 0 + $_ }
  4         7  
131 9   100     20 return $self->{orgttl} || 0;
132             }
133              
134              
135             sub sigexpiration {
136 11     11 1 848 my ( $self, @value ) = @_;
137 11         14 for (@value) { $self->{sigexpiration} = _string2time($_) }
  4         8  
138 11         123 my $time = $self->{sigexpiration};
139 11 100 100     33 return unless defined wantarray && defined $time;
140 6         9 return UTIL ? Scalar::Util::dualvar( $time, _time2string($time) ) : _time2string($time);
141             }
142              
143             sub siginception {
144 11     11 1 822 my ( $self, @value ) = @_;
145 11         20 for (@value) { $self->{siginception} = _string2time($_) }
  4         7  
146 11         86 my $time = $self->{siginception};
147 11 100 100     30 return unless defined wantarray && defined $time;
148 6         40 return UTIL ? Scalar::Util::dualvar( $time, _time2string($time) ) : _time2string($time);
149             }
150              
151 2     2 1 596 sub sigex { return &sigexpiration; } ## historical
152              
153 2     2 1 531 sub sigin { return &siginception; } ## historical
154              
155             sub sigval {
156 2     2 1 5 my ( $self, @value ) = @_;
157 2     2   16 no integer;
  2         3  
  2         13  
158 2         5 return ( $self->{sigval} ) = map { int( 86400 * $_ ) } @value;
  2         20  
159             }
160              
161              
162             sub keytag {
163 9     9 1 853 my ( $self, @value ) = @_;
164 9         13 for (@value) { $self->{keytag} = 0 + $_ }
  4         7  
165 9   100     31 return $self->{keytag} || 0;
166             }
167              
168              
169             sub signame {
170 6     6 1 736 my ( $self, @value ) = @_;
171 6         8 for (@value) { $self->{signame} = Net::DNS::DomainName->new($_) }
  4         12  
172 6 100       50 return $self->{signame} ? $self->{signame}->name : undef;
173             }
174              
175              
176             sub sig {
177 8     8 1 587 my ( $self, @value ) = @_;
178 8 100       19 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 21 my ( $self, @value ) = @_;
185 16         18 for (@value) { $self->{sigbin} = $_ }
  4         9  
186 16   100     77 return $self->{sigbin} || "";
187             }
188              
189              
190 6     6 1 1799 sub signature { return &sig; }
191              
192              
193             sub create {
194 1     1 1 226 unless (DNSSEC) {
195 1         112 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 306 unless (DNSSEC) {
249 1         100 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 531 my $self = shift;
329 2         5 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   11 my $arg = shift;
371 8         25 my $key = uc $arg; # synthetic key
372 8         16 $key =~ s/[\W_]//g; # strip non-alphanumerics
373 8         15 my $val = $algbyname{$key};
374 8 100       37 return $val if defined $val;
375 2 100       229 return $key =~ /^\d/ ? $arg : croak qq[unknown algorithm $arg];
376             }
377              
378             sub _algbyval {
379 3     3   5 my $value = shift;
380 3   100     16 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   327 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   29 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   249 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   1727 my ( $n1, $n2 ) = @_;
497              
498 12 100       26 return 0 unless defined $n2; # ( any, undef )
499 11 100       16 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   5408 use integer; # fold, leaving $n2 non-negative
  2         3  
  2         12  
503 10         17 $n1 = ( $n1 & 0xFFFFFFFF ) ^ ( $n2 & 0x80000000 ); # -2**31 <= $n1 < 2**32
504 10         12 $n2 = ( $n2 & 0x7FFFFFFF ); # 0 <= $n2 < 2**31
505              
506 10 100       43 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   4574 my $arg = shift;
521 16 100       34 return int($arg) if length($arg) < 12;
522 15         64 my ( $y, $m, @dhms ) = unpack 'a4 a2 a2 a2 a2 a2', $arg . '00';
523 15 100       35 if ( $arg lt '20380119031408' ) { # calendar folding
    100          
524 10 100       33 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         19 my $z = timegm( reverse(@dhms), $m - 1, $y - 84 ); # expunge 29 Feb 2100
528 4 100       163 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   4822 my $arg = shift;
536 26         41 my $ls31 = int( $arg & 0x7FFFFFFF );
537 26 100       58 if ( $arg & 0x80000000 ) {
    100          
538              
539 10 100       31 if ( $ls31 > $t2082 ) {
540 8 100       16 $ls31 += 86400 unless $ls31 < $t2100; # expunge 29 Feb 2100
541 8         28 my ( $yy, $mm, @dhms ) = reverse( ( gmtime( $ls31 + $m2054 ) )[0 .. 5] );
542 8         44 return sprintf '%d%02d%02d%02d%02d%02d', $yy + 1984, $mm + 1, @dhms;
543             }
544              
545 2         12 my ( $yy, $mm, @dhms ) = reverse( ( gmtime( $ls31 + $m2026 ) )[0 .. 5] );
546 2         13 return sprintf '%d%02d%02d%02d%02d%02d', $yy + 1956, $mm + 1, @dhms;
547              
548              
549             } elsif ( $ls31 > $y2026 ) {
550 2         10 my ( $yy, $mm, @dhms ) = reverse( ( gmtime( $ls31 - $y2026 ) )[0 .. 5] );
551 2         11 return sprintf '%d%02d%02d%02d%02d%02d', $yy + 1956, $mm + 1, @dhms;
552             }
553              
554 14         44 my ( $yy, $mm, @dhms ) = reverse( ( gmtime $ls31 )[0 .. 5] );
555 14         100 return sprintf '%d%02d%02d%02d%02d%02d', $yy + 1900, $mm + 1, @dhms;
556             }
557              
558             ########################################
559              
560              
561             1;
562             __END__