1package Munin::Common::TLS;
2
3use warnings;
4use strict;
5
6use Carp;
7use English qw(-no_match_vars);
8
9sub new {
10    my ($class, $args) = @_;
11
12    my $self = {
13        logger             => $args->{logger},
14        read_fd            => $args->{read_fd},
15        read_func          => $args->{read_func},
16        write_fd           => $args->{write_fd},
17        write_func         => $args->{write_func},
18    };
19
20    for my $key (keys %$self) {
21        croak "Required argument missing: $key" unless defined $self->{$key};
22    }
23
24    $self = {
25        %$self,
26        DEBUG              => $args->{DEBUG} || 0,
27        tls_ca_cert        => $args->{tls_ca_cert} || '',
28        tls_cert           => $args->{tls_cert} || '',
29        tls_paranoia       => $args->{tls_paranoia}|| 0,
30        tls_priv           => $args->{tls_priv} || '',
31        tls_vdepth         => $args->{tls_vdepth} || 0,
32        tls_verify         => $args->{tls_verify} || 0,
33        tls_match          => $args->{tls_match} || '',
34    };
35
36    for my $args_key (keys %$args) {
37        croak "Unrecognized argument: $args_key" unless exists $self->{$args_key};
38    }
39
40    $self = {
41        %$self,
42        tls_context        => undef,
43        tls_session        => undef,
44        private_key_loaded => 0,
45    };
46
47    return bless $self, $class;
48}
49
50
51sub _start_tls {
52    my $self = shift;
53
54    my %tls_verified = (
55        level          => 0,
56        cert           => "",
57        verified       => 0,
58        required_depth => $self->{tls_vdepth},
59        verify         => $self->{tls_verify},
60    );
61
62    $self->{logger}("[TLS] Enabling TLS.") if $self->{DEBUG};
63
64    $self->_load_net_ssleay()
65        or return 0;
66
67    $self->_initialize_net_ssleay();
68
69    $self->{tls_context} = $self->_creat_tls_context();
70
71    $self->_load_private_key()
72        or return 0;
73
74    $self->_load_certificate();
75
76    $self->_load_ca_certificate();
77
78    $self->_initial_communication()
79        or return 0;
80
81    $self->_set_peer_requirements(\%tls_verified);
82
83    if (! ($self->{tls_session} = Net::SSLeay::new($self->{tls_context})))
84    {
85	$self->{logger}("[ERROR] Could not create TLS: $!");
86	return 0;
87    }
88
89    $self->_log_cipher_list() if $self->{DEBUG};
90
91    $self->_set_ssleay_file_descriptors();
92
93    $self->_accept_or_connect(\%tls_verified);
94
95    return $self->{tls_session};
96}
97
98
99sub _load_net_ssleay {
100    my ($self) = @_;
101
102    eval {
103        require Net::SSLeay;
104    };
105    if ($@) {
106	$self->{logger}("[ERROR] TLS enabled but Net::SSLeay unavailable.");
107	return 0;
108    }
109
110    return 1;
111}
112
113
114sub _initialize_net_ssleay {
115    my ($self) = @_;
116
117    Net::SSLeay::load_error_strings();
118    Net::SSLeay::SSLeay_add_ssl_algorithms();
119    Net::SSLeay::randomize();
120}
121
122
123sub _creat_tls_context {
124    my ($self) = @_;
125
126    my $ctx = Net::SSLeay::CTX_new();
127    if (!$ctx) {
128	$self->{logger}("[ERROR] Could not create SSL_CTX");
129	return 0;
130    }
131
132    # Tune a few things...
133    Net::SSLeay::CTX_set_options($ctx, Net::SSLeay::OP_ALL());
134    if (my $errno = Net::SSLeay::ERR_get_error()) {
135	$self->{logger}("[ERROR] Could not set SSL_CTX options: " + Net::SSLeay::ERR_error_string($errno));
136	return 0;
137    }
138
139    return $ctx;
140}
141
142
143sub _load_private_key {
144    my ($self) = @_;
145
146    if (defined $self->{tls_priv} and length $self->{tls_priv}) {
147    	if (-e $self->{tls_priv} or $self->{tls_paranoia} eq "paranoid") {
148	    if (Net::SSLeay::CTX_use_PrivateKey_file($self->{tls_context},
149                                                     $self->{tls_priv},
150                                                     &Net::SSLeay::FILETYPE_PEM)) {
151                $self->{private_key_loaded} = 1;
152            }
153            else {
154	        if ($self->{tls_paranoia} eq "paranoid") {
155                    $self->{logger}("[ERROR] Problem occurred when trying to read file with private key \"$self->{tls_priv}\": $!");
156		    return 0;
157	        }
158	        else {
159                    $self->{logger}("[ERROR] Problem occurred when trying to read file with private key \"$self->{tls_priv}\": $!. Continuing without private key.");
160	        }
161	    }
162	}
163	else {
164	    $self->{logger}("[WARNING] No key file \"$self->{tls_priv}\". Continuing without private key.");
165        }
166    }
167
168    return 1;
169}
170
171
172sub _load_certificate {
173    my ($self) = @_;
174
175    if ($self->{tls_cert} && -e $self->{tls_cert}) {
176        if (defined $self->{tls_cert} and length $self->{tls_cert}) {
177	    if (!Net::SSLeay::CTX_use_certificate_file($self->{tls_context},
178                                                       $self->{tls_cert},
179                                                       &Net::SSLeay::FILETYPE_PEM)) {
180	        $self->{logger}("[WARNING] Problem occurred when trying to read file with certificate \"$self->{tls_cert}\": $!. Continuing without certificate.");
181	    }
182        }
183    }
184    else {
185	$self->{logger}("[WARNING] No certificate file \"$self->{tls_cert}\". Continuing without certificate.");
186    }
187
188    return 1;
189}
190
191
192sub _load_ca_certificate {
193    my ($self) = @_;
194
195    if ($self->{tls_ca_cert} && -e $self->{tls_ca_cert}) {
196    	if(!Net::SSLeay::CTX_load_verify_locations($self->{tls_context}, $self->{tls_ca_cert}, '')) {
197            $self->{logger}("[WARNING] Problem occurred when trying to read file with the CA's certificate \"$self->{tls_ca_cert}\": ".&Net::SSLeay::print_errs("").". Continuing without CA's certificate.");
198   	 }
199    }
200
201    return 1;
202}
203
204
205sub _set_peer_requirements {
206    my ($self, $tls_verified) = @_;
207
208    $self->{tls_vdepth} = 5 if !defined $self->{tls_vdepth};
209    Net::SSLeay::CTX_set_verify_depth ($self->{tls_context}, $self->{tls_vdepth});
210    my $err = &Net::SSLeay::print_errs("");
211    if (defined $err and length $err) {
212        $self->{logger}("[WARNING] in set_verify_depth: $err");
213    }
214    Net::SSLeay::CTX_set_verify ($self->{tls_context},
215                                 $self->{tls_verify}  ? &Net::SSLeay::VERIFY_PEER :
216                                                        &Net::SSLeay::VERIFY_NONE,
217                                 $self->_tls_verify_callback($tls_verified));
218    $err = &Net::SSLeay::print_errs("");
219    if (defined $err and length $err) {
220        $self->{logger}("[WARNING] in set_verify: $err");
221    }
222
223    return 1;
224}
225
226
227sub _tls_verify_callback {
228    my ($self, $tls_verified) = @_;
229
230    return sub {
231        my ($ok, $subj_cert, $issuer_cert, $depth,
232	    $errorcode, $arg, $chain) = @_;
233
234        $tls_verified->{"level"}++;
235
236        if ($ok) {
237            $tls_verified->{"verified"} = 1;
238            $self->{logger}("[TLS] Verified certificate.") if $self->{DEBUG};
239            return 1;           # accept
240        }
241
242        if (!($tls_verified->{"verify"})) {
243            $self->{logger}("[TLS] Certificate failed verification, but we aren't verifying.") if $self->{DEBUG};
244            $tls_verified->{"verified"} = 1;
245            return 1;
246        }
247
248        if ($tls_verified->{"level"} > $tls_verified->{"required_depth"}) {
249            $self->{logger}("[TLS] Certificate verification failed at depth ".$tls_verified->{"level"}.".");
250            $tls_verified->{"verified"} = 0;
251            return 0;
252        }
253
254        return 0;               # Verification failed
255    }
256}
257
258
259sub _log_cipher_list {
260    my ($self) = @_;
261
262    my $i = 0;
263    my $p = '';
264    my $cipher_list = 'Cipher list: ';
265    $p=Net::SSLeay::get_cipher_list($self->{tls_session},$i);
266    $cipher_list .= $p if $p;
267    do {
268        $i++;
269        $cipher_list .= ', ' . $p if $p;
270        $p=Net::SSLeay::get_cipher_list($self->{tls_session},$i);
271    } while $p;
272    $cipher_list .= '\n';
273    $self->{logger}("[TLS] Available cipher list: $cipher_list.") if $self->{DEBUG};
274}
275
276
277sub _set_ssleay_file_descriptors {
278    my ($self) = @_;
279
280    Net::SSLeay::set_rfd($self->{tls_session}, $self->{read_fd});
281    my $err = &Net::SSLeay::print_errs("");
282    if (defined $err and length $err) {
283        $self->{logger}("[TLS] Warning in set_rfd: $err");
284    }
285    Net::SSLeay::set_wfd($self->{tls_session}, $self->{write_fd});
286    $err = &Net::SSLeay::print_errs("");
287    if (defined $err and length $err) {
288        $self->{logger}("[TLS] Warning in set_wfd: $err");
289    }
290}
291
292
293sub _accept_or_connect {
294    my ($self, $tls_verified) = @_;
295
296    $self->{logger}("[TLS] Accept/Connect: $self->{private_key_loaded}, " . $self->_use_key_if_present()) if $self->{DEBUG};
297    my $res;
298    if ($self->_use_key_if_present()) {
299        $res = Net::SSLeay::accept($self->{tls_session});
300    }
301    else {
302        $res = Net::SSLeay::connect($self->{tls_session});
303    }
304    $self->{logger}("[TLS] Done Accept/Connect") if $self->{DEBUG};
305
306    my $err = &Net::SSLeay::print_errs("");
307    if (defined $err and length $err)
308    {
309	$self->{logger}("[ERROR] Could not enable TLS: " . $err);
310	Net::SSLeay::free ($self->{tls_session});
311	Net::SSLeay::CTX_free ($self->{tls_context});
312	$self->{tls_session} = undef;
313    }
314    elsif (!$tls_verified->{"verified"} and $self->{tls_paranoia} eq "paranoid")
315    {
316	$self->{logger}("[ERROR] Could not verify CA: " . Net::SSLeay::dump_peer_certificate($self->{tls_session}));
317	$self->_on_unverified_cert();
318	Net::SSLeay::free ($self->{tls_session});
319	Net::SSLeay::CTX_free ($self->{tls_context});
320	$self->{tls_session} = undef;
321    }
322    elsif ($self->{"tls_match"} and
323    	Net::SSLeay::dump_peer_certificate($self->{tls_session}) !~ /$self->{tls_match}/)
324    {
325	$self->{logger}("[ERROR] Could not match pattern \"" . $self->{tls_match} .
326		"\" in dump of certificate.");
327	$self->_on_unmatched_cert();
328	Net::SSLeay::free ($self->{tls_session});
329	Net::SSLeay::CTX_free ($self->{tls_context});
330	$self->{tls_session} = undef;
331    }
332    else
333    {
334	$self->{logger}("[TLS] TLS enabled.") if $self->{DEBUG};
335	$self->{logger}("[TLS] Cipher `" . Net::SSLeay::get_cipher($self->{tls_session}) . "'.") if $self->{DEBUG};
336	$self->{logger}("[TLS] client cert: " . Net::SSLeay::dump_peer_certificate($self->{tls_session})) if $self->{DEBUG};
337    }
338}
339
340
341# Abstract method
342sub _initial_communication {
343    my ($self) = @_;
344    croak "Abstract method called '_initial_communication', "
345        . "needs to be defined in child"
346            if ref $self eq __PACKAGE__;
347}
348
349
350# Abstract method
351sub _use_key_if_present {
352    my ($self) = @_;
353    croak "Abstract method called '_use_key_if_present', "
354        . "needs to be defined in child"
355            if ref $self eq __PACKAGE__;
356}
357
358
359# Redefine in sub class if needed
360sub _on_unverified_cert {}
361
362# Redefine in sub class if needed
363sub _on_unmatched_cert {}
364
365sub read {
366    my ($self) = @_;
367
368    croak "Tried to do an encrypted read, but a TLS session is not started"
369        unless $self->session_started();
370
371    my $read = Net::SSLeay::read($self->{tls_session});
372    my $err = &Net::SSLeay::print_errs("");
373    if (defined $err and length $err) {
374        $self->{logger}("[TLS] Warning in read: $err");
375        return;
376    }
377    undef $read if($read eq ''); # returning '' signals EOF
378
379    $self->{logger}("DEBUG: < $read") if $self->{DEBUG} && defined $read;
380    return $read;
381}
382
383
384sub write {
385    my ($self, $text) = @_;
386
387    croak "Tried to do an encrypted write, but a TLS session is not started"
388        unless $self->session_started();
389
390    $self->{logger}("DEBUG: > $text") if $self->{DEBUG};
391
392    Net::SSLeay::write($self->{tls_session}, $text);
393    my $err = &Net::SSLeay::print_errs("");
394    if (defined $err and length $err) {
395        $self->{logger}("[TLS] Warning in write: $err");
396        return 0;
397    }
398
399    return 1;
400}
401
402
403sub session_started {
404    my ($self) = @_;
405
406    return defined $self->{tls_session};
407}
408
409
4101;
411
412__END__
413
414=head1 NAME
415
416Munin::Node::TLS - Abstract base class implementing the STARTTLS protocol
417
418
419=head1 SYNOPSIS
420
421Should not be called directly. See synopsis for
422L<Munin::Common::TLSServer> and L<Munin::Common::TLSClient>.
423
424
425=head1 METHODS
426
427=over
428
429=item B<new>
430
431 my $tls = Munin::Common::TLSFoo->new({ # Substitute Foo with Client or Server
432     # Mandatory attributes:
433     logger      => \&a_logger_func,
434     read_fd     => fileno($socket),
435     read_func   => \&a_socket_read_func,
436     write_fd    => fileno($socket),
437     write_func  => \&a_socket_read_func,
438
439     # Optional attributes                          DEFAULTS
440     DEBUG              => 0,                       # 0
441     tls_ca_cert        => "path/to/ca/cert.pem",   # ''
442     tls_cert           => "path/to/cert.pem",      # ''
443     tls_paranoia       => 1,                       # 0
444     tls_priv           => "path/to/priv_key.pem",  # ''
445     tls_vdepth         => 5,                       # 0
446     tls_verify         => 1,                       # 0
447 });
448
449Constructor. Should not be called directly. This documents the
450attributes that are in common for L<Munin::Common::TLSServer> and
451L<Munin::Common::TLSClient>.
452
453=item B<read>
454
455 my $msg = $tls->read();
456
457Encrypted read.
458
459=item B<write>
460
461 $tls->write($msg);
462
463Encrypted write.
464
465=item B<session_started>
466
467 my $bool = $tls->session_started();
468
469Returns true if the TLS object is ready to read/write encrypted data.
470
471=back
472