aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xclient44
-rw-r--r--config/lacme-accountd.conf8
-rw-r--r--config/lacme-certs.conf2
-rw-r--r--config/lacme.conf47
-rwxr-xr-xlacme89
-rwxr-xr-xlacme-accountd37
-rw-r--r--lacme-accountd.md12
-rw-r--r--lacme.md52
8 files changed, 219 insertions, 72 deletions
diff --git a/client b/client
index 2566c9b..3bf0bad 100755
--- a/client
+++ b/client
@@ -257,6 +257,7 @@ elsif ($COMMAND =~ /\Areg=(\p{Print}+)\Z/) {
#
elsif ($COMMAND eq 'new-cert') {
die unless @ARGV;
+ my $timeout = $CONFIG->{timeout} // 10;
foreach my $domain (@ARGV) {
print STDERR "Processing new DNS authz for $domain\n" if $ENV{DEBUG};
my $r = acme_resource('new-authz', identifier => {type => 'dns', value => $domain});
@@ -284,14 +285,28 @@ elsif ($COMMAND eq 'new-cert') {
keyAuthorization => $keyAuthorization
});
# wait until the status become 'valid'
- for ( my $i = 0, my $status;
- $status = request_json_decode($r)->{status} // 'pending',
+ for ( my $i = 0, my $content, my $status;
+ $content = request_json_decode($r),
+ $status = $content->{status} // 'pending',
$status ne 'valid';
- $r = request('GET' => $challenge->{uri}), $i++ ) {
+ $r = request('GET' => $challenge->{uri})) {
+ if (defined (my $problem = $content->{error})) { # problem document (RFC 7807)
+ my $msg = $problem->{status};
+ $msg .= " " .$problem->{title} if defined $problem->{title};
+ $msg .= " (".$problem->{detail}.")" if defined $problem->{detail};
+ die $msg, "\n";
+ }
die "Error: Invalid challenge for $domain (status: ".$status.")\n" if $status ne 'pending';
- die "Timeout exceeded while waiting for challenge to pass ($domain)\n"
- if $i >= ($CONFIG->{timeout} // 10);
- sleep 1;
+
+ my $sleep = 1;
+ if (defined (my $retry_after = $r->header('Retry-After'))) {
+ print STDERR "Retrying after $retry_after seconds...\n";
+ $sleep = $retry_after;
+ }
+
+ $i += $sleep;
+ die "Timeout exceeded while waiting for challenge to pass ($domain)\n" if $timeout > 0 and $i >= $timeout;
+ sleep $sleep;
}
}
@@ -302,12 +317,17 @@ elsif ($COMMAND eq 'new-cert') {
# https://acme-v01.api.letsencrypt.org/acme/cert/$serial
print STDERR "Certificate URI: $uri\n";
- # wait for the cert
- for (my $i = 0; $r->decoded_content() eq ''; $r = request('GET' => $uri), $i++) {
- die request_status_line($r), "\n" unless $r->is_success();
- die "Timeout exceeded while waiting for certificate\n"
- if $i >= ($CONFIG->{timeout} // 10);
- sleep 1;
+ if ($r->decoded_content() eq '') { # wait for the cert
+ for (my $i = 0;;) {
+ $r = request('GET' => $uri);
+ die request_status_line($r), "\n" unless $r->is_success();
+ last unless $r->code == 202; # Accepted
+ my $retry_after = $r->header('Retry-After') // 1;
+ print STDERR "Retrying after $retry_after seconds...\n";
+ $i += $retry_after;
+ die "Timeout exceeded while waiting for certificate\n" if $timeout > 0 and $i >= $timeout;
+ sleep $retry_after;
+ }
}
my $der = $r->decoded_content();
diff --git a/config/lacme-accountd.conf b/config/lacme-accountd.conf
index 0a8b81a..94d2556 100644
--- a/config/lacme-accountd.conf
+++ b/config/lacme-accountd.conf
@@ -4,18 +4,18 @@
# - file:FILE, to specify an encrypted private key (in PEM format)
# - gpg:FILE, to specify a gpg-encrypted private key (in PEM format)
#
-#privkey = gpg:/path/to/encrypted/priv.key.gpg
-#privkey = file:/path/to/priv.key
+#privkey = gpg:/path/to/encrypted/account.key.gpg
+#privkey = file:/path/to/account.key
# For a gpg-encrypted private account key, "gpg" specifies the binary
# gpg(1) to use, as well as some default options. Default: "gpg
# --quiet".
#
-#gpg = gpg2 --quiet --no-auto-check-trustdb
+#gpg = gpg --quiet --no-auto-check-trustdb
# The value of "socket" specifies the UNIX-domain socket to bind against
# for signature requests from the ACME client. An error is raised if
-# the path exists exists or if its parent directory is writable by other
+# the path exists or if its parent directory is writable by other
# users.
# Default: "$XDG_RUNTIME_DIR/S.lacme" if the XDG_RUNTIME_DIR
# environment variable is set.
diff --git a/config/lacme-certs.conf b/config/lacme-certs.conf
index fbce5e2..9b9df2f 100644
--- a/config/lacme-certs.conf
+++ b/config/lacme-certs.conf
@@ -32,7 +32,7 @@
# required.
#subject = /CN=example.org
-# Comma-separated list of Subject Alternative Names.
+# Comma-separated list of Subject Alternative Names.
#subjectAltName = DNS:example.org,DNS:www.example.org
# username[:groupname] to chown the issued certificate and
diff --git a/config/lacme.conf b/config/lacme.conf
index c5c643d..39cfd36 100644
--- a/config/lacme.conf
+++ b/config/lacme.conf
@@ -1,15 +1,18 @@
# For certificate issuance (new-cert command), specify the certificate
# configuration file to use
#
-#config-certs = config/lacme-certs.conf
+#config-certs = /etc/lacme/lacme-certs.conf
[client]
-# The value of "socket" specifies 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 other users,
-# or if its parent directory is writable by other users.
+# 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
+# other users, or if its parent directory is writable by other users.
# Default: "$XDG_RUNTIME_DIR/S.lacme" if the XDG_RUNTIME_DIR environment
# variable is set.
+# This option is ignored when lacme-accountd(1) is spawned by lacme(1),
+# since the two processes communicate through a socket pair. See the
+# "accountd" section below for details.
#
#socket = /run/user/1000/S.lacme
@@ -29,7 +32,7 @@
#command = /usr/lib/lacme/client
# Root URI of the ACME server. NOTE: Use the staging server for testing
-# as it has relaxed ratelimit.
+# as it has relaxed rate-limiting.
#
#server = https://acme-v01.api.letsencrypt.org/
#server = https://acme-staging.api.letsencrypt.org/
@@ -72,7 +75,7 @@
# and also setting the list of supplementary gids to that single group).
# Preserve root privileges if the value is empty (not recommended).
#
-#user = www-data
+#group = www-data
# Path to the ACME webserver executable.
#command = /usr/lib/lacme/webserver
@@ -83,4 +86,34 @@
#
#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]
+
+# username to drop privileges to (setting both effective and real uid).
+# Preserve root privileges if the value is empty.
+#
+#user = root
+
+# 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.
+#
+#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 f6d2b7a..cf2f9eb 100755
--- a/lacme
+++ b/lacme
@@ -30,7 +30,7 @@ use File::Temp ();
use Getopt::Long qw/:config posix_default no_ignore_case gnu_getopt auto_version/;
use List::Util 'first';
use POSIX ();
-use Socket qw/PF_INET PF_INET6 PF_UNIX INADDR_ANY IN6ADDR_ANY
+use Socket qw/AF_UNIX PF_INET PF_INET6 PF_UNIX PF_UNSPEC INADDR_ANY IN6ADDR_ANY
SOCK_STREAM SOL_SOCKET SO_REUSEADDR SHUT_RDWR/;
use Config::Tiny ();
@@ -60,7 +60,7 @@ sub usage(;$$) {
}
exit $rv;
}
-usage(1) unless GetOptions(\%OPTS, qw/config=s config-certs=s socket=s agreement-uri=s 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");
@@ -82,6 +82,7 @@ do {
my $h = Config::Tiny::->read_string($conf) or die Config::Tiny::->errstr()."\n";
my $defaults = delete $h->{_} // {};
+ my $accountd = exists $h->{accountd} ? 1 : 0;
my %valid = (
client => {
socket => (defined $ENV{XDG_RUNTIME_DIR} ? "$ENV{XDG_RUNTIME_DIR}/S.lacme" : undef),
@@ -99,6 +100,14 @@ do {
command => '/usr/lib/lacme/webserver',
iptables => 'Yes'
+ },
+ accountd => {
+ user => '',
+ group => '',
+ command => '/usr/bin/lacme-accountd',
+ config => '/etc/lacme/lacme-accountd.conf',
+ privkey => undef,
+ quiet => 'Yes',
}
);
foreach my $s (keys %valid) {
@@ -110,6 +119,8 @@ do {
}
die "Invalid section(s): ".join(', ', keys %$h)."\n" if %$h;
$CONFIG->{_} = $defaults;
+ delete $CONFIG->{accountd} unless $accountd;
+ $OPTS{quiet} = 0 if $OPTS{debug};
};
# Regular expressions for domain validation
@@ -384,35 +395,66 @@ 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 @stat;
+ my $client;
my $conf = $CONFIG->{client};
- my $sockname = $OPTS{socket} // $conf->{socket} // die "Missing socket option\n";
- $sockname = $sockname =~ /\A(\p{Print}+)\z/ ? $1 : die "Invalid socket name\n"; # untaint $sockname
-
- # ensure we're the only user with write access to the parent dir
- my $dirname = $sockname =~ s/[^\/]+$//r;
- @stat = stat($dirname) or die "Can't stat $dirname: $!";
- die "Error: insecure permissions on $dirname\n" if ($stat[2] & 0022) != 0;
-
- # ensure we're the only user with read/write access to the socket
- @stat = stat($sockname) or die "Can't stat $sockname: $! (Is lacme-accountd running?)\n";
- die "Error: insecure permissions on $sockname\n" if ($stat[2] & 0066) != 0;
-
- # connect(2) to the socket
- socket(my $client, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
- my $sockaddr = Socket::sockaddr_un($sockname) // die "Invalid address $sockname\n";
- until (connect($client, $sockaddr)) {
- next if $! == EINTR; # try again if connect(2) was interrupted by a signal
- die "connect: $!";
+ 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;
+ };
+ }
+ }
+ else {
+ my @stat;
+ my $sockname = $OPTS{socket} // $conf->{socket} // die "Missing socket option\n";
+ $sockname = $sockname =~ /\A(\p{Print}+)\z/ ? $1 : die "Invalid socket name\n"; # untaint $sockname
+
+ # ensure we're the only user with write access to the parent dir
+ my $dirname = $sockname =~ s/[^\/]+$//r;
+ @stat = stat($dirname) or die "Can't stat $dirname: $!";
+ die "Error: insecure permissions on $dirname\n" if ($stat[2] & 0022) != 0;
+
+ # ensure we're the only user with read/write access to the socket
+ @stat = stat($sockname) or die "Can't stat $sockname: $! (Is lacme-accountd running?)\n";
+ die "Error: insecure permissions on $sockname\n" if ($stat[2] & 0066) != 0;
+
+ # connect(2) to the socket
+ socket($client, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
+ my $sockaddr = Socket::sockaddr_un($sockname) // die "Invalid address $sockname\n";
+ until (connect($client, $sockaddr)) {
+ next if $! == EINTR; # try again if connect(2) was interrupted by a signal
+ die "connect: $!";
+ }
}
# use execve(2) rather than a Perl pseudo-process to ensure that the
# 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() {
drop_privileges($conf->{user}, $conf->{group}, $args->{chdir} // '/');
set_FD_CLOEXEC($_, 0) foreach ($CONFFILE, $client);
@@ -513,9 +555,6 @@ if ($COMMAND eq 'new-reg' or $COMMAND =~ /^reg=/) {
#############################################################################
# new-cert [SECTION ..]
-# TODO: renewal without the account key, see
-# https://github.com/letsencrypt/acme-spec/pull/168
-# https://github.com/letsencrypt/acme-spec/issues/191
#
elsif ($COMMAND eq 'new-cert') {
my $conf;
@@ -558,7 +597,7 @@ elsif ($COMMAND eq 'new-cert') {
my $d = $conf->{'min-days'} // 10;
if ($d > 0 and $t - time > $d*86400) {
my $d = POSIX::strftime('%Y-%m-%d %H:%M:%S UTC', gmtime($t));
- print STDERR "[$s] Valid until $d, skipping\n";
+ print STDERR "[$s] Valid until $d, skipping\n" unless $OPTS{quiet};
next;
}
}
diff --git a/lacme-accountd b/lacme-accountd
index fbf1bcb..411538d 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 quiet|q debug help|h/);
+usage(1) unless GetOptions(\%OPTS, qw/config=s privkey=s socket=s fdopen=i quiet|q debug help|h/);
usage(0) if $OPTS{help};
do {
@@ -137,7 +137,10 @@ $JWK = JSON::->new->encode($JWK);
# to support the abstract namespace.) The downside is that we have to
# delete the file manually.
#
-do {
+if (defined $OPTS{fdopen}) {
+ die "Invalid file descriptor" unless $OPTS{fdopen} =~ /\A(\d+)\z/;
+ open $S, '+<&=', $1 or die "fdopen $1: $!";
+} else {
my $sockname = $OPTS{socket} // (defined $ENV{XDG_RUNTIME_DIR} ? "$ENV{XDG_RUNTIME_DIR}/S.lacme" : undef);
die "Missing socket option\n" unless defined $sockname;
$sockname = $sockname =~ /\A(\p{Print}+)\z/ ? $1 : die "Invalid socket name\n"; # untaint $sockname
@@ -165,26 +168,34 @@ do {
# For each new connection, send the protocol version and the account key's
# public parameters, then sign whatever comes in
#
-$SIG{PIPE} = 'IGNORE'; # ignore broken pipes
-for (my $count = 0;; $count++) {
- accept(my $conn, $S) or do {
- next if $! == EINTR; # try again if accept(2) was interrupted by a signal
- die "accept: $!";
- };
- print STDERR "[$count]>> Accepted new connection\n" unless $OPTS{quiet};
-
+sub conn($;$) {
+ my $conn = shift;
+ my $count = shift;
$conn->printflush( "$PROTOCOL_VERSION OK", "\r\n", $JWK, "\r\n" );
# sign whatever comes in
while (defined (my $data = $conn->getline())) {
$data =~ s/\r\n\z// or die;
- print STDERR "[$count]>> Issuing SHA-256 signature for: $data\n" unless $OPTS{quiet};
+ print STDERR "[$count] >>> Issuing SHA-256 signature for: $data\n" unless $OPTS{quiet};
my $sig = $SIGN->($data);
$conn->printflush( encode_base64url($sig), "\r\n" );
}
+}
- print STDERR "[$count]>> Connection terminated\n" unless $OPTS{quiet};
- close $conn or warn "Can't close: $!";
+if (defined $OPTS{fdopen}) {
+ conn($S, $$);
+} else {
+ $SIG{PIPE} = 'IGNORE'; # ignore broken pipes
+ for (my $count = 0;; $count++) {
+ accept(my $conn, $S) or do {
+ next if $! == EINTR; # try again if accept(2) was interrupted by a signal
+ die "accept: $!";
+ };
+ print STDERR "[$count] >>> Accepted new connection\n" unless $OPTS{quiet};
+ conn($conn, $count);
+ print STDERR "[$count] >>> Connection terminated\n" unless $OPTS{quiet};
+ close $conn or warn "Can't close: $!";
+ }
}
diff --git a/lacme-accountd.md b/lacme-accountd.md
index 81c0802..4d3e1a5 100644
--- a/lacme-accountd.md
+++ b/lacme-accountd.md
@@ -58,15 +58,15 @@ Options
The following command can be used to generate a new 4096-bits RSA
key in PEM format with mode 0600:
- openssl genrsa 4096 | install -m0600 /dev/stdin /path/to/priv.key
+ openssl genrsa 4096 | install -m0600 /dev/stdin /path/to/account.key
-`-socket=`*path*
+`--socket=`*path*
: Use *path* as the UNIX-domain socket to bind against for signature
requests from the [ACME] client. `lacme-accountd` aborts if *path*
exists or if its parent directory is writable by other users.
-`-?`, `--help`
+`-h`, `--help`
: Display a brief help and exit.
@@ -117,17 +117,17 @@ Examples
Run `lacme-accountd` in a first terminal:
- ~$ lacme-accountd --privkey=file:/path/to/priv.key --socket=/run/user/1000/S.lacme
+ ~$ lacme-accountd --privkey=file:/path/to/account.key --socket=$XDG_RUNTIME_DIR/S.lacme
Then, while `lacme-accountd` is running, execute locally [`lacme`(1)] in
another terminal:
- ~$ sudo lacme --socket=/run/user/1000/S.lacme new-cert
+ ~$ sudo lacme --socket=$XDG_RUNTIME_DIR/S.lacme new-cert
Alternatively, use [OpenSSH] 6.7 or later to forward the socket and
execute [`lacme`(1)] remotely:
- ~$ ssh -oExitOnForwardFailure=yes -tt -R /path/to/remote.sock:/run/user/1000/S.lacme user@example.org \
+ ~$ ssh -oExitOnForwardFailure=yes -tt -R /path/to/remote.sock:$XDG_RUNTIME_DIR/S.lacme user@example.org \
sudo lacme --socket=/path/to/remote.sock new-cert
See also
diff --git a/lacme.md b/lacme.md
index b7a7f49..b086fe7 100644
--- a/lacme.md
+++ b/lacme.md
@@ -26,7 +26,9 @@ with its own executable:
the [ACME] client.)
One can use the UNIX-domain socket forwarding facility of OpenSSH
6.7 and later to run [`lacme-accountd`(1)] and `lacme` on different
- hosts.
+ hosts. Alternatively, the [`lacme-accountd`(1)] process can be
+ spawned by the “master” `lacme` process below; in that case, the
+ two processes communicate through a socket pair.
2. A “master” `lacme` process, which runs as root and is the only
component with access to the private key material of the server
@@ -117,10 +119,14 @@ Generic options
*socket* option of the [`[client]` section](#client-section) of the
configuration file.
-`-?`, `--help`
+`-h`, `--help`
: Display a brief help and exit.
+`-q`, `--quiet`
+
+: Be quiet.
+
`--debug`
: Turn on debug mode.
@@ -249,6 +255,44 @@ This section is used for configuring the [ACME] webserver.
automatically removed once `lacme` exits.
Default: `Yes`.
+`[accountd]` section
+---------------------
+
+This section is used for configuring the [`lacme-accountd`(1)] process.
+If the section (including its header) is absent or commented out,
+`lacme` connects to an existing UNIX-domain socket bound by a running
+[`lacme-accountd`(1)] process.
+
+*user*
+
+: The username to drop privileges to (setting both effective and real
+ uid). Preserve root privileges if the value is empty.
+
+*group*
+
+: The 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.
+
+*command*
+
+: Path to the [`lacme-accountd`(1)] executable.
+ Default: `/usr/bin/lacme-accountd`.
+
+*config*
+
+: Path to the [`lacme-accountd`(1)] configuration file.
+ Default: `/etc/lacme/lacme-accountd.conf`.
+
+*privkey*
+
+: The (private) account key to use for signing requests. See
+ [`lacme-accountd`(1)] for details.
+
+*quiet*
+
+: Be quiet. Possible values: `Yes`/`No`.
+
Certificate configuration file
==============================
@@ -282,7 +326,7 @@ Valid options are:
following command can be used to generate a new 4096-bits RSA key in
PEM format with mode 0600:
- openssl genrsa 4096 | install -m0600 /dev/stdin /path/to/priv.key
+ openssl genrsa 4096 | install -m0600 /dev/stdin /path/to/srv.key
*min-days*
@@ -339,7 +383,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.0.1-July-27-2015.pdf
+ ~$ sudo lacme reg=/acme/reg/137760 --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