aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Changelog23
-rw-r--r--INSTALL1
-rw-r--r--Makefile1
-rw-r--r--config/lacme-accountd.conf4
-rw-r--r--config/lacme.conf4
-rwxr-xr-xlacme39
-rwxr-xr-xlacme-accountd35
-rw-r--r--lacme-accountd.1.md67
-rw-r--r--lacme.8.md90
-rw-r--r--tests/accountd13
-rw-r--r--tests/spec-expansion130
11 files changed, 341 insertions, 66 deletions
diff --git a/Changelog b/Changelog
index 8952ba6..966b0b0 100644
--- a/Changelog
+++ b/Changelog
@@ -19,9 +19,16 @@ lacme (0.7.1) upstream;
validate provided X.509 chains using that self-contained bundle,
regardless of which CAs is marqued as trusted under /etc/ssl/certs.
This change bumps the minimum OpenSSL version to 1.1.0.
- * Breaking change: lacme(8) resp. lacme-accountd(1) no longer consider
- ./lacme.conf resp. ./lacme-accountd.conf as default location for the
- configuration file.
+ * Breaking change: lacme(8) and lacme-accountd(1) respectively load
+ their configuration file from /etc/lacme/lacme.conf resp.
+ /etc/lacme/lacme-accountd.conf when running as root, and
+ $XDG_CONFIG_HOME/lacme/lacme.conf resp.
+ $XDG_CONFIG_HOME/lacme/lacme-accountd.conf when running as a normal
+ user. There is no fallback to /etc anymore, and the lookup in the
+ current directory as prefered choice is removed too. However
+ lacme-accountd(1) can be used without configuration file under
+ ~/.config/lacme as it treats a non-existent default location as an
+ empty file.
* The client, webserver, and accountd commands are now split on
whitespace. This doesn't change the default behavior but allows
using `ssh -T lacme@account.example.net lacme-accountd` to spawn a
@@ -30,11 +37,17 @@ lacme (0.7.1) upstream;
https://letsencrypt.org/docs/staging-environment/ .
* lacme(8)'s 'config' option in the [accountd] section no longer have a
default value. The previous default /etc/lacme/lacme-accountd.conf
- is still honored when there is the user running lacme doesn't have a
- ~/.config/lacme/lacme-account.conf configuration file.
+ is still honored when root privileges are preserved (the default).
* Deprecate setting 'privkey' in [accountd] section of the lacme(8)
configuration file. One need to use the lacme-accountd(1)
configuration file for that instead.
+ * lacme(8): add %-specifiers support for --config=, --socket=,
+ --config-certs= (and 'socket'/'config-certs'/'challenge-directory'
+ configuration options *before* privilege drop; and for the [accountd]
+ section 'command'/'config' configuration options *after* privilege
+ drop).
+ * lacme-accountd(1): add %-specifiers support for --config=, --socket=
+ and --privkey= (and 'socket'/'privkey' configuration options).
+ Improve nginx/apache2 snippets for direct serving of challenge files
(with the new 'challenge-directory' logic symlinks can be disabled).
+ Split Nginx and Apapche2 static configuration snippets into seperate
diff --git a/INSTALL b/INSTALL
index 85bd0c2..092ef16 100644
--- a/INSTALL
+++ b/INSTALL
@@ -7,7 +7,6 @@ lacme-accountd depends on the following Perl modules:
- File::Basename (core module)
- Getopt::Long (core module)
- JSON (optionally C/XS-accelerated with JSON::XS)
- - List::Util (core module)
- MIME::Base64 (core module)
- Socket (core module)
diff --git a/Makefile b/Makefile
index 2ac524b..a4caff0 100644
--- a/Makefile
+++ b/Makefile
@@ -56,6 +56,7 @@ $(BUILDDIR)/%: %
s#@@sbindir@@#$(sbindir)#g; \
s#@@libexecdir@@#$(libexecdir)#g; \
s#@@datadir@@#$(datadir)#g; \
+ s#@@localstatedir@@#$(localstatedir)#g; \
s#@@runstatedir@@#$(runstatedir)#g; \
s#@@sysconfdir@@#$(sysconfdir)#g; \
s#@@lacme_www_user@@#$(lacme_www_user)#g; \
diff --git a/config/lacme-accountd.conf b/config/lacme-accountd.conf
index 10f332e..f31cf67 100644
--- a/config/lacme-accountd.conf
+++ b/config/lacme-accountd.conf
@@ -17,10 +17,8 @@
# for signature requests from the ACME client. An error is raised if
# 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.
#
-#socket = /run/user/1000/S.lacme
+#socket = %t/S.lacme
# Be quiet. Possible values: "Yes"/"No".
#
diff --git a/config/lacme.conf b/config/lacme.conf
index 98ecacb..198729d 100644
--- a/config/lacme.conf
+++ b/config/lacme.conf
@@ -10,13 +10,11 @@
# UNIX-domain socket to connect to for signature requests from the ACME
# client. lacme(8) 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 setting is ignored when lacme-accountd(1) is spawned by lacme(8),
# since the two processes communicate through a socket pair. See the
# "accountd" section below for details.
#
-#socket =
+#socket = %t/S.lacme
# username to drop privileges to (setting both effective and real uid).
# Skip privilege drop if the value is empty (not recommended).
diff --git a/lacme b/lacme
index 9a62cbb..ad7e1d8 100755
--- a/lacme
+++ b/lacme
@@ -75,13 +75,33 @@ $COMMAND = $COMMAND =~ /\A(account|newOrder|new-cert|revokeCert|revoke-cert)\z/
: usage(1, "Invalid command: $COMMAND"); # validate and untaint $COMMAND
@ARGV = map { /\A(\p{Print}*)\z/ ? $1 : die } @ARGV; # untaint @ARGV
+sub env_fallback($$) {
+ my $v = $ENV{ shift() };
+ return (defined $v and $v ne "") ? $v : shift;
+}
+sub spec_expand($) {
+ my $str = shift;
+ $str =~ s#%(.)# my $x =
+ $1 eq "C" ? ($< == 0 ? "@@localstatedir@@/cache" : env_fallback(XDG_CACHE_HOME => "$ENV{HOME}/.cache"))
+ : $1 eq "E" ? ($< == 0 ? "@@sysconfdir@@" : env_fallback(XDG_CONFIG_HOME => "$ENV{HOME}/.config"))
+ : $1 eq "g" ? (getgrgid((split /\s/,$()[0]))[0]
+ : $1 eq "G" ? $( =~ s/\s.*//r
+ : $1 eq "h" ? (getpwuid($<))[7]
+ : $1 eq "u" ? (getpwuid($<))[0]
+ : $1 eq "U" ? $<
+ : $1 eq "t" ? ($< == 0 ? "@@runstatedir@@" : $ENV{XDG_RUNTIME_DIR})
+ : $1 eq "T" ? env_fallback(TMPDIR => "/tmp")
+ : $1 eq "%" ? "%"
+ : die "Error: \"$str\" has unknown specifier %$1\n";
+ die "Error: undefined expansion %$1 in \"$str\"\n" unless defined $x;
+ $x;
+ #ge;
+ return $str;
+}
+
sub set_FD_CLOEXEC($$);
-my $CONFFILENAME = $OPTS{config} // first { -f $_ }
- ( ($ENV{XDG_CONFIG_HOME} // "$ENV{HOME}/.config") . "/lacme/$NAME.conf"
- , "@@sysconfdir@@/lacme/$NAME.conf"
- );
+my $CONFFILENAME = spec_expand($OPTS{config} // "%E/lacme/$NAME.conf");
do {
- 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> };
@@ -93,7 +113,7 @@ do {
my $accountd = defined $OPTS{socket} ? 0 : exists $h->{accountd} ? 1 : 0;
my %valid = (
client => {
- socket => (defined $ENV{XDG_RUNTIME_DIR} ? "$ENV{XDG_RUNTIME_DIR}/S.lacme" : undef),
+ socket => '%t/S.lacme',
user => '@@lacme_client_user@@',
group => '@@lacme_client_group@@',
command => '@@libexecdir@@/lacme/client',
@@ -285,6 +305,7 @@ sub spawn_webserver() {
# Use existing HTTPd to serve challenge files using 'challenge-directory'
# as document root
if (defined (my $dir = $conf->{'challenge-directory'})) {
+ $dir = spec_expand($dir);
print STDERR "[$$] Using existing webserver on $dir\n" if $OPTS{debug};
# lacme(8) doesn't have the list of challenge files to delete on
# cleanup -- instead, we unlink all files and fails at
@@ -513,8 +534,9 @@ sub acme_client($@) {
set_FD_CLOEXEC($s, 1);
$ENV{GPG_TTY} = $GPG_TTY if defined $GPG_TTY;
my ($cmd, @args) = split(/\s+/, $accountd->{command}) or die "Empty accountd command\n";
+ $_ = spec_expand($_) foreach ($cmd, @args); # expand %-specifiers after privilege drop and whitespace split
push @args, '--stdio';
- push @args, '--config='.$accountd->{config} if $accountd->{config} ne '';
+ push @args, '--config='.spec_expand($accountd->{config}) if $accountd->{config} ne '';
push @args, '--privkey='.$accountd->{privkey} if $accountd->{privkey} ne ''; # XXX deprecated in 0.8.0
push @args, '--quiet' unless lc $accountd->{quiet} eq 'no';
push @args, '--debug' if $OPTS{debug};
@@ -531,7 +553,7 @@ sub acme_client($@) {
}
else {
my @stat;
- my $sockname = $OPTS{socket} // $conf->{socket} // die "Missing socket option\n";
+ my $sockname = spec_expand($OPTS{socket} // $conf->{socket});
$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
@@ -697,6 +719,7 @@ elsif ($COMMAND eq 'newOrder' or $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/" ];
+ $_ = spec_expand($_) foreach @$conffiles;
my ($conf, %defaults);
foreach my $conffile (@$conffiles) {
$conffile = dirname($CONFFILENAME) .'/'. $conffile unless $conffile =~ /\A\//;
diff --git a/lacme-accountd b/lacme-accountd
index b9a6e33..e170637 100755
--- a/lacme-accountd
+++ b/lacme-accountd
@@ -30,7 +30,6 @@ my $NAME = 'lacme-accountd';
use Errno 'EINTR';
use File::Basename 'dirname';
use Getopt::Long qw/:config posix_default no_ignore_case gnu_getopt auto_version/;
-use List::Util 'first';
use MIME::Base64 'encode_base64url';
use Socket qw/PF_UNIX SOCK_STREAM SHUT_RDWR/;
@@ -64,11 +63,32 @@ sub usage(;$$) {
usage(1) unless GetOptions(\%OPTS, qw/config=s privkey=s socket=s stdio quiet|q debug help|h/);
usage(0) if $OPTS{help};
+sub env_fallback($$) {
+ my $v = $ENV{ shift() };
+ return (defined $v and $v ne "") ? $v : shift;
+}
+sub spec_expand($) {
+ my $str = shift;
+ $str =~ s#%(.)# my $x =
+ $1 eq "C" ? ($< == 0 ? "@@localstatedir@@/cache" : env_fallback(XDG_CACHE_HOME => "$ENV{HOME}/.cache"))
+ : $1 eq "E" ? ($< == 0 ? "@@sysconfdir@@" : env_fallback(XDG_CONFIG_HOME => "$ENV{HOME}/.config"))
+ : $1 eq "g" ? (getgrgid((split /\s/,$()[0]))[0]
+ : $1 eq "G" ? $( =~ s/\s.*//r
+ : $1 eq "h" ? (getpwuid($<))[7]
+ : $1 eq "u" ? (getpwuid($<))[0]
+ : $1 eq "U" ? $<
+ : $1 eq "t" ? ($< == 0 ? "@@runstatedir@@" : $ENV{XDG_RUNTIME_DIR})
+ : $1 eq "T" ? env_fallback(TMPDIR => "/tmp")
+ : $1 eq "%" ? "%"
+ : die "Error: \"$str\" has unknown specifier %$1\n";
+ die "Error: undefined expansion %$1 in \"$str\"\n" unless defined $x;
+ $x;
+ #ge;
+ return $str;
+}
+
do {
- my $conffile = $OPTS{config} // first { -f $_ }
- ( ($ENV{XDG_CONFIG_HOME} // "$ENV{HOME}/.config") . "/lacme/$NAME.conf"
- , "@@sysconfdir@@/lacme/$NAME.conf"
- );
+ my $conffile = spec_expand($OPTS{config} // "%E/lacme/$NAME.conf");
if (defined $OPTS{config} or -e $conffile) {
print STDERR "Using configuration file: $conffile\n" if $OPTS{debug};
@@ -94,7 +114,7 @@ do {
#
my ($JWK, $SIGN);
if ($OPTS{privkey} =~ /\A(file|gpg):(\p{Print}+)\z/) {
- my ($method, $filename) = ($1,$2);
+ my ($method, $filename) = ($1, spec_expand($2));
my ($fh, @command);
if ($method eq 'file') {
# generate with `openssl genpkey -algorithm RSA`
@@ -142,8 +162,7 @@ my $JWK_STR = JSON::->new->encode($JWK);
# delete the file manually.
#
unless (defined $OPTS{stdio}) {
- my $sockname = $OPTS{socket} // (defined $ENV{XDG_RUNTIME_DIR} ? "$ENV{XDG_RUNTIME_DIR}/S.lacme" : undef);
- die "Missing socket option\n" unless defined $sockname;
+ my $sockname = spec_expand($OPTS{socket} // '%t/S.lacme');
$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
diff --git a/lacme-accountd.1.md b/lacme-accountd.1.md
index cd6352c..4c494f2 100644
--- a/lacme-accountd.1.md
+++ b/lacme-accountd.1.md
@@ -41,9 +41,12 @@ Options
`--config=`*filename*
-: Use *filename* as configuration file. `lacme-accountd` fails when
- `--config=` is used with a non-existent file, but a non-existent
- default location is treated as if it were an empty file.
+: Use *filename* as configuration file instead of
+ `%E/lacme/lacme-accountd.conf`. The value is subject to
+ [%-specifier expansion](#percent-specifiers). `lacme-accountd`
+ fails when `--config=` is used with a non-existent file, but a
+ non-existent default location is treated as if it were an empty
+ file.
See the **[configuration file](#configuration-file)** section below
for the configuration options.
@@ -57,6 +60,8 @@ Options
symmetrically encrypted)
* `gpg:`*FILE*, for a [`gpg`(1)]-encrypted private key
+ *FILE* is subject to [%-specifier expansion](#percent-specifiers).
+
The [`genpkey`(1ssl)] command can be used to generate a new private
(account) key:
@@ -67,9 +72,14 @@ Options
`--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.
+: Use *path* as the UNIX-domain socket to bind to for signature
+ requests from the [ACME] client. The value is subject to
+ [%-specifier expansion](#percent-specifiers). `lacme-accountd`
+ aborts if *path* exists or if its parent directory is writable by
+ other users.
+ Default: `%t/S.lacme` (omitting `--socket=` therefore yields an
+ error when `lacme-accountd` doesn't run as and the `XDG_RUNTIME_DIR`
+ environment variable is unset or empty).
`-h`, `--help`
@@ -86,12 +96,6 @@ Options
Configuration file
==================
-If `--config=` is not given, `lacme-accountd` uses the first existing
-configuration file among *$XDG_CONFIG_HOME/lacme/lacme-accountd.conf*
-(or *~/.config/lacme/lacme-accountd.conf* if the `XDG_CONFIG_HOME`
-environment variable is not set), and
-*@@sysconfdir@@/lacme/lacme-accountd.conf*.
-
When given on the command line, the `--privkey=`, `--socket=` and
`--quiet` options take precedence over their counterpart (without
leading `--`) in the configuration file. Valid settings are:
@@ -110,13 +114,48 @@ leading `--`) in the configuration file. Valid settings are:
*socket*
: See `--socket=`.
- Default: *$XDG_RUNTIME_DIR/S.lacme* if the `XDG_RUNTIME_DIR`
- environment variable is set.
*quiet*
: Be quiet. Possible values: `Yes`/`No`.
+%-specifiers {#percent-specifiers}
+============
+
+The value the `--config=`, `--privkey=` and `--socket=` CLI options (and
+*privkey* and *socket* configuration options) are subject to %-expansion
+for the following specifiers.
+
+---- ------------------------------------------------------------------
+`%C` `@@localstatedir@@/cache` for the root user, and `$XDG_CACHE_HOME`
+ for other users (or `$HOME/.cache` if the `XDG_CACHE_HOME`
+ environment variable is unset or empty).
+
+`%E` `@@sysconfdir@@` for the root user, and `$XDG_CONFIG_HOME` for
+ other users (or `$HOME/.config` if the `XDG_CONFIG_HOME`
+ environment variable is unset or empty).
+
+`%g` Current group name.
+
+`%G` Current group ID.
+
+`%h` Home directory of the current user.
+
+`%t` `@@runstatedir@@` for the root user, and `$XDG_RUNTIME_DIR` for
+ other users. Non-root users may only use `%t` when the
+ `XDG_RUNTIME_DIR` environment variable is set to a non-empty
+ value.
+
+`%T` `$TMPDIR`, or `/tmp` if the `TMPDIR` environment variable is unset
+ or empty.
+
+`%u` Current user name.
+
+`%U` Current user ID.
+
+`%%` A literal `%`.
+---- ------------------------------------------------------------------
+
Examples
========
diff --git a/lacme.8.md b/lacme.8.md
index 4dfc67e..aab448f 100644
--- a/lacme.8.md
+++ b/lacme.8.md
@@ -100,16 +100,22 @@ Generic settings
`--config=`*filename*
-: Use *filename* as configuration file. See the **[configuration
- file](#configuration-file)** section below for the configuration
- options.
+: Use *filename* as configuration file instead of
+ `%E/lacme/lacme.conf`. The value is subject to [%-specifier
+ expansion](#percent-specifiers).
+
+ See the **[configuration file](#configuration-file)** section below
+ for the configuration options.
`--socket=`*path*
: 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.
+ connect to for signature requests from the [ACME] client. The value
+ is subject to [%-specifier expansion](#percent-specifiers).
+ `lacme` aborts if *path* exists or if its parent directory is
+ writable by other users.
+ Default: `%t/S.lacme`.
+
This command-line option overrides the *socket* setting of the
[`[client]` section](#client-section) of the configuration file; it
also causes the [`[accountd]` section](#accountd-section) to be
@@ -130,10 +136,6 @@ Generic settings
Configuration file
==================
-If `--config=` is not given, `lacme` uses the first existing
-configuration file among *$XDG_CONFIG_HOME/lacme/lacme.conf* (or
-*~/.config/lacme/lacme.conf* if the `XDG_CONFIG_HOME` environment
-variable is not set), and *@@sysconfdir@@/lacme/lacme.conf*.
Valid settings are:
Default section
@@ -145,13 +147,15 @@ Default section
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).
+ configuration options). Each item in that list is independently
+ subject to [%-specifier expansion](#percent-specifiers).
- 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`.
+ Paths not starting with `/` (after %-expansion) are relative to the
+ parent directory of the **[configuration filename](#configuration-file)**.
+ The list of files and directories is processed in the specified
+ 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/`.
@@ -164,8 +168,6 @@ of [ACME] commands and dialogues with the remote [ACME] server).
*socket*
: See `--socket=`.
- Default: *$XDG_RUNTIME_DIR/S.lacme* if the `XDG_RUNTIME_DIR`
- environment variable is set.
*user*
@@ -247,7 +249,9 @@ served during certificate issuance.
lacme client user (by default `@@lacme_client_user@@`) needs to be
able to create files under it.
- This setting is required when *listen* is empty.
+ This setting is required when *listen* is empty. Moreover its value
+ is subject to [%-specifier expansion](#percent-specifiers) _before_
+ privilege drop.
*user*
@@ -308,13 +312,18 @@ UNIX-domain socket.
the first item being the command to execute, the second its first
argument etc. (Note that `lacme` appends more arguments when
executing the command internally.)
+ Each item in that list is independently subject to [%-specifier
+ expansion](#percent-specifiers) _after_ privilege drop.
+ Default: `@@bindir@@/lacme-accountd`.
+
Use for instance `ssh -T lacme@account.example.net lacme-accountd`
- in order to spawn a remote [`lacme-accountd`(1)] server. Default:
- `@@bindir@@/lacme-accountd`.
+ in order to spawn a remote [`lacme-accountd`(1)] server.
*config*
-: Path to the [`lacme-accountd`(1)] configuration file.
+: Path to the [`lacme-accountd`(1)] configuration file. The value is
+ subject to [%-specifier expansion](#percent-specifiers) _after_
+ privilege drop.
*quiet*
@@ -428,6 +437,43 @@ Valid settings are:
after successful installation of the *certificate* and/or
*certificate-chain*.
+%-specifiers {#percent-specifiers}
+============
+
+Some CLI options and configuration settings are subject to %-expansion
+for the following specifiers. Check the documentation of each setting
+to see which ones are affected.
+
+---- ------------------------------------------------------------------
+`%C` `@@localstatedir@@/cache` for the root user, and `$XDG_CACHE_HOME`
+ for other users (or `$HOME/.cache` if the `XDG_CACHE_HOME`
+ environment variable is unset or empty).
+
+`%E` `@@sysconfdir@@` for the root user, and `$XDG_CONFIG_HOME` for
+ other users (or `$HOME/.config` if the `XDG_CONFIG_HOME`
+ environment variable is unset or empty).
+
+`%g` Current group name.
+
+`%G` Current group ID.
+
+`%h` Home directory of the current user.
+
+`%t` `@@runstatedir@@` for the root user, and `$XDG_RUNTIME_DIR` for
+ other users. Non-root users may only use `%t` when the
+ `XDG_RUNTIME_DIR` environment variable is set to a non-empty
+ value.
+
+`%T` `$TMPDIR`, or `/tmp` if the `TMPDIR` environment variable is unset
+ or empty.
+
+`%u` Current user name.
+
+`%U` Current user ID.
+
+`%%` A literal `%`.
+---- ------------------------------------------------------------------
+
Examples
========
diff --git a/tests/accountd b/tests/accountd
index 2f3985f..4626c78 100644
--- a/tests/accountd
+++ b/tests/accountd
@@ -20,12 +20,17 @@ grepstderr -Fxq "Can't stat $SOCKET: No such file or directory (Is lacme-account
#######################################################################
+# missing configuration at default location
+! runuser -u lacme-account -- lacme-accountd --debug 2>"$STDERR" || fail
+grepstderr -Fxq "Ignoring missing configuration file at default location /home/lacme-account/.config/lacme/lacme-accountd.conf"
+grepstderr -Fxq "Error: 'privkey' is not specified"
+
install -olacme-account -glacme-account -Ddm0700 ~lacme-account/.config/lacme
mv -t ~lacme-account/.config/lacme /etc/lacme/account.key
chown lacme-account: ~lacme-account/.config/lacme/account.key
cat >~lacme-account/.config/lacme/lacme-accountd.conf <<-EOF
- privkey = file:/home/lacme-account/.config/lacme/account.key
+ privkey = file:%E/lacme/account.key
EOF
# non-existent parent directory
@@ -33,9 +38,13 @@ EOF
grepstderr -Fxq "stat(/nonexistent): No such file or directory"
# word-writable parent directory
-! runuser -u lacme-account -- lacme-accountd --socket="/tmp/S.lacme" account 2>"$STDERR" || fail
+! runuser -u lacme-account -- lacme-accountd --socket="%T/S.lacme" account 2>"$STDERR" || fail
grepstderr -Fxq "Error: insecure permissions on /tmp"
+# unset XDG_RUNTIME_DIR
+! runuser -u lacme-account -- lacme-accountd 2>"$STDERR" || fail
+grepstderr "Error: undefined expansion %t in \"%t/S.lacme\""
+
# non-existent $XDG_RUNTIME_DIR
! runuser -u lacme-account -- env XDG_RUNTIME_DIR="/nonexistent" lacme-accountd 2>"$STDERR" || fail
grepstderr -Fxq "stat(/nonexistent): No such file or directory"
diff --git a/tests/spec-expansion b/tests/spec-expansion
new file mode 100644
index 0000000..722bdfc
--- /dev/null
+++ b/tests/spec-expansion
@@ -0,0 +1,130 @@
+# %-specifiers expansion
+
+# lacme --config=, all specifiers, root privileges
+! lacme --config="%C %E %t %h %T %g %G %u %U %%.conf" account 2>"$STDERR" || fail
+grepstderr -Fxq "Can't open /var/cache /etc /run /root /tmp root 0 root 0 %.conf: No such file or directory"
+
+# lacme --config=, all specifiers, root privileges, defined XDG_*
+! env XDG_CACHE_HOME=/foo/cache XDG_CONFIG_HOME=/foo/config XDG_RUNTIME_DIR=/foo/run HOME=/foo/home USER=myuser TMPDIR=/foo/tmp \
+ lacme --config="%C %E %t %h %T %g %G %u %U %%.conf" account 2>"$STDERR" || fail
+grepstderr -Fxq "Can't open /var/cache /etc /run /root /foo/tmp root 0 root 0 %.conf: No such file or directory"
+
+# lacme --config=, all specifiers, non-root, unset XDG_RUNTIME_DIR
+! runuser -u nobody -- lacme --config="%C %E %t %h %T %g %G %u %U %%.conf" account 2>"$STDERR" || fail
+grepstderr -Fxq "Error: undefined expansion %t in \"%C %E %t %h %T %g %G %u %U %%.conf\""
+
+# lacme --config=, all specifiers, non-root, defined XDG_RUNTIME_DIR, no other XDG_*
+! runuser -u nobody -g www-data -- env XDG_RUNTIME_DIR=/foo/run \
+ lacme --config="%C %E %t %h %T %g %G %u %U %%.conf" account 2>"$STDERR" || fail
+grepstderr -Fxq "Can't open /nonexistent/.cache /nonexistent/.config /foo/run /nonexistent /tmp www-data 33 nobody 65534 %.conf: No such file or directory"
+
+# lacme --config=, all specifiers, non-root, defined XDG_*
+! runuser -u nobody -- env XDG_CACHE_HOME=/foo/cache XDG_CONFIG_HOME=/foo/config XDG_RUNTIME_DIR=/foo/run HOME=/foo/home USER=myuser TMPDIR=/foo/tmp \
+ lacme --config="%C %E %t %h %T %g %G %u %U %%.conf" account 2>"$STDERR" || fail
+grepstderr -Fxq "Can't open /foo/cache /foo/config /foo/run /nonexistent /foo/tmp nogroup 65534 nobody 65534 %.conf: No such file or directory"
+
+# lacme --socket=
+! lacme --config="%E/lacme/lacme.conf" --socket="%t/S.lacme2" account --debug 2>"$STDERR" || fail
+grepstderr -Fxq "Using configuration file: /etc/lacme/lacme.conf"
+grepstderr -Fxq "Can't stat /run/S.lacme2: No such file or directory (Is lacme-accountd running?)"
+
+# 'challenge-directory' setting (expands before privilege drop)
+sed -ri 's|^#?challenge-directory\s*=.*|challenge-directory = /nonexistent/%u:%g|' /etc/lacme/lacme.conf
+! lacme newOrder --debug 2>"$STDERR" || fail
+grepstderr -Fq "Using existing webserver on /nonexistent/root:root"
+
+# lacme --config-certs= and 'config-certs' settings (expands before privilege drop)
+! lacme newOrder --debug nonexistent 2>"$STDERR" || fail
+grepstderr -Fxq "Reading /etc/lacme/lacme-certs.conf"
+
+sed -ri 's|^#?config-certs\s*=.*|config-certs = /nonexistent/%u:%g.conf|' /etc/lacme/lacme.conf
+! lacme newOrder --debug nonexistent 2>"$STDERR" || fail
+grepstderr -Fxq "Reading /nonexistent/root:root.conf"
+
+! lacme newOrder --config-certs="%E/lacme/certs.conf.d" --debug nonexistent 2>"$STDERR" || fail
+grepstderr -vFxq "Reading /etc/lacme/lacme-certs.conf"
+grepstderr -Fxq "Reading /etc/lacme/certs.conf.d"
+
+# 'config' setting in [accountd] section (expands after privilege drop)
+sed -ri 's|^#?config\s*=\s*$|config = /nonexistent/%u:%g.conf|' /etc/lacme/lacme.conf
+! lacme account 2>"$STDERR" || fail
+grepstderr -Fxq "Failed to open file '/nonexistent/root:root.conf' for reading: No such file or directory"
+
+sed -ri 's|^#?user\s*=\s*$|user = nobody|' /etc/lacme/lacme.conf
+! lacme account 2>"$STDERR" || fail
+grepstderr -Fxq "Failed to open file '/nonexistent/nobody:root.conf' for reading: No such file or directory"
+
+# 'command' setting in [accountd] section (expands after privilege drop)
+sed -ri 's|^#?command\s*=.*/lacme-accountd$|command = /usr/bin/lacme-accountd --%u|' /etc/lacme/lacme.conf
+! lacme account 2>"$STDERR" || fail
+grepstderr -Fxq "Unknown option: nobody"
+
+sed -ri 's|^#?command\s*=.*/lacme-accountd .*|command = /nonexistent/%u/%g %u %g|' /etc/lacme/lacme.conf
+! lacme account 2>"$STDERR" || fail
+grepstderr -Eq "^Can't exec \"/nonexistent/nobody/root\": No such file or directory"
+
+
+#######################################################################
+
+# lacme-accountd --config=, all specifiers, root privileges
+! lacme-accountd --config="%C %E %t %h %T %g %G %u %U %%.conf" 2>"$STDERR" || fail
+grepstderr -Fxq "Failed to open file '/var/cache /etc /run /root /tmp root 0 root 0 %.conf' for reading: No such file or directory"
+
+# lacme-accountd --config=, all specifiers, root privileges, defined XDG_*
+! env XDG_CACHE_HOME=/foo/cache XDG_CONFIG_HOME=/foo/config XDG_RUNTIME_DIR=/foo/run HOME=/foo/home USER=myuser TMPDIR=/foo/tmp \
+ lacme-accountd --config="%C %E %t %h %T %g %G %u %U %%.conf" 2>"$STDERR" || fail
+grepstderr -Fxq "Failed to open file '/var/cache /etc /run /root /foo/tmp root 0 root 0 %.conf' for reading: No such file or directory"
+
+# lacme-accountd --config=, all specifiers, non-root, unset XDG_RUNTIME_DIR
+! runuser -u nobody -- lacme-accountd --config="%C %E %t %h %T %g %G %u %U %%.conf" account 2>"$STDERR" || fail
+grepstderr -Fxq "Error: undefined expansion %t in \"%C %E %t %h %T %g %G %u %U %%.conf\""
+
+# lacme-accountd --config=, all specifiers, non-root, defined XDG_RUNTIME_DIR, no other XDG_*
+! runuser -u nobody -g www-data -- env XDG_RUNTIME_DIR=/foo/run \
+ lacme-accountd --config="%C %E %t %h %T %g %G %u %U %%.conf" 2>"$STDERR" || fail
+grepstderr -Fxq "Failed to open file '/nonexistent/.cache /nonexistent/.config /foo/run /nonexistent /tmp www-data 33 nobody 65534 %.conf' for reading: No such file or directory"
+
+# lacme-accountd --config=, all specifiers, non-root, defined XDG_*
+! runuser -u nobody -- env XDG_CACHE_HOME=/foo/cache XDG_CONFIG_HOME=/foo/config XDG_RUNTIME_DIR=/foo/run HOME=/foo/home USER=myuser TMPDIR=/foo/tmp \
+ lacme-accountd --config="%C %E %t %h %T %g %G %u %U %%.conf" 2>"$STDERR" || fail
+grepstderr -Fxq "Failed to open file '/foo/cache /foo/config /foo/run /nonexistent /foo/tmp nogroup 65534 nobody 65534 %.conf' for reading: No such file or directory"
+
+# lacme-accountd --privkey=
+! lacme-accountd --privkey="file:%h/lacme-accountd.key" --debug 2>"$STDERR" || fail
+grepstderr -Fxq "Error: Can't open /root/lacme-accountd.key: No such file or directory"
+
+# lacme-accountd, default socket location
+lacme-accountd --debug 2>"$STDERR" & PID=$!
+sleep 1
+kill $PID || fail
+wait || fail
+grepstderr -Fxq "Using configuration file: /etc/lacme/lacme-accountd.conf"
+grepstderr -Fxq "Starting lacme Account Key Manager at /run/S.lacme"
+grepstderr -Fxq "Unlinking /run/S.lacme"
+
+# lacme-accountd --config= --socket= --privkey=
+ln -s lacme-accountd.conf /etc/lacme/accountd.conf
+lacme-accountd --config="%E/lacme/accountd.conf" --socket="%t/S.lacme2" --privkey="file:%E/lacme/account.key" --debug 2>"$STDERR" & PID=$!
+sleep 1
+kill $PID || fail
+wait || fail
+grepstderr -Fxq "Using configuration file: /etc/lacme/accountd.conf"
+grepstderr -Fxq "Starting lacme Account Key Manager at /run/S.lacme2"
+grepstderr -Fxq "Unlinking /run/S.lacme2"
+
+# lacme-accountd, custom 'socket' setting
+sed -ri 's|^#?socket\s*=.*|socket = %t/S.lacme3|' /etc/lacme/lacme-accountd.conf
+lacme-accountd --debug 2>"$STDERR" & PID=$!
+sleep 1
+kill $PID || fail
+wait || fail
+grepstderr -Fxq "Using configuration file: /etc/lacme/lacme-accountd.conf"
+grepstderr -Fxq "Starting lacme Account Key Manager at /run/S.lacme3"
+grepstderr -Fxq "Unlinking /run/S.lacme3"
+
+# lacme-accountd, custom 'privkey' setting
+sed -ri 's|^privkey\s*=.*|privkey = file:%h/lacme-accountd.key|' /etc/lacme/lacme-accountd.conf
+! lacme-accountd --debug 2>"$STDERR" || fail
+grepstderr -Fxq "Error: Can't open /root/lacme-accountd.key: No such file or directory"
+
+# vim: set filetype=sh :