From d6c3820cb889a6bba190b5c0c3b04032494ae2ea Mon Sep 17 00:00:00 2001
From: =?utf8?q?Rapha=C3=ABl=20Gertz?= <git@rapsys.eu>
Date: Tue, 20 Jun 2017 18:17:20 +0200
Subject: [PATCH] Rewrite code based on config file Version 0.8

---
 Acme.pm    | 177 +++++++++++++++++++++++++-------------------------
 acmecert   | 127 +++++++++++++++++++++++++++++-------
 acmecert.1 |  24 ++-----
 acmeconf   |  24 ++++++-
 acmeconf.1 |   8 ++-
 acmecron   | 186 ++++++++++++++++++++++-------------------------------
 acmecron.1 |  12 ++--
 7 files changed, 309 insertions(+), 249 deletions(-)

diff --git a/Acme.pm b/Acme.pm
index 1f5675f..adcc99e 100644
--- a/Acme.pm
+++ b/Acme.pm
@@ -22,16 +22,20 @@ package Acme;
 use strict;
 use warnings;
 
+# Fix use of acl
+use filetest qw(access);
+
 # 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);
+our @EXPORT_OK = qw(VERSION);
 
 # Load dependancies
 use Carp qw(carp confess);
 use Date::Parse qw(str2time);
 use Digest::SHA qw(sha256_base64);
 use Email::Valid;
+use File::Copy qw(copy);
 use File::Path qw(make_path);
 use File::Slurp qw(read_file write_file);
 use File::Temp; # qw( :seekable );
@@ -43,6 +47,9 @@ use Net::Domain::TLD;
 use POSIX qw(EXIT_FAILURE);
 use Tie::IxHash;
 
+# Debug
+use Data::Dumper;
+
 # Documentation links
 #XXX: see https://letsencrypt.github.io/acme-spec/ (probably based on https://ietf-wg-acme.github.io/acme/)
 #XXX: see jwk rfc http://www.rfc-editor.org/rfc/rfc7517.txt
@@ -50,30 +57,9 @@ use Tie::IxHash;
 
 # Set constants
 use constant {
-	# Directory separator
-	DS => '/',
-
-	# Directory for certificates
-	CERT_DIR => 'cert',
-
-	# Directory for keys
-	KEY_DIR => 'key',
-
-	# Directory for pending cache
-	PENDING_DIR => 'pending',
-
 	# Request certificate file name
 	REQUEST_CSR => 'request.der',
 
-	# Account key file name
-	ACCOUNT_KEY => 'account.pem',
-
-	# Server private key
-	SERVER_KEY => 'server.pem',
-
-	# Server public certificate
-	SERVER_CRT => 'server.crt',
-
 	# rsa
 	KEY_TYPE => 'rsa',
 
@@ -84,13 +70,9 @@ use constant {
 	ACME_CERT => 'https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem',
 	ACME_DIR => 'https://acme-staging.api.letsencrypt.org/directory',
 	ACME_PROD_DIR => 'https://acme-v01.api.letsencrypt.org/directory',
-	ACME_TERMS => 'https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf',
 
 	# Version
-	VERSION => 'v0.7',
-
-	# Config
-	CONFIG => '/etc/acme/config'
+	VERSION => 'v0.8',
 };
 
 # User agent object
@@ -124,7 +106,7 @@ tie(%{$jwk{jwk}{jwk}}, 'Tie::IxHash', e => undef, kty => uc(KEY_TYPE), n => unde
 # Constructor
 sub new {
 	# Extract params
-	my ($class, $mail, $debug, $prod, @domains) = @_;
+	my ($class, $debug, $domain, $config) = @_;
 
 	# Create self hash
 	my $self = {};
@@ -135,21 +117,27 @@ sub new {
 	# Save debug
 	$self->{debug} = $debug;
 
-	# Save prod
-	$self->{prod} = $prod;
+	# Save domain
+	$self->{domain} = $domain;
+
+	# Save config
+	$self->{config} = $config;
+
+	# Save domains
+	@{$self->{domains}} = ($domain->{domain}, @{$domain->{domains}});
 
 	# Add extra check to mail validity
 	#XXX: mxcheck fail if there is only a A record on the domain
 	my $ev = Email::Valid->new(-fqdn => 1, -tldcheck => 1, -mxcheck => 1);
 
 	# Show error if check fail
-	if (! defined $ev->address($mail)) {
+	if (! defined $ev->address($self->{domain}{mail})) {
 		map { carp 'failed check: '.$_ if ($self->{debug}) } $ev->details();
 		confess 'Email::Valid->address failed';
 	}
 
 	# Save mail
-	$self->{mail} = $mail;
+	$self->{mail} = $self->{domain}{mail};
 
 	# Create resolver
 	my $res = new Net::DNS::Resolver();
@@ -177,10 +165,7 @@ sub new {
 				confess 'search recursively A record for '.$_.' failed';
 			}
 		}
-	} @domains;
-
-	# Save domains
-	@{$self->{domains}} = @domains;
+	} @{$self->{domains}};
 
 	# Return class reference
 	return $self;
@@ -190,19 +175,58 @@ sub new {
 sub prepare {
 	my ($self) = @_;
 
+	# Extract cert directory and filename
+	my ($certFile, $certDir) = File::Spec->splitpath($self->{domain}{cert});
+
+	# Extract key directory and filename
+	my ($keyFile, $keyDir) = File::Spec->splitpath($self->{domain}{key});
+
+	# Extract account directory and filename
+	my ($accountFile, $accountDir) = File::Spec->splitpath($self->{domain}{account});
+
 	# Create all paths
-	make_path(CERT_DIR, KEY_DIR, PENDING_DIR.'/'.$self->{mail}.'.'.($self->{prod} ? 'prod' : 'staging'), {error => \my $err});
-	if (@$err) {
-		map {
-			my ($file, $msg) = %$_;
-			carp ($file eq '' ? '' : $file.': ').$msg if ($self->{debug});
-		} @$err;
-		confess 'make_path failed';
+	{
+		make_path($certDir, $keyDir, $accountDir, $self->{config}{pending}.'/'.$self->{mail}.'.'.($self->{domain}{prod} ? 'prod' : 'staging'), {error => \my $err});
+		if (@$err) {
+			map {
+				my ($file, $msg) = %$_;
+				carp ($file eq '' ? '' : $file.': ').$msg if ($self->{debug});
+			} @$err;
+			confess 'make_path failed';
+		}
 	}
 
 	# Create user agent
 	$ua = LWP::UserAgent->new;
-	$ua->agent(__PACKAGE__.'/'.VERSION)
+	$ua->agent(__PACKAGE__.'/'.VERSION);
+
+	# Check that certificate is writable
+	unless (-w $certDir || -w $self->{domain}{cert}) {
+		confess('Directory '.$certDir.' or file '.$self->{domain}{cert}.' must be writable: '.$!);
+	}
+
+	# Check that key is writable
+	unless (-r $self->{domain}{key} || -w $keyDir) {
+		confess('File '.$self->{domain}{key}.' must be readable or directory '.$keyDir.' must be writable: '.$!);
+	}
+
+	# Check that account is writable
+	unless (-r $self->{domain}{account} || -w $accountDir) {
+		confess('File '.$self->{domain}{account}.' must be readable or directory '.$accountDir.' must be writable: '.$!);
+	}
+
+	# Backup old certificate if possible
+	if (-w $certDir && -f $self->{domain}{cert}) {
+		my ($dt, $suffix) = undef;
+
+		# Extract datetime suffix
+		$suffix = ($dt = DateTime->from_epoch(epoch => stat($self->{domain}{cert})->mtime))->ymd('').$dt->hms('');
+
+		# Rename old certificate
+		unless(copy($self->{domain}{cert}, $self->{domain}{cert}.'.'.$suffix)) {
+			carp('Copy '.$self->{domain}{cert}.' to '.$self->{domain}{cert}.'.'.$suffix.' failed: '.$!);
+		}
+	}
 }
 
 # Drop stderr
@@ -239,7 +263,7 @@ sub genKeys {
 			# Restore stderr
 			_restoreStdErr();
 		}
-	} (KEY_DIR.DS.ACCOUNT_KEY, KEY_DIR.DS.SERVER_KEY);
+	} ($self->{domain}{account}, $self->{domain}{key});
 
 	# Extract modulus and publicExponent jwk
 	#XXX: same here we tie to keep ordering
@@ -252,12 +276,12 @@ sub genKeys {
 			# Extract to binary from int, trim leading zeros and convert to base64 url
 			chomp ($self->{account}{jwk}{jwk}{e} = encode_base64url(pack("N", $1) =~ s/^\0+//r));
 		}
-	} capturex('openssl', ('rsa', '-text', '-in', KEY_DIR.DS.ACCOUNT_KEY, '-noout', '-modulus'));
+	} capturex('openssl', ('rsa', '-text', '-in', $self->{domain}{account}, '-noout', '-modulus'));
 
 	# Drop stderr
 	_dropStdErr();
 	# Extract account public key
-	$self->{account}{pubkey} = join('', map { chomp; $_; } capturex('openssl', ('rsa', '-in', KEY_DIR.DS.ACCOUNT_KEY, '-pubout')));
+	$self->{account}{pubkey} = join('', map { chomp; $_; } capturex('openssl', ('rsa', '-in', $self->{domain}{account}, '-pubout')));
 	# Restore stderr
 	_restoreStdErr();
 
@@ -273,18 +297,21 @@ sub genCsr {
 	# Openssl config template
 	my $oct = File::Temp->new();
 
+	# Save data start position
+	my $pos = tell DATA;
+
 	# Load template from data
 	map { s/__EMAIL_ADDRESS__/$self->{mail}/; s/__COMMON_NAME__/$self->{domains}[0]/; print $oct $_; } <DATA>;
 
-	# Close data
-	close(DATA);
+	# Reseek data
+	seek(DATA, $pos, 0);
 
 	# Append domain names
 	my $i = 1;
 	map { print $oct 'DNS.'.$i++.' = '.$_."\n"; } @{$self->{domains}};
 
 	# Generate csr
-	capturex('openssl', ('req', '-new', '-outform', 'DER', '-key', KEY_DIR.DS.SERVER_KEY, '-config', $oct->filename, '-out', CERT_DIR.DS.REQUEST_CSR));
+	capturex('openssl', ('req', '-new', '-outform', 'DER', '-key', $self->{domain}{key}, '-config', $oct->filename, '-out', $self->{config}{pending}.'/'.$self->{mail}.'.'.($self->{domain}{prod} ? 'prod' : 'staging').'/'.REQUEST_CSR));
 
 	# Close oct
 	close($oct);
@@ -338,7 +365,7 @@ sub _post {
 	close($stf);
 
 	# Generate digest of stf
-	my $signature = encode_base64url(join('', capturex('openssl', ('dgst', '-sha256', '-binary', '-sign', KEY_DIR.DS.ACCOUNT_KEY, $stf->filename))) =~ s/^\0+//r);
+	my $signature = encode_base64url(join('', capturex('openssl', ('dgst', '-sha256', '-binary', '-sign', $self->{domain}{account}, $stf->filename))) =~ s/^\0+//r);
 
 	# Create a request
 	my $req = HTTP::Request->new(POST => $uri);
@@ -398,23 +425,10 @@ sub _httpCheck {
 	# Create a request
 	my $req = HTTP::Request->new(GET => 'http://'.$domain.'/.well-known/acme-challenge/'.$token);
 
-	# Load config if available
-	my $config = undef;
-	if (
-		#XXX: use eval to workaround a fatal in from_json
-		defined eval {
-			# Check that file exists
-			-f CONFIG &&
-			# Read it
-			($config = read_file(CONFIG)) &&
-			# Decode it
-			($config = from_json($config)) &&
-			# Check defined
-			$config->{thumbprint}
-		}
-	) {
+	# Check if thumbprint is writeable
+	if (-w $self->{config}{thumbprint}) {
 		# Try to write thumbprint
-		write_file($config->{thumbprint}, $self->{account}{thumbprint});
+		write_file($self->{config}{thumbprint}, $self->{account}{thumbprint});
 	}
 
 	# Get request
@@ -443,7 +457,7 @@ sub register {
 
 	# Post new-reg request
 	#XXX: contact array may contain a tel:+33612345678 for example
-	my $res = $self->_post($self->{'new-reg'}, {resource => 'new-reg', contact => ['mailto:'.$self->{mail}], agreement => ACME_TERMS});
+	my $res = $self->_post($self->{'new-reg'}, {resource => 'new-reg', contact => ['mailto:'.$self->{mail}], agreement => $self->{term}});
 
 	# Handle error
 	unless ($res->is_success || $res->code eq 409) {
@@ -482,7 +496,7 @@ sub authorize {
 		my $content = undef;
 
 		# Init file
-		my $file = PENDING_DIR.'/'.$self->{mail}.'.'.($self->{prod} ? 'prod' : 'staging').'/'.$_;
+		my $file = $self->{config}{pending}.'/'.$self->{mail}.'.'.($self->{domain}{prod} ? 'prod' : 'staging').'/'.$_;
 
 		# Load auth request content or post a new one
 		#TODO: add more check on cache file ???
@@ -624,23 +638,10 @@ sub authorize {
 		} map { $self->{challenges}{$_}{status} eq 'pending' ? $_ : (); } keys %{$self->{challenges}};
 	} 
 
-	# Load config if available
-	my $config = undef;
-	if (
-		#XXX: use eval to workaround a fatal in from_json
-		defined eval {
-			# Check that file exists
-			-f CONFIG &&
-			# Read it
-			($config = read_file(CONFIG)) &&
-			# Decode it
-			($config = from_json($config)) &&
-			# Check defined
-			$config->{thumbprint}
-		}
-	) {
+	# Check if thumbprint is writeable
+	if (-w $self->{config}{thumbprint}) {
 		# Try to write thumbprint
-		write_file($config->{thumbprint}, '');
+		write_file($self->{config}{thumbprint}, '');
 	}
 
 	# Stop here with remaining chanllenge
@@ -671,7 +672,7 @@ sub issue {
 	my ($self) = @_;
 
 	# Open csr file
-	open(my $fh, '<', CERT_DIR.DS.REQUEST_CSR) or die $!;
+	open(my $fh, '<', $self->{config}{pending}.'/'.$self->{mail}.'.'.($self->{domain}{prod} ? 'prod' : 'staging').'/'.REQUEST_CSR) or die $!;
 
 	# Load csr
 	my $csr = encode_base64url(join('', <$fh>) =~ s/^\0+//r);
@@ -688,7 +689,7 @@ sub issue {
 	}
 
 	# Open crt file
-	open($fh, '>', CERT_DIR.DS.SERVER_CRT) or die $!;
+	open($fh, '>', $self->{domain}{cert}) or die $!;
 
 	# Convert to pem
 	print $fh '-----BEGIN CERTIFICATE-----'."\n".encode_base64($res->content).'-----END CERTIFICATE-----'."\n";
@@ -711,7 +712,7 @@ sub issue {
 	close($fh) or die $!;
 
 	# Print success
-	carp 'Success, pem certificate in '.CERT_DIR.DS.SERVER_CRT if ($self->{debug});
+	carp 'Success, pem certificate in '.$self->{domain}{cert} if ($self->{debug});
 }
 
 1;
diff --git a/acmecert b/acmecert
index f8694cb..3f8a559 100755
--- a/acmecert
+++ b/acmecert
@@ -19,7 +19,13 @@
 use strict;
 use warnings;
 
-# Load Acme
+# Fix use of acl
+use filetest qw(access);
+
+# Load dependancies
+use File::stat qw(stat);
+use File::Slurp qw(read_file);
+use JSON qw(decode_json);
 use Acme;
 
 # Load POSIX
@@ -28,44 +34,119 @@ use POSIX qw(EXIT_SUCCESS EXIT_FAILURE);
 # Init debug
 my $debug = 0;
 
-# Init prod
-my $prod = 0;
+# Init config
+my $config = undef;
+
+# Init config file name
+my $configFilename = '/etc/acme/config';
+
+# Init domains
+my @domains = ();
 
 # Strip and enable debug
 @ARGV = map { if ($_ eq '-d') { $debug = 1; (); } else { $_; } } @ARGV;
 
-# Strip and enable prod
-@ARGV = map { if ($_ eq '-p') { $prod = 1; (); } else { $_; } } @ARGV;
+# Strip and enable debug
+for (my $i = 0; $i <= $#ARGV; $i++) {
+	# Match redhat types
+	if ($ARGV[$i] =~ /^(?:(\-c|\-\-config)(?:=(.+))?)$/) {
+		if (defined($2) && -f $2) {
+			$configFilename = $2;
+			splice(@ARGV, $i, 1);
+			$i--;
+		# Extract next parameter
+		} elsif(defined($ARGV[$i+1]) && $ARGV[$i+1] =~ /^(.+)$/ && -f $1) {
+			$configFilename = $1;
+			splice(@ARGV, $i, 2);
+			$i--;
+		# Set default
+		} else {
+			print 'Config parameter without valid file name'."\n";
+			exit EXIT_FAILURE;
+		}
+	}
+}
+
+# Load config
+unless (
+	#XXX: use eval to workaround a fatal in decode_json
+	eval {
+		# Check file
+		(-f $configFilename) &&
+		# Read it
+		($config = read_file($configFilename)) &&
+		# 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 '.$configFilename.' is not readable or invalid'."\n";
+	exit EXIT_FAILURE;
+}
+
+# Deal with specified domains
+if (scalar(@ARGV) > 0) {
+	# Check that domains are present in config
+	foreach my $domain (@ARGV) {
+		my $found = undef;
+		foreach my $certificate (@{$config->{certificates}}) {
+			if ($certificate->{domain} eq $domain) {
+				push(@domains, $certificate);
+				$found = 1;
+			}
+		}
+		unless($found) {
+			print 'Domain '.$domain.' not found in config file '.$configFilename."\n";
+			exit EXIT_FAILURE;
+		}
+	}
+# Without it
+} else {
+	# Populate domains array with available ones
+	foreach my $certificate (@{$config->{certificates}}) {
+		push(@domains, $certificate);
+	}
+}
 
 # Show usage
-if (scalar(@ARGV) < 2) {
-	print "Usage: $0 user\@example.com www.example.com [example.com] [...]\n";
+if (scalar(@domains) < 1) {
+	print "Usage: $0 [-(c|-config)[=/etc/acme/config]] [example.com] [...]\n";
 	exit EXIT_FAILURE;
 }
 
-# Create new object
-my $acme = Acme->new(shift @ARGV, $debug, $prod, @ARGV);
+# Deal with each domain
+foreach my $domain (@domains) {
+	# Create new object
+	my $acme = Acme->new($debug, $domain, {thumbprint => $config->{thumbprint}, pending => $config->{pending}, term => $config->{term}});
 
-# Prepare environement
-$acme->prepare();
+	# Prepare environement
+	$acme->prepare();
 
-# Generate required keys
-$acme->genKeys();
+	# Generate required keys
+	$acme->genKeys();
 
-# Generate csr
-$acme->genCsr();
+	# Generate csr
+	$acme->genCsr();
 
-# Directory
-$acme->directory();
+	# Directory
+	$acme->directory();
 
-# Register
-$acme->register();
+	# Register
+	$acme->register();
 
-# Authorize
-$acme->authorize();
+	# Authorize
+	$acme->authorize();
 
-# Issue
-$acme->issue();
+	# Issue
+	$acme->issue();
+}
 
 # Exit with success
 exit EXIT_SUCCESS;
diff --git a/acmecert.1 b/acmecert.1
index cbacb9b..81bf2d8 100644
--- a/acmecert.1
+++ b/acmecert.1
@@ -1,30 +1,18 @@
 .\" Manpage for acmecert.
 .\" Contact acme@rapsys.eu to correct errors or typos.
-.TH man 1 "05 Apr 2017" "0.7" "acmecert man page"
+.TH man 1 "20 Jun 2017" "0.8" "acmecert man page"
 .SH NAME
-acmecert \- generate a single certificate
+acmecert \- generate all certificate listed in configuration file or provided subset
 .SH SYNOPSIS
-acmecert [-(r|-redhat|d|-debian)] [example.com[,www.example.com,...]] [...]
+acmecert [-d] [-(c|-config)[=acme.config]] [example.com] [...]
 .SH DESCRIPTION
-acmecert is a basic script generating a single certificate based on parameters.
+acmecert is a basic script generating all certificate listed in configuration.
 .SH OPTIONS
-The acmecert takes options.
+The acmecert script takes one option.
 
 Use -d for debug directive.
-
-Use -p for production mode.
-
-These directives require to be followed by an email address and the domain and alternative domain list.
-.SH EXAMPLE 1
-.TP
-.B acmecert -d webmaster@example.com example.com www.example.com ssl.example.com
-will generate a certificate for example.com with www.example.com and ssl.example.com alternatives domains with debug mode active.
-.SH EXAMPLE 2
-.TP
-.B acmecert -p webmaster@example.com example.com www.example.com ssl.example.com
-will generate a certificate for example.com with www.example.com and ssl.example.com alternatives domains with production mode active.
 .SH SEE ALSO
-acmecron(1),acmecert(1)
+acmeconf(1),acmecron(1)
 .SH BUGS
 No known bugs.
 .SH AUTHOR
diff --git a/acmeconf b/acmeconf
index 10595c8..c34ee25 100755
--- a/acmeconf
+++ b/acmeconf
@@ -34,7 +34,7 @@ my @debian = ();
 
 # Init root
 my %root = ();
-tie(%root, 'Tie::IxHash', thumbprint => '/etc/acme/thumbprint', certificates => []);
+tie(%root, 'Tie::IxHash', thumbprint => '/etc/acme/thumbprint', term => 'https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf', pending => '/tmp/acme.pending', certificates => []);
 
 # Init prod
 my $prod = 0;
@@ -42,6 +42,8 @@ my $prod = 0;
 # Strip and enable prod
 @ARGV = map { if ($_ eq '-p') { $prod = 1; (); } else { $_; } } @ARGV;
 
+use Data::Dumper;
+
 # Strip and enable debug
 for (my $i = 0; $i <= $#ARGV; $i++) {
 	# Match redhat types
@@ -68,12 +70,28 @@ for (my $i = 0; $i <= $#ARGV; $i++) {
 		} else {
 			push(@debian, ['www.example.com','example.com','...']);
 		}
+	# Match term
+	} elsif ($ARGV[$i] =~ /^(?:(\-t|\-\-term)(?:=(https:\/\/letsencrypt\.org\/documents\/[a-zA-Z0-9\._-]+\.pdf))?)$/) {
+		if (defined($2)) {
+			$root{term} = $2;
+			splice(@ARGV, $i, 1);
+			$i--;
+		# Extract next parameter
+		} elsif(defined($ARGV[$i+1]) && $ARGV[$i+1] =~ /^(https:\/\/letsencrypt\.org\/documents\/[a-zA-Z0-9\._-]+\.pdf)$/) {
+			$root{term} = $1;
+			splice(@ARGV, $i, 2);
+			$i--;
+		# Set default
+		} else {
+			print 'Term parameter without valid link'."\n";
+			exit EXIT_FAILURE;
+		}
 	}
 }
 
 # Show usage
-if (scalar(@ARGV) < 1) {
-	print "Usage: $0 [(-d|--debian)[=example.com[,...]] [(-r|--redhat)[=example.com[,...]]] [...] > /etc/acme/config\n";
+if (scalar(@redhat) < 1 && scalar(@debian) < 1) {
+	print "Usage: $0 [(-d|--debian)[=example.com[,...]] [(-r|--redhat)[=example.com[,...]]] [(-t|--term)[=https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf]] [...] > /etc/acme/config\n";
 	exit EXIT_FAILURE;
 }
 
diff --git a/acmeconf.1 b/acmeconf.1
index 4fdc92a..beda4b5 100644
--- a/acmeconf.1
+++ b/acmeconf.1
@@ -1,20 +1,22 @@
 .\" Manpage for acmeconf.
 .\" Contact acme@rapsys.eu to correct errors or typos.
-.TH man 1 "05 Apr 2017" "0.7" "acmeconf man page"
+.TH man 1 "20 Jun 2017" "0.8" "acmeconf man page"
 .SH NAME
 acmeconf \- create a new configuration template
 .SH SYNOPSIS
-acmeconf [-(r|-redhat|d|-debian)] [example.com[,www.example.com,...]] [...]
+acmeconf [-(r|-redhat|d|-debian)] [example.com[,www.example.com,...]] [-(t|-term)[=https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf]] [...] > /etc/acme/config
 .SH DESCRIPTION
 acmeconf is a basic script generating a template configuration for generating letsencrypt certificate.
 .SH OPTIONS
-The acmeconf takes options.
+The acmeconf script takes multiple options.
 
 Use -r or --redhat directive for a distribution using redhat certificate path (/etc/pki/tls).
 
 Use -d or --debian for a distribution using debian like certificate base path (/etc/ssl).
 
 These directives can be followed by domain and alternative(s) domain(s) list each separated by a coma, the first one of the list will be used as principal domain name.
+
+Use -t or --term for letsencrypt license agreement.
 .SH EXAMPLE 1
 .TP
 .B acmeconf -r example.com,www.example.com,ssl.example.com > /etc/acme/config
diff --git a/acmecron b/acmecron
index 0ffeaad..365399e 100755
--- a/acmecron
+++ b/acmecron
@@ -20,21 +20,16 @@ use strict;
 use warnings;
 
 # Fix use of acl
-use filetest 'access';
+use filetest qw(access);
 
 # Load dependancies
-use Carp qw(carp confess);
-use DateTime;
-use File::Path qw(make_path);
 use File::stat qw(stat);
-use File::Spec;
-use File::Slurp qw(read_file write_file);
+use File::Slurp qw(read_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 ACCOUNT_KEY);
+use Acme;
 
 # Load POSIX
-use POSIX qw(strftime EXIT_SUCCESS EXIT_FAILURE);
+use POSIX qw(EXIT_SUCCESS EXIT_FAILURE);
 
 # Init debug
 my $debug = 0;
@@ -42,21 +37,44 @@ my $debug = 0;
 # Init config
 my $config = undef;
 
+# Init config file name
+my $configFilename = '/etc/acme/config';
+
+# Init domains
+my @domains = ();
+
 # 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;
+# Strip and enable debug
+for (my $i = 0; $i <= $#ARGV; $i++) {
+	# Match redhat types
+	if ($ARGV[$i] =~ /^(?:(\-c|\-\-config)(?:=(.+))?)$/) {
+		if (defined($2) && -f $2) {
+			$configFilename = $2;
+			splice(@ARGV, $i, 1);
+			$i--;
+		# Extract next parameter
+		} elsif(defined($ARGV[$i+1]) && $ARGV[$i+1] =~ /^(.+)$/ && -f $1) {
+			$configFilename = $1;
+			splice(@ARGV, $i, 2);
+			$i--;
+		# Set default
+		} else {
+			print 'Config parameter without valid file name'."\n";
+			exit EXIT_FAILURE;
+		}
+	}
 }
 
 # Load config
 unless (
 	#XXX: use eval to workaround a fatal in decode_json
 	eval {
+		# Check file
+		(-f $configFilename) &&
 		# Read it
-		($config = read_file(CONFIG)) &&
+		($config = read_file($configFilename)) &&
 		# Decode it
 		($config = decode_json($config)) &&
 		# Check hash validity
@@ -69,120 +87,70 @@ unless (
 		! scalar map {unless(defined($_->{cert}) && defined($_->{key}) && defined($_->{mail}) && defined($_->{domain}) && defined($_->{domains})) {1;} else {();}} @{$config->{certificates}}
 	}
 ) {
-	print 'Config file '.CONFIG.' is not readable or invalid'."\n";
+	print 'Config file '.$configFilename.' is not readable or 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;
-	}
-
-	# Check that key directory exists
-	if (! -d KEY_DIR) {
-		# Create all paths
-		make_path(KEY_DIR, {error => \my $err});
-		if (@$err) {
-			map {
-				my ($file, $msg) = %$_;
-				carp ($file eq '' ? '' : $file.': ').$msg if ($debug);
-			} @$err;
-			confess 'make_path failed';
+# Deal with specified domains
+if (scalar(@ARGV) > 0) {
+	# Check that domains are present in config
+	foreach my $domain (@ARGV) {
+		my $found = undef;
+		foreach my $certificate (@{$config->{certificates}}) {
+			if ($certificate->{domain} eq $domain) {
+				push(@domains, $certificate);
+				$found = 1;
+			}
 		}
-	}
-
-	# 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;
+		unless($found) {
+			print 'Domain '.$domain.' not found in config file '.$configFilename."\n";
+			exit EXIT_FAILURE;
 		}
 	}
-
-	# Symlink to key
-	unless(symlink($_->{key}, KEY_DIR.DS.SERVER_KEY)) {
-		carp('symlink '.$_->{key}.' to '.KEY_DIR.DS.SERVER_KEY.' failed: '.$!);
-		next;
+# Without it
+} else {
+	# Populate domains array with available ones
+	foreach my $certificate (@{$config->{certificates}}) {
+		push(@domains, $certificate);
 	}
+}
 
-	# Unlink if is a symlink
-	if (-l KEY_DIR.DS.ACCOUNT_KEY) {
-		unless(unlink(KEY_DIR.DS.ACCOUNT_KEY)) {
-			carp('unlink '.KEY_DIR.DS.ACCOUNT_KEY.' failed: '.$!);
-			next;
-		}
-	}
+# Show usage
+if (scalar(@domains) < 1) {
+	print "Usage: $0 [-(c|-config)[=/etc/acme/config]] [example.com] [...]\n";
+	exit EXIT_FAILURE;
+}
 
-	# Symlink to key
-	unless(symlink($_->{account}, KEY_DIR.DS.ACCOUNT_KEY)) {
-		carp('symlink '.$_->{account}.' to '.KEY_DIR.DS.ACCOUNT_KEY.' failed: '.$!);
+# Deal with each domain
+foreach my $domain (@domains) {
+	# Skip certificate without 60 days
+	if (-f $domain->{cert} && stat($domain->{cert})->mtime >= (time() - 60*24*3600)) {
 		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');
-	}
+	# Create new object
+	my $acme = Acme->new($debug, $domain, {thumbprint => $config->{thumbprint}, pending => $config->{pending}, term => $config->{term}});
 
-	# Preprend debug
-	if ($debug) {
-		unshift(@args, '-d');
-	}
+	# Prepare environement
+	$acme->prepare();
 
-	# Run acmecert with args
-	my @out = capturex([0..1], 'acmecert', @args);
+	# Generate required keys
+	$acme->genKeys();
 
-	# Deal with error
-	if ($EXITVAL != 0) {
-		print join("\n", @out) if ($debug);
-		carp('acmecert '.join(', ', @args).' failed: '.$!);
-		next;
-	}
+	# Generate csr
+	$acme->genCsr();
 
-	# Read cert
-	my $content;
-	unless($content = read_file(CERT_DIR.DS.SERVER_CRT)) {
-		carp('read_file '.CERT_DIR.DS.SERVER_CRT.' failed: '.$!);
-		next;
-	}
+	# Directory
+	$acme->directory();
 
-	# Handle old certificate
-	if (-w $certDir && -f $_->{cert}) {
-		# Extract datetime suffix
-		$suffix = ($dt = DateTime->from_epoch(epoch => $mtime))->ymd('').$dt->hms('');
+	# Register
+	$acme->register();
 
-		# Rename old certificate
-		unless(rename($_->{cert}, $_->{cert}.'.'.$suffix)) {
-			carp('rename '.$_->{cert}.' to '.$_->{cert}.'.'.$suffix.' failed: '.$!);
-			next;
-		}
-	}
+	# Authorize
+	$acme->authorize();
 
-	# Save cert
-	unless(write_file($_->{cert}, $content)) {
-		carp('write_file '.$_->{cert}.' failed: '.$!);
-		next;
-	}
+	# Issue
+	$acme->issue();
 }
 
 # Exit with success
diff --git a/acmecron.1 b/acmecron.1
index f4d36ab..db610c6 100644
--- a/acmecron.1
+++ b/acmecron.1
@@ -1,14 +1,16 @@
 .\" Manpage for acmecron.
 .\" Contact acme@rapsys.eu to correct errors or typos.
-.TH man 1 "05 Apr 2017" "0.7" "acmecron man page"
+.TH man 1 "20 Jun 2017" "0.8" "acmecron man page"
 .SH NAME
-acmecron \- generate all certificate listed in configuration file if required
+acmecron \- generate all certificate listed in configuration file or provided subset if required
 .SH SYNOPSIS
-acmecron [-d]
+acmecron [-d] [-(c|-config)[=acme.config]] [example.com] [...]
 .SH DESCRIPTION
-acmecron is a basic script generating all certificate listed in configuration if not present or older than 60 days. It will run acmecert with right options for every listed certificate if required.
+acmecron is a basic script generating all certificate listed in configuration if not present or older than 60 days.
 .SH OPTIONS
-The acmeconf takes one option for enabling debug mode.
+The acmecron script takes one option.
+
+Use -d for debug directive.
 .SH SEE ALSO
 acmeconf(1),acmecert(1)
 .SH BUGS
-- 
2.41.3