From e52d27fd32caf42240ec361ae4d8b407d13b3e53 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Thu, 29 Dec 2016 18:26:46 +0100 Subject: [PATCH] Fix expiration detection of authz Add new cron script --- acme.pm | 13 ++--- letscron | 145 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+), 6 deletions(-) create mode 100755 letscron diff --git a/acme.pm b/acme.pm index fcaccf0..6a17c3c 100644 --- a/acme.pm +++ b/acme.pm @@ -8,11 +8,12 @@ use warnings; # Symbol export use Exporter; our @ISA = qw(Exporter); +our @EXPORT_OK = qw(DS CERT_DIR KEY_DIR REQUEST_CSR ACCOUNT_KEY SERVER_KEY SERVER_CRT CONFIG); # Load dependancies use Carp qw(carp confess); +use Data::Dumper; use Date::Parse qw(str2time); -use DateTime; use Digest::SHA qw(sha256_base64); use Email::Valid; use File::Path qw(make_path); @@ -23,8 +24,8 @@ use JSON qw(encode_json decode_json); use LWP; use MIME::Base64 qw(encode_base64url encode_base64); use Net::Domain::TLD; -use Tie::IxHash; use POSIX qw(EXIT_FAILURE); +use Tie::IxHash; # Documentation links #XXX: see https://letsencrypt.github.io/acme-spec/ (probably based on https://ietf-wg-acme.github.io/acme/) @@ -477,10 +478,9 @@ sub authorize { # Read it ($content = read_file($file)) && # Decode it - ($content = decode_json($content)) && - # Check expiration - (DateTime->from_epoch(epoch => str2time($content->{expires})) >= DateTime->now()->add(hours => 1)) - } + ($content = decode_json($content)) + # Check expiration + } || (str2time($content->{expires}) <= time()+3600) ) { # Post new-authz request my $res = $self->_post($self->{'new-authz'}, {resource => 'new-authz', identifier => {type => 'dns', value => $_}, existing => 'accept'}); @@ -669,6 +669,7 @@ sub issue { # Handle error unless ($res->is_success) { + #print Dumper($res); confess 'POST '.$self->{'new-cert'}.' failed: '.$res->status_line; } diff --git a/letscron b/letscron new file mode 100755 index 0000000..b566bae --- /dev/null +++ b/letscron @@ -0,0 +1,145 @@ +#! /usr/bin/perl + +# Best practice +use strict; +use warnings; + +# Fix use of acl +use filetest 'access'; + +# Load dependancies +use Carp qw(carp); +use DateTime; +use File::stat qw(stat); +use File::Spec; +use File::Slurp qw(read_file write_file); +use JSON qw(decode_json); +use IPC::System::Simple qw(capturex $EXITVAL); +use acme qw(CERT_DIR CONFIG DS KEY_DIR SERVER_CRT SERVER_KEY); + +# Load POSIX +use POSIX qw(strftime EXIT_SUCCESS EXIT_FAILURE); + +# Init debug +my $debug = 0; + +# Init config +my $config = undef; + +# Strip and enable debug +@ARGV = map { if ($_ eq '-d') { $debug = 1; (); } else { $_; } } @ARGV; + +# Check config file +if (! -f CONFIG) { + print 'Config file '.CONFIG.' do not exists'."\n"; + exit EXIT_FAILURE; +} + +# Load config +unless ( + #XXX: use eval to workaround a fatal in decode_json + eval { + # Read it + ($config = read_file(CONFIG)) && + # Decode it + ($config = decode_json($config)) && + # Check hash validity + defined($config->{certificates}) && + # Check not empty + scalar($config->{certificates}) && + # Check hash validity + defined($config->{thumbprint}) && + # Check certificates array + ! scalar map {unless(defined($_->{cert}) && defined($_->{key}) && defined($_->{mail}) && defined($_->{domain}) && defined($_->{domains})) {1;} else {();}} @{$config->{certificates}} + } +) { + print 'Config file '.CONFIG.' is invalid'."\n"; + exit EXIT_FAILURE; +} + +# Deal with certificates +foreach (@{$config->{certificates}}) { + # Init variables + my ($mtime, $dt, $suffix) = undef; + + # Skip certificate without 60 days + if (-f $_->{cert} && ($mtime = stat($_->{cert})->mtime) >= (time() - 60*24*3600)) { + next; + } + + # Extract cert directory and filename + my (undef, $certDir) = File::Spec->splitpath($_->{cert}); + + # Check that certificate is writable + unless (-w $certDir || -w $_->{cert}) { + carp('directory '.$certDir.' or file '.$_->{cert}.' must be writable: '.$!); + next; + } + + # Unlink if is a symlink + if (-l KEY_DIR.DS.SERVER_KEY) { + unless(unlink(KEY_DIR.DS.SERVER_KEY)) { + carp('unlink '.KEY_DIR.DS.SERVER_KEY.' failed: '.$!); + next; + } + } + + # Symlink to key + unless(symlink($_->{key}, KEY_DIR.DS.SERVER_KEY)) { + carp('symlink '.$_->{key}.' to '.KEY_DIR.DS.SERVER_KEY.' failed: '.$!); + next; + } + + # Init args + my @args = @{$_->{domains}}; + + # Prepend mail and domain to other args + unshift(@args, $_->{mail}, $_->{domain}); + + # Preprend prod + if (defined $_->{prod} && $_->{prod}) { + unshift(@args, '-p'); + } + + # Preprend debug + if ($debug) { + unshift(@args, '-d'); + } + + # Run letscert with args + my @out = capturex([0..1], './letscert', @args); + + # Deal with error + if ($EXITVAL != 0) { + print join("\n", @out) if ($debug); + carp('letscert '.join(', ', @args).' failed: '.$!); + next; + } + + # Read cert + my $content; + unless($content = read_file(CERT_DIR.DS.SERVER_CRT)) { + carp('read_file '.CERT_DIR.DS.SERVER_CRT.' failed: '.$!); + next; + } + + # Handle old certificate + if (-w $certDir && -f $_->{cert}) { + # Extract datetime suffix + $suffix = ($dt = DateTime->from_epoch(epoch => $mtime))->ymd('').$dt->hms(''); + + # Rename old certificate + unless(rename($_->{cert}, $_->{cert}.'.'.$suffix)) { + carp('rename '.$_->{cert}.' to '.$_->{cert}.'.'.$suffix.' failed: '.$!); + next; + } + } + + # Save cert + unless(write_file($_->{cert}, $content)) { + carp('write_file '.$_->{cert}.' failed: '.$!); + next; + } +} + +exit EXIT_SUCCESS; -- 2.41.1