オンラインで証明書の期限切れをチェック

モジュールを使うのじゃ

昨日の作ったチェックツールはNet::SSL::ExpireDateを使っていなかったので,今日は使うようにしてみました.id:hirose31さんのNet::SSL::ExpireDate + Test::Base で証明書の期限切れをチェック - (ひ)メモを見てやっとTest::Baseの使い方がわかってきたので,それも利用.成功しているときには

ok 1 - foo.co.jp
ok 2 - foo.co.jp expired 2007-03-29T23:59:59
ok 3 # skip bar.foo.co.jp not using https
ok 4 # skip bar.foo.co.jp not using https

失敗したときには

not ok 5 - hoge.foo.co.jp expired 2006-07-15T23:59:59
#   Failed test 'hoge.foo.co.jp expired 2006-07-15T23:59:59'
#   in ./fraud3.pl at line 79.
#          got: '2006-07-15T23:59:59'
#     expected: undef
not ok 6 - hoge.foo.co.jp
#   Failed test 'hoge.foo.co.jp'
#   in ./fraud3.pl at line 77.
#                   'hoge2.foo.co.jp'
#     doesn't match '(?i-xsm:hoge.foo.co.jp)'

のように出力します.

スクリプト

#!/usr/local/bin/perl

{

    package Net::SSL::ExpireDate::WithCnCheck;
    use base qw ( Net::SSL::ExpireDate );
    use Date::Parse;
    use Carp;

    sub expire2_date {
        my $self = shift;

        if ( !$self->{expire_date} ) {
            if ( $self->{type} eq 'https' ) {
                my ( $host, $port ) = split /:/, $self->{target}, 2;
                $port ||= 443;
                ### $host
                ### $port
                my $sock = IO::Socket::SSL->new("$host:$port");
                croak IO::Socket::SSL::errstr() if !$sock;
                my $cert = $sock->peer_certificate();

                my $expire_date_asn1 = Net::SSLeay::X509_get_notAfter($cert);
                my $expire_date_str =
                  Net::SSLeay::P_ASN1_UTCTIME_put2string($expire_date_asn1);
                ### $expire_date_str
                my $begin_date_asn1 = Net::SSLeay::X509_get_notBefore($cert);
                my $begin_date_str =
                  Net::SSLeay::P_ASN1_UTCTIME_put2string($begin_date_asn1);
                ### $begin_date_str

                my $sub =
                  Net::SSLeay::X509_NAME_oneline(
                    Net::SSLeay::X509_get_subject_name($cert) );
                ( $self->{cn} ) = $sub =~ m{CN=(\S+)};

                $sock->close;

                $self->{expire_date} =
                  DateTime->from_epoch( epoch => str2time($expire_date_str) );
                $self->{begin_date} =
                  DateTime->from_epoch( epoch => str2time($begin_date_str) );

            }
            else {
                croak "you need https as type for expire2_date";
            }
        }

        return $self->{expire_date};
    }

    sub has_cn {
        my $self = shift;
        return $self->{cn};
    }
}

package main;
use Test::Base;

my $duration = '1 week';

plan tests => 2 * blocks;

run {
    my $block = shift;

    my ( $host, $ed );
    $host = $block->name();
    $ed = Net::SSL::ExpireDate::WithCnCheck->new( https => $host );
    eval { $ed->expire2_date(); };

  SKIP: {
        skip "$host not using https", 2 if ($@);

        like( $ed->has_cn(), qr{$host}i, $host );
        my $ex = $ed->expire_date();
        is( $ed->is_expired($duration) && $ed->expire_date->iso8601,
            undef, "$host expired $ex");
    }
}
__DATA__

=== foo.co.jp
=== bar.foo.co.jp
...

ちょっとした解説

expire_dateの中で証明書のCommonNameを抽出したかったので,Net::SSL::ExpireDate::WithCnCheckというモジュールを作って,その中でexpire2_dateというメソッドとhas_cnというメソッドを追加してみました.expire2_dateでの変更点はCommonNameを抽出しているところだけで,

my $sub =
    Net::SSLeay::X509_NAME_oneline(
        Net::SSLeay::X509_get_subject_name($cert) );
( $self->{cn} ) = $sub =~ m{CN=(\S+)};

というもの.has_cnは↑で$self->{cn}に蓄えたホスト名を取得しています.get_cnというメソッド名の方がよかったかも.その他,昨日と違う点は,

  SKIP: {
        skip "$host not using https", 2 if ($@);

という形で,httpsに対応していないサーバの場合はskipという表示をするようにしたこと,

    like( $ed->has_cn(), qr{$host}i, $host );

CommonNameに大文字でホスト名が書いてある場合用に正規表現を使ったことです.