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