1package Net::DNS::SEC::Keyset; 2 3use strict; 4use warnings; 5 6our $VERSION = (qw$Id: Keyset.pm 1853 2021-10-11 10:40:59Z willem $)[2]; 7 8 9=head1 NAME 10 11Net::DNS::SEC::Keyset - DNSSEC Keyset object class 12 13 14=head1 SYNOPSIS 15 16 use Net::DNS::SEC::Keyset; 17 18 19=head1 DESCRIPTION 20 21A keyset is an "administrative" unit used for DNSSEC maintenance. 22 23This class provides interfaces for creating, reading and writing keysets. 24 25Object methods are provided to extract DNSKEY, RRSIG and DS records. 26 27Note that this class is still being developed. 28Attributes and methods are subject to change. 29 30=cut 31 32 33use Carp; 34use File::Spec; 35use IO::File; 36 37use Net::DNS::ZoneFile; 38 39our $keyset_err; 40 41 42sub new { 43 my ( $class, $arg1, $arg2 ) = @_; 44 45 my $ref1 = ref($arg1); 46 return &_new_from_file unless $ref1; 47 48 return &_new_from_packet if $ref1 eq 'Net::DNS::Packet'; 49 50 return &_new_from_keys unless ref($arg2); 51 52 return &_new_from_keys_sigs; 53} 54 55 56=head2 new (from file) 57 58 $keyset = Net::DNS::SEC::Keyset->new( $filename ); 59 $keyset = Net::DNS::SEC::Keyset->new( $filename, $directory ); 60 die Net::DNS::SEC::Keyset->keyset_err unless $keyset; 61 62Constructor method which reads the specified keyset file and returns a 63keyset object. 64 65The optional second argument specifies the filename base directory. 66 67Sets keyset_err and returns undef on failure. 68 69=cut 70 71sub _new_from_file { 72 my ( $class, $name, @path ) = @_; 73 74 my $file = File::Spec->catfile( @path, $name ); 75 76 my @rr = Net::DNS::ZoneFile->new($file)->read; 77 78 return $class->_new_from_keys_sigs( \@rr, \@rr ); 79} 80 81 82=head2 new (by signing keys) 83 84 $keyset = Net::DNS::SEC::Keyset->new( [@keyrr], $privatekeypath ); 85 die Net::DNS::SEC::Keyset->keyset_err unless $keyset; 86 87Creates a keyset object from the keys provided through the reference to an 88array of Net::DNS::RR::DNSKEY objects. 89 90The method will create and self-sign the whole keyset. The private keys as 91generated by the BIND dnssec-keygen tool are assumed to be in the current 92directory or, if specified, the directory indicated by $privatekeypath. 93 94Sets keyset_err and returns undef on failure. 95 96=cut 97 98sub _new_from_keys { 99 my ( $class, $keylist, @keypath ) = @_; 100 101 my @sigrr; 102 foreach my $key ( grep { $_->type eq 'DNSKEY' } @$keylist ) { 103 my $keyname = $key->privatekeyname; 104 my $keyfile = File::Spec->catfile( @keypath, $keyname ); 105 my @rrsig = Net::DNS::RR::RRSIG->create( $keylist, $keyfile ); 106 push @sigrr, grep {defined} @rrsig; 107 } 108 109 return $class->_new_from_keys_sigs( $keylist, \@sigrr ); 110} 111 112 113=head2 new (from key and sig RRsets) 114 115 $keyset = Net::DNS::Keyset->new( [@keyrr], [@sigrr] ); 116 die Net::DNS::SEC::Keyset->keyset_err unless $keyset; 117 118Creates a keyset object from the keys provided through the references 119to arrays of Net::DNS::RR::DNSKEY and Net::DNS::RR::RRSIG objects. 120 121Sets keyset_err and returns undef on failure. 122 123=cut 124 125sub _new_from_keys_sigs { 126 my ( $class, $key_ref, $sig_ref ) = @_; 127 128 my @keyrr = grep { $_->type eq 'DNSKEY' } @$key_ref; 129 my @sigrr = grep { $_->type eq 'RRSIG' } @$sig_ref; 130 131 my $keyset = bless {keys => \@keyrr, sigs => \@sigrr}, $class; 132 133 return scalar( $keyset->verify ) ? $keyset : undef; 134} 135 136 137=head2 new (from Packet) 138 139 $resolver = Net::DNS::Resolver->new; 140 $resolver->dnssec(1); 141 142 $reply = $res->send ( "example.com", "DNSKEY" ); 143 144 $keyset = Net::DNS::SEC::Keyset->new( $reply ); 145 die Net::DNS::SEC::Keyset->keyset_err unless $keyset; 146 147Creates a keyset object from a Net::DNS::Packet that contains the answer 148to a query for key records at the zone apex. 149 150This is the method you should use for automatically fetching keys. 151 152Sets keyset_err and returns undef on failure. 153 154=cut 155 156sub _new_from_packet { 157 my ( $class, $packet ) = @_; 158 my @rrset = $packet->answer; 159 return $class->_new_from_keys_sigs( \@rrset, \@rrset ); 160} 161 162 163=head2 keys 164 165 @keyrr = $keyset->keys; 166 167Returns an array of Net::DNS::RR::DNSKEY objects. 168 169=cut 170 171sub keys { 172 my $self = shift; 173 my @keys = @{$self->{keys}}; 174 return @keys; 175} 176 177 178=head2 sigs 179 180 @sigrr = $keyset->sigs; 181 182Returns an array of Net::DNS::RR::RRSIG objects. 183 184=cut 185 186sub sigs { 187 my $self = shift; 188 my @sigs = @{$self->{sigs}}; 189 return @sigs; 190} 191 192 193=head2 extract_ds 194 195 @ds = $keyset->extract_ds; 196 die Net::DNS::SEC::Keyset->keyset_err unless @ds; 197 198Extracts DS records from the keyset. Note that the keyset will be verified 199during extraction. All keys will need to have a valid self-signature. 200 201The method sets keyset_err if verification fails. 202 203=cut 204 205sub extract_ds { 206 my $self = shift; 207 my @ds; 208 @ds = map { Net::DNS::RR::DS->create($_) } $self->keys if $self->verify; 209 return @ds; 210} 211 212 213=head2 verify 214 215 @keytags = $keyset->verify(); 216 die Net::DNS::SEC::Keyset->keyset_err unless @keytags; 217 218 $keyset->verify( $keytag ) || die $keyset->keyset_err; 219 220If no arguments are given: 221 222=over 2 223 224=item 225 226Verifies if all signatures present verify the keyset. 227 228=item 229 230Verifies if there are DNSKEYs with the SEP flag set, there is at 231least one RRSIG made using that key. 232 233=item 234 235Verifies that if there are no DNSKEYs with the SEP flag set there 236is at least one RRSIG made with one of the keys from the keyset. 237 238=back 239 240If an argument is given, it is should be the numeric keytag of the key 241in the keyset which will be verified using the corresponding RRSIG. 242 243The method returns a list of keytags of verified keys in the keyset. 244 245The method sets keyset_err and returns empty list if verification fails. 246 247=cut 248 249sub verify { 250 my ( $self, $keyid ) = @_; 251 252 my @keys = $self->keys; 253 254 my %keysbytag; 255 push( @{$keysbytag{$_->keytag}}, $_ ) foreach @keys; 256 257 my @sigs = $self->sigs; 258 259 my @keyset_err; 260 my %names = map { ( $_->name => $_ ) } @keys, @sigs; 261 my @names = CORE::keys %names; 262 push @keyset_err, "Multiple names in keyset: @names" if scalar(@names) > 1; 263 264 if ($keyid) { 265 @sigs = grep { $_->keytag == $keyid } @sigs; 266 push @keyset_err, "No signature made with $keyid found" unless @sigs; 267 } elsif ( my @sepkeys = grep { $_->sep } @keys ) { 268 my %sepkey = map { ( $_->keytag => $_ ) } @sepkeys; 269 push @keyset_err, 'No signature found for key with SEP flag' 270 unless grep { $sepkey{$_->keytag} } @sigs; 271 } 272 273 foreach my $sig (@sigs) { 274 my $keytag = $sig->keytag; 275 next if $sig->verify( \@keys, $keysbytag{$keytag} || [] ); 276 my $vrfyerr = $sig->vrfyerrstr; 277 my $signame = $sig->signame; 278 push @keyset_err, "$vrfyerr on key $signame $keytag "; 279 } 280 281 $keyset_err = join "\n", @keyset_err; 282 283 my @tags_verified; 284 @tags_verified = map { $_->keytag } @sigs unless $keyset_err; 285 return @tags_verified; 286} 287 288 289=head2 keyset_err 290 291 $keyset_err = Net::DNS::SEC::Keyset->keyset_err; 292 293Returns the keyset error string. 294 295=cut 296 297sub keyset_err { 298 return $keyset_err; 299} 300 301 302=head2 string 303 304 $string = $keyset->string; 305 306Returns a string representation of the keyset. 307 308=cut 309 310sub string { 311 my $self = shift; 312 return join "\n", map { $_->string } ( $self->keys, $self->sigs ); 313} 314 315 316=head2 print 317 318 $keyset->print; # similar to print( $keyset->string ) 319 320Prints the keyset. 321 322=cut 323 324sub print { 325 my $self = shift; 326 foreach ( $self->keys, $self->sigs ) { $_->print } 327 return; 328} 329 330 331=head2 writekeyset 332 333 $keyset->writekeyset; 334 $keyset->writekeyset( $path ); 335 $keyset->writekeyset( $prefix ); 336 $keyset->writekeyset( $prefix, $path ); 337 338Writes the keyset to a file named "keyset-<domain>." in the current 339working directory or directory defined by the optional $path argument. 340 341The optional $prefix argument specifies the prefix that will be 342prepended to the domain name to form the keyset filename. 343 344=cut 345 346sub writekeyset { 347 my ( $self, $arg1, @path ) = @_; 348 shift; 349 @path = shift() if $arg1 && File::Spec->file_name_is_absolute($arg1); 350 my $prefix = shift || 'keyset-'; 351 352 my @keysetrr = ( $self->keys, $self->sigs ); 353 my $domainname = $keysetrr[0]->name; 354 my $keysetname = "$prefix$domainname."; 355 my $filename = File::Spec->catfile( @path, $keysetname ); 356 $filename =~ s/[.]+/\./; ## avoid antisocial consequences of $path with .. 357 my $handle = IO::File->new( $filename, '>' ) or croak qq("$filename": $!); 358 select( ( select($handle), $self->print )[0] ); 359 close($handle); 360 return $filename; 361} 362 363 3641; 365 366__END__ 367 368 369=head1 COPYRIGHT 370 371Copyright (c)2002 RIPE NCC. Author Olaf M. Kolkman 372 373Portions Copyright (c)2014 Dick Franks 374 375All Rights Reserved 376 377 378=head1 LICENSE 379 380Permission to use, copy, modify, and distribute this software and its 381documentation for any purpose and without fee is hereby granted, provided 382that the original copyright notices appear in all copies and that both 383copyright notice and this permission notice appear in supporting 384documentation, and that the name of the author not be used in advertising 385or publicity pertaining to distribution of the software without specific 386prior written permission. 387 388THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 389IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 390FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 391THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 392LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 393FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 394DEALINGS IN THE SOFTWARE. 395 396=cut 397 398