From ca0ee9a050f4c8e0f2956fd03cab6c452f4347c6 Mon Sep 17 00:00:00 2001 From: Tobias Oetiker Date: Sat, 13 Feb 2016 18:25:02 +0100 Subject: [PATCH] * --checkonly option added * make format for certs and keys configurable * updated to Protocol::ACME 0.11 * fix missing use FindBin --- CHANGES | 5 ++- Makefile.in | 2 +- PERL_MODULES | 2 +- README.md | 9 ++++ VERSION | 2 +- bin/acmefetch | 100 ++++++++++++++++++++++++++++------------- configure | 22 ++++----- doc/acmefetch.pod | 1 + etc/acmefetch.cfg.dist | 9 ++-- man/acmefetch.1 | 5 ++- 10 files changed, 107 insertions(+), 50 deletions(-) diff --git a/CHANGES b/CHANGES index 8194d10..c1fbaad 100644 --- a/CHANGES +++ b/CHANGES @@ -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 diff --git a/Makefile.in b/Makefile.in index 8807335..463115e 100644 --- a/Makefile.in +++ b/Makefile.in @@ -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 diff --git a/PERL_MODULES b/PERL_MODULES index 0994f5a..da98d2c 100644 --- a/PERL_MODULES +++ b/PERL_MODULES @@ -1,5 +1,5 @@ Crypt::RSA::Parse@0.041 -Protocol::ACME@0.09 +Protocol::ACME@0.11 Data::Processor Pod::Usage JSON diff --git a/README.md b/README.md index 6f3a6a5..c3ecd9e 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/VERSION b/VERSION index 42045ac..1d0ba9e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.4 +0.4.0 diff --git a/bin/acmefetch b/bin/acmefetch index 73e9bea..48c9037 100755 --- a/bin/acmefetch +++ b/bin/acmefetch @@ -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; @@ -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) } @@ -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 @@ -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, @@ -181,18 +206,22 @@ 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}; @@ -200,7 +229,7 @@ sub loadCfg { "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"; } } } @@ -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(); } @@ -264,8 +294,9 @@ 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 => { @@ -273,29 +304,32 @@ sub getCertificates { 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 ($@){ @@ -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; } @@ -344,6 +383,7 @@ B [I...] --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 diff --git a/configure b/configure index 57d37b5..929120a 100755 --- a/configure +++ b/configure @@ -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 . # @@ -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='tobi@oetiker.ch' PACKAGE_URL='' @@ -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]... @@ -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 @@ -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. @@ -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 $@ @@ -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 @@ -2865,7 +2865,7 @@ fi mod_ok=1 MISSING_PERL_MODULES="" if test "$enable_pkgonly" != yes; then - for module in Crypt::RSA::Parse@0.041 Protocol::ACME@0.09 Data::Processor Pod::Usage JSON Crypt::OpenSSL::X509 Net::SSLeay ; do + for module in Crypt::RSA::Parse@0.041 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 @@ -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 @@ -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\\" diff --git a/doc/acmefetch.pod b/doc/acmefetch.pod index a0a3344..e315cc1 100644 --- a/doc/acmefetch.pod +++ b/doc/acmefetch.pod @@ -11,6 +11,7 @@ B [I...] --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 diff --git a/etc/acmefetch.cfg.dist b/etc/acmefetch.cfg.dist index ae42ec7..1c62e42 100644 --- a/etc/acmefetch.cfg.dist +++ b/etc/acmefetch.cfg.dist @@ -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": { diff --git a/man/acmefetch.1 b/man/acmefetch.1 index 974ce69..a36ea09 100644 --- a/man/acmefetch.1 +++ b/man/acmefetch.1 @@ -133,7 +133,7 @@ .\" ======================================================================== .\" .IX Title "ACMEFETCH 1" -.TH ACMEFETCH 1 "2016-01-26" "0.3.4" "AcmeFetch" +.TH ACMEFETCH 1 "2016-02-13" "0.4.0" "AcmeFetch" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l @@ -144,12 +144,13 @@ acmefetch \- generate ACME based certificates .IX Header "SYNOPSIS" \&\fBacmefetch\fR [\fIoptions\fR...] .PP -.Vb 7 +.Vb 8 \& \-\-man show man\-page and exit \& \-h, \-\-help display this help and exit \& \-\-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 .Ve