Ansible roles
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

110 lines
3.7 KiB

#!/usr/bin/perl
=head1 SYNOPSIS
check_ssl_certificate.pl
--url,-u URL
--sni,-s HOSTNAME SNI servername (SSL vhost) that will be requested during SSL handshake.
This tells the server which certificate to return.
Default to the host passed with --url
=cut
use strict;
use warnings;
use IO::Socket::SSL;
use LWP::UserAgent;
use URI::URL;
use DateTime::Format::ISO8601;
use Getopt::Long qw/:config auto_help/;
use Pod::Usage;
use JSON qw(to_json);
use constant TIMEOUT => 10;
my ($url, $sni, $status, @san);
sub ssl_opts {
my ($sni, $expiration_date_ref, $status_ref, $san_ref) = @_;
return (
'verify_hostname' => 0,
'SSL_ca_file' => '/etc/pki/tls/certs/ca-bundle.crt',
'SSL_hostname' => $sni,
'SSL_verifycn_name' => $sni,
'SSL_verify_scheme' => 'http',
'SSL_verify_callback' => sub {
my (undef, $ctx_store) = @_;
# Get the error message from openssl verification
$$status_ref = Net::SSLeay::X509_verify_cert_error_string(Net::SSLeay::X509_STORE_CTX_get_error($ctx_store));
# Get the raw cert, to extract the expiration
my $cert = Net::SSLeay::X509_STORE_CTX_get_current_cert($ctx_store);
$$expiration_date_ref = Net::SSLeay::P_ASN1_TIME_get_isotime(Net::SSLeay::X509_get_notAfter($cert));
# Get Alt names so we can check later if the hostname match
@$san_ref = Net::SSLeay::X509_get_subjectAltNames($cert);
# Keep only odd elements. Even ones contains subject types which we're not interested in
@$san_ref = @$san_ref[grep $_ % 2, 0..scalar(@$san_ref)];
# Always return success
return 1;
}
)
}
sub https_get {
my ($url, $sni, $expiration_date_ref, $status_ref, $san_ref) = @_;
my $ua = LWP::UserAgent->new();
$ua->timeout(TIMEOUT);
$ua->ssl_opts( ssl_opts($sni, $expiration_date_ref, $status_ref, $san_ref) );
my $request = HTTP::Request->new('GET', $url);
$request->header(Host => $sni);
my $response = $ua->simple_request($request);
return $response;
}
sub wildcard_match {
my ($cn, $host) = @_;
my $match = 0;
return 0 if $cn !~ m/^\*\.(.*)$/;
my $cn_dom = $1;
my $host_dom = ($sni =~ m/^[^\.]+\.(.*)$/)[0];
return ($cn_dom eq $host_dom);
}
GetOptions ("url|u=s" => \$url,
"sni|s=s" => \$sni) or pod2usage(1);
if (@ARGV) {
print "This script takes no arguments...\n";
pod2usage(1);
}
pod2usage(1) if (!$url);
my $expiration_date;
my $uri = URI->new($url);
die "Only https urls are supported\n" unless $uri->scheme eq 'https';
$sni ||= $uri->host;
my $response = https_get($url, $sni, \$expiration_date, \$status, \@san);
my $out = {
code => $response->code,
status => $response->message,
days_left => undef,
cert_cn => undef,
issuer => undef
};
if ($response->code != 500) { # Even a 404 is good enough, as far as cert validation goes...
my $now = DateTime->now;
$expiration_date = DateTime::Format::ISO8601->parse_datetime( $expiration_date );
$out->{issuer} = $response->headers->{'client-ssl-cert-issuer'};
$out->{cert_cn} = ($response->headers->{'client-ssl-cert-subject'} =~ m/CN=(.*)$/)[0];
$status = "no common name" if !$out->{cert_cn};
$out->{status} = ($status eq 'ok' and !grep { $sni eq $_ } @san and !wildcard_match($out->{cert_cn},$sni)) ?
$out->{status} = "hostname mismatch ($sni doesn't match any of " . join(" ", @san) . ")" :
$status;
$out->{days_left} = ($expiration_date < $now) ? -1 * $expiration_date->delta_days($now)->delta_days :
$expiration_date->delta_days($now)->delta_days
}
print to_json($out, { pretty => 1 });