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