File Coverage

blib/lib/Net/SSL/ExpireDate.pm
Criterion Covered Total %
statement 64 233 27.4
branch 16 80 20.0
condition 5 13 38.4
subroutine 16 22 72.7
pod 4 4 100.0
total 105 352 29.8


line stmt bran cond sub pod time code
1             package Net::SSL::ExpireDate;
2              
3 6     6   3309 use strict;
  6         12  
  6         143  
4 6     6   37 use warnings;
  6         8  
  6         111  
5 6     6   89 use Carp;
  6         15  
  6         448  
6              
7             our $VERSION = '1.23';
8              
9 6     6   50 use base qw(Class::Accessor);
  6         16  
  6         496  
10 6     6   4845 use Crypt::OpenSSL::X509 qw(FORMAT_ASN1);
  6         209731  
  6         688  
11 6     6   2885 use Date::Parse;
  6         41322  
  6         676  
12 6     6   4912 use DateTime;
  6         535261  
  6         283  
13 6     6   44 use DateTime::Duration;
  6         14  
  6         119  
14 6     6   2992 use Time::Duration::Parse;
  6         10625  
  6         45  
15 6     6   2756 use UNIVERSAL::require;
  6         6482  
  6         51  
16              
17             my $Socket = 'IO::Socket::INET6';
18             unless ($Socket->require) {
19             $Socket = 'IO::Socket::INET';
20             $Socket->require or die $@;
21             }
22              
23             __PACKAGE__->mk_accessors(qw(type target));
24              
25             my $SSL3_RT_CHANGE_CIPHER_SPEC = 20;
26             my $SSL3_RT_ALERT = 21;
27             my $SSL3_RT_HANDSHAKE = 22;
28             my $SSL3_RT_APPLICATION_DATA = 23;
29              
30             my $SSL3_MT_HELLO_REQUEST = 0;
31             my $SSL3_MT_CLIENT_HELLO = 1;
32             my $SSL3_MT_SERVER_HELLO = 2;
33             my $SSL3_MT_CERTIFICATE = 11;
34             my $SSL3_MT_SERVER_KEY_EXCHANGE = 12;
35             my $SSL3_MT_CERTIFICATE_REQUEST = 13;
36             my $SSL3_MT_SERVER_DONE = 14;
37             my $SSL3_MT_CERTIFICATE_VERIFY = 15;
38             my $SSL3_MT_CLIENT_KEY_EXCHANGE = 16;
39             my $SSL3_MT_FINISHED = 20;
40              
41             my $SSL3_AL_WARNING = 0x01;
42             my $SSL3_AL_FATAL = 0x02;
43              
44             my $SSL3_AD_CLOSE_NOTIFY = 0;
45             my $SSL3_AD_UNEXPECTED_MESSAGE = 10; # fatal
46             my $SSL3_AD_BAD_RECORD_MAC = 20; # fatal
47             my $SSL3_AD_DECOMPRESSION_FAILURE = 30; # fatal
48             my $SSL3_AD_HANDSHAKE_FAILURE = 40; # fatal
49             my $SSL3_AD_NO_CERTIFICATE = 41;
50             my $SSL3_AD_BAD_CERTIFICATE = 42;
51             my $SSL3_AD_UNSUPPORTED_CERTIFICATE = 43;
52             my $SSL3_AD_CERTIFICATE_REVOKED = 44;
53             my $SSL3_AD_CERTIFICATE_EXPIRED = 45;
54             my $SSL3_AD_CERTIFICATE_UNKNOWN = 46;
55             my $SSL3_AD_ILLEGAL_PARAMETER = 47; # fatal
56              
57             sub new {
58 3     3 1 24 my ($class, %opt) = @_;
59              
60 3         15 my $self = bless {
61             type => undef,
62             target => undef,
63             expire_date => undef,
64             timeout => undef,
65             }, $class;
66              
67 3 100 66     27 if ( $opt{https} or $opt{ssl} ) {
    50          
68 1         8 $self->{type} = 'ssl';
69 1   33     5 $self->{target} = $opt{https} || $opt{ssl};
70             } elsif ($opt{file}) {
71 2         12 $self->{type} = 'file';
72 2         6 $self->{target} = $opt{file};
73 2 50       76 if (! -r $self->{target}) {
74 0         0 croak "$self->{target}: $!";
75             }
76             } else {
77 0         0 croak "missing option: neither ssl nor file";
78             }
79 3 50       11 if ($opt{timeout}) {
80 0         0 $self->{timeout} = $opt{timeout};
81             }
82 3 50       7 if ($opt{sni}) {
83 0         0 $self->{sni} = $opt{sni};
84             }
85              
86 3         12 return $self;
87             }
88              
89             sub expire_date {
90 2     2 1 8 my $self = shift;
91              
92 2 100       9 if (! $self->{expire_date}) {
93 1 50       6 if ($self->{type} eq 'ssl') {
    50          
94 0         0 my ($host, $port) = split /:/, $self->{target}, 2;
95 0   0     0 $port ||= 443;
96             ### $host
97             ### $port
98 0         0 my $cert = eval { _peer_certificate($host, $port, $self->{timeout}, $self->{sni}); };
  0         0  
99 0 0       0 warn $@ if $@;
100 0 0       0 return unless $cert;
101 0         0 my $x509 = Crypt::OpenSSL::X509->new_from_string($cert, FORMAT_ASN1);
102 0         0 my $begin_date_str = $x509->notBefore;
103 0         0 my $expire_date_str = $x509->notAfter;
104              
105 0         0 $self->{expire_date} = DateTime->from_epoch(epoch => str2time($expire_date_str));
106 0         0 $self->{begin_date} = DateTime->from_epoch(epoch => str2time($begin_date_str));
107              
108             } elsif ($self->{type} eq 'file') {
109 1         179 my $x509 = Crypt::OpenSSL::X509->new_from_file($self->{target});
110 1         23 $self->{expire_date} = DateTime->from_epoch(epoch => str2time($x509->notAfter));
111 1         910 $self->{begin_date} = DateTime->from_epoch(epoch => str2time($x509->notBefore));
112             } else {
113 0         0 croak "unknown type: $self->{type}";
114             }
115             }
116              
117 2         466 return $self->{expire_date};
118             }
119              
120             sub begin_date {
121 2     2 1 3 my $self = shift;
122              
123 2 50       10 if (! $self->{begin_date}) {
124 0         0 $self->expire_date;
125             }
126              
127 2         12 return $self->{begin_date};
128             }
129              
130             *not_after = \&expire_date;
131             *not_before = \&begin_date;
132              
133             sub is_expired {
134 3     3 1 93 my ($self, $duration) = @_;
135 3   66     18 $duration ||= DateTime::Duration->new();
136              
137 3 50       152 if (! $self->{begin_date}) {
138 0         0 $self->expire_date;
139             }
140              
141 3 100       19 if (! ref($duration)) { # if scalar
142 1         6 $duration = DateTime::Duration->new(seconds => parse_duration($duration));
143             }
144              
145 3         133 my $dx = DateTime->now()->add_duration( $duration );
146             ### dx: $dx->iso8601
147              
148 3 100       2275 return DateTime->compare($dx, $self->{expire_date}) >= 0 ? 1 : ();
149             }
150              
151             sub _peer_certificate {
152 0     0     my($host, $port, $timeout, $sni) = @_;
153              
154 0           my $cert;
155              
156 6     6   4367 no warnings 'once';
  6         20  
  6         282  
157 6     6   41 no strict 'refs'; ## no critic
  6         15  
  6         8685  
158 0           *{$Socket.'::write_atomically'} = sub {
159 0     0     my($self, $data) = @_;
160              
161 0           my $length = length $data;
162 0           my $offset = 0;
163 0           my $read_byte = 0;
164              
165 0           while ($length > 0) {
166 0   0       my $r = $self->syswrite($data, $length, $offset) || last;
167 0           $offset += $r;
168 0           $length -= $r;
169 0           $read_byte += $r;
170             }
171              
172 0           return $read_byte;
173 0           };
174              
175 0           my $sock = {
176             PeerAddr => $host,
177             PeerPort => $port,
178             Proto => 'tcp',
179             Timeout => $timeout,
180             };
181              
182 0 0         $sock = $Socket->new( %$sock ) or croak "cannot create socket: $!";
183              
184 0           my $servername;
185 0 0         if ($sni) {
    0          
186 0           $servername = $sni;
187             } elsif ($host !~ /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/) {
188 0           $servername = $host;
189             }
190 0           _send_client_hello($sock, $servername);
191              
192 0           my $do_loop = 1;
193 0           while ($do_loop) {
194 0           my $record = _get_record($sock);
195 0 0         if ($record->{type} != $SSL3_RT_HANDSHAKE) {
196 0 0         if ($record->{type} == $SSL3_RT_ALERT) {
197 0           my $d1 = unpack 'C', substr $record->{data}, 0, 1;
198 0           my $d2 = unpack 'C', substr $record->{data}, 1, 1;
199 0 0         if ($d1 eq $SSL3_AL_WARNING) {
200             ; # go ahead
201             } else {
202 0           croak "record type is SSL3_AL_FATAL. [desctioption: $d2]";
203             }
204             } else {
205 0           croak "record type is not HANDSHAKE";
206             }
207             }
208              
209 0           while (my $handshake = _get_handshake($record)) {
210 0 0         croak "too many loop" if $do_loop++ >= 10;
211 0 0         if ($handshake->{type} == $SSL3_MT_HELLO_REQUEST) {
    0          
    0          
    0          
    0          
    0          
212             ;
213             } elsif ($handshake->{type} == $SSL3_MT_CERTIFICATE_REQUEST) {
214             ;
215             } elsif ($handshake->{type} == $SSL3_MT_SERVER_HELLO) {
216             ;
217             } elsif ($handshake->{type} == $SSL3_MT_CERTIFICATE) {
218 0           my $data = $handshake->{data};
219 0           my $len1 = $handshake->{length};
220 0           my $len2 = (vec($data, 0, 8)<<16)+(vec($data, 1, 8)<<8)+vec($data, 2, 8);
221 0           my $len3 = (vec($data, 3, 8)<<16)+(vec($data, 4, 8)<<8)+vec($data, 5, 8);
222 0 0         croak "X509: length error" if $len1 != $len2 + 3;
223 0           $cert = substr $data, 6; # DER format
224             } elsif ($handshake->{type} == $SSL3_MT_SERVER_KEY_EXCHANGE) {
225             ;
226             } elsif ($handshake->{type} == $SSL3_MT_SERVER_DONE) {
227 0           $do_loop = 0;
228             } else {
229             ;
230             }
231             }
232              
233             }
234              
235 0 0         _sendalert($sock, $SSL3_AL_FATAL, $SSL3_AD_HANDSHAKE_FAILURE) or croak $!;
236 0           $sock->close;
237              
238 0           return $cert;
239             }
240              
241             sub _send_client_hello {
242 0     0     my($sock, $servername) = @_;
243              
244 0           my(@buf,$len);
245             ## record
246 0           push @buf, $SSL3_RT_HANDSHAKE;
247 0           push @buf, 3, 1;
248 0           push @buf, undef, undef;
249 0           my $pos_record_len = $#buf-1;
250              
251             ## handshake
252 0           push @buf, $SSL3_MT_CLIENT_HELLO;
253 0           push @buf, undef, undef, undef;
254 0           my $pos_handshake_len = $#buf-2;
255              
256             ## ClientHello
257             # client_version
258 0           push @buf, 3, 3;
259             # random
260 0           my $time = time;
261 0           push @buf, (($time>>24) & 0xFF);
262 0           push @buf, (($time>>16) & 0xFF);
263 0           push @buf, (($time>> 8) & 0xFF);
264 0           push @buf, (($time ) & 0xFF);
265 0           for (1..28) {
266 0           push @buf, int(rand(0xFF));
267             }
268             # session_id
269 0           push @buf, 0;
270             # cipher_suites
271 0           my @decCipherSuites = (
272             49199,
273             49195,
274             49200,
275             49196,
276             158,
277             162,
278             163,
279             159,
280             49191,
281             49187,
282             49171,
283             49161,
284             49192,
285             49188,
286             49172,
287             49162,
288             103,
289             51,
290             64,
291             107,
292             56,
293             57,
294             49170,
295             49160,
296             156,
297             157,
298             60,
299             61,
300             47,
301             53,
302             49186,
303             49185,
304             49184,
305             165,
306             161,
307             106,
308             105,
309             104,
310             55,
311             54,
312             49183,
313             49182,
314             49181,
315             164,
316             160,
317             63,
318             62,
319             50,
320             49,
321             48,
322             10,
323             136,
324             135,
325             134,
326             133,
327             132,
328             69,
329             68,
330             67,
331             66,
332             65,
333             );
334 0           $len = scalar(@decCipherSuites) * 2;
335 0           push @buf, (($len >> 8) & 0xFF);
336 0           push @buf, (($len ) & 0xFF);
337 0           foreach my $i (@decCipherSuites) {
338 0           push @buf, (($i >> 8) & 0xFF);
339 0           push @buf, (($i ) & 0xFF);
340             }
341              
342             # compression
343 0           push @buf, 1;
344 0           push @buf, 0;
345              
346             # Extensions length
347 0           my @ext = (undef, undef);
348              
349             # Extension: server_name
350 0 0         if ($servername) {
351             # my $buf_len = scalar(@buf);
352             # my $buf_len_pos = $#buf+1;
353             # push @buf, undef, undef;
354              
355             # SNI (Server Name Indication)
356 0           my $sn_len = length $servername;
357             # Extension Type: Server Name
358 0           push @ext, 0, 0;
359             # Length
360 0           push @ext, ((($sn_len+5) >> 8) & 0xFF);
361 0           push @ext, ((($sn_len+5) ) & 0xFF);
362             # Server Name Indication Length
363 0           push @ext, ((($sn_len+3) >> 8) & 0xFF);
364 0           push @ext, ((($sn_len+3) ) & 0xFF);
365             # Server Name Type: host_name
366 0           push @ext, 0;
367             # Length of servername
368 0           push @ext, (($sn_len >> 8) & 0xFF);
369 0           push @ext, (($sn_len ) & 0xFF);
370             # Servername
371 0           for my $c (split //, $servername) {
372 0           push @ext, ord($c);
373             }
374             }
375              
376             # Extension: supported_groups
377 0           push @ext, 0x00, 0x0a; # supported_groups
378 0           my @supportedGroups = (
379             0x000a, # sect163r1
380             0x0017, # secp256r1
381             0x0018, # secp384r1
382             0x0019, # secp521r1
383             0x001d, # x25519
384             0x001e, # x448
385             );
386 0           $len = scalar(@supportedGroups) * 2;
387 0           push @ext, (($len >> 8) & 0xFF);
388 0           push @ext, (($len ) & 0xFF);
389 0           foreach my $i (@supportedGroups) {
390 0           push @ext, (($i >> 8) & 0xFF);
391 0           push @ext, (($i ) & 0xFF);
392             }
393              
394             # Extension: signature_algorithms (>= TLSv1.2)
395 0           push @ext, 0x00, 0x0D; # signature_algorithms
396 0           push @ext, 0, 32; # length
397 0           push @ext, 0, 30; # signature hash algorithms length
398             # enum {
399             # none(0), md5(1), sha1(2), sha224(3), sha256(4), sha384(5),
400             # sha512(6), (255)
401             # } HashAlgorithm;
402 0           for my $ha (2..6) {
403             # enum { anonymous(0), rsa(1), dsa(2), ecdsa(3), (255) }
404             # SignatureAlgorithm;
405 0           for my $sa (1..3) {
406 0           push @ext, $ha, $sa;
407             }
408             }
409              
410             # Extension: Heartbeat
411 0           push @ext, 0x00, 0x0F; # heartbeat
412 0           push @ext, 0x00, 0x01; # length
413 0           push @ext, 0x01; # peer_allowed_to_send
414              
415 0           my $ext_len = scalar(@ext) - 2;
416 0 0         if ($ext_len > 0) {
417 0           $ext[0] = (($ext_len) >> 8) & 0xFF;
418 0           $ext[1] = (($ext_len) ) & 0xFF;
419 0           push @buf, @ext;
420             }
421              
422             # record length
423 0           $len = scalar(@buf) - $pos_record_len - 2;
424 0           $buf[ $pos_record_len ] = (($len >> 8) & 0xFF);
425 0           $buf[ $pos_record_len+1 ] = (($len ) & 0xFF);
426              
427             # handshake length
428 0           $len = scalar(@buf) - $pos_handshake_len - 3;
429 0           $buf[ $pos_handshake_len ] = (($len >> 16) & 0xFF);
430 0           $buf[ $pos_handshake_len+1 ] = (($len >> 8) & 0xFF);
431 0           $buf[ $pos_handshake_len+2 ] = (($len ) & 0xFF);
432              
433 0           my $data;
434 0           for my $c (@buf) {
435 0 0         if ($c =~ /^[0-9]+$/) {
436 0           $data .= pack('C', $c);
437             } else {
438 0           $data .= $c;
439             }
440             }
441              
442 0           return $sock->write_atomically($data);
443             }
444              
445             sub _get_record {
446 0     0     my($sock) = @_;
447              
448 0           my $record = {
449             type => -1,
450             version => -1,
451             length => -1,
452             read => 0,
453             data => "",
454             };
455              
456 0 0         $sock->read($record->{type} , 1) or croak "cannot read type";
457 0           $record->{type} = unpack 'C', $record->{type};
458              
459 0 0         $sock->read($record->{version}, 2) or croak "cannot read version";
460 0           $record->{version} = unpack 'n', $record->{version};
461              
462 0 0         $sock->read($record->{length}, 2) or croak "cannot read length";
463 0           $record->{length} = unpack 'n', $record->{length};
464              
465 0 0         $sock->read($record->{data}, $record->{length}) or croak "cannot read data";
466              
467 0           return $record;
468             }
469              
470             sub _get_handshake {
471 0     0     my($record) = @_;
472              
473 0           my $handshake = {
474             type => -1,
475             length => -1,
476             data => "",
477             };
478              
479 0 0         return if $record->{read} >= $record->{length};
480              
481 0           $handshake->{type} = vec($record->{data}, $record->{read}++, 8);
482 0 0         return if $record->{read} + 3 > $record->{length};
483              
484             $handshake->{length} =
485             (vec($record->{data}, $record->{read}++, 8)<<16)
486             +(vec($record->{data}, $record->{read}++, 8)<< 8)
487 0           +(vec($record->{data}, $record->{read}++, 8) );
488              
489 0 0         if ($handshake->{length} > 0) {
490 0           $handshake->{data} = substr($record->{data}, $record->{read}, $handshake->{length});
491 0           $record->{read} += $handshake->{length};
492 0 0         return if $record->{read} > $record->{length};
493             } else {
494 0           $handshake->{data}= undef;
495             }
496              
497 0           return $handshake;
498             }
499              
500             sub _sendalert {
501 0     0     my($sock, $level, $desc) = @_;
502              
503 0           my $data = "";
504              
505 0           $data .= pack('C', $SSL3_RT_ALERT);
506 0           $data .= pack('C', 3);
507 0           $data .= pack('C', 0);
508 0           $data .= pack('C', 0);
509 0           $data .= pack('C', 2);
510 0           $data .= pack('C', $level);
511 0           $data .= pack('C', $desc);
512              
513 0           return $sock->write_atomically($data);
514             }
515              
516             1; # Magic true value required at end of module
517             __END__
518              
519             =head1 NAME
520              
521             Net::SSL::ExpireDate - obtain expiration date of certificate
522              
523             =head1 SYNOPSIS
524              
525             use Net::SSL::ExpireDate;
526              
527             $ed = Net::SSL::ExpireDate->new( https => 'example.com' );
528             $ed = Net::SSL::ExpireDate->new( https => 'example.com:10443' );
529             $ed = Net::SSL::ExpireDate->new( ssl => 'example.com:465' ); # smtps
530             $ed = Net::SSL::ExpireDate->new( ssl => 'example.com:995' ); # pop3s
531             $ed = Net::SSL::ExpireDate->new( file => '/etc/ssl/cert.pem' );
532              
533             if (defined $ed->expire_date) {
534             # do something
535             $expire_date = $ed->expire_date; # return DateTime instance
536              
537             $expired = $ed->is_expired; # examine already expired
538              
539             $expired = $ed->is_expired('2 months'); # will expire after 2 months
540             $expired = $ed->is_expired(DateTime::Duration->new(months=>2)); # ditto
541             }
542              
543             =head1 DESCRIPTION
544              
545             Net::SSL::ExpireDate get certificate from network (SSL) or local
546             file and obtain its expiration date.
547              
548             =head1 METHODS
549              
550             =head2 new
551              
552             $ed = Net::SSL::ExpireDate->new( %option )
553              
554             This method constructs a new "Net::SSL::ExpireDate" instance and
555             returns it. %option is to specify certificate.
556              
557             KEY VALUE
558             ----------------------------
559             ssl "hostname[:port]"
560             https (same as above ssl)
561             file "path/to/certificate"
562             timeout "Timeout in seconds"
563             sni "Server Name Indicator"
564              
565             =head2 expire_date
566              
567             $expire_date = $ed->expire_date;
568              
569             Return expiration date by "DateTime" instance.
570              
571             =head2 begin_date
572              
573             $begin_date = $ed->begin_date;
574              
575             Return beginning date by "DateTime" instance.
576              
577             =head2 not_after
578              
579             Synonym for expire_date.
580              
581             =head2 not_before
582              
583             Synonym for begin_date.
584              
585             =head2 is_expired
586              
587             $expired = $ed->is_expired;
588              
589             Obtain already expired or not.
590              
591             You can specify interval to obtain will expire on the future time.
592             Acceptable intervals are human readable string (parsed by
593             "Time::Duration::Parse") and "DateTime::Duration" instance.
594              
595             # will expire after 2 months
596             $expired = $ed->is_expired('2 months');
597             $expired = $ed->is_expired(DateTime::Duration->new(months=>2));
598              
599             =head2 type
600              
601             return type of examinee certificate. "ssl" or "file".
602              
603             =head2 target
604              
605             return hostname or path of examinee certificate.
606              
607             =head1 BUGS AND LIMITATIONS
608              
609             No bugs have been reported.
610              
611             Please report any bugs or feature requests to
612             C<bug-net-ssl-expiredate@rt.cpan.org>, or through the web interface at
613             L<http://rt.cpan.org>.
614              
615             =head1 AUTHOR
616              
617             HIROSE Masaaki E<lt>hirose31 _at_ gmail.comE<gt>
618              
619             =head1 REPOSITORY
620              
621             L<http://github.com/hirose31/net-ssl-expiredate>
622              
623             git clone git://github.com/hirose31/net-ssl-expiredate.git
624              
625             patches and collaborators are welcome.
626              
627             =head1 SEE ALSO
628              
629             =head1 COPYRIGHT & LICENSE
630              
631             Copyright HIROSE Masaaki
632              
633             This library is free software; you can redistribute it and/or modify
634             it under the same terms as Perl itself.
635              
636             =cut
637