aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGuilhem Moulin <guilhem@fripost.org>2016-12-05 16:52:52 +0100
committerGuilhem Moulin <guilhem@fripost.org>2016-12-05 16:52:52 +0100
commit47ff80f540e71229832c972dcc2653958a0d7b56 (patch)
tree331a34f4848f216382067ede028e84a484c2d8eb
parent94a72b73a2b7e4309a7bc6434e85dfba8b1c8a7d (diff)
parent0eb9f40182299b2615f5ac0190d40429f5f64ed7 (diff)
Merge tag 'upstream/0.2' into debian
Upstream version 0.2
-rw-r--r--Changelog17
-rw-r--r--Makefile1
-rw-r--r--config/lacme-certs.conf25
-rw-r--r--config/lacme.conf24
-rwxr-xr-xlacme119
-rwxr-xr-xlacme-accountd9
-rw-r--r--lacme.md33
7 files changed, 148 insertions, 80 deletions
diff --git a/Changelog b/Changelog
index b8d5780..6f212b0 100644
--- a/Changelog
+++ b/Changelog
@@ -1,3 +1,20 @@
+lacme (0.2) upstream;
+
+ + Honor Retry-After headers for certificate issuance and challenge
+ responses.
+ + Update example of Subscriber Agreement URL to v1.1.1.
+ + lacme: automaticall spawn lacme-acountd when a "[accountd]" section
+ is present in the configuration file. The "socket" option is then
+ ignored, and the two processes communicate through a socket pair.
+ + lacme: add an option --quiet to avoid mentioning valid certs (useful
+ in cronjobs)
+ + "config-certs" now points to a space separated list of files or
+ directories. New default "lacme-certs.conf lacme-certs.conf.d/".
+ - Minor manpage fixes
+ - More useful message upon Validation Challenge failure.
+
+ -- Guilhem Moulin <guilhem@guilhem.org> Sat, 03 Dec 2016 16:40:56 +0100
+
lacme (0.1) upstream;
* Initial public release. Development was started in December 2015.
diff --git a/Makefile b/Makefile
index 4e5f06d..3b61341 100644
--- a/Makefile
+++ b/Makefile
@@ -25,6 +25,7 @@ all: ${MANPAGES}
install: ${MANPAGES}
install -d $(DESTDIR)/etc/lacme
+ install -d $(DESTDIR)/etc/lacme/lacme-certs.conf.d
install -m0644 -t $(DESTDIR)/etc/lacme config/*.conf
install -d $(DESTDIR)/usr/share/lacme
install -m0644 -t $(DESTDIR)/usr/share/lacme certs/lets-encrypt-x[1-4]-cross-signed.pem
diff --git a/config/lacme-certs.conf b/config/lacme-certs.conf
index 9b9df2f..12fcd54 100644
--- a/config/lacme-certs.conf
+++ b/config/lacme-certs.conf
@@ -1,49 +1,62 @@
-# Each non-default section denotes a separate certificate issuance.
-# Options in the default section apply to each sections.
+# Each non-default section refer to separate certificate issuance
+# requests. Options in the default section apply to each sections.
# Message digest to sign the Certificate Signing Request with.
+#
#hash = sha512
# Comma-separated list of Key Usages, see x509v3_config(5ssl).
+#
#keyUsage = digitalSignature, keyEncipherment
+
#[www]
+# Path the service's private key. This option is required.
+#
+#certificate-key = /etc/nginx/ssl/srv.key
+
# Where to store the issued certificate (in PEM format).
+#
#certificate = /etc/nginx/ssl/srv.pem
# Where to store the issued certificate, concatenated with the content
# of the file specified specified with the CAfile option (in PEM format).
+#
#certificate-chain = /etc/nginx/ssl/srv.chain.pem
-# Path the service's private key. This option is required.
-#certificate-key = /etc/nginx/ssl/srv.key
-
# For an existing certificate, the minimum number of days before its
# expiration date the section is considered for re-issuance.
+#
#min-days = 10
# Path to the issuer's certificate. This is used for certificate-chain
# and to verify the validity of each issued certificate. Specifying an
# empty value skip certificate validation.
+#
#CAfile = /usr/share/lacme/lets-encrypt-x3-cross-signed.pem
# Subject field of the Certificate Signing Request. This option is
# required.
+#
#subject = /CN=example.org
# Comma-separated list of Subject Alternative Names.
+#
#subjectAltName = DNS:example.org,DNS:www.example.org
# username[:groupname] to chown the issued certificate and
# certificate-chain with.
+#
#chown = root:root
-# octal mode to chmod the issued certificate and certificate-chain with.
+# Octal mode to chmod the issued certificate and certificate-chain with.
+#
#chmod = 0644
# Command to pass the the system's command shell ("/bin/sh -c") after
# successful installation of the certificate and/or certificate-chain.
+#
#notify = /bin/systemctl reload nginx
diff --git a/config/lacme.conf b/config/lacme.conf
index 39cfd36..c5efb03 100644
--- a/config/lacme.conf
+++ b/config/lacme.conf
@@ -1,9 +1,11 @@
-# For certificate issuance (new-cert command), specify the certificate
-# configuration file to use
+# For certificate issuance (new-cert command), specify a space-separated
+# certificate configuration files or directories to use
#
-#config-certs = /etc/lacme/lacme-certs.conf
+#config-certs = lacme-certs.conf lacme-certs.conf.d/
+
[client]
+
# The value of "socket" specifies the path to the lacme-accountd(1)
# UNIX-domain socket to connect to for signature requests from the ACME
# client. lacme(1) aborts if the socket is readable or writable by
@@ -25,10 +27,12 @@
# groupname to drop privileges to (setting both effective and real gid,
# and also setting the list of supplementary gids to that single group).
# Preserve root privileges if the value is empty (not recommended).
+# Default: "nogroup".
#
#group = nogroup
# Path to the ACME client executable.
+#
#command = /usr/lib/lacme/client
# Root URI of the ACME server. NOTE: Use the staging server for testing
@@ -43,12 +47,15 @@
#timeout = 10
# Whether to verify the server certificate chain.
+#
#SSL_verify = yes
# Specify the version of the SSL protocol used to transmit data.
+#
#SSL_version = SSLv23:!TLSv1_1:!TLSv1:!SSLv3:!SSLv2
# Specify the cipher list for the connection.
+#
#SSL_cipher_list = EECDH+AESGCM:!MEDIUM:!LOW:!EXP:!aNULL:!eNULL
@@ -78,6 +85,7 @@
#group = www-data
# Path to the ACME webserver executable.
+#
#command = /usr/lib/lacme/webserver
# Whether to automatically install iptables(8) rules to open the
@@ -87,10 +95,10 @@
#iptables = Yes
-# lacme-accound(1) section. Comment out the following section to make
-# lacme(1) connect to an existing UNIX-domain socket bound by a running
-# acme-accountd(1) process.
[accountd]
+# lacme-accound(1) section. Comment out this section (including its
+# header) to make lacme(1) connect to an existing UNIX-domain socket
+# bound by a running acme-accountd(1) process.
# username to drop privileges to (setting both effective and real uid).
# Preserve root privileges if the value is empty.
@@ -104,16 +112,20 @@
#group = root
# Path to the lacme-accountd(1) executable.
+#
#command = /usr/bin/lacme-accountd
# Path to the lacme-accountd(1) configuration file.
+#
#config = /etc/lacme/lacme-accountd.conf
# The (private) account key to use for signing requests. See
# lacme-accountd(1) for details.
+#
#privkey = file:/path/to/account.key
# Be quiet.
+#
#quiet = Yes
; vim:ft=dosini
diff --git a/lacme b/lacme
index cf2f9eb..cb49818 100755
--- a/lacme
+++ b/lacme
@@ -60,7 +60,7 @@ sub usage(;$$) {
}
exit $rv;
}
-usage(1) unless GetOptions(\%OPTS, qw/config=s config-certs=s socket=s agreement-uri=s quiet|q debug help|h/);
+usage(1) unless GetOptions(\%OPTS, qw/config=s config-certs=s@ socket=s agreement-uri=s quiet|q debug help|h/);
usage(0) if $OPTS{help};
$COMMAND = shift(@ARGV) // usage(1, "Missing command");
@@ -68,15 +68,15 @@ $COMMAND = $COMMAND =~ /\A(new-reg|reg=\p{Print}*|new-cert|revoke-cert)\z/ ? $1
: usage(1, "Invalid command: $COMMAND"); # validate and untaint $COMMAND
@ARGV = map { /\A(\p{Print}*)\z/ ? $1 : die } @ARGV; # untaint @ARGV
+my $CONFFILENAME = $OPTS{config} // first { -f $_ }
+ ( "./$NAME.conf"
+ , ($ENV{XDG_CONFIG_HOME} // "$ENV{HOME}/.config")."/lacme/$NAME.conf"
+ , "/etc/lacme/$NAME.conf"
+ );
do {
- my $conffile = $OPTS{config} // first { -f $_ }
- ( "./$NAME.conf"
- , ($ENV{XDG_CONFIG_HOME} // "$ENV{HOME}/.config")."/lacme/$NAME.conf"
- , "/etc/lacme/$NAME.conf"
- );
- die "Error: Can't find configuration file\n" unless defined $conffile;
- print STDERR "Using configuration file: $conffile\n" if $OPTS{debug};
- open $CONFFILE, '<', $conffile or die "Can't open $conffile: $!\n";
+ die "Error: Can't find configuration file\n" unless defined $CONFFILENAME;
+ print STDERR "Using configuration file: $CONFFILENAME\n" if $OPTS{debug};
+ open $CONFFILE, '<', $CONFFILENAME or die "Can't open $CONFFILENAME: $!\n";
my $conf = do { local $/ = undef, <$CONFFILE> };
# don't close $CONFFILE so we can pass it to the client
@@ -395,38 +395,34 @@ sub spawn_webserver() {
# If $args->{in} is defined, the data is written to the client's STDIN.
# If $args->{out} is defined, its value is set to client's STDOUT data.
#
-my $ACCOUNTD = 0;
sub acme_client($@) {
my $args = shift;
my @args = @_;
- my $client;
+ my ($client, $cleanup);
my $conf = $CONFIG->{client};
if (defined (my $accountd = $CONFIG->{accountd})) {
- unless ($ACCOUNTD) {
- socketpair($client, my $s, AF_UNIX, SOCK_STREAM, PF_UNSPEC) or die "socketpair: $!";
- my $pid = fork() // "fork: $!";
- unless ($pid) {
- drop_privileges($accountd->{user}, $accountd->{group}, '/');
- set_FD_CLOEXEC($s, 0);
- $client->close() or die "Can't close: $!";
- my @cmd = ($accountd->{command}, '--fdopen='.fileno($s));
- push @cmd, '--config='.$accountd->{config} if defined $accountd->{config};
- push @cmd, '--privkey='.$accountd->{privkey} if defined $accountd->{privkey};
- push @cmd, '--quiet' unless lc $accountd->{quiet} eq 'no';
- push @cmd, '--debug' if $OPTS{debug};
- exec { $cmd[0] } @cmd or die;
- }
- print STDERR "[$$] Forking lacme-accountd, child PID $pid\n" if $OPTS{debug};
- $ACCOUNTD = $pid;
- $s->close() or die "Can't close: $!";
- push @CLEANUP, sub() {
- print STDERR "[$$] Shutting down lacme-accountd\n" if $OPTS{debug};
- shutdown($client, SHUT_RDWR) or warn "shutdown: $!";
- kill 15 => $pid;
- waitpid $pid => 0;
- };
+ socketpair($client, my $s, AF_UNIX, SOCK_STREAM, PF_UNSPEC) or die "socketpair: $!";
+ my $pid = fork() // "fork: $!";
+ unless ($pid) {
+ drop_privileges($accountd->{user}, $accountd->{group}, '/');
+ set_FD_CLOEXEC($s, 0);
+ $client->close() or die "Can't close: $!";
+ my @cmd = ($accountd->{command}, '--conn-fd='.fileno($s));
+ push @cmd, '--config='.$accountd->{config} if defined $accountd->{config};
+ push @cmd, '--privkey='.$accountd->{privkey} if defined $accountd->{privkey};
+ push @cmd, '--quiet' unless lc $accountd->{quiet} eq 'no';
+ push @cmd, '--debug' if $OPTS{debug};
+ exec { $cmd[0] } @cmd or die;
}
+ print STDERR "[$$] Forking lacme-accountd, child PID $pid\n" if $OPTS{debug};
+ $s->close() or die "Can't close: $!";
+ $cleanup = sub() {
+ print STDERR "[$$] Shutting down lacme-accountd\n" if $OPTS{debug};
+ shutdown($client, SHUT_RDWR) or warn "shutdown: $!";
+ $client->close() or warn "close: $!";
+ };
+ push @CLEANUP, $cleanup;
}
else {
my @stat;
@@ -455,12 +451,18 @@ sub acme_client($@) {
# child doesn't have access to the parent's memory
my @fileno = map { fileno($_) =~ /^(\d+)$/ ? $1 : die } ($CONFFILE, $client); # untaint fileno
set_FD_CLOEXEC($client, 1);
- spawn({%$args{qw/in out/}, child => sub() {
+ my $rv = spawn({%$args{qw/in out/}, child => sub() {
drop_privileges($conf->{user}, $conf->{group}, $args->{chdir} // '/');
set_FD_CLOEXEC($_, 0) foreach ($CONFFILE, $client);
seek($CONFFILE, SEEK_SET, 0) or die "Can't seek: $!";
$ENV{DEBUG} = $OPTS{debug};
}}, $conf->{command}, $COMMAND, @fileno, @args);
+
+ if (defined $cleanup) {
+ @CLEANUP = grep { $_ ne $cleanup } @CLEANUP;
+ $cleanup->();
+ }
+ return $rv;
}
sub spawn($@) {
@@ -557,24 +559,39 @@ if ($COMMAND eq 'new-reg' or $COMMAND =~ /^reg=/) {
# new-cert [SECTION ..]
#
elsif ($COMMAND eq 'new-cert') {
+ my $conffiles = defined $OPTS{'config-certs'} ? $OPTS{'config-certs'}
+ : defined $CONFIG->{_}->{'config-certs'} ? [ split(/\s+/, $CONFIG->{_}->{'config-certs'}) ]
+ : [ "$NAME-certs.conf", "$NAME-certs.conf.d/" ];
my $conf;
- do {
- my $conffile = $OPTS{'config-certs'} // $CONFIG->{_}->{'config-certs'} // first { -f $_ }
- ( "./$NAME-certs.conf"
- , ($ENV{XDG_CONFIG_HOME} // "$ENV{HOME}/.config")."/lacme/$NAME-certs.conf"
- , "/etc/lacme/$NAME-certs.conf"
- );
- die "Error: Can't find certificate configuration file\n" unless defined $conffile;
- my $h = Config::Tiny::->read($conffile) or die Config::Tiny::->errstr()."\n";
- my $defaults = delete $h->{_} // {};
- my @valid = qw/certificate certificate-chain certificate-key min-days CAfile
- hash keyUsage subject subjectAltName chown chmod notify/;
- foreach my $s (keys %$h) {
- $conf->{$s} = { map { $_ => delete $h->{$s}->{$_} } @valid };
- die "Unknown option(s) in [$s]: ".join(', ', keys %{$h->{$s}})."\n" if %{$h->{$s}};
- $conf->{$s}->{$_} //= $defaults->{$_} foreach keys %$defaults;
+ foreach my $conffile (@$conffiles) {
+ $conffile = ($CONFFILENAME =~ s#[^/]+\z##r).$conffile unless $conffile =~ /\A\//;
+ my @filenames;
+ unless ($conffile =~ s#/\z## or -d $conffile) {
+ @filenames = ($conffile);
+ } else {
+ opendir my $dh, $conffile or die "Can't opendir $conffile: $!\n";
+ while (readdir $dh) {
+ if (/\.conf\z/) {
+ push @filenames, "$conffile/$_";
+ } elsif ($_ ne '.' and $_ ne '..') {
+ warn "$conffile/$_ has unknown suffix, skipping\n";
+ }
+ }
+ closedir $dh;
}
- };
+ foreach my $filename (sort @filenames) {
+ print STDERR "Reading $filename\n" if $OPTS{debug};
+ my $h = Config::Tiny::->read($filename) or die Config::Tiny::->errstr()."\n";
+ my $defaults = delete $h->{_} // {};
+ my @valid = qw/certificate certificate-chain certificate-key min-days CAfile
+ hash keyUsage subject subjectAltName chown chmod notify/;
+ foreach my $s (keys %$h) {
+ $conf->{$s} = { map { $_ => delete $h->{$s}->{$_} } @valid };
+ die "Unknown option(s) in [$s]: ".join(', ', keys %{$h->{$s}})."\n" if %{$h->{$s}};
+ $conf->{$s}->{$_} //= $defaults->{$_} foreach keys %$defaults;
+ }
+ }
+ }
my $challenge_dir;
my $rv = 0;
diff --git a/lacme-accountd b/lacme-accountd
index 411538d..00d6ccd 100755
--- a/lacme-accountd
+++ b/lacme-accountd
@@ -59,7 +59,7 @@ sub usage(;$$) {
}
exit $rv;
}
-usage(1) unless GetOptions(\%OPTS, qw/config=s privkey=s socket=s fdopen=i quiet|q debug help|h/);
+usage(1) unless GetOptions(\%OPTS, qw/config=s privkey=s socket=s conn-fd=i quiet|q debug help|h/);
usage(0) if $OPTS{help};
do {
@@ -137,8 +137,9 @@ $JWK = JSON::->new->encode($JWK);
# to support the abstract namespace.) The downside is that we have to
# delete the file manually.
#
-if (defined $OPTS{fdopen}) {
- die "Invalid file descriptor" unless $OPTS{fdopen} =~ /\A(\d+)\z/;
+if (defined $OPTS{'conn-fd'}) {
+ die "Invalid file descriptor" unless $OPTS{'conn-fd'} =~ /\A(\d+)\z/;
+ # untaint and fdopen(3) our end of the socket pair
open $S, '+<&=', $1 or die "fdopen $1: $!";
} else {
my $sockname = $OPTS{socket} // (defined $ENV{XDG_RUNTIME_DIR} ? "$ENV{XDG_RUNTIME_DIR}/S.lacme" : undef);
@@ -182,7 +183,7 @@ sub conn($;$) {
}
}
-if (defined $OPTS{fdopen}) {
+if (defined $OPTS{'conn-fd'}) {
conn($S, $$);
} else {
$SIG{PIPE} = 'IGNORE'; # ignore broken pipes
diff --git a/lacme.md b/lacme.md
index b086fe7..f5b5559 100644
--- a/lacme.md
+++ b/lacme.md
@@ -115,9 +115,13 @@ Generic options
: Use *path* as the [`lacme-accountd`(1)] UNIX-domain socket to
connect to for signature requests from the [ACME] client. `lacme`
aborts if `path` is readable or writable by other users, or if its
- parent directory is writable by other users. This overrides the
- *socket* option of the [`[client]` section](#client-section) of the
- configuration file.
+ parent directory is writable by other users.
+ This command-line option overrides the *socket* option of the
+ [`[client]` section](#client-section) of the configuration file.
+ Moreover this option is ignored when the configuration file has an
+ [`[accountd]` section](#accountd-section); in that case `lacme`
+ spawns [`lacme-accountd`(1)], and the two processes communicate
+ through a socket pair.
`-h`, `--help`
@@ -147,9 +151,18 @@ Default section
*config-certs*
: For certificate issuances (`new-cert` command), specify the
- certificate configuration file to use (see the **[certificate
- configuration file](#certificate-configuration-file)** section below
- for the configuration options).
+ space-separated list of certificate configuration files or
+ directories to use (see the **[certificate configuration
+ file](#certificate-configuration-file)** section below for the
+ configuration options).
+
+ Paths not starting with `/` are relative to the directory name of
+ the **[configuration filename](#configuration-file)**. The list of
+ files and directories is processed in order, with the later items
+ taking precedence. Files in a directory are processed in
+ lexicographic order, only considering the ones with suffix `.conf`.
+
+ Default: `lacme-certs.conf lacme-certs.conf.d/`.
`[client]` section
------------------
@@ -299,12 +312,6 @@ Certificate configuration file
For certificate issuances (`new-cert` command), a separate file is used
to configure paths to the certificate and key, as well as the subject,
subjectAltName, etc. to generate Certificate Signing Requests.
-If `--config-certs=` is not given, and if the `config-certs`
-configuration option is absent, then `lacme` uses the first existing
-configuration file among *./lacme-certs.conf*,
-*$XDG_CONFIG_HOME/lacme/lacme-certs.conf* (or
-*~/.config/lacme/lacme-certs.conf* if the `XDG_CONFIG_HOME` environment
-variable is not set), and */etc/lacme/lacme-certs.conf*.
Each section denotes a separate certificate issuance.
Valid options are:
@@ -383,7 +390,7 @@ Examples
========
~$ sudo lacme new-reg mailto:noreply@example.com
- ~$ sudo lacme reg=/acme/reg/137760 --agreement-uri=https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf
+ ~$ sudo lacme reg=/acme/reg/123456 --agreement-uri=https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf
~$ sudo lacme new-cert
~$ sudo lacme revoke-cert /path/to/server/certificate.pem