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 );
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
# 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',
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
# Constructor
sub new {
# Extract params
- my ($class, $mail, $debug, $prod, @domains) = @_;
+ my ($class, $debug, $domain, $config) = @_;
# Create self hash
my $self = {};
# 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();
confess 'search recursively A record for '.$_.' failed';
}
}
- } @domains;
-
- # Save domains
- @{$self->{domains}} = @domains;
+ } @{$self->{domains}};
# Return class reference
return $self;
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
# 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
# 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();
# 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);
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);
# 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
# 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) {
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 ???
} 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
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);
}
# 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";
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;
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
# 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;
.\" 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
# 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;
# 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
} 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;
}
.\" 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
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;
# 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
! 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
.\" 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