aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Changes11
-rw-r--r--MANIFEST10
-rw-r--r--Makefile.PL25
-rwxr-xr-xacmeman977
-rw-r--r--lib/App/Acmeman/Apache/Layout.pm117
-rw-r--r--lib/App/Acmeman/Config.pm359
-rw-r--r--lib/App/Acmeman/Domain.pm138
-rw-r--r--lib/App/Acmeman/Source/Apache.pm284
9 files changed, 1493 insertions, 429 deletions
diff --git a/.gitignore b/.gitignore
index 24cd9e0..1bf5703 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
.emacs*
*~
+*.bak
/MYMETA.json
/MYMETA.yml
Makefile
diff --git a/Changes b/Changes
index d61a564..5352924 100644
--- a/Changes
+++ b/Changes
@@ -1,3 +1,14 @@
+1.04 2017-09-13
+
+This version introduces acmeman configuration file, which can be used to
+direct its action if a server other than Apache is used. It also can
+be instructed to store certificate, certificate chain, and certificate
+key in a single file, instead of three different ones. This can be used
+with such servers as pound(8).
+
+In the absense of a configuration file, the program operates as in
+previous versions.
+
1.00 2017-02-09
* Initial version (Git)
diff --git a/MANIFEST b/MANIFEST
index d7ed194..3fa916f 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -1,6 +1,10 @@
+acmeman
Changes
+inc/ExtUtils/AutoInstall.pm
+lib/App/Acmeman/Apache/Layout.pm
+lib/App/Acmeman/Config.pm
+lib/App/Acmeman/Domain.pm
+lib/App/Acmeman/Source/Apache.pm
LICENSE
Makefile.PL
-MANIFEST This list of files
-inc/ExtUtils/AutoInstall.pm
-acmeman
+MANIFEST This list of files
diff --git a/Makefile.PL b/Makefile.PL
index 1798216..edb81a9 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -1,11 +1,15 @@
# -*- perl -*-
use strict;
use lib 'inc';
+use Module::Metadata;
+
use ExtUtils::AutoInstall (
-core => {
'Getopt::Long' => 2.34,
'File::Path' => 2.08,
'File::Basename' => 2.84,
+ 'Test::NoWarnings' => 0,
+ 'Crypt::RSA::Parse' => 0.043,
'Protocol::ACME' => 1.01,
'Protocol::ACME::Challenge::LocalFile' => 1.01,
'Crypt::Format' => 0.06,
@@ -17,14 +21,29 @@ use ExtUtils::AutoInstall (
'LWP::Protocol::https' => 6.07,
'Pod::Usage' => 1.51,
'Pod::Man' => 2.25,
+ 'Text::ParseWords' => 3.27,
'Data::Dumper' => 0
}
);
-WriteMakefile(NAME => 'acmeman',
+WriteMakefile(NAME => 'App::Acmeman',
ABSTRACT_FROM => 'acmeman',
VERSION_FROM => 'acmeman',
AUTHOR => 'Sergey Poznyakoff <gray@gnu.org>',
- LICENSE => 'gpl',
- EXE_FILES => [ 'acmeman' ]
+ LICENSE => 'gpl_3',
+ EXE_FILES => [ 'acmeman' ],
+ MIN_PERL_VERSION => 5.006,
+ META_MERGE => {
+ 'meta-spec' => { version => 2 },
+ resources => {
+ repository => {
+ type => 'git',
+ url => 'git://git.gnu.org.ua/gsc/acmeman.git',
+ web => 'http://git.gnu.org.ua/cgit/gsc/acmeman.git/',
+ },
+ },
+ provides => Module::Metadata->provides(version => '1.4',
+ dir => 'lib')
+ }
+
);
diff --git a/acmeman b/acmeman
index bef9732..4a28e50 100755
--- a/acmeman
+++ b/acmeman
@@ -30,9 +30,12 @@ use Pod::Usage;
use Pod::Man;
use Getopt::Long qw(:config gnu_getopt no_ignore_case);
use POSIX qw(strftime time floor);
+use App::Acmeman::Config;
+use App::Acmeman::Domain qw(:files);
use Data::Dumper;
+use Text::ParseWords;
-our $VERSION = '1.03';
+our $VERSION = '1.04';
=head1 NAME
@@ -43,32 +46,22 @@ acmeman - manages ACME certificates
B<acmeman>
[B<-Fadns>]
[B<-D> I<N>]
-[B<-I> I<DIR>]
[B<-f> I<FILE>]
-[B<-l> B<slackware>|B<debian>|B<rh>]
[B<--alt-names>]
[B<--config-file=>I<FILE>]
[B<--debug>]
[B<--dry-run>]
[B<--force>]
-[B<--incdir=>I<DIR>]
-[B<--include-directory=>I<DIR>]
-[B<--layout=>B<slackware>|B<debian>|B<rh>]
-[B<--restart=>I<COMMAND>]
[B<--stage>]
[B<--time-delta=>I<N>]
[I<DOMAIN>...]
B<acmeman> B<--setup> | B<-S>
[B<-Fdn>]
-[B<-f> I<FILE>]
-[B<-l> B<slackware>|B<debian>|B<rh>]
[B<--config-file=>I<FILE>]
[B<--debug>]
[B<--dry-run>]
[B<--force>]
-[B<--layout=>B<slackware>|B<debian>|B<rh>]
-[I<DIR>]
B<acmeman>
[B<-h>]
@@ -78,21 +71,258 @@ B<acmeman>
=head1 DESCRIPTION
A tool for automatic creation and renewal of ACME (LetsEncrypt) SSL
-certificates. It assumes that HTTP is served by Apache server, version
-2.4 or later (although only minor changes are necessary to make it work
-with version 2.2). Three most popular layouts of Apache configuration
-files are supported: Debian, Slackware, and Red Hat. A special
-directory should be configured for receiving ACME challenges.
+certificates. The list of domains to handle can be obtained from
+B<acmeman> or B<apache> configuration files, or from both. If the
+default B<acmeman> configuration file doesn't exist, the program
+scans B<apache> configuration files for a list of domains.
+
+B<Acmeman> is normally run periodically as a cronjob.
+
+If you plan to serve SSL protected domains using apache, you can skip
+right to the B<APACHE> section.
+
+The following is a short introduction to the B<acmeman> configuration. For
+a detailed discussion, see the B<CONFIGURATION> section below.
+
+The configuration file, B</etc/acmeman.conf>, consists of statements,
+which have the form B<I<KW>=I<VAL>>, grouped into sections, declared
+as B<[I<NAME>]> (square brackets are part of the syntax). Empty lines
+and comments (introduced by a hash sign) are ignored.
+
+Domains which require LetsEncrypt certificates are declared in B<domain>
+section. Each section introduces a single domain. E.g.:
+
+ [domain example.com]
+ alt = www.example.com
+ files = default
+
+This section instructs B<acmeman> that a certificate is needed for
+domain B<example.com>, using B<www.example.com> as its alternative name,
+The B<files> statement identifies the name of a B<files> section containing
+rules for creating certificate files for that domain. This section must be
+defined elsewhere in the configuration file. For example:
+
+ [files default]
+ type = split
+ certificate-file = /etc/ssl/acme/$domain/cert.pem
+ key-file = /etc/ssl/acme/$domain/privkey.pem
+ ca-file = /etc/ssl/acme/$domain/ca.pem
+ argument = $domain
+
+This definition tells B<acmeman> that it should store certificate, certficate
+key, and certificate authority chain in three separate files. Names of these
+files will be created by replacing the B<$domain> string in the corresponding
+definition with the domain name from the B<domain> section,
+
+In fact, the B<files> section above is the default one. It will be created
+implicitly if no other B<files> section is defined in the configuration file.
+Moreover, the string B<default> is the default identifier, which is used if
+the B<domain> section lacks the B<files> keyword.
+
+The special section B<[core]> contains basic settings that control the
+program behavior. One of the important settings is B<source>, which declares
+an external source from which domain settings must be obtained. As
+of B<acmeman> version 2.00, the only available external source is B<apache>.
+Consider the following configuration:
+
+ [core]
+ source = apache
+
+It instructs B<acmeman> to read domain settings from Apache configuration
+files. This is basically the configuration that is used in the absense of
+the configuration file. See the B<APACHE> section for a detailed discussion
+of this operation mode.
+
+=head1 CONFIGURATION
+
+Configuration file controls the operation of B<acmeman>. By default,
+its name is B</etc/acmeman.conf>. If it is absent, B<acmeman> falls
+back to the legacy operation mode, scanning Apache configuration files
+for domains that use LetsEncrypt SSL certificates. See the B<APACHE>
+section below for a detailed description.
+
+The configuration file has a traditional line-oriented syntax. Comments
+are introduced with a hash sign. Empty lines are ignored. Leading and
+trailing whitespace is removed prior to parsing. Long statements can be
+split over several physical lines by ending each line excepting the last
+one with a backslash immediately followed by a newline character.
+
+Statements have the following syntax:
+
+ KEYWORD = VALUES
+
+where I<KEYWORD> stands for a symbolic name consisting of alphanumeric
+characters, dashes and underscores, and I<VALUE> stands for any sequence
+of characters.
+
+Statements are grouped in sections. Each section is identified by its name
+and optional arguments. The section begins with the construct
+
+ [NAME]
+
+or, if arguments are present:
+
+ [NAME ARG1 ARG2 ...]
+
+The square brackets are part of the syntax.
+
+The statements in the configuration file form a directed graph. Often
+in this document we will identify the statement by its I<path>, i.e. a
+list of section name, arguments and the keyword, separated by dots. For
+example, the path B<files.apache.type> corresponds to the following
+configuration file fragment:
+
+ [file apache]
+ type = single
+
+The following describes the available sections and keywords
+
+=head2 B<[core]>
+
+This section defines the behavior of the program as a whole.
+
+=over 4
+
+=item B<rootdir=>I<DIR>
+
+Defines the root directory to use instead of the default </var/www/acme>.
+Root directory is the directory under which the
+F<.well-known/acme-challenge> subdirectory is located.
+
+=item B<time-delta=>I<SECONDS>
+
+Sets the time window before the actual expiration time, when the certificate
+becomes eligible for renewal. I<N> is time in seconds. The default
+value is 86400, which means that B<acmeman> will attempt to renew any
+certificate that expires within 24 hours.
+
+The command line option B<--time-delta> overrides this setting.
+
+=item B<restart=>I<COMMAND>
+
+Defines the command to be run at the end of the run if at least one
+certificate has been updated. Normally this command reloads the httpd
+server (or whatever server is using the certificates). If more than one
+B<restart> statements are defined, they will be run in sequenct in the
+same order as they appeared in the configuration file.
+
+=item B<source=>I<ID> [I<LAYOUT>]
+
+Defines additional source of information. As of B<acmeman> version 2.00,
+only one I<ID> is supported: B<apache>. This source module scans apache
+configuration files as described in section B<APACHE>. The optional
+I<LAYOUT> argument defines the apache configuration layout. Allowed values
+are: B<debian>, B<slackware>, B<suse> and B<rh> (for Red Hat). If I<LAYOUT>
+is absent, it will be autodetected.
+
+=item B<files=>I<NAME>
+
+Identifies the B<[files]> section which describes how to create certificate
+files for domains which lack explicit B<files> keyword. Default I<NAME> is
+B<default>. See the description of the B<files> statement in B<domain>
+section.
+
+=item B<check-alt-names=>I<BOOL>
+
+When set to B<true>, it instructs the program to compare the list of
+alternative names of each certificate with the one gathered from the
+Apache configuration, and reissue the certificate if the two lists
+don't match. This uses an ad-hoc logic, due to the deficiency of the
+underlying X509 module, and therefore is not enabled by default.
+
+Valid values for I<BOOL> are: B<1>, B<on>, B<true>, or B<yes>, for
+true, and B<0>, B<off>, B<false>, or B<no> for false (all values
+case-insensitive).
+
+=back
+
+=head2 B<[domain I<CN>]>
+
+Declares the domain for which a certificate should be maintained. I<CN> is
+the canonical name for the domain. Alternative names can be specified using
+the B<alt> keyword.
+
+=over 4
+
+=item B<files=>I<ID>
+
+Identifies the B<[files]> section which describes how to create certificate
+files for this domain. In the absense of this statement, the B<files>
+statement from the B<[core]> section will be used.
+
+=item B<alt=>I<NAME>
+
+Defines alternative name for the certificate. Multiple B<alt> statements
+are allowed.
+
+=back
+
+=head2 B<[files I<ID>]>
+
+The B<files> section instructs B<acmeman> how to create certificate files.
+It is applied to particular domains by placing the B<files=I<ID>> statement
+in the coresponding domain sections.
+
+The I<FILENAME> arguments to the keywords below can contain references to
+a I<meta-variable>, which will be replaced by the actual domain name when
+handling this section for a particular domain. By default, this meta-variable
+is B<$domain>.
+
+=over 4
+
+=item B<type=>B<single>|B<split>
+
+Type of the certificate to create. When set to B<single>, a single
+certificate file will be created. Its name is determined by the
+B<certificate-file> statement. The file will contain the certificate,
+certificate chain and the signature, in this order.
+
+When set to B<split>, the certificate, certificate chain and the signature
+will be saved to three distinct files, whose names are defined by
+B<certificate-file>, B<ca-file>, and B<key-file>, correspondingly. If
+B<ca-file> is not defined, only certificate and key files will be created.
+
+The default is B<split>.
+
+=item B<certificate-file=>I<FILENAME>
+
+Defines the name of the certificate file for this domain. This statement
+is mandatory.
+
+=item B<key-file=>I<FILENAME>
+
+Defines the name of the certificate key file. This statement must be present
+if B<type> is set to B<split>.
+
+=item B<ca-file=>I<FILENAME>
+
+Defines the name of the certificate authority file. This statement may
+be present if B<type> is set to B<split>.
+
+=item B<argument=>I<STRING>
+
+Defines the name of the meta-variable in I<FILENAME> arguments, which will
+be replaced with the actual domain name. Default is B<$domain>.
+
+=back
+
+=head1 APACHE
+
+This is the default mode. It assumes Apache httpd, version 2.4 or later
+(although only minor changes are necessary to make it work with version 2.2).
+Four most popular layouts of Apache configuration files are supported:
+Debian, Slackware, SuSe, and Red Hat. A special directory should be
+configured for receiving ACME challenges.
The package provides two Apache macros: for serving ACME challenges and
declaring SSL virtual hosts.
-B<Acmeman> should be started periodically, as a cronjob. Upon startup,
-it scans Apache configuration for virtual hosts using ACME certificates,
-checks their expiration times, and renews those of the certificates that
-are nearing their expiration times within a predefined number of seconds
-(24 hours by default). If any of the certificates were updated during
-the run, B<acmeman> will restart the B<httpd> server.
+Upon startup the program scans Apache configuration for virtual hosts
+that use ACME certificates, checks their expiration times, and renews those
+of the certificates that are nearing their expiration times within a
+predefined number of seconds (24 hours by default). If any of the
+certificates were updated during the run, B<acmeman> will restart the
+B<httpd> server.
=head2 Setup
@@ -114,12 +344,6 @@ will be created for each domain name needing SSL, and two files will be placed
there: F<cert.pem>, containing the leaf and intermediate certificates for that
domain, and F<privkey.pem>, containing the private key for that domain.
-If the program is unable to determine the Apache configuration layout, you can
-declare it using the B<--layout> option. It takes a single argument, one of:
-B<debian>, B<slackware>, or B<rh> (all lower case). Alternatively, you can
-specify the location of the main Apache configuration file, using the
-B<--config-file> option.
-
The program will refuse to overwrite existing files B<httpd-letsencrypt.conf>,
unless given the B<--force> (B<-F>) option.
@@ -206,12 +430,14 @@ will use the B<LetsEncryptSSL> macro to configure the correct certificate:
=over 4
-=item B<-D>, B<--tile-delta=>I<N>
+=item B<-D>, B<--time-delta=>I<N>
Sets the time window before the actual expiration time, when the certificate
becomes eligible for renewal. I<N> is time in seconds. The default
value is 86400, which means that B<acmeman> will attempt to renew any
certificate that expires within 24 hours.
+
+This option overrides the B<core.time-delta> configuration setting.
=item B<-F>, B<--force>
@@ -219,11 +445,6 @@ Force renewal of certificates, no matter their expire date. With B<--setup>,
force installing the B<httpd-letsencrypt.conf> file even if it already
exists.
-=item B<-I>, B<--incdir>, B<--include-directory=>I<DIR>
-
-Specifies base directory for Apache B<Include> and B<IncludeOptional>
-statements with relative pathnames.
-
=item B<-a>, B<--alt-names>
Compare the list of alternative names of each certificate with the one
@@ -231,6 +452,8 @@ gathered from the Apache configuration, and reissue the certificate
if the two lists don't match. This uses an ad-hoc logic, due to the
deficiency of the underlying X509 module, and therefore is not enabled
by default.
+
+This option overrides the B<core.check-alt-names> configuration setting.
=item B<-d>, B<--debug>
@@ -257,22 +480,13 @@ debugging information about ACME transactions for each certificate.
=item B<-f>, B<--config-file=>I<FILE>
-Read I<FILE> as main Apache configuration file.
-
-=item B<-l>, B<--layout=>I<NAME>
-
-Defines Apache configuration file layout. Valid values for I<NAME> are:
-B<slackware>, B<debian>, and B<rh> (for Red Hat).
+Read configuration from I<FILE>, instead of the default F</etc/acmeman.conf>.
=item B<-n>, B<--dry-run>
Don't modify any files, just print what would have been done.
Implies B<--debug>.
-=item B<--restart=>I<COMMAND>
-
-Specifies the command to restart Apache daemon.
-
=item B<-S>, B<--setup>
Set up the B<acmeman> infrastructure files.
@@ -317,18 +531,11 @@ my %acme_endpoint = (prod => 'acme-v01.api.letsencrypt.org',
staging => 'acme-staging.api.letsencrypt.org');
my $letsencrypt_root_cert_url =
'https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem';
-my $time_delta = 86400;
my $force = 0;
-my $check_alt_names = 0;
my %select; # Hash of selected domain names
-# Variables read from the Apache configuration files
-my $server_root;
-my $www_root; # Root directory for ACME challenges
-my @virthost; # List of virtual hosts using ACME certificates
-my $filename_arg; # Name of the formal argument to the LetsEncrypServer
-my %filename_pattern; # File name pattern.
- # Valid keys are: cert, key and chain.
+my $config_file = '/etc/acmeman.conf';
+my $config;
my $account_key;
my $challenge;
@@ -365,15 +572,6 @@ sub abend {
exit $code;
}
-sub make_filename {
- my ($type, $arg) = @_;
- abend(EX_SOFTWARE, "no $type in \%filename_pattern")
- unless exists $filename_pattern{$type};
- my $res = $filename_pattern{$type};
- $res =~ s{$filename_arg}{$arg}g;
- return $res;
-}
-
sub prep_dir {
my $dir = dirname(shift);
if (! -d $dir) {
@@ -385,7 +583,7 @@ sub prep_dir {
if ($file eq '') {
error($message);
} else {
- error("problem unlinking $file: $message");
+ error("mkdir $file: $message");
}
}
exit(EX_CANTCREAT);
@@ -400,50 +598,51 @@ sub debug_to_loglevel {
}
sub make_csr {
- my $cn = shift;
+ my $dom = shift;
my $req = Crypt::OpenSSL::PKCS10->new(4096);
- $req->set_subject("/CN=$cn");
+ $req->set_subject("/CN=".$dom->cn);
$req->add_ext(Crypt::OpenSSL::PKCS10::NID_subject_alt_name,
- join(',', map { "DNS:$_" } @_)) if @_;
+ join(',', map { "DNS:$_" } $dom->alt))
+ if $dom->alt > 0;
$req->add_ext_final();
$req->sign();
- if (exists($filename_pattern{key})) {
- my $filename = make_filename('key', $cn);
- debug(3, "writing $filename");
- prep_dir($filename);
- my $u = umask(077);
- $req->write_pem_pk($filename);
- umask($u);
- }
- return $req->get_pem_req();
+ return $req;
}
sub save_crt {
- my $type = shift;
my $domain = shift;
+ my $type = shift;
- my $filename = make_filename($type, $domain);
- debug(3, "writing $filename");
- prep_dir($filename);
- open(my $fd, '>', $filename);
+ if (my $filename = $domain->file($type)) {
+ debug(3, "writing $filename");
+ prep_dir($filename);
+ open(my $fd, '>', $filename);
- foreach my $der (@_) {
- my $pem = Crypt::Format::der2pem($der, 'CERTIFICATE');
- print $fd $pem;
- print $fd "\n";
+ foreach my $der (@_) {
+ my $pem = Crypt::Format::der2pem($der, 'CERTIFICATE');
+ print $fd $pem;
+ print $fd "\n";
+ }
+ close $fd;
+ return $filename;
}
- close $fd;
+}
+
+sub selected_domain {
+ my $dom = shift;
+ return 1 unless %select;
+ return grep { $select{$_} } $dom->names;
}
sub domain_cert_expires {
my $domain = shift;
- my $crt = make_filename('cert', $domain);
+ my $crt = $domain->certificate_file;
if (-f $crt) {
my $x509 = Crypt::OpenSSL::X509->new_from_file($crt);
my $exts = $x509->extensions_by_name();
if (exists($exts->{subjectAltName})) {
- my $msg = $check_alt_names
+ my $msg = $config->get(qw(core check-alt-names))
? 'will renew' : 'use -a to trigger renewal';
# FIXME: Crypt::OpenSSL::X509 returns extensions as strings,
# instead of as ASN.1 objects. Until it is fixed, the
@@ -451,8 +650,7 @@ sub domain_cert_expires {
# names:
my $blob = $exts->{subjectAltName}->value();
my @missing;
- foreach my $vh (map { lc } sort { length($b) <=> length($a) }
- uniq($domain, @_)) {
+ foreach my $vh (sort { length($b) <=> length($a) } $domain->names) {
unless ($blob =~ s/\Q$vh\E\b//) {
push @missing, $vh;
}
@@ -461,7 +659,7 @@ sub domain_cert_expires {
debug(1, "$crt: the following SANs are missing: "
. join(', ', @missing)
."; $msg");
- return 1 if $check_alt_names;
+ return 1 if $config->get(qw(core check-alt-names));
}
}
@@ -485,7 +683,7 @@ sub domain_cert_expires {
$in = "today";
}
debug(2, "$crt expires on $expiry, $in");
- if ($now + $time_delta < $ts) {
+ if ($now + $config->get(qw(core time-delta)) < $ts) {
return 0;
} else {
debug(2, "will renew $crt (expires on $expiry, $in)");
@@ -501,20 +699,19 @@ sub register_domain_certificate {
my $domain = shift;
if ($debug) {
- my $crt = make_filename('cert', $domain);
+ my $crt = $domain->certificate_file;
+ my $alt = join(',', $domain->alt);
if (-f $crt) {
- debug(1, "renewing $crt: CN=$domain, alternatives=@_");
+ debug(1, "renewing $crt: CN=$domain, alternatives=$alt");
} else {
- debug(1, "issuing $crt: CN=$domain, alternatives=@_");
+ debug(1, "issuing $crt: CN=$domain, alternatives=$alt");
}
}
return 1 if $dry_run;
my $acme = Protocol::ACME->new(
host => $acme_endpoint{$acme_host},
- account_key => {
- buffer => $account_key->get_private_key_string(),
- format => 'PEM' },
+ account_key => { buffer => $account_key->get_private_key_string(), format => 'PEM' },
loglevel => debug_to_loglevel()
);
@@ -523,21 +720,43 @@ sub register_domain_certificate {
$acme->register();
$acme->accept_tos();
- foreach my $name ($domain, @_) {
+ foreach my $name ($domain->names) {
$acme->authz($name);
$acme->handle_challenge($challenge);
$acme->check_challenge();
$acme->cleanup_challenge($challenge);
}
- my $csr = make_csr($domain, @_);
- my $cert = $acme->sign({ format => 'PEM', buffer => $csr });
+
+ my $csr = make_csr($domain);
+ my $cert = $acme->sign({ format => 'PEM', buffer => $csr->get_pem_req() });
my $chain = $acme->chain();
- if (exists($filename_pattern{chain})) {
- save_crt('cert', $domain, $cert);
- save_crt('chain', $domain, $chain);
+
+ if (my $filename = $domain->file(KEY_FILE)) {
+ debug(3, "writing $filename");
+ prep_dir($filename);
+ my $u = umask(077);
+ $csr->write_pem_pk($filename);
+ umask($u);
+
+ if ($filename = $domain->file(CA_FILE)) {
+ save_crt($domain, CA_FILE, $chain);
+ }
+ save_crt($domain, CERT_FILE, $cert);
} else {
- save_crt('cert', $domain, $cert, $chain);
- }
+ $filename = $domain->certificate_file;
+ debug(3, "writing $filename");
+ prep_dir($filename);
+ my $u = umask(077);
+ open(my $fd, '>', $filename)
+ or abend(EX_CANTCREAT, "can't open $filename for writing: $!");
+ print $fd Crypt::Format::der2pem($cert, 'CERTIFICATE');
+ print $fd "\n";
+ print $fd Crypt::Format::der2pem($chain, 'CERTIFICATE');
+ print $fd "\n";
+ print $fd $csr->get_pem_pk();
+ print $fd "\n";
+ umask($u);
+ }
};
if ($@) {
if (UNIVERSAL::isa($@, 'Protocol::ACME::Exception')) {
@@ -559,251 +778,44 @@ sub register_domain_certificate {
return 1;
}
-sub dequote {
- my ($arg) = @_;
- $arg =~ s{^"(.*?)"$}{$1};
- return $arg;
-}
-
-sub uniq {
- my %h;
- map {
- if (exists($h{$_})) {
- ()
- } else {
- $h{$_} = 1;
- $_
- }
- } @_;
-}
-
-sub examine_http_config {
- my ($file) = @_;
-
- use constant {
- STATE_INITIAL => 0, # Initial state
- STATE_VIRTUAL_HOST => 1, # In VirtualHost block
- STATE_USE_CHALLENGE => 2, # Ditto, but challenge macro was used
- STATE_USE_REFERENCE => 3, # Ditto, but reference macro was used
- STATE_MACRO_CHALLENGE => 4, # In LetsEncryptChallenge macro
- STATE_MACRO_SSL => 5
- };
-
- state $state = STATE_INITIAL;
-
- debug(3, "reading apache configuration file \"$file\"");
- if (open(my $fd, '<', $file)) {
- my $server_name;
- my @server_aliases;
- my $reference;
- my $line;
-
- while (<$fd>) {
- ++$line;
- chomp;
- s/^\s+//;
- next if /^(#.*)?$/;
- if (/^include(optional)?\s+(.+?)\s*$/i) {
- debug(3, "$file:$line: state $state: Include$1 $2");
- http_include(dequote($2), $1 ne '');
- next;
- }
-
- if ($state == STATE_INITIAL) {
- if (/^<VirtualHost/i) {
- $state = STATE_VIRTUAL_HOST;
- $server_name = undef;
- @server_aliases = ();
- $reference = undef;
- } elsif (/^ServerRoot\s+(.+)/i) {
- $server_root = dequote($1);
- } elsif (/^<(?:(?i)Macro)\s+LetsEncryptChallenge/) {
- $state = STATE_MACRO_CHALLENGE;
- } elsif (/^<(?:(?i)Macro)\s+LetsEncryptSSL\s+(.+?)\s*>/) {
- $state = STATE_MACRO_SSL;
- $filename_arg = $1;
- $filename_arg =~ s{\$}{\\\$};
- }
- } elsif ($state == STATE_VIRTUAL_HOST
- || $state == STATE_USE_CHALLENGE
- || $state == STATE_USE_REFERENCE) {
- if (m{</VirtualHost}i) {
- unshift @server_aliases, $server_name
- if defined $server_name;
- if (@server_aliases
- && (!%select || exists($select{$server_name}))) {
- if ($state == STATE_USE_CHALLENGE) {
- my @names = uniq(@server_aliases);
- debug(3, "$file:$line: will handle @names");
- push @virthost, \@names;
- } elsif ($state == STATE_USE_REFERENCE) {
- my @names = uniq(@server_aliases);
- unshift @names, $reference;
- debug(3, "$file:$line: reference: @names");
- push @virthost, \@names;
- }
- }
- $state = STATE_INITIAL;
- } elsif (/^(?:(?i)Use)\s+LetsEncryptChallenge/) {
- if ($state == STATE_VIRTUAL_HOST) {
- $state = STATE_USE_CHALLENGE;
- } elsif ($state == STATE_USE_CHALLENGE) {
- error("$file:$line: duplicate use of LetsEncryptChallenge");
- } else {
- error("$file:$line: LetsEncryptChallenge and LetsEncryptReference can't be used together");
- }
- } elsif (/^(?:(?i)Use)\s+LetsEncryptReference\s+(.+)\s*$/) {
- if ($state == STATE_VIRTUAL_HOST) {
- $state = STATE_USE_REFERENCE;
- $reference = $1;
- } elsif ($state == STATE_USE_REFERENCE) {
- error("$file:$line: duplicate use of LetsEncryptReference");
- } else {
- error("$file:$line: LetsEncryptChallenge and LetsEncryptReference can't be used together");
- }
- } elsif (/^(?:(?i)ServerName)\s+(\S+)/) {
- $server_name = dequote($1);
- } elsif (/^(?:(?i)ServerAlias)\s+(.+)\s*$/) {
- push @server_aliases,
- map { /^\*\./ ? () : dequote($_) } split /\s+/, $1;
- }
- } elsif ($state == STATE_MACRO_CHALLENGE) {
- if (m{^</macro}i) {
- $state = STATE_INITIAL;
- } elsif (m{^(?:(?i)Alias)\s+/.well-known/acme-challenge\s+(.+)}) {
- my $dir = dequote($1);
- $dir =~ s{/.well-known/acme-challenge$}{};
- $www_root = $dir;
- debug(3, "ACME challenge root dir: $www_root");
- }
- } elsif ($state == STATE_MACRO_SSL) {
- if (m{^</macro}i) {
- $state = STATE_INITIAL;
- } elsif (/(?:(?i)SSLCertificate((?:Key)|(?:Chain))?File)\s+(.+)/) {
- $filename_pattern{lc($1 ? $1 : 'cert')} = $2;
- }
- }
- }
- close $fd;
- } else {
- error("can't open file \"$file\": $!");
- return 0;
- }
- return 1;
-}
-
-sub http_include {
- my ($pattern, $optional) = @_;
- $pattern = "$server_root/$pattern" unless $pattern =~ m{^/};
- $pattern =~ s{/*$}{};
- $pattern .= '/*' if -d $pattern;
- foreach my $file (glob $pattern) {
- if ($optional && ! -e $file) {
- debug(1, "optional include file \"$file\" doesn't exist");
- next;
- }
- examine_http_config($file);
- }
-}
-# ##
-my $apache_layout;
-
-sub incdir {
- if (exists($apache_layout->{incdir})) {
- if (ref($apache_layout->{incdir}) eq 'CODE') {
- return &{$apache_layout->{incdir}};
- } else {
- return $apache_layout->{incdir};
- }
- }
- return dirname($apache_layout->{config});
-}
-
sub get_root_cert {
my $name = shift;
- prep_dir($name);
+
+ prep_dir($name) unless $dry_run;
debug(1, "downloading $letsencrypt_root_cert_url to \"$name\"");
my $ua = LWP::UserAgent->new;
my $response = $ua->get($letsencrypt_root_cert_url);
if ($response->is_success) {
- open(my $fd, '>', $name)
- or abend(EX_CANTCREAT, "can't open \"$name\" for writing: $!");
- print $fd $response->decoded_content;
- close $fd;
+ unless ($dry_run) {
+ open(my $fd, '>', $name)
+ or abend(EX_CANTCREAT, "can't open \"$name\" for writing: $!");
+ print $fd $response->decoded_content;
+ close $fd;
+ }
} else {
error("error downloading certificate from $letsencrypt_root_cert_url");
abend(EX_NOINPUT, $response->status_line);
}
}
-
+
sub initial_setup {
- if (defined($www_root)) {
- error("acmeman macros seem to be already installed");
- abend(EX_NOPERM, "use --force to continue") unless $force;
+ get_root_cert('/etc/ssl/acme/lets-encrypt-x3-cross-signed.pem');
+ my $source = $config->get(qw(core source));
+ unless (defined($source)) {
+ require App::Acmeman::Source::Apache;
+ $source = new App::Acmeman::Source::Apache;
+ $source->configure($config);
+ $source = undef unless $config->success;
+ $config->clrerr;
}
- if ($#ARGV == -1) {
- $www_root = '/var/www/acme';
- } elsif ($#ARGV == 0) {
- $www_root = $ARGV[0];
- } else {
- abend(EX_USAGE, "too many arguments in setup mode");
- }
- my $filename = incdir() . "/httpd-letsencrypt.conf";
- if (-e $filename) {
- error("the file \"$filename\" already exists");
- abend(EX_NOPERM, "use --force to continue") unless $force;
- }
- debug(2, "writing $filename");
- unless ($dry_run) {
- prep_dir($filename);
- open(my $fd, '>', $filename)
- or abend(EX_CANTCREAT, "can't open \"$filename\" for writing: $!");
- print $fd <<EOT;
-<Macro LetsEncryptChallenge>
- Alias /.well-known/acme-challenge $www_root/.well-known/acme-challenge
- <Directory $www_root/.well-known/acme-challenge>
- Options None
- Require all granted
- </Directory>
- <IfModule mod_rewrite.c>
- RewriteEngine On
- RewriteRule /.well-known/acme-challenge - [L]
- </IfModule>
-</Macro>
-
-<Macro LetsEncryptReference \$domain>
- Use LetsEncryptChallenge
- Alias /.dummy/\$domain /dev/null
-</Macro>
-
-<Macro LetsEncryptSSL \$domain>
- SSLEngine on
- SSLProtocol all -SSLv2 -SSLv3
- SSLHonorCipherOrder on
- SSLCipherSuite EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:EECDH+RC4:RSA+RC4:!MD5
- SSLCertificateFile /etc/ssl/acme/\$domain/cert.pem
- SSLCertificateKeyFile /etc/ssl/acme/\$domain/privkey.pem
- SSLCACertificateFile /etc/ssl/acme/lets-encrypt-x3-cross-signed.pem
-</Macro>
-
-<Macro LetsEncryptServer \$domain>
- ServerName \$domain
- Use LetsEncryptSSL \$domain
-</Macro>
-
-EOT
-;
- close $fd;
-
- get_root_cert('/etc/ssl/acme/lets-encrypt-x3-cross-signed.pem');
- if (exists($apache_layout->{post_setup})) {
- &{$apache_layout->{post_setup}}($filename);
+ if ($source) {
+ unless ($source->setup(dry_run => $dry_run, force => $force)) {
+ exit(1);
}
}
- error("created file \"$filename\"", prefix => 'NOTE');
- error("please, enable mod_macro and make sure your Apache configuration includes this file", prefix => 'NOTE');
+
exit(EX_OK);
}
@@ -811,51 +823,51 @@ sub coalesce {
my $ref = shift;
debug(2, "coalescing virtual hosts");
my $i = 0;
- my @vhost;
- foreach my $ent (