Skip to content

Commit

Permalink
* --checkonly option added
Browse files Browse the repository at this point in the history
* make format for certs and keys configurable
* updated to Protocol::ACME 0.11
* fix missing use FindBin
  • Loading branch information
oetiker committed Feb 13, 2016
1 parent b934ca2 commit ca0ee9a
Show file tree
Hide file tree
Showing 10 changed files with 107 additions and 50 deletions.
5 changes: 4 additions & 1 deletion CHANGES
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
2016-01-26 20:08:59 +0100 (HEAD, origin/master, master) make bla work without quotes ... -- Tobias Oetiker
2016-02-12 19:47:07 +0100 - a couple of fixes -- Dominik Hassler
2016-01-27 11:18:46 +0100 output the key to a temp file -- Tobias Oetiker
2016-01-26 20:40:59 +0100 (tag: v0.3.4) fix version string -- Tobias Oetiker
2016-01-26 20:08:59 +0100 make bla work without quotes ... -- Tobias Oetiker
2016-01-26 20:02:21 +0100 (tag: v0.3.3) add some documentation -- Tobias Oetiker
2016-01-26 16:39:24 +0100 (tag: v0.3.2) better documentation -- Tobias Oetiker
2016-01-26 16:31:51 +0100 added progress reporting -- Tobias Oetiker
Expand Down
2 changes: 1 addition & 1 deletion Makefile.in
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ POST_UNINSTALL = :
subdir = .
DIST_COMMON = $(srcdir)/Makefile.in $(srcdir)/Makefile.am \
$(top_srcdir)/configure $(am__configure_deps) \
$(dist_bin_SCRIPTS) AUTHORS README conftools/install-sh \
$(dist_bin_SCRIPTS) AUTHORS README TODO conftools/install-sh \
conftools/missing $(top_srcdir)/conftools/install-sh \
$(top_srcdir)/conftools/missing
ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
Expand Down
2 changes: 1 addition & 1 deletion PERL_MODULES
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Crypt::RSA::[email protected]
Protocol::ACME@0.09
Protocol::ACME@0.11
Data::Processor
Pod::Usage
JSON
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ Configuration

Take a look at the etc/acmefetch.cfg.dist file for inspiration.

Documentation
-------------

First make sure you understand how letsencrypt certificates work
by reading https://letsencrypt.org/howitworks/technology/

The read the acmefetch documentation in the doc directory and finally take
some inspiration from the sample configuration file provided.

Enjoy!

Tobias Oetiker <[email protected]>
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.3.4
0.4.0
100 changes: 70 additions & 30 deletions bin/acmefetch
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
#!/usr/bin/env perl

use warnings;
use strict;
use lib qw(); # PERL5LIB
use FindBin;use lib "$FindBin::RealBin/../lib";use lib "$FindBin::RealBin/../thirdparty/lib/perl5"; # LIBDIR
use FindBin;
use lib "$FindBin::RealBin/../lib";use lib "$FindBin::RealBin/../thirdparty/lib/perl5"; # LIBDIR

use lib qw(/home/oetiker/checkouts/Protocol-ACME/lib);
use Protocol::ACME;
use Data::Processor;
use Data::Processor::ValidatorFactory;
Expand All @@ -17,13 +18,19 @@ use FindBin;

my $VERSION = '0.dev'; # VERSION

my %formatMap = (
DER => Crypt::OpenSSL::X509::FORMAT_ASN1(),
PEM => Crypt::OpenSSL::X509::FORMAT_PEM(),
);


# parse options
my %opt = ();
sub main()
{

GetOptions(\%opt, 'help|h', 'man', 'version','force',
'cfg=s', 'staging','verbose') or exit(1);
GetOptions(\%opt, 'help|h', 'man', 'version','force','debug',
'cfg=s', 'staging','verbose','checkonly') or exit(1);
if($opt{help}) { pod2usage(1) }
if($opt{man}) { pod2usage(-exitstatus => 0, -verbose => 2) }
if($opt{version}) { print "acmefetch $VERSION\n"; exit(0) }
Expand All @@ -32,15 +39,18 @@ sub main()

$cfg->{GENERAL}{host} = $opt{staging} ? $cfg->{GENERAL}->{ACMEstaging}
: $cfg->{GENERAL}->{ACMEservice};

getCertificates($cfg);
}

main;

sub bla {
my $level = shift;
my $text = shift;
print "* $text\n" if $opt{verbose};
if ($opt{verbose}
or ($opt{checkonly} and $level eq 'info')){
print "* $text\n";
}
}

# get a validator for our configuration file
Expand Down Expand Up @@ -90,13 +100,28 @@ sub getDataProcessor {
validator => $vf->file('>>','cert file'),
},
certFormat => {
description => 'PEM or ASN1 output format for the cert',
validator => $vf->rx('^(ASN1|PEM)','Pick ASN1 or PEM'),
description => 'PEM or DER output format for the cert',
validator => $vf->rx('^(DER|PEM)','Pick DER or PEM'),
default => 'PEM',
},
keyOutput => {
description => 'key output file',
validator => $vf->file('>>','key file')
},
keyFormat => {
description => 'PEM or DER output format for the cert',
validator => $vf->rx('^(DER|PEM)','Pick DER or PEM'),
default => 'PEM',
},
chainOutput => {
description => 'chain output file',
validator => $vf->file('>>','chain file'),
},
chainFormat => {
description => 'PEM or DER output format for the chian file',
validator => $vf->rx('^(DER|PEM)','Pick DER or PEM'),
default => 'PEM',
},
commonName => {
description => 'designate the common name in the certificate. the other sites will be listed as subjectAltName entries.',
validator => $string,
Expand Down Expand Up @@ -181,26 +206,30 @@ sub loadJSONCfg {
return $raw_cfg;
}

sub convertCert {
my $cert = shift;
my $inForm = shift;
my $outForm = shift;
return (Crypt::OpenSSL::X509->new_from_string($cert,$formatMap{$inForm})->as_string($formatMap{$outForm}));
}

sub loadCfg {
my $file = shift;
my $cfg = loadJSONCfg($file);
my $err = getDataProcessor()->validate($cfg);
my $hasErrors;
for my $cert (@{$cfg->{CERTS}}){
if (not exists $cert->{SITES}{$cert->{commonName}} ){
die "commonName ($cert->{commonName}) has no matching site entry.\n"
}
$cert->{certFormat} = {
DER => Crypt::OpenSSL::X509::FORMAT_ASN1,
PEM => Crypt::OpenSSL::X509::FORMAT_PEM,
}->{$cert->{certFormat}};

for my $site (sort keys %{$cert->{SITES}}){
my $sp = $cert->{SITES}{$site};
$sp->{challengeObj} = eval {
"Protocol::ACME::Challenge::$sp->{challengeHandler}"->new($sp->{challengeConfig});
};
if ($@){
die "Failed to instanciate Challenge handler for $key ($@)\n";
die "Failed to instanciate Challenge handler for $site - $sp->{challengeHandler} ($@)\n";
}
}
}
Expand Down Expand Up @@ -243,9 +272,10 @@ CSRcfg_END
my $csrFh = File::Temp->new( UNLINK => 0,SUFFIX => '.csr');
system $openssl,qw(req -nodes -newkey rsa:2048 -batch -reqexts SAN -outform der),
-keyout => $cert->{keyOutput}.'.'.$$,
-keyform => $cert->{keyFormat},
-out => $csrFh->filename(),
-config => $cfgFh->filename();
say $cfgFh;
chmod 0600, $cert->{keyOutput}.'.'.$$;
unlink $cfgFh->filename();
return $csrFh->filename();
}
Expand All @@ -264,38 +294,42 @@ sub getCertificates {
my $cfg = shift;
my $openssl = $cfg->{GENERAL}{opensslBin};
for my $cert (@{$cfg->{CERTS}}){
bla "## $cert->{commonName} ##";
next if checkCert($cert->{certOutput},$cert->{SITES}) and not $opt{force};
bla 'debug',"## $cert->{commonName} ##";
next if checkCert($cert->{certOutput},$cert->{certFormat},$cert->{SITES}) and not $opt{force};
next if $opt{checkonly};
my $acme = Protocol::ACME->new(
host => $cfg->{GENERAL}{host},
account_key => {
filename => getAccountKey($cfg),
format => 'PEM',
},
openssl => $cfg->{GENERAL}{opensslBin},
loglevel => ($opt{verbose} ? 'debug': 'error'),
debug => $opt{verbose},
loglevel => ($opt{debug} ? 'debug': 'error'),
debug => $opt{debug},
);
bla "talk to $cfg->{GENERAL}{host}";
bla 'debug',"talk to $cfg->{GENERAL}{host}";
$acme->directory();
$acme->register();
$acme->accept_tos();
for my $domain (sort keys %{$cert->{SITES}}){
bla "authorize $domain via $cert->{SITES}{$domain}{challengeConfig}{www_root}";
bla 'debug',"authorize $domain via $cert->{SITES}{$domain}{challengeConfig}{www_root}";
$acme->authz( $domain );
$acme->handle_challenge( $cert->{SITES}{$domain}{challengeObj} );
$acme->check_challenge();
}
my $csrFile = makeCsr($cfg,$cert);
eval {
my $certData = Crypt::OpenSSL::X509->new_from_string(
$acme->sign( $csrFile ),Crypt::OpenSSL::X509::FORMAT_ASN1
)->as_string($cert->{certFormat});
$certFile = $cert->{certOutput};
my $certData = convertCert($acme->sign( $csrFile ),'DER',$cert->{certFormat});
my $certFile = $cert->{certOutput};
my $fh = IO::File->new( $certFile, "w" )
|| die "Write $certFile: $!";
print $fh $certData;
$fh->close();
my $chainData = convertCert($acme->chain(),'DER',$cert->{chainFormat});
my $fh2 = IO::File->new( $cert->{chainOutput}, "w" )
|| die "Write $cert->{chainOutput}: $!";
print $fh2 $chainData;
$fh2->close();
rename $cert->{keyOutput}.'.'.$$, $cert->{keyOutput};
};
if ($@){
Expand All @@ -309,23 +343,28 @@ sub getCertificates {

sub checkCert {
my $file = shift;
my $format = shift;

my $domains = shift;
return 0 if not -r $file or -z $file;
my $x509 = Crypt::OpenSSL::X509->new_from_file($file,$cert->{certFormat});
if (not -r $file or -z $file){
bla 'info',"Cert $file is missing. Generating.\n";
return 0;
};
my $x509 = Crypt::OpenSSL::X509->new_from_file($file,$formatMap{$format});
my %dns;
$x509->subject() =~ m{CN=([^,/\s]+)} and $dns{$1} = 1;
map { /DNS:([^\s]+)/ and $dns{$1} =1 } split /\s*,\s*/, $x509->extensions_by_oid->{"2.5.29.17"}->to_string;
for my $domain (keys %$domains){
if (not $dns{$domain}){
bla "Cert is missing domain $domain. Renewing.\n";
bla 'info',"Cert $file missing domain $domain. Renewing.\n";
return 0;
};
}
if ($x509->checkend(30*24*3600)){
bla "Cert expires within 30 days. Renewing.\n";
bla 'info',"Cert expires within 30 days. Renewing.\n";
return 0;
}
bla "Cert still ok. Skipping. (use --force to override)\n";
bla 'debug',"Cert still ok. Skipping. (use --force to override)\n";
return 1;
}

Expand All @@ -344,6 +383,7 @@ B<acmefetch> [I<options>...]
--version output version information and exit
--cfg=file alternate config file (not ../etc/acmefetch.cfg)
--staging use the server specified in ACMEstaging
--checkonly only check validity of existing certs
--force will renew certs even when they are not expired
--verbose talk more while working
Expand Down
22 changes: 11 additions & 11 deletions configure
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#! /bin/sh
# Guess values for system-dependent variables and create Makefiles.
# Generated by GNU Autoconf 2.69 for AcmeFetch 0.3.4.
# Generated by GNU Autoconf 2.69 for AcmeFetch 0.4.0.
#
# Report bugs to <[email protected]>.
#
Expand Down Expand Up @@ -580,8 +580,8 @@ MAKEFLAGS=
# Identity of this package.
PACKAGE_NAME='AcmeFetch'
PACKAGE_TARNAME='acmefetch'
PACKAGE_VERSION='0.3.4'
PACKAGE_STRING='AcmeFetch 0.3.4'
PACKAGE_VERSION='0.4.0'
PACKAGE_STRING='AcmeFetch 0.4.0'
PACKAGE_BUGREPORT='[email protected]'
PACKAGE_URL=''

Expand Down Expand Up @@ -1219,7 +1219,7 @@ if test "$ac_init_help" = "long"; then
# Omit some internal or obsolete options to make the list less imposing.
# This message is too long to be a string in the A/UX 3.1 sh.
cat <<_ACEOF
\`configure' configures AcmeFetch 0.3.4 to adapt to many kinds of systems.
\`configure' configures AcmeFetch 0.4.0 to adapt to many kinds of systems.
Usage: $0 [OPTION]... [VAR=VALUE]...
Expand Down Expand Up @@ -1285,7 +1285,7 @@ fi

if test -n "$ac_init_help"; then
case $ac_init_help in
short | recursive ) echo "Configuration of AcmeFetch 0.3.4:";;
short | recursive ) echo "Configuration of AcmeFetch 0.4.0:";;
esac
cat <<\_ACEOF
Expand Down Expand Up @@ -1371,7 +1371,7 @@ fi
test -n "$ac_init_help" && exit $ac_status
if $ac_init_version; then
cat <<\_ACEOF
AcmeFetch configure 0.3.4
AcmeFetch configure 0.4.0
generated by GNU Autoconf 2.69
Copyright (C) 2012 Free Software Foundation, Inc.
Expand All @@ -1388,7 +1388,7 @@ cat >config.log <<_ACEOF
This file contains any messages produced by compilers while
running configure, to aid debugging if configure makes a mistake.
It was created by AcmeFetch $as_me 0.3.4, which was
It was created by AcmeFetch $as_me 0.4.0, which was
generated by GNU Autoconf 2.69. Invocation command line was
$ $0 $@
Expand Down Expand Up @@ -2255,7 +2255,7 @@ fi
# Define the identity of the package.
PACKAGE='acmefetch'
VERSION='0.3.4'
VERSION='0.4.0'
cat >>confdefs.h <<_ACEOF
Expand Down Expand Up @@ -2865,7 +2865,7 @@ fi
mod_ok=1
MISSING_PERL_MODULES=""
if test "$enable_pkgonly" != yes; then
for module in Crypt::RSA::[email protected] Protocol::ACME@0.09 Data::Processor Pod::Usage JSON Crypt::OpenSSL::X509 Net::SSLeay ; do
for module in Crypt::RSA::[email protected] Protocol::ACME@0.11 Data::Processor Pod::Usage JSON Crypt::OpenSSL::X509 Net::SSLeay IO::Socket::SSL ; do
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for perl module '$module'" >&5
$as_echo_n "checking for perl module '$module'... " >&6; }
if ${PERL} -I`dirname $0`/thirdparty/lib/perl5 -e 'my($m,$v) = split /\@/, q{'$module'};eval "use $m"; exit 1 if $@; exit 1 if not $v or eval(q{$}.$m.q{::VERSION}) ne $v' ; then
Expand Down Expand Up @@ -3443,7 +3443,7 @@ cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1
# report actual input values of CONFIG_FILES etc. instead of their
# values after options handling.
ac_log="
This file was extended by AcmeFetch $as_me 0.3.4, which was
This file was extended by AcmeFetch $as_me 0.4.0, which was
generated by GNU Autoconf 2.69. Invocation command line was
CONFIG_FILES = $CONFIG_FILES
Expand Down Expand Up @@ -3496,7 +3496,7 @@ _ACEOF
cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
ac_cs_config="`$as_echo "$ac_configure_args" | sed 's/^ //; s/[\\""\`\$]/\\\\&/g'`"
ac_cs_version="\\
AcmeFetch config.status 0.3.4
AcmeFetch config.status 0.4.0
configured by $0, generated by GNU Autoconf 2.69,
with options \\"\$ac_cs_config\\"
Expand Down
1 change: 1 addition & 0 deletions doc/acmefetch.pod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ B<acmefetch> [I<options>...]
--version output version information and exit
--cfg=file alternate config file (not ../etc/acmefetch.cfg)
--staging use the server specified in ACMEstaging
--checkonly only check validity of existing certs
--force will renew certs even when they are not expired
--verbose talk more while working

Expand Down
9 changes: 6 additions & 3 deletions etc/acmefetch.cfg.dist
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@
"GENERAL": {
"ACMEstaging": "acme-staging.api.letsencrypt.org",
"ACMEservice": "acme-v01.api.letsencrypt.org",
"accountKeyPath": "/tmp/accountKey.key"
"accountKeyPath": "/etc/ssl/private/letsencryptAccountKey.key"
},
"CERTS": [
{
"certOutput": "/tmp/testCert.pem",
"certOutput": "/etc/ssl/certs/testCert.pem",
"certFormat": "PEM",
"keyOutput": "/tmp/testCert.key",
"keyOutput": "/etc/ssl/private/testCert.key",
"keyFormat": "PEM",
"chainFormat": "/etc/ssl/certs/testCertChain.pem",
"chainFormat": "PEM",
"commonName": "my.web.domain",
"SITES": {
"my.web.domain": {
Expand Down
Loading

0 comments on commit ca0ee9a

Please sign in to comment.