From 08d9f95505bb11c3d1b6a8c649362ede7dab4138 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Mon, 13 Jun 2016 23:14:00 +0200 Subject: =?UTF-8?q?Rename=20=E2=80=98letsencrypt-tiny=E2=80=99=20to=20?= =?UTF-8?q?=E2=80=98lacme=E2=80=99.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 16 +- README | 58 ++-- client | 8 +- config/lacme-accountd.conf | 29 ++ config/lacme-certs.conf | 56 ++++ config/lacme.conf | 86 +++++ config/letsencrypt-accountd.conf | 29 -- config/letsencrypt-certs.conf | 56 ---- config/letsencrypt.conf | 86 ----- lacme | 705 +++++++++++++++++++++++++++++++++++++++ lacme-accountd | 202 +++++++++++ lacme-accountd.1 | 152 +++++++++ lacme.1 | 364 ++++++++++++++++++++ letsencrypt | 704 -------------------------------------- letsencrypt-accountd | 202 ----------- letsencrypt-accountd.1 | 153 --------- letsencrypt.1 | 370 -------------------- 17 files changed, 1635 insertions(+), 1641 deletions(-) create mode 100644 config/lacme-accountd.conf create mode 100644 config/lacme-certs.conf create mode 100644 config/lacme.conf delete mode 100644 config/letsencrypt-accountd.conf delete mode 100644 config/letsencrypt-certs.conf delete mode 100644 config/letsencrypt.conf create mode 100755 lacme create mode 100755 lacme-accountd create mode 100644 lacme-accountd.1 create mode 100644 lacme.1 delete mode 100755 letsencrypt delete mode 100755 letsencrypt-accountd delete mode 100644 letsencrypt-accountd.1 delete mode 100644 letsencrypt.1 diff --git a/Makefile b/Makefile index 7fd32f8..d5e8b8e 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,15 @@ all: install: - install -d $(DESTDIR)/etc/letsencrypt-tiny - install -m0644 -t $(DESTDIR)/etc/letsencrypt-tiny config/*.conf - install -d $(DESTDIR)/usr/share/letsencrypt-tiny - install -m0644 -t $(DESTDIR)/usr/share/letsencrypt-tiny lets-encrypt-x[1-4]-cross-signed.pem - install -d $(DESTDIR)/usr/lib/letsencrypt-tiny - install -m0755 -t $(DESTDIR)/usr/lib/letsencrypt-tiny client webserver + install -d $(DESTDIR)/etc/lacme + install -m0644 -t $(DESTDIR)/etc/lacme config/*.conf + install -d $(DESTDIR)/usr/share/lacme + install -m0644 -t $(DESTDIR)/usr/share/lacme lets-encrypt-x[1-4]-cross-signed.pem + install -d $(DESTDIR)/usr/lib/lacme + install -m0755 -t $(DESTDIR)/usr/lib/lacme client webserver install -d $(DESTDIR)/usr/share/man/man1 - install -m0644 -t $(DESTDIR)/usr/share/man/man1 letsencrypt-accountd.1 letsencrypt.1 + install -m0644 -t $(DESTDIR)/usr/share/man/man1 lacme-accountd.1 lacme.1 install -d $(DESTDIR)/usr/bin - install -m0644 -t $(DESTDIR)/usr/bin letsencrypt-accountd letsencrypt + install -m0644 -t $(DESTDIR)/usr/bin lacme-accountd lacme .PHONY: all install diff --git a/README b/README index 37c531a..4e66129 100644 --- a/README +++ b/README @@ -1,29 +1,6 @@ -Requesting new Certificate Issuance with the ACME protocol generally -works as follows: - - 1. Generate a Certificate Signing Request. This requires access to - the private part of the server key. - 2. Issue an issuance request against the ACME server. - 3. Answer the ACME Identifier Validation Challenges. The challenge - type "http-01" requires a webserver to listen on port 80 for each - address for which an authorization request is issued; if there is - no running webserver, root privileges are required to bind against - port 80 and to install firewall rules to temporarily open the port. - 4. Install the certificate (after verification) and restart the - service. This usually requires root access as well. - -Steps 1,3,4 need to be run on the host for which an authorization -request is issued. However the the issuance itself (step 2) could be -done from another machine. Furthermore, each ACME command (step 2), as -well as the key authorization token in step 3, need to be signed using -an account key. The account key can be stored on another machine, or -even on a smartcard. - -_______________________________________________________________________ - -letsencrypt is a tiny ACME client written with process isolation and -minimal privileges in mind. It is divided into four components, each -with its own executable: +lacme is a small ACME client written with process isolation and minimal +privileges in mind. It is divided into four components, each with its +own executable: * A process to manage the account key and issue SHA-256 signatures needed for each ACME command. (This process binds to a UNIX-domain @@ -56,11 +33,34 @@ with its own executable: Consult the manuals for more information. - https://guilhem.org/man/letsencrypt.1.html - https://guilhem.org/man/letsencrypt-accountd.1.html + https://guilhem.org/man/lacme.1.html + https://guilhem.org/man/lacme-accountd.1.html + +_______________________________________________________________________ + +Requesting new Certificate Issuance with the ACME protocol generally +works as follows: + + 1. Generate a Certificate Signing Request. This requires access to + the private part of the server key. + 2. Issue an issuance request against the ACME server. + 3. Answer the ACME Identifier Validation Challenges. The challenge + type "http-01" requires a webserver to listen on port 80 for each + address for which an authorization request is issued; if there is + no running webserver, root privileges are required to bind against + port 80 and to install firewall rules to temporarily open the port. + 4. Install the certificate (after verification) and restart the + service. This usually requires root access as well. + +Steps 1,3,4 need to be run on the host for which an authorization +request is issued. However the the issuance itself (step 2) could be +done from another machine. Furthermore, each ACME command (step 2), as +well as the key authorization token in step 3, need to be signed using +an account key. The account key can be stored on another machine, or +even on a smartcard. _______________________________________________________________________ -letsencrypt is Copyright© 2016 Guilhem Moulin ⟨guilhem@fripost.org⟩, and +lacme is Copyright© 2016 Guilhem Moulin ⟨guilhem@fripost.org⟩, and licensed for use under the GNU General Public License version 3 or later. See ‘COPYING’ for specific terms and distribution information. diff --git a/client b/client index 70150ca..409fc62 100755 --- a/client +++ b/client @@ -24,9 +24,9 @@ use warnings; # Usage: client COMMAND CONFIG_FD SOCKET_FD [ARGUMENTS] # # fdopen(3) the file descriptor SOCKET_FD (corresponding to the -# listening letsencrypt-accountd socket), connect(2) to it to retrieve -# the account key's public parameters and later send data to be signed -# by the master component (using the account key). +# listening lacme-accountd socket), connect(2) to it to retrieve the +# account key's public parameters and later send data to be signed by +# the master component (using the account key). # # CONFIG_FD is a read-only file descriptor associated with the # configuration file at pos 0. (This is needed since this process @@ -66,7 +66,7 @@ open my $S, '+<&=', $1 or die "fdopen $1: $!"; ############################################################################# # Read the protocol version and JSON Web Key (RFC 7517) from the -# letsencrypt-accountd socket +# lacme-accountd socket # die "Error: Invalid client version\n" unless $S->getline() =~ /\A(\d+) OK(?:.*)\r\n\z/ and $1 == $PROTOCOL_VERSION; diff --git a/config/lacme-accountd.conf b/config/lacme-accountd.conf new file mode 100644 index 0000000..0a8b81a --- /dev/null +++ b/config/lacme-accountd.conf @@ -0,0 +1,29 @@ +# The value of "privkey" specifies the (private) account key to use +# for signing requests. Currently supported values are: +# +# - 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 + +# 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 + +# 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 +# users. +# Default: "$XDG_RUNTIME_DIR/S.lacme" if the XDG_RUNTIME_DIR +# environment variable is set. +# +#socket = /run/user/1000/S.lacme + +# Be quiet. Possible values: "Yes"/"No". +# +#quiet = Yes + +; vim:ft=dosini diff --git a/config/lacme-certs.conf b/config/lacme-certs.conf new file mode 100644 index 0000000..fbce5e2 --- /dev/null +++ b/config/lacme-certs.conf @@ -0,0 +1,56 @@ +# Each non-default section denotes a separate certificate issuance. +# 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] + +# 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. +#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 + + +#[smtp] +#certificate-key = /etc/postfix/ssl/srv.key +#certificate-chain = /etc/postfix/ssl/srv.pem +#subject = /CN=smtp.example.org +#notify = /bin/systemctl reload postfix + +; vim:ft=dosini diff --git a/config/lacme.conf b/config/lacme.conf new file mode 100644 index 0000000..edcbbb0 --- /dev/null +++ b/config/lacme.conf @@ -0,0 +1,86 @@ +# For certificate issuance (new-cert command), specify the certificate +# configuration file to use +# +#config-certs = config/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 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. +# +#socket = /run/user/1000/S.lacme + +# username to drop privileges to (setting both effective and real uid). +# Preserve root privileges if the value is empty (not recommended). +# Default: "nobody". +# +#user = lacme + +# 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). +# +#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 +# as it has relaxed ratelimit. +# +#server = https://acme-v01.api.letsencrypt.org/ +#server = https://acme-staging.api.letsencrypt.org/ + +# Timeout in seconds after which the client stops polling the ACME +# server and considers the request failed. +# +#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 + + +[webserver] + +# Specify the local address to listen on, in the form ADDRESS[:PORT]. +# +#listen = 0.0.0.0:80 +#listen = [::]:80 + +# If a webserver is already running, specify a non-existent directory +# under which the webserver is configured to serve GET requests for +# challenge files under "/.well-known/acme-challenge/" (for each virtual +# hosts requiring authorization) as static files. +# +#challenge-directory = /var/www/acme-challenge + +# username to drop privileges to (setting both effective and real uid). +# Preserve root privileges if the value is empty (not recommended). +# +#user = www-data + +# 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). +# +#user = www-data + +# Path to the ACME webserver executable. +#command = /usr/lib/lacme/webserver + +# Whether to automatically install iptables(1) rules to open the +# ADDRESS[:PORT] specified with listen. Theses rules are automatically +# removed once lacme(1) exits. +# +#iptables = Yes + +; vim:ft=dosini diff --git a/config/letsencrypt-accountd.conf b/config/letsencrypt-accountd.conf deleted file mode 100644 index c372190..0000000 --- a/config/letsencrypt-accountd.conf +++ /dev/null @@ -1,29 +0,0 @@ -# The value of "privkey" specifies the (private) account key to use -# for signing requests. Currently supported values are: -# -# - 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 - -# 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 - -# 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 -# users. -# Default: "$XDG_RUNTIME_DIR/S.letsencrypt" if the XDG_RUNTIME_DIR -# environment variable is set. -# -#socket = /run/user/1000/S.letsencrypt - -# Be quiet. Possible values: "Yes"/"No". -# -#quiet = Yes - -; vim:ft=dosini diff --git a/config/letsencrypt-certs.conf b/config/letsencrypt-certs.conf deleted file mode 100644 index 2ee9b20..0000000 --- a/config/letsencrypt-certs.conf +++ /dev/null @@ -1,56 +0,0 @@ -# Each non-default section denotes a separate certificate issuance. -# 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] - -# 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/letsencrypt-tiny/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. -#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 - - -#[smtp] -#certificate-key = /etc/postfix/ssl/srv.key -#certificate-chain = /etc/postfix/ssl/srv.pem -#subject = /CN=smtp.example.org -#notify = /bin/systemctl reload postfix - -; vim:ft=dosini diff --git a/config/letsencrypt.conf b/config/letsencrypt.conf deleted file mode 100644 index 1502020..0000000 --- a/config/letsencrypt.conf +++ /dev/null @@ -1,86 +0,0 @@ -# For certificate issuance (new-cert command), specify the certificate -# configuration file to use -# -#config-certs = config/letsencrypt-certs.conf - -[client] -# The value of "socket" specifies the letsencrypt-accountd(1) -# UNIX-domain socket to connect to for signature requests from the ACME -# client. letsencrypt 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.letsencrypt" if the XDG_RUNTIME_DIR -# environment variable is set. -# -#socket = /run/user/1000/S.letsencrypt - -# username to drop privileges to (setting both effective and real uid). -# Preserve root privileges if the value is empty (not recommended). -# Default: "nobody". -# -#user = letsencrypt - -# 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). -# -#group = nogroup - -# Path to the ACME client executable. -#command = /usr/lib/letsencrypt-tiny/client - -# Root URI of the ACME server. NOTE: Use the staging server for testing -# as it has relaxed ratelimit. -# -#server = https://acme-v01.api.letsencrypt.org/ -#server = https://acme-staging.api.letsencrypt.org/ - -# Timeout in seconds after which the client stops polling the ACME -# server and considers the request failed. -# -#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 - - -[webserver] - -# Specify the local address to listen on, in the form ADDRESS[:PORT]. -# -#listen = 0.0.0.0:80 -#listen = [::]:80 - -# If a webserver is already running, specify a non-existent directory -# under which the webserver is configured to serve GET requests for -# challenge files under "/.well-known/acme-challenge/" (for each virtual -# hosts requiring authorization) as static files. -# -#challenge-directory = /var/www/acme-challenge - -# username to drop privileges to (setting both effective and real uid). -# Preserve root privileges if the value is empty (not recommended). -# -#user = www-data - -# 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). -# -#user = www-data - -# Path to the ACME webserver executable. -#command = /usr/lib/letsencrypt-tiny/webserver - -# Whether to automatically install iptables(1) rules to open the -# ADDRESS[:PORT] specified with listen. Theses rules are automatically -# removed once letsencrypt exits. -# -#iptables = Yes - -; vim:ft=dosini diff --git a/lacme b/lacme new file mode 100755 index 0000000..12fb181 --- /dev/null +++ b/lacme @@ -0,0 +1,705 @@ +#!/usr/bin/perl -T + +#---------------------------------------------------------------------- +# ACME client +# Copyright © 2016 Guilhem Moulin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +#---------------------------------------------------------------------- + +use strict; +use warnings; + +our $VERSION = '0.0.1'; +my $NAME = 'lacme'; + +use Errno qw/EADDRINUSE EINTR/; +use Fcntl qw/F_GETFD F_SETFD FD_CLOEXEC SEEK_SET/; +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 + SOCK_STREAM SOL_SOCKET SO_REUSEADDR SHUT_RDWR/; + +use Config::Tiny (); +use Net::SSLeay (); + +# Clean up PATH +$ENV{PATH} = join ':', qw{/usr/bin /bin}; +delete @ENV{qw/IFS CDPATH ENV BASH_ENV/}; + +my ($COMMAND, %OPTS, $CONFFILE, $CONFIG, @CLEANUP); +$SIG{$_} = sub() { exit 1 } foreach qw/INT TERM/; # run the END block upon SIGINT/SIGTERM + + +############################################################################# +# Parse and validate configuration +# +sub usage(;$$) { + my $rv = shift // 0; + if ($rv) { + my $msg = shift; + print STDERR $msg."\n" if defined $msg; + print STDERR "Try '$NAME --help' or consult the manpage for more information.\n"; + } + else { + print STDERR "Usage: $NAME [--config=FILE] [--socket=PATH] [OPTIONS] COMMAND [ARGUMENT ..]\n" + ."Consult the manpage for more information.\n"; + } + exit $rv; +} +usage(1) unless GetOptions(\%OPTS, qw/config=s config-certs=s socket=s agreement-uri=s debug help|h/); +usage(0) if $OPTS{help}; + +$COMMAND = shift(@ARGV) // usage(1, "Missing command"); +$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 + +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"; + my $conf = do { local $/ = undef, <$CONFFILE> }; + # don't close $CONFFILE so we can pass it to the client + + my $h = Config::Tiny::->read_string($conf) or die Config::Tiny::->errstr()."\n"; + my $defaults = delete $h->{_} // {}; + my %valid = ( + client => { + socket => (defined $ENV{XDG_RUNTIME_DIR} ? "$ENV{XDG_RUNTIME_DIR}/S.lacme" : undef), + user => 'nobody', + group => 'nogroup', + command => '/usr/lib/lacme/client', + # the rest is for the ACME client + map {$_ => undef} qw/server timeout SSL_verify SSL_version SSL_cipher_list/ + }, + webserver => { + listen => ':80', + 'challenge-directory' => '/var/www/acme-challenge', + user => 'www-data', + group => 'www-data', + command => '/usr/lib/lacme/webserver', + iptables => 'Yes' + + } + ); + foreach my $s (keys %valid) { + my $h = delete $h->{$s} // {}; + my %v = map { $_ => delete $h->{$_} // $valid{$s}->{$_} } keys %{$valid{$s}}; + die "Unknown option(s) in [$s]: ".join(', ', keys %$h)."\n" if %$h; + $h->{$_} //= $defaults->{$_} foreach keys %$defaults; + $CONFIG->{$s} = \%v; + } + die "Invalid section(s): ".join(', ', keys %$h)."\n" if %$h; + $CONFIG->{_} = $defaults; +}; + +# Regular expressions for domain validation +my $RE_LABEL = qr/[0-9a-z](?:[0-9a-z\x2D]{0,61}[0-9a-z])?/aai; +my $RE_DOMAIN = qr/$RE_LABEL(?:\.$RE_LABEL)+/; + + +############################################################################# +# Generate a Certificate Signing Request (in DER format) +# +sub gen_csr(%) { + my %args = @_; + return unless defined $args{'certificate-key'} and defined $args{subject}; + return if defined $args{hash} and !grep { $args{hash} eq $_ } qw/md5 rmd160 sha1 sha224 sha256 sha384 sha512/; + + my $config = File::Temp::->new(SUFFIX => '.conf', TMPDIR => 1) // die; + $config->print( + "[ req ]\n", + "distinguished_name = req_distinguished_name\n", + "req_extensions = v3_req\n", + + "[ req_distinguished_name ]\n", + + "[ v3_req ]\n", + # XXX Golang errors on extensions marked critical + # https://github.com/letsencrypt/boulder/issues/565 + #"basicConstraints = critical, CA:FALSE\n", + "basicConstraints = CA:FALSE\n", + "subjectKeyIdentifier = hash\n" + ); + #$config->print("keyUsage = critical, $args{keyUsage}\n") if defined $args{keyUsage}; + $config->print("keyUsage = $args{keyUsage}\n") if defined $args{keyUsage}; + $config->print("subjectAltName = $args{subjectAltName}\n") if defined $args{subjectAltName}; + $config->close() or die "Can't close: $!"; + + my @args = (qw/-new -batch -key/, $args{'certificate-key'}); + push @args, "-$args{hash}" if defined $args{hash}; + push @args, '-subj', $args{subject}, '-config', $config->filename(), qw/-reqexts v3_req/; + + open my $fh, '-|', qw/openssl req -outform DER/, @args or die "fork: $!"; + my $csr = do { local $/ = undef; <$fh> }; + close $fh or $! ? die "Can't close: $!" : return; + + if ($OPTS{debug}) { + # print out the CSR in text form + pipe my $rd, my $wd or die "pipe: $!"; + my $pid = fork // die "fork: $!"; + unless ($pid) { + open STDIN, '<&', $rd or die "Can't dup: $!"; + open STDOUT, '>&', \*STDERR or die "Can't dup: $!"; + exec qw/openssl req -noout -text -inform DER/ or die; + } + $rd->close() or die "Can't close: $!"; + $wd->print($csr); + $wd->close() or die "Can't close: $!"; + + waitpid $pid => 0; + die $? if $? > 0; + } + + return $csr; +} + + +############################################################################# +# Get the timestamp of the given cert's expiration date. +# Internally the expiration date is stored as a RFC3339 string (such as +# yyyy-mm-ddThh:mm:ssZ); we convert it to a timestamp manually. +# +sub x509_enddate($) { + my $filename = shift; + my ($bio, $x509, $time, $dt); + + $bio = Net::SSLeay::BIO_new_file($filename, 'r'); + $x509 = Net::SSLeay::PEM_read_bio_X509($bio) if defined $bio; + $time = Net::SSLeay::X509_get_notAfter($x509) if defined $x509; + $dt = Net::SSLeay::P_ASN1_TIME_get_isotime($time) if defined $time; + + my $t; + if (defined $dt and $dt =~ s/\A(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})//) { + # RFC3339 datetime strings; assume epoch is on January 1 of $epoch_year + my ($y, $m, $d, $h, $min, $s) = ($1, $2, $3, $4, $5, $6); + my (undef,undef,undef,undef,undef,$epoch_year,undef,undef,undef) = gmtime(0); + $t = 0; + foreach (($epoch_year+1900) .. $y-1) { + $t += 365*86400; + $t += 86400 if ($_ % 4 == 0 and $_ % 100 != 0) or ($_ % 400 == 0); # leap + } + + if ($m > 1) { + my @m = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31); + $m[1]++ if ($y % 4 == 0 and $y % 100 != 0) or ($y % 400 == 0); # leap + $t += 86400*$m[$_] for (0 .. $m-2); + } + + $t += 86400*($d-1); + $t += $s + 60*($min + 60*$h); + + $dt =~ s/\A\.(\d{1,9})\d*//; # ignore nanosecs + + if ($dt =~ /\A([+-])(\d{2}):(\d{2})\z/) { + my $tz = 60*($3 + 60*$2); + $t = $1 eq '-' ? ($t+$tz) : ($t-$tz); + } + } + + Net::SSLeay::X509_free($x509) if defined $x509; + Net::SSLeay::BIO_free($bio) if defined $bio; + return $t; +} + + +############################################################################# +# Drop privileges and chdir afterwards +# +sub drop_privileges($$$) { + my ($user, $group, $dir) = @_; + + # set effective and real gid; also set the list of supplementary gids to that single gid + if ($group ne '') { + my $gid = getgrnam($group) // die "Can't getgrnam($group): $!"; + $) = "$gid $gid"; + die "Can't setgroups: $!" if $@; + POSIX::setgid($gid) or die "Can't setgid: $!"; + die "Couldn't setgid/setguid" unless $( eq "$gid $gid" and $) eq "$gid $gid"; # safety check + } + + # set effective and real uid + if ($user ne '') { + my $uid = getpwnam($user) // die "Can't getpwnam($user): $!"; + POSIX::setuid($uid) or die "Can't setuid: $!"; + die "Couldn't setuid/seteuid" unless $< == $uid and $> == $uid; # safety check + } + + chdir $dir or die "Can't chdir to $dir: $!"; +} + + +############################################################################# +# Ensure the FD_CLOEXEC bit is $set on $fd +# +sub set_FD_CLOEXEC($$) { + my ($fd, $set) = @_; + my $flags = fcntl($fd, F_GETFD, 0) or die "Can't fcntl F_GETFD: $!"; + my $flags2 = $set ? ($flags | FD_CLOEXEC) : ($flags & ~FD_CLOEXEC); + return if $flags == $flags2; + fcntl($fd, F_SETFD, $flags2) or die "Can't fcntl F_SETFD: $!"; +} + + +############################################################################# +# Try to spawn a webserver to serve ACME challenges, and return the +# temporary challenge directory. +# If a webserver is already listening, symlink the 'challenge-directory' +# configuration option to the temporary challenge directory. +# Otherwise, bind(2) a socket, pass its fileno to the webserver +# component, and optionally install iptables rules. +# +sub spawn_webserver() { + # create a temporary directory; give write access to the ACME client + # and read access to the webserver + my $tmpdir = File::Temp::->newdir(CLEANUP => 1, TMPDIR => 1) // die; + chmod 0755, $tmpdir or die "Can't chmod: $!"; + if ((my $username = $CONFIG->{client}->{user}) ne '') { + my $uid = getpwnam($username) // die "Can't getgrnam($username): $!"; + chown($uid, -1, $tmpdir) or die "Can't chown: $!"; + } + + my $conf = $CONFIG->{webserver}; + my ($fam, $addr, $port) = (PF_INET, $conf->{listen}, 80); + $port = $1 if $addr =~ s/:(\d+)$//; + $addr = Socket::inet_ntop(PF_INET, INADDR_ANY) if $addr eq ''; + $fam = PF_INET6 if $addr =~ s/^\[(.+)\]$/$1/; + + my $proto = getprotobyname("tcp") // die; + socket(my $srv, $fam, SOCK_STREAM, $proto) or die "socket: $!"; + setsockopt($srv, SOL_SOCKET, SO_REUSEADDR, pack("l", 1)) or die "setsockopt: $!"; + $addr = Socket::inet_pton($fam, $addr) // die "Invalid address $conf->{listen}\n"; + my $sockaddr = $fam == PF_INET ? Socket::pack_sockaddr_in($port, $addr) + : $fam == PF_INET6 ? Socket::pack_sockaddr_in6($port, $addr) + : die; + + # try to bind aginst the specified address:port + bind($srv, $sockaddr) or do { + die "Can't bind to $conf->{listen}: $!" if $! != EADDRINUSE; + print STDERR "[$$] Using existing webserver on $conf->{listen}\n" if $OPTS{debug}; + my $dir = $conf->{'challenge-directory'}; + symlink $tmpdir, $dir or die "Can't symlink $dir -> $tmpdir: $!"; + push @CLEANUP, sub() { + print STDERR "Unlinking $dir\n" if $OPTS{debug}; + unlink $dir or warn "Warning: Can't unlink $dir: $!"; + }; + return $tmpdir; + }; + listen($srv, 5) or die "listen: $!"; + + # spawn the webserver component + my $pid = fork() // "fork: $!"; + unless ($pid) { + drop_privileges($conf->{user}, $conf->{group}, $tmpdir); + set_FD_CLOEXEC($srv, 0); + $ENV{DEBUG} = $OPTS{debug}; + # use execve(2) rather than a Perl pseudo-process to ensure that + # the child doesn't have access to the parent's memory + exec $conf->{command}, fileno($srv) or die; + } + + print STDERR "[$$] Forking ACME webserver, child PID $pid\n" if $OPTS{debug}; + set_FD_CLOEXEC($srv, 1); + push @CLEANUP, sub() { + print STDERR "[$$] Shutting down ACME webserver\n" if $OPTS{debug}; + shutdown($srv, SHUT_RDWR) or warn "shutdown: $!"; + kill 15 => $pid; + waitpid $pid => 0; + }; + + return $tmpdir if lc ($conf->{iptables} // 'Yes') eq 'no'; + + # install iptables + my $iptables_bin = $fam == PF_INET ? 'iptables' : $fam == PF_INET6 ? 'ip6tables' : die; + my $iptables_tmp = File::Temp::->new(TMPDIR => 1) // die; + set_FD_CLOEXEC($iptables_tmp, 1); + + my $pid2 = fork() // die "fork: $!"; + unless ($pid2) { + open STDIN, '<', '/dev/null' or die "Can't open /dev/null: $!"; + open STDOUT, '>&', $iptables_tmp or die "Can't dup: $!"; + $| = 1; # turn off buffering for STDOUT + exec "/sbin/$iptables_bin-save", "-c" or die; + } + waitpid $pid2 => 0; + die "Error: /sbin/$iptables_bin-save exited with value ".($? >> 8) if $? > 0; + + # seek back to the begining, as we'll restore directly from the + # handle and not from the file. XXX if there was a way in Perl to + # use open(2) with the O_TMPFILE flag we would use that to avoid + # creating a file to start with + seek($iptables_tmp, SEEK_SET, 0) or die "Can't seek: $!"; + + push @CLEANUP, sub() { + print STDERR "[$$] Restoring iptables\n" if $OPTS{debug}; + my $pid2 = fork() // die "fork: $!"; + unless ($pid2) { + open STDIN, '<&', $iptables_tmp or die "Can't dup: $!"; + open STDOUT, '>', '/dev/null' or die "Can't open /dev/null: $!"; + exec "/sbin/$iptables_bin-restore", "-c" or die; + } + waitpid $pid2 => 0; + warn "Warning: /sbin/$iptables_bin-restore exited with value ".($? >> 8) if $? > 0; + }; + + # it's safe to install the new iptables to open $port now that the + # restore hook is in place + my $mask = $fam == PF_INET ? ($addr eq INADDR_ANY ? '0' : '32') + : $fam == PF_INET6 ? ($addr eq IN6ADDR_ANY ? '0' : '128') + : die; + my $dest = Socket::inet_ntop($fam, $addr) .'/'. $mask; + + system ("/sbin/$iptables_bin", qw/-I INPUT -p tcp -m tcp -m state/, + '-d', $dest, '--dport', $port, + '--state', 'NEW,ESTABLISHED', '-j', 'ACCEPT') == 0 or die; + system ("/sbin/$iptables_bin", qw/-I OUTPUT -p tcp -m tcp -m state/, + '-s', $dest, '--sport', $port, + '--state', 'ESTABLISHED', '-j', 'ACCEPT') == 0 or die; + + return $tmpdir; +} + + +############################################################################# +# Spawn the client component, and wait for it to return. +# 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. +# +sub acme_client($@) { + my $args = shift; + my @args = @_; + + my @stat; + 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: $!"; + } + + # 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 + 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); +} + +sub spawn($@) { + my $args = shift; + my @exec = @_; + + # create communication pipes if needed + my ($in_rd, $in_wd, $out_rd, $out_wd); + if (defined $args->{in}) { + pipe $in_rd, $in_wd or die "pipe: $!"; + } + if (defined $args->{out}) { + pipe $out_rd, $out_wd or die "pipe: $!"; + } + + my $pid = fork() // "fork: $!"; + unless ($pid) { + # child + $args->{child}->() if defined $args->{child}; + if (defined $args->{in}) { + close $in_wd or die "Can't close: $!"; + open STDIN, '<&', $in_rd or die "Can't dup: $!"; + } else { + open STDIN, '<', '/dev/null' or die "Can't open /dev/null: $!"; + } + if (defined $args->{out}) { + close $out_rd or die "Can't close: $!"; + open STDOUT, '>&', $out_wd or die "Can't dup: $!"; + } else { + open STDOUT, '>', '/dev/null' or die "Can't open /dev/null: $!"; + } + exec { $exec[0] } @exec or die; + } + push @CLEANUP, sub() { + kill 15 => $pid; + waitpid $pid => 0; + }; + + # parent + print STDERR "[$$] Forking $exec[0], child PID $pid\n" if $OPTS{debug}; + if (defined $args->{in}) { + $in_rd->close() or die "Can't close: $!"; + $in_wd->print($args->{in}); + $in_wd->close() or die "Can't close: $!"; + } + if (defined $args->{out}) { + $out_wd->close() or die "Can't close: $!"; + ${$args->{out}} = do { local $/ = undef; $out_rd->getline() }; + $out_rd->close() or die "Can't close: $!"; + } + waitpid $pid => 0; + pop @CLEANUP; + undef ${$args->{out}} if defined $args->{out} and $? > 0; + return $? > 255 ? ($? >> 8) : $? > 0 ? 1 : 0; +} + + +############################################################################# +# Install the certificate +# +sub install_cert($$@) { + my $filename = shift; + my $x509 = shift; + + open my $fh, '>', $filename or die "Can't open $filename: $!"; + print $fh $x509; + foreach (@_) { # append the chain + open my $fh2, '<', $_ or die "Can't open $_: $!"; + my $ca = do { local $/ = undef; $fh2->getline() }; + print $fh $ca; + close $fh2 or die "Can't close: $!"; + } + close $fh or die "Can't close: $!"; +} + + +############################################################################# +# new-reg [--agreement-uri=URI] [CONTACT ..] +# reg=URI [--agreement-uri=URI] [CONTACT ..] +# +if ($COMMAND eq 'new-reg' or $COMMAND =~ /^reg=/) { + die "Invalid registration URI (use the 'new-reg' command to determine the URI)\n" + if $COMMAND eq 'reg='; + $OPTS{'agreement-uri'} = $OPTS{'agreement-uri'} =~ /\A(\p{Print}+)\z/ ? $1 + : die "Invalid value for --agreement-uri\n" + if defined $OPTS{'agreement-uri'}; + + unshift @ARGV, ($OPTS{'agreement-uri'} // ''); + exit acme_client({}, @ARGV); +} + + +############################################################################# +# 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; + 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; + } + }; + + my $challenge_dir; + my $rv = 0; + foreach my $s (@ARGV ? @ARGV : keys %$conf) { + my $conf = $conf->{$s} // do { + print STDERR "Warning: No such section $s, skipping\n"; + $rv = 1; + next; + }; + + my $certtype = first { defined $conf->{$_} } qw/certificate certificate-chain/; + unless (defined $certtype) { + print STDERR "[$s] Warning: Missing 'certificate' and 'certificate-chain', skipping\n"; + $rv = 1; + next; + } + + # skip certificates that expire at least $conf->{'min-days'} days in the future + if (-f $conf->{$certtype} and defined (my $t = x509_enddate($conf->{$certtype}))) { + 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"; + next; + } + } + + # generate the CSR + my $csr = gen_csr(%$conf{qw/certificate-key subject subjectAltName keyUsage hash/}) // do { + print STDERR "[$s] Warning: Couldn't generate CSR, skipping\n"; + $rv = 1; + next; + }; + + # spawn the webserver if not done already + $challenge_dir //= spawn_webserver(); + + # list all authorization domains to request + my @authz; + push @authz, $1 if defined $conf->{subject} =~ /\A.*\/CN=($RE_DOMAIN)\z/o; + if (defined $conf->{subjectAltName}) { + foreach my $d (split /,/, $conf->{subjectAltName}) { + next unless $d =~ s/\A\s*DNS://; + if ($d =~ /\A$RE_DOMAIN\z/o) { + push @authz, $d unless grep {$_ eq $d} @authz; + } else { + print STDERR "[$s] Warning: Ignoring invalid domain $d\n"; + } + } + } + + my ($x509, $csr_pubkey, $x509_pubkey); + print STDERR "[$s] Will request authorization for: ".join(", ", @authz), "\n" if $OPTS{debug}; + if (acme_client({chdir => $challenge_dir, in => $csr, out => \$x509}, @authz)) { + print STDERR "[$s] Error: Couldn't issue X.509 certificate!\n"; + $rv = 1; + next; + } + + # extract pubkeys from CSR and cert, and ensure they match + spawn({in => $csr, out => \$csr_pubkey }, qw/openssl req -inform DER -noout -pubkey/); + spawn({in => $x509, out => \$x509_pubkey}, qw/openssl x509 -inform PEM -noout -pubkey/); + unless (defined $x509_pubkey and defined $csr_pubkey and $x509_pubkey eq $csr_pubkey) { + print STDERR "[$s] Error: Received bogus X.509 certificate from ACME server!\n"; + $rv = 1; + next; + }; + + # verify certificate validity against the CA + $conf->{CAfile} //= '/usr/share/lacme/lets-encrypt-x3-cross-signed.pem'; + if ($conf->{CAfile} ne '' and spawn({in => $x509}, 'openssl', 'verify', '-CAfile', $conf->{CAfile}, + qw/-purpose sslserver -x509_strict/)) { + print STDERR "[$s] Error: Received invalid X.509 certificate from ACME server!\n"; + $rv = 1; + next; + } + + # install certificate + if (defined $conf->{'certificate'}) { + print STDERR "Installing X.509 certificate $conf->{'certificate'}\n"; + install_cert($conf->{'certificate'}, $x509); + } + if (defined $conf->{'certificate-chain'}) { + print STDERR "Installing X.509 certificate chain $conf->{'certificate-chain'}\n"; + install_cert($conf->{'certificate-chain'}, $x509, $conf->{CAfile}); + } + + if (defined $conf->{chown}) { + my ($user, $group) = split /:/, $conf->{chown}, 2; + my $uid = getpwnam($user) // die "Can't getpwnam($user): $!"; + my $gid = defined $group ? (getgrnam($group) // die "Can't getgrnam($group): $!") : -1; + foreach (grep defined, @$conf{qw/certificate certificate-chain/}) { + chown($uid, $gid, $_) or die "Can't chown: $!"; + } + } + if (defined $conf->{chmod}) { + my $mode = oct($conf->{chmod}) // die; + foreach (grep defined, @$conf{qw/certificate certificate-chain/}) { + chmod($mode, $_) or die "Can't chown: $!"; + } + } + + my @certopts = join ',', qw/no_header no_version no_pubkey no_sigdump/; + open my $fh, '|-', qw/openssl x509 -noout -fingerprint -sha256 -text -certopt/, @certopts + or die "fork: $!"; + print $fh $x509; + close $fh or die $! ? + "Can't close: $!" : + "Error: x509(1ssl) exited with value ".($? >> 8)."\n"; + + if (defined $conf->{notify}) { + print STDERR "Running notification command `$conf->{notify}`\n"; + if (system($conf->{notify}) != 0) { + print STDERR "Warning: notification command exited with value ".($? >> 8)."\n"; + $rv = 1; + } + } + } + undef $challenge_dir; + exit $rv; +} + + +############################################################################# +# revoke-cert FILE [FILE ..] +# +elsif ($COMMAND eq 'revoke-cert') { + die "Nothing to revoke\n" unless @ARGV; + my $rv = 0; + foreach my $filename (@ARGV) { + print STDERR "Revoking $filename\n"; + + # conversion PEM -> DER + open my $fh, '-|', qw/openssl x509 -outform DER -in/, $filename or die "fork: $!"; + my $der = do { local $/ = undef; <$fh> }; + close $fh or die $! ? + "Can't close: $!" : + "Error: x509(1ssl) exited with value ".($? >> 8)."\n"; + + my @certopts = join ',', qw/no_header no_version no_pubkey no_sigdump no_extensions/; + open my $fh2, '|-', qw/openssl x509 -inform DER -noout -fingerprint -sha256 -text -certopt/, @certopts + or die "fork: $!"; + print $fh2 $der; + close $fh2 or die $! ? + "Can't close: $!" : + "Error: x509(1ssl) exited with value ".($? >> 8)."\n"; + + if (acme_client({in => $der})) { + print STDERR "Warning: Couldn't revoke $filename\n"; + $rv = 1; + } + } + exit $rv; +} + + +############################################################################# +# +else { + die "Unknown command $COMMAND" +} + + +END { + local $?; + $_->() foreach reverse @CLEANUP; +} diff --git a/lacme-accountd b/lacme-accountd new file mode 100755 index 0000000..2bc648f --- /dev/null +++ b/lacme-accountd @@ -0,0 +1,202 @@ +#!/usr/bin/perl -T + +#---------------------------------------------------------------------- +# ACME client (account key manager) +# Copyright © 2016 Guilhem Moulin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +#---------------------------------------------------------------------- + +use strict; +use warnings; + +our $VERSION = '0.0.1'; +my $PROTOCOL_VERSION = 1; +my $NAME = 'lacme-accountd'; + +use Errno 'EINTR'; +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/; + +use Config::Tiny (); +use JSON (); + +# Clean up PATH +$ENV{PATH} = join ':', qw{/usr/bin /bin}; +delete @ENV{qw/IFS CDPATH ENV BASH_ENV/}; + +my ($SOCKNAME, $S, %OPTS); +$SIG{$_} = sub() { exit } foreach qw/INT TERM/; # run the END block upon SIGINT/SIGTERM + + +############################################################################# +# Parse and validate configuration +# +sub usage(;$$) { + my $rv = shift // 0; + if ($rv) { + my $msg = shift; + print STDERR $msg."\n" if defined $msg; + print STDERR "Try '$NAME --help' or consult the manpage for more information.\n"; + } + else { + print STDERR "Usage: $NAME [--config=FILE] [--privkey=ARG] [--socket=PATH] [--quiet]\n" + ."Consult the manpage for more information.\n"; + } + exit $rv; +} +usage(1) unless GetOptions(\%OPTS, qw/config=s privkey=s socket=s quiet|q debug help|h/); +usage(0) if $OPTS{help}; + +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}; + + my $h = Config::Tiny::->read($conffile) or die Config::Tiny::->errstr()."\n"; + my $h2 = delete $h->{_} // {}; + die "Invalid section(s): ".join(', ', keys %$h)."\n" if %$h; + my %h = map { $_ => delete $h2->{$_} } qw/privkey gpg socket quiet/; + die "Unknown option(s): ".join(', ', keys %$h2)."\n" if %$h2; + $h{quiet} = lc $h{quiet} eq 'yes' ? 1 : 0 if defined $h{quiet}; + $OPTS{$_} //= $h{$_} foreach grep {defined $h{$_}} keys %h; + + $OPTS{quiet} = 0 if $OPTS{debug}; + die "Error: 'privkey' is not specified\n" unless defined $OPTS{privkey}; +}; + + +############################################################################# +# Build the JSON Web Key (RFC 7517) from the account key's public parameters, +# and determine the signing method $SIGN. +# +my ($JWK, $SIGN); +if ($OPTS{privkey} =~ /\A(file|gpg):(\p{Print}+)\z/) { + my ($method, $filename) = ($1,$2); + my ($fh, @command); + if ($method eq 'file') { + # generate with `openssl genrsa 4096 | install --mode=0600 /dev/stdin /tmp/privkey` + open $fh, '<', $filename or die "Error: Can't open $filename: $!\n"; + } + elsif ($method eq 'gpg') { + @command = split /\s+/, ($OPTS{gpg} // 'gpg --quiet'); + open $fh, '-|', @command, qw/-o - --decrypt --/, $filename or die "fork: $!"; + } + else { + die; # impossible + } + + my $str = do {local $/ = undef; <$fh>}; + close $fh or die $! ? + "Can't close: $!" : + "Error: $command[0] exited with value ".($? >> 8)."\n"; + + require 'Crypt/OpenSSL/RSA.pm'; + my $rsa = Crypt::OpenSSL::RSA->new_private_key($str); + undef $str; + + die "Error: $filename: Not a private key\n" unless $rsa->is_private(); + die "Error: $filename: Invalid key\n" unless $rsa->check_key(); + $rsa->use_sha256_hash(); + + require 'Crypt/OpenSSL/Bignum.pm'; + my ($n, $e) = $rsa->get_key_parameters(); # don't include private params! + $_ = encode_base64url($_->to_bin()) foreach ($n, $e); + + $JWK = { kty => 'RSA', n => $n, e => $e }; + $SIGN = sub($) { $rsa->sign($_[0]) }; +} +else { + die "Error: unsupported method: $OPTS{privkey}\n"; +} +$JWK = JSON::->new->encode($JWK); + + +############################################################################# +# Create the server UNIX socket and bind(2) against it. +# NOTE: We don't use the abstract namespace so we can rely on the file +# permissions to keep other users out. (Also, OpenSSH 7.1 doesn't seem +# to support the abstract namespace.) The downside is that we have to +# delete the file manually. +# +do { + 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 + + # ensure we're the only user with write access to the parent dir + my $dirname = $sockname =~ s/[^\/]+$//r; + my @stat = stat($dirname) or die "Can't stat $dirname: $!"; + die "Error: insecure permissions on $dirname\n" if ($stat[2] & 0022) != 0; + + my $umask = umask(0177) // die "umask: $!"; + + print STDERR "Starting Let's Encrypt Account Key Manager at $sockname\n" unless $OPTS{quiet}; + socket(my $sock, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!"; + my $sockaddr = Socket::sockaddr_un($sockname) // die; + bind($sock, $sockaddr) or die "bind: $!"; + + ($SOCKNAME, $S) = ($sockname, $sock); + listen($S, 1) or die "listen: $!"; + + umask($umask) // die "umask: $!"; +}; + + +############################################################################# +# 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}; + + $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}; + 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: $!"; +} + + +############################################################################# +# +END { + if (defined $SOCKNAME and -S $SOCKNAME) { + print STDERR "Unlinking $SOCKNAME\n" if $OPTS{debug}; + unlink $SOCKNAME or print STDERR "Can't unlink $SOCKNAME: $!\n"; + } + if (defined $S) { + print STDERR "Shutting down and closing Let's Encrypt Account Key Manager\n" unless $OPTS{quiet}; + shutdown($S, SHUT_RDWR) or warn "shutdown: $!"; + close $S or print STDERR "Can't close: $!\n"; + } +} diff --git a/lacme-accountd.1 b/lacme-accountd.1 new file mode 100644 index 0000000..46a6a0d --- /dev/null +++ b/lacme-accountd.1 @@ -0,0 +1,152 @@ +.TH LACME\-ACCOUNTD "1" "MARCH 2016" "ACME client (account key manager)" "User Commands" + +.SH NAME +lacme\-accountd \- ACME client (account key manager) + +.SH SYNOPSIS +.B lacme\-accountd\fR [\fB\-\-config=\fIFILENAME\fR] +[\fB\-\-privkey=\fIARG\fR] [\fB\-\-socket=\fIPATH\fR] [\fB\-\-quiet\fR] + + +.SH DESCRIPTION +.PP +.B lacme\-accountd\fR is the account key manager component of +\fIlacme\fR(1), a tiny ACME client written with process isolation and +minimal privileges in mind. No other \fIlacme\fR(1) component need +access to the account key; in fact the account key could also be stored +on a smartcard. + +.B lacme\-accountd\fR binds to a UNIX\-domain socket (specified with +\fB\-\-socket=\fR), which ACME clients can connect to in order to +request data signatures. +As a consequence, \fBlacme\-accountd\fR needs to be up and running +before using \fIlacme\fR(1) to issue ACME commands. Also, the process +does not automatically terminate after the last signature request: +instead, one sends an \fIINT\fR or \fITERM\fR signal to bring the server +down. + +Furthermore, one can use the UNIX\-domain socket forwarding facility of +OpenSSH 6.7 and later to run \fBlacme\-accountd\fR and \fIlacme\fR(1) on +different hosts. For instance one could store the account key on a +machine that is not exposed to the internet. See the \fBEXAMPLES\fR +section below. + + +.SH OPTIONS +.TP +.B \-\-config=\fIfilename\fR +Use \fIfilename\fR as configuration file. See the \fBCONFIGURATION +FILE\fR section below for the configuration options. + +.TP +.B \-\-privkey=\fIarg\fR +Specify the (private) account key to use for signing requests. +Currently supported \fIarg\fRuments are: + +.RS +.IP \[bu] 2 +file:\fIFILE\fR, to specify an encrypted private key (in PEM format); and +.IP \[bu] +gpg:\fIFILE\fR, to specify a \fIgpg\fR(1)\-encrypted private key (in PEM format). + +.PP +The following command can be used to generate a new 4096\-bits RSA key in +PEM format with mode 0600: + +.nf + openssl genrsa 4096 | install -m0600 /dev/stdin /path/to/priv.key +.fi +.RE + +.TP +.B \-\-socket=\fIpath\fR +Use \fIpath\fR as the UNIX\-domain socket to bind against for signature +requests from the ACME client. \fBlacme\-accountd\fR aborts if +\fIpath\fR exists or if its parent directory is writable by other users. + +.TP +.B \-?\fR, \fB\-\-help\fR +Display a brief help and exit. + +.TP +.B \-q\fR, \fB\-\-quiet\fR +Be quiet. + +.TP +.B \-\-debug +Turn on debug mode. + + +.SH CONFIGURATION FILE +If \fB\-\-config=\fR is not given, \fBlacme\-accountd\fR uses the first +existing configuration file among \fI./lacme\-accountd.conf\fR, +\fI$XDG_CONFIG_HOME/lacme/lacme\-accountd.conf\fR (or +\fI~/.config/lacme/lacme\-accountd.conf\fR if the XDG_CONFIG_HOME +environment variable is not set), and +\fI/etc/lacme/lacme\-accountd.conf\fR. + +When given on the command line, the \fB\-\-privkey=\fR, +\fB\-\-socket=\fR and \fB\-\-quiet\fR options take precedence over their +counterpart (without leading \(lq\-\-\(rq) in the configuration file. +Valid options are: + +.TP +.I privkey +See \fB\-\-privkey=\fR. +This option is required when \fB\-\-privkey=\fR is not specified on the +command line. + +.TP +.I gpg +For a \fIgpg\fR(1)\-encrypted private account key, specify the binary +\fIgpg\fR(1) to use, as well as some default options. +Default: \(lqgpg \-\-quiet\(rq. + +.TP +.I socket +See \fB\-\-socket=\fR. +Default: \(lq$XDG_RUNTIME_DIR/S.lacme\(rq if the XDG_RUNTIME_DIR +environment variable is set. + +.TP +.I quiet +Be quiet. Possible values: \(lqYes\(rq/\(lqNo\(rq. + + +.SH EXAMPLES + +Run \fBlacme\-accountd\fR in a first terminal: + +.nf + ~$ lacme\-accountd \-\-privkey=file:/path/to/priv.key \-\-socket=/run/user/1000/S.lacme +.fi + +Then, while \fBlacme\-accountd\fR is running, execute locally +\fIlacme\fR(1) in another terminal: + +.nf + ~$ sudo lacme \-\-socket=/run/user/1000/S.lacme new\-cert +.fi + +Alternatively, use \fIssh\fR(1) to forward the socket and execute +\fIlacme\fR(1) remotely: + +.nf + ~$ ssh -oExitOnForwardFailure=yes -tt -R /path/to/remote.sock:/run/user/1000/S.lacme user@example.org \\ + sudo lacme --socket=/path/to/remote.sock new-cert +.fi + + +.SH SEE ALSO +\fBlacme\fR(1), \fBssh\fR(1) + +.SH AUTHOR +.ie \n[www-html] \{\ + Written by +. MTO guilhem@fripost.org "Guilhem Moulin" . +\} +.el \{\ + Written by Guilhem Moulin +. MT guilhem@fripost.org +. ME . +\} diff --git a/lacme.1 b/lacme.1 new file mode 100644 index 0000000..bfe4f45 --- /dev/null +++ b/lacme.1 @@ -0,0 +1,364 @@ +.TH LACME "1" "MARCH 2016" "ACME client" "User Commands" + +.SH NAME +lacme \- ACME client + +.SH SYNOPSIS +.B lacme\fR [\fB\-\-config=\fIFILENAME\fR] [\fB\-\-socket=\fIPATH\fR] +[\fIOPTION\fR ...] \fICOMMAND\fR [\fIARGUMENT\fR ...] + + +.SH DESCRIPTION +.PP +.B lacme\fR is a tiny ACME client written with process isolation and +minimal privileges in mind. +It is divided into four components, each with its own executable: + +.IP \[bu] 4 +A \fIlacme\-accountd\fR(1) process to manage the account key and issue +SHA\-256 signatures needed for each ACME command. (This process binds +to a UNIX\-domain socket to reply to signature requests from the ACME +client.) +One can use the UNIX\-domain socket forwarding facility of OpenSSH 6.7 +and later to run \fIlacme\-accountd\fR(1) and \fBlacme\fR on different +hosts. + +.IP \[bu] 4 +A \(lqmaster\(rq \fBlacme\fR process, which runs as root and is the only +component with access to the private key material of the server keys. +It is used to fork the ACME client (and optionally the ACME webserver) +after dropping root privileges. +For certificate issuances (\fBnew\-cert\fR command), it also generates +Certificate Signing Requests, then verifies the validity of the issued +certificate, and optionally reloads or restarts services when the +\fInotify\fR option is set. + +.IP \[bu] 4 +An actual ACME client (specified with the \fIcommand\fR option of the +\(lq[client]\(rq section of the configuration file), which builds ACME +commands and dialogues with the remote ACME server. +Since ACME commands need to be signed with the account key, the +\(lqmaster\(rq \fBlacme\fR process passes the \fIlacme\-accountd\fR(1) +UNIX\-domain socket to the ACME client: data signatures are requested by +writing the data to be signed to the socket. + +.IP \[bu] 4 +For certificate issuances (\fBnew\-cert\fR command), an optional +webserver (specified with the \fIcommand\fR option of the +\(lq[webserver]\(rq section of the configuration file), which is spawned +by the \(lqmaster\(rq \fBlacme\fR process when no service is listening +on the HTTP port. +(The only challenge type currently supported by \fBlacme\fR is +\(lqhttp\-01\(rq, which requires a webserver to answer challenges.) +That webserver only processes GET and HEAD requests under the +\(lq/.well\-known/acme\-challenge/\(rq URI. +By default some \fIiptables\fR(1) rules are automatically installed to +open the HTTP port, and removed afterwards. + +.SH COMMANDS +.TP +.B lacme \fR[\fB\-\-agreement\-uri=\fIURI\fR]\fB \fBnew\-reg +\fR[\fICONTACT\fR ...] Register the account key managed by +\fIlacme\-accountd\fR(1). A list of \fICONTACT\fR information (such as +\(lqmaito:\(rq URIs) can be specified in order for the server to contact +the client for issues related to this registration (such as +notifications about server\-initiated revocations). + +\fB\-\-agreement\-uri=\fR can be used to specify a \fIURI\fR referring +to a subscriber agreement or terms of service provided by the server; +adding this options indicates the client's agreement with the referenced +terms. Note that the server might require the client to agree to +subscriber agreement before performing any further actions. + +If the account key is already registered, \fBlacme\fR prints the URI of +the existing registration and aborts. + +.TP +.B lacme \fR[\fB\-\-agreement\-uri=\fIURI\fR]\fB \fBreg=\fIURI\fR \fR[\fICONTACT\fR ...] + +Dump or edit the registration \fIURI\fR (relative to the ACME server URI, +which is specified with the \fIserver\fR option of the \(lq[client]\(rq +section of the configuration file). + +When specified, the list of \fICONTACT\fR information and the agreement +\fIURI\fR are sent to the server to replace the existing values. + +.TP +.B lacme \fR[\fB\-\-config\-certs=\fIFILE\fR]\fB \fBnew\-cert \fR[\fISECTION\fR ...] + +Read the certificate configuration \fIFILE\fR (see the \fBCERTIFICATE +CONFIGURATION FILE\fR section below for the configuration options), and +request new Certificate Issuance for each of its sections (or the given +list of \fISECTION\fRs). + +.TP +.B lacme \fBrevoke\-cert \fIFILE\fR [\fIFILE\fR ...] + +Request that the given certificate(s) \fIFILE\fR(s) be revoked. For +this command, \fIlacme\-accountd\fR(1) can be pointed to either the +account key or the server's private key. + + +.SH GENERIC OPTIONS +.TP +.B \-\-config=\fIfilename\fR +Use \fIfilename\fR as configuration file. See the \fBCONFIGURATION +FILE\fR section below for the configuration options. + +.TP +.B \-\-socket=\fIpath\fR +Use \fIpath\fR as the \fIlacme\-accountd\fR(1) UNIX\-domain socket to +connect to for signature requests from the ACME client. \fBlacme\fR +aborts if \fIpath\fR is readable or writable by other users, or if its +parent directory is writable by other users. This overrides the +\fIsocket\fR option of the \(lq[client]\(rq section of the configuration +file. + +.TP +.B \-?\fR, \fB\-\-help\fR +Display a brief help and exit. + +.TP +.B \-\-debug +Turn on debug mode. + + +.SH CONFIGURATION FILE +If \fB\-\-config=\fR is not given, \fBlacme\fR uses the first existing +configuration file among \fI./lacme.conf\fR, +\fI$XDG_CONFIG_HOME/lacme/lacme.conf\fR (or +\fI~/.config/lacme/lacme.conf\fR if the XDG_CONFIG_HOME environment +variable is not set), and \fI/etc/lacme/lacme.conf\fR. +Valid options are: + +.TP +Default section +.RS +.TP +.I config\-certs +For certificate issuances (\fBnew\-cert\fR command), specify the +certificate configuration file to use (see the \fBCERTIFICATE +CONFIGURATION FILE\fR section below for the configuration options). +.RE + +.TP +\(lq[client]\(rq section +This section is used for configuring the ACME client (which takes care +of ACME commands and dialogues with the remote ACME server). + +.RS +.TP +.I socket +See \fB\-\-socket=\fR. +Default: \(lq$XDG_RUNTIME_DIR/S.lacme\(rq if the XDG_RUNTIME_DIR +environment variable is set. + +.TP +.I user +The username to drop privileges to (setting both effective and real +uid). +Preserve root privileges if the value is empty (not recommended). +Default: \(lqnobody\(rq. + +.TP +.I 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 (not +recommended). +Default: \(lqnogroup\(rq. + +.TP +.I command +Path to the ACME client executable. +Default: \(lq/usr/lib/lacme/client\(rq. + +.TP +.I server +Root URI of the ACME server. +Default: \(lqhttps://acme\-v01.api.letsencrypt.org/\(rq. + +.TP +.I timeout +Timeout in seconds after which the client stops polling the ACME server +and considers the request failed. +Default: \(lq10\(rq. + +.TP +.I SSL_verify +Whether to verify the server certificate chain. +Default: \(lqYes\(rq. + +.TP +.I SSL_version +Specify the version of the SSL protocol used to transmit data. + +.TP +.I SSL_cipher_list +Specify the cipher list for the connection. +.RE + +.TP +\(lq[webserver]\(rq section +This section is used for configuring the ACME webserver. + +.RS +.TP +.I listen +Specify the local address to listen on, in the form +\fIADDRESS\fR[:\fIPORT\fR]. +If \fIADDRESS\fR is enclosed with brackets \(oq[\(cq/\(oq]\(cq then it +denotes an IPv6; an empty \fIADDRESS\fR means \(oq0.0.0.0\(cq. +Default: \(lq:80\(rq. + +.TP +.I challenge\-directory +If a webserver is already running, specify a non\-existent directory +under which the webserver is configured to serve GET requests for +challenge files under \(lq/.well\-known/acme\-challenge/\(rq (for each +virtual hosts requiring authorization) as static files. +Default: \(lq/var/www/acme\-challenge\(rq. + +.TP +.I user +The username to drop privileges to (setting both effective and real +uid). +Preserve root privileges if the value is empty (not recommended). +Default: \(lqwww\-data\(rq. + +.TP +.I 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 (not +recommended). +Default: \(lqwww\-data\(rq. + +.TP +.I command +Path to the ACME webserver executable. +Default: \(lq/usr/lib/lacme/webserver\(rq. + +.TP +.I iptables +Whether to automatically install \fIiptables\fR(1) rules to open the +\fIADDRESS\fR[:\fIPORT\fR] specified with \fIlisten\fR. +Theses rules are automatically removed once \fBlacme\fR exits. +Default: \(lqYes\(rq. +.RE + + +.SH CERTIFICATE CONFIGURATION FILE +For certificate issuances (\fBnew\-cert\fR 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 \fB\-\-config\-certs=\fR is not given, and if the \fIconfig\-certs\fR +configuration option is absent, then \fBlacme\fR uses the first existing +configuration file among \fI./lacme\-certs.conf\fR, +\fI$XDG_CONFIG_HOME/lacme/lacme\-certs.conf\fR (or +\fI~/.config/lacme/lacme\-certs.conf\fR if the XDG_CONFIG_HOME +environment variable is not set), and +\fI/etc/lacme/lacme\-certs.conf\fR. +Each section denotes a separate certificate issuance. +Valid options are: + +.TP +.I certificate +Where to store the issued certificate (in PEM format). +At least one of \fIcertificate\fR or \fIcertificate\-chain\fR is +required. + +.TP +.I certificate\-chain +Where to store the issued certificate, concatenated with the content of +the file specified specified with the \fICAfile\fR option (in PEM +format). +At least one of \fIcertificate\fR or \fIcertificate\-chain\fR is +required. + +.TP +.I certificate\-key +Path the service's private key. This option is required. The following +command can be used to generate a new 4096\-bits RSA key in PEM format +with mode 0600: + +.nf + openssl genrsa 4096 | install -m0600 /dev/stdin /path/to/priv.key +.fi + +.TP +.I min\-days +For an existing certificate, the minimum number of days before its +expiration date the section is considered for re\-issuance. +Default: \(lq10\(rq. + + +.TP +.I CAfile +Path to the issuer's certificate. This is used for +\fIcertificate\-chain\fR and to verify the validity of each issued +certificate. +Specifying an empty value skip certificate validation. +Default: \(lq/usr/share/lacme/lets\-encrypt\-x3\-cross\-signed.pem\(rq. + +.TP +.I hash +Message digest to sign the Certificate Signing Request with. + +.TP +.I keyUsage +Comma\-separated list of Key Usages, see \fIx509v3_config\fR(5ssl). + +.TP +.I subject +Subject field of the Certificate Signing Request, in the form +\fR/\fItype0\fR=\fIvalue0\fR/\fItype1\fR=\fIvalue1\fR/\fItype2\fR=... +This option is required. + +.TP +.I subjectAltName +Comma\-separated list of Subject Alternative Names, in the form +\fItype0\fR:\fIvalue1\fR,\fItype1\fR:\fIvalue1\fR,\fItype2\fR:... +The only \fItype\fR currently supported is \(lqDNS\(rq, to specify an +alternative domain name. + +.TP +.I chown +An optional \fIusername\fR[:\fIgroupname\fR] to chown the issued +\fIcertificate\fR and \fIcertificate\-chain\fR with. + +.TP +.I chmod +An optional octal mode to chmod the issued \fIcertificate\fR and +\fIcertificate\-chain\fR with. + +.TP +.I notify +Command to pass the the system's command shell (\(lq/bin/sh \-c\(rq) +after successful installation of the \fIcertificate\fR and/or +\fIcertificate\-chain\fR. + + +.SH EXAMPLES + +.nf + ~$ 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 new-cert + ~$ sudo lacme revoke-cert /path/to/server/certificate.pem +.fi + + +.SH SEE ALSO +\fBlacme\-accountd\fR(1) + +.SH AUTHOR +.ie \n[www-html] \{\ + Written by +. MTO guilhem@fripost.org "Guilhem Moulin" . +\} +.el \{\ + Written by Guilhem Moulin +. MT guilhem@fripost.org +. ME . +\} diff --git a/letsencrypt b/letsencrypt deleted file mode 100755 index d11b569..0000000 --- a/letsencrypt +++ /dev/null @@ -1,704 +0,0 @@ -#!/usr/bin/perl -T - -#---------------------------------------------------------------------- -# Let's Encrypt ACME client -# Copyright © 2016 Guilhem Moulin -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -#---------------------------------------------------------------------- - -use strict; -use warnings; - -our $VERSION = '0.0.1'; -my $NAME = 'letsencrypt'; - -use Errno qw/EADDRINUSE EINTR/; -use Fcntl qw/F_GETFD F_SETFD FD_CLOEXEC SEEK_SET/; -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 - SOCK_STREAM SOL_SOCKET SO_REUSEADDR SHUT_RDWR/; - -use Config::Tiny (); -use Net::SSLeay (); - -# Clean up PATH -$ENV{PATH} = join ':', qw{/usr/bin /bin}; -delete @ENV{qw/IFS CDPATH ENV BASH_ENV/}; - -my ($COMMAND, %OPTS, $CONFFILE, $CONFIG, @CLEANUP); -$SIG{$_} = sub() { exit 1 } foreach qw/INT TERM/; # run the END block upon SIGINT/SIGTERM - - -############################################################################# -# Parse and validate configuration -# -sub usage(;$$) { - my $rv = shift // 0; - if ($rv) { - my $msg = shift; - print STDERR $msg."\n" if defined $msg; - print STDERR "Try '$NAME --help' or consult the manpage for more information.\n"; - } - else { - print STDERR "Usage: $NAME [--config=FILE] [--socket=PATH] [OPTIONS] COMMAND [ARGUMENT ..]\n" - ."Consult the manpage for more information.\n"; - } - exit $rv; -} -usage(1) unless GetOptions(\%OPTS, qw/config=s config-certs=s socket=s agreement-uri=s debug help|h/); -usage(0) if $OPTS{help}; - -$COMMAND = shift(@ARGV) // usage(1, "Missing command"); -$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 - -do { - my $conffile = $OPTS{config} // first { -f $_ } - ( "./$NAME.conf" - , ($ENV{XDG_CONFIG_HOME} // "$ENV{HOME}/.config")."/letsencrypt-tiny/$NAME.conf" - , "/etc/letsencrypt-tiny/$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"; - my $conf = do { local $/ = undef, <$CONFFILE> }; - # don't close $CONFFILE so we can pass it to the client - - my $h = Config::Tiny::->read_string($conf) or die Config::Tiny::->errstr()."\n"; - my $defaults = delete $h->{_} // {}; - my %valid = ( - client => { - socket => (defined $ENV{XDG_RUNTIME_DIR} ? "$ENV{XDG_RUNTIME_DIR}/S.letsencrypt" : undef), - user => 'nobody', - group => 'nogroup', - command => '/usr/lib/letsencrypt-tiny/client', - # the rest is for the ACME client - map {$_ => undef} qw/server timeout SSL_verify SSL_version SSL_cipher_list/ - }, - webserver => { - listen => ':80', - 'challenge-directory' => '/var/www/acme-challenge', - user => 'www-data', - group => 'www-data', - command => '/usr/lib/letsencrypt-tiny/webserver', - iptables => 'Yes' - - } - ); - foreach my $s (keys %valid) { - my $h = delete $h->{$s} // {}; - my %v = map { $_ => delete $h->{$_} // $valid{$s}->{$_} } keys %{$valid{$s}}; - die "Unknown option(s) in [$s]: ".join(', ', keys %$h)."\n" if %$h; - $h->{$_} //= $defaults->{$_} foreach keys %$defaults; - $CONFIG->{$s} = \%v; - } - die "Invalid section(s): ".join(', ', keys %$h)."\n" if %$h; - $CONFIG->{_} = $defaults; -}; - -# Regular expressions for domain validation -my $RE_LABEL = qr/[0-9a-z](?:[0-9a-z\x2D]{0,61}[0-9a-z])?/aai; -my $RE_DOMAIN = qr/$RE_LABEL(?:\.$RE_LABEL)+/; - - -############################################################################# -# Generate a Certificate Signing Request (in DER format) -# -sub gen_csr(%) { - my %args = @_; - return unless defined $args{'certificate-key'} and defined $args{subject}; - return if defined $args{hash} and !grep { $args{hash} eq $_ } qw/md5 rmd160 sha1 sha224 sha256 sha384 sha512/; - - my $config = File::Temp::->new(SUFFIX => '.conf', TMPDIR => 1) // die; - $config->print( - "[ req ]\n", - "distinguished_name = req_distinguished_name\n", - "req_extensions = v3_req\n", - - "[ req_distinguished_name ]\n", - - "[ v3_req ]\n", - # XXX Golang errors on extensions marked critical - # https://github.com/letsencrypt/boulder/issues/565 - #"basicConstraints = critical, CA:FALSE\n", - "basicConstraints = CA:FALSE\n", - "subjectKeyIdentifier = hash\n" - ); - #$config->print("keyUsage = critical, $args{keyUsage}\n") if defined $args{keyUsage}; - $config->print("keyUsage = $args{keyUsage}\n") if defined $args{keyUsage}; - $config->print("subjectAltName = $args{subjectAltName}\n") if defined $args{subjectAltName}; - $config->close() or die "Can't close: $!"; - - my @args = (qw/-new -batch -key/, $args{'certificate-key'}); - push @args, "-$args{hash}" if defined $args{hash}; - push @args, '-subj', $args{subject}, '-config', $config->filename(), qw/-reqexts v3_req/; - - open my $fh, '-|', qw/openssl req -outform DER/, @args or die "fork: $!"; - my $csr = do { local $/ = undef; <$fh> }; - close $fh or $! ? die "Can't close: $!" : return; - - if ($OPTS{debug}) { - # print out the CSR in text form - pipe my $rd, my $wd or die "pipe: $!"; - my $pid = fork // die "fork: $!"; - unless ($pid) { - open STDIN, '<&', $rd or die "Can't dup: $!"; - open STDOUT, '>&', \*STDERR or die "Can't dup: $!"; - exec qw/openssl req -noout -text -inform DER/ or die; - } - $rd->close() or die "Can't close: $!"; - $wd->print($csr); - $wd->close() or die "Can't close: $!"; - - waitpid $pid => 0; - die $? if $? > 0; - } - - return $csr; -} - - -############################################################################# -# Get the timestamp of the given cert's expiration date. -# Internally the expiration date is stored as a RFC3339 string (such as -# yyyy-mm-ddThh:mm:ssZ); we convert it to a timestamp manually. -# -sub x509_enddate($) { - my $filename = shift; - my ($bio, $x509, $time, $dt); - - $bio = Net::SSLeay::BIO_new_file($filename, 'r'); - $x509 = Net::SSLeay::PEM_read_bio_X509($bio) if defined $bio; - $time = Net::SSLeay::X509_get_notAfter($x509) if defined $x509; - $dt = Net::SSLeay::P_ASN1_TIME_get_isotime($time) if defined $time; - - my $t; - if (defined $dt and $dt =~ s/\A(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})//) { - # RFC3339 datetime strings; assume epoch is on January 1 of $epoch_year - my ($y, $m, $d, $h, $min, $s) = ($1, $2, $3, $4, $5, $6); - my (undef,undef,undef,undef,undef,$epoch_year,undef,undef,undef) = gmtime(0); - $t = 0; - foreach (($epoch_year+1900) .. $y-1) { - $t += 365*86400; - $t += 86400 if ($_ % 4 == 0 and $_ % 100 != 0) or ($_ % 400 == 0); # leap - } - - if ($m > 1) { - my @m = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31); - $m[1]++ if ($y % 4 == 0 and $y % 100 != 0) or ($y % 400 == 0); # leap - $t += 86400*$m[$_] for (0 .. $m-2); - } - - $t += 86400*($d-1); - $t += $s + 60*($min + 60*$h); - - $dt =~ s/\A\.(\d{1,9})\d*//; # ignore nanosecs - - if ($dt =~ /\A([+-])(\d{2}):(\d{2})\z/) { - my $tz = 60*($3 + 60*$2); - $t = $1 eq '-' ? ($t+$tz) : ($t-$tz); - } - } - - Net::SSLeay::X509_free($x509) if defined $x509; - Net::SSLeay::BIO_free($bio) if defined $bio; - return $t; -} - - -############################################################################# -# Drop privileges and chdir afterwards -# -sub drop_privileges($$$) { - my ($user, $group, $dir) = @_; - - # set effective and real gid; also set the list of supplementary gids to that single gid - if ($group ne '') { - my $gid = getgrnam($group) // die "Can't getgrnam($group): $!"; - $) = "$gid $gid"; - die "Can't setgroups: $!" if $@; - POSIX::setgid($gid) or die "Can't setgid: $!"; - die "Couldn't setgid/setguid" unless $( eq "$gid $gid" and $) eq "$gid $gid"; # safety check - } - - # set effective and real uid - if ($user ne '') { - my $uid = getpwnam($user) // die "Can't getpwnam($user): $!"; - POSIX::setuid($uid) or die "Can't setuid: $!"; - die "Couldn't setuid/seteuid" unless $< == $uid and $> == $uid; # safety check - } - - chdir $dir or die "Can't chdir to $dir: $!"; -} - - -############################################################################# -# Ensure the FD_CLOEXEC bit is $set on $fd -# -sub set_FD_CLOEXEC($$) { - my ($fd, $set) = @_; - my $flags = fcntl($fd, F_GETFD, 0) or die "Can't fcntl F_GETFD: $!"; - my $flags2 = $set ? ($flags | FD_CLOEXEC) : ($flags & ~FD_CLOEXEC); - return if $flags == $flags2; - fcntl($fd, F_SETFD, $flags2) or die "Can't fcntl F_SETFD: $!"; -} - - -############################################################################# -# Try to spawn a webserver to serve ACME challenges, and return the -# temporary challenge directory. -# If a webserver is already listening, symlink the 'challenge-directory' -# configuration option to the temporary challenge directory. -# Otherwise, bind(2) a socket, pass its fileno to the webserver -# component, and optionally install iptables rules. -# -sub spawn_webserver() { - # create a temporary directory; give write access to the ACME client - # and read access to the webserver - my $tmpdir = File::Temp::->newdir(CLEANUP => 1, TMPDIR => 1) // die; - chmod 0755, $tmpdir or die "Can't chmod: $!"; - if ((my $username = $CONFIG->{client}->{user}) ne '') { - my $uid = getpwnam($username) // die "Can't getgrnam($username): $!"; - chown($uid, -1, $tmpdir) or die "Can't chown: $!"; - } - - my $conf = $CONFIG->{webserver}; - my ($fam, $addr, $port) = (PF_INET, $conf->{listen}, 80); - $port = $1 if $addr =~ s/:(\d+)$//; - $addr = Socket::inet_ntop(PF_INET, INADDR_ANY) if $addr eq ''; - $fam = PF_INET6 if $addr =~ s/^\[(.+)\]$/$1/; - - my $proto = getprotobyname("tcp") // die; - socket(my $srv, $fam, SOCK_STREAM, $proto) or die "socket: $!"; - setsockopt($srv, SOL_SOCKET, SO_REUSEADDR, pack("l", 1)) or die "setsockopt: $!"; - $addr = Socket::inet_pton($fam, $addr) // die "Invalid address $conf->{listen}\n"; - my $sockaddr = $fam == PF_INET ? Socket::pack_sockaddr_in($port, $addr) - : $fam == PF_INET6 ? Socket::pack_sockaddr_in6($port, $addr) - : die; - - # try to bind aginst the specified address:port - bind($srv, $sockaddr) or do { - die "Can't bind to $conf->{listen}: $!" if $! != EADDRINUSE; - print STDERR "[$$] Using existing webserver on $conf->{listen}\n" if $OPTS{debug}; - my $dir = $conf->{'challenge-directory'}; - symlink $tmpdir, $dir or die "Can't symlink $dir -> $tmpdir: $!"; - push @CLEANUP, sub() { - print STDERR "Unlinking $dir\n" if $OPTS{debug}; - unlink $dir or warn "Warning: Can't unlink $dir: $!"; - }; - return $tmpdir; - }; - listen($srv, 5) or die "listen: $!"; - - # spawn the webserver component - my $pid = fork() // "fork: $!"; - unless ($pid) { - drop_privileges($conf->{user}, $conf->{group}, $tmpdir); - set_FD_CLOEXEC($srv, 0); - $ENV{DEBUG} = $OPTS{debug}; - # use execve(2) rather than a Perl pseudo-process to ensure that - # the child doesn't have access to the parent's memory - exec $conf->{command}, fileno($srv) or die; - } - - print STDERR "[$$] Forking ACME webserver, child PID $pid\n" if $OPTS{debug}; - set_FD_CLOEXEC($srv, 1); - push @CLEANUP, sub() { - print STDERR "[$$] Shutting down ACME webserver\n" if $OPTS{debug}; - shutdown($srv, SHUT_RDWR) or warn "shutdown: $!"; - kill 15 => $pid; - waitpid $pid => 0; - }; - - return $tmpdir if lc ($conf->{iptables} // 'Yes') eq 'no'; - - # install iptables - my $iptables_bin = $fam == PF_INET ? 'iptables' : $fam == PF_INET6 ? 'ip6tables' : die; - my $iptables_tmp = File::Temp::->new(TMPDIR => 1) // die; - set_FD_CLOEXEC($iptables_tmp, 1); - - my $pid2 = fork() // die "fork: $!"; - unless ($pid2) { - open STDIN, '<', '/dev/null' or die "Can't open /dev/null: $!"; - open STDOUT, '>&', $iptables_tmp or die "Can't dup: $!"; - $| = 1; # turn off buffering for STDOUT - exec "/sbin/$iptables_bin-save", "-c" or die; - } - waitpid $pid2 => 0; - die "Error: /sbin/$iptables_bin-save exited with value ".($? >> 8) if $? > 0; - - # seek back to the begining, as we'll restore directly from the - # handle and not from the file. XXX if there was a way in Perl to - # use open(2) with the O_TMPFILE flag we would use that to avoid - # creating a file to start with - seek($iptables_tmp, SEEK_SET, 0) or die "Can't seek: $!"; - - push @CLEANUP, sub() { - print STDERR "[$$] Restoring iptables\n" if $OPTS{debug}; - my $pid2 = fork() // die "fork: $!"; - unless ($pid2) { - open STDIN, '<&', $iptables_tmp or die "Can't dup: $!"; - open STDOUT, '>', '/dev/null' or die "Can't open /dev/null: $!"; - exec "/sbin/$iptables_bin-restore", "-c" or die; - } - waitpid $pid2 => 0; - warn "Warning: /sbin/$iptables_bin-restore exited with value ".($? >> 8) if $? > 0; - }; - - # it's safe to install the new iptables to open $port now that the - # restore hook is in place - my $mask = $fam == PF_INET ? ($addr eq INADDR_ANY ? '0' : '32') - : $fam == PF_INET6 ? ($addr eq IN6ADDR_ANY ? '0' : '128') - : die; - my $dest = Socket::inet_ntop($fam, $addr) .'/'. $mask; - - system ("/sbin/$iptables_bin", qw/-I INPUT -p tcp -m tcp -m state/, - '-d', $dest, '--dport', $port, - '--state', 'NEW,ESTABLISHED', '-j', 'ACCEPT') == 0 or die; - system ("/sbin/$iptables_bin", qw/-I OUTPUT -p tcp -m tcp -m state/, - '-s', $dest, '--sport', $port, - '--state', 'ESTABLISHED', '-j', 'ACCEPT') == 0 or die; - - return $tmpdir; -} - - -############################################################################# -# Spawn the client component, and wait for it to return. -# 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. -# -sub acme_client($@) { - my $args = shift; - my @args = @_; - - my @stat; - 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 letsencrypt-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: $!"; - } - - # 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 - 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); -} - -sub spawn($@) { - my $args = shift; - my @exec = @_; - - # create communication pipes if needed - my ($in_rd, $in_wd, $out_rd, $out_wd); - if (defined $args->{in}) { - pipe $in_rd, $in_wd or die "pipe: $!"; - } - if (defined $args->{out}) { - pipe $out_rd, $out_wd or die "pipe: $!"; - } - - my $pid = fork() // "fork: $!"; - unless ($pid) { - # child - $args->{child}->() if defined $args->{child}; - if (defined $args->{in}) { - close $in_wd or die "Can't close: $!"; - open STDIN, '<&', $in_rd or die "Can't dup: $!"; - } else { - open STDIN, '<', '/dev/null' or die "Can't open /dev/null: $!"; - } - if (defined $args->{out}) { - close $out_rd or die "Can't close: $!"; - open STDOUT, '>&', $out_wd or die "Can't dup: $!"; - } else { - open STDOUT, '>', '/dev/null' or die "Can't open /dev/null: $!"; - } - exec { $exec[0] } @exec or die; - } - push @CLEANUP, sub() { - kill 15 => $pid; - waitpid $pid => 0; - }; - - # parent - print STDERR "[$$] Forking $exec[0], child PID $pid\n" if $OPTS{debug}; - if (defined $args->{in}) { - $in_rd->close() or die "Can't close: $!"; - $in_wd->print($args->{in}); - $in_wd->close() or die "Can't close: $!"; - } - if (defined $args->{out}) { - $out_wd->close() or die "Can't close: $!"; - ${$args->{out}} = do { local $/ = undef; $out_rd->getline() }; - $out_rd->close() or die "Can't close: $!"; - } - waitpid $pid => 0; - pop @CLEANUP; - undef ${$args->{out}} if defined $args->{out} and $? > 0; - return $? > 255 ? ($? >> 8) : $? > 0 ? 1 : 0; -} - - -############################################################################# -# Install the certificate -# -sub install_cert($$@) { - my $filename = shift; - my $x509 = shift; - - open my $fh, '>', $filename or die "Can't open $filename: $!"; - print $fh $x509; - foreach (@_) { # append the chain - open my $fh2, '<', $_ or die "Can't open $_: $!"; - my $ca = do { local $/ = undef; $fh2->getline() }; - print $fh $ca; - close $fh2 or die "Can't close: $!"; - } - close $fh or die "Can't close: $!"; -} - - -############################################################################# -# new-reg [--agreement-uri=URI] [CONTACT ..] -# reg=URI [--agreement-uri=URI] [CONTACT ..] -# -if ($COMMAND eq 'new-reg' or $COMMAND =~ /^reg=/) { - die "Invalid registration URI (use the 'new-reg' command to determine the URI)\n" - if $COMMAND eq 'reg='; - $OPTS{'agreement-uri'} = $OPTS{'agreement-uri'} =~ /\A(\p{Print}+)\z/ ? $1 - : die "Invalid value for --agreement-uri\n" - if defined $OPTS{'agreement-uri'}; - - unshift @ARGV, ($OPTS{'agreement-uri'} // ''); - exit acme_client({}, @ARGV); -} - - -############################################################################# -# new-cert [SECTION ..] -# TODO: renewal without the account key, see -# https://github.com/letsencrypt/acme-spec/pull/168 -# -elsif ($COMMAND eq 'new-cert') { - my $conf; - do { - my $conffile = $OPTS{'config-certs'} // $CONFIG->{_}->{'config-certs'} // first { -f $_ } - ( "./$NAME-certs.conf" - , ($ENV{XDG_CONFIG_HOME} // "$ENV{HOME}/.config")."/letsencrypt-tiny/$NAME-certs.conf" - , "/etc/letsencrypt-tiny/$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; - } - }; - - my $challenge_dir; - my $rv = 0; - foreach my $s (@ARGV ? @ARGV : keys %$conf) { - my $conf = $conf->{$s} // do { - print STDERR "Warning: No such section $s, skipping\n"; - $rv = 1; - next; - }; - - my $certtype = first { defined $conf->{$_} } qw/certificate certificate-chain/; - unless (defined $certtype) { - print STDERR "[$s] Warning: Missing 'certificate' and 'certificate-chain', skipping\n"; - $rv = 1; - next; - } - - # skip certificates that expire at least $conf->{'min-days'} days in the future - if (-f $conf->{$certtype} and defined (my $t = x509_enddate($conf->{$certtype}))) { - 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"; - next; - } - } - - # generate the CSR - my $csr = gen_csr(%$conf{qw/certificate-key subject subjectAltName keyUsage hash/}) // do { - print STDERR "[$s] Warning: Couldn't generate CSR, skipping\n"; - $rv = 1; - next; - }; - - # spawn the webserver if not done already - $challenge_dir //= spawn_webserver(); - - # list all authorization domains to request - my @authz; - push @authz, $1 if defined $conf->{subject} =~ /\A.*\/CN=($RE_DOMAIN)\z/o; - if (defined $conf->{subjectAltName}) { - foreach my $d (split /,/, $conf->{subjectAltName}) { - next unless $d =~ s/\A\s*DNS://; - if ($d =~ /\A$RE_DOMAIN\z/o) { - push @authz, $d unless grep {$_ eq $d} @authz; - } else { - print STDERR "[$s] Warning: Ignoring invalid domain $d\n"; - } - } - } - - my ($x509, $csr_pubkey, $x509_pubkey); - print STDERR "[$s] Will request authorization for: ".join(", ", @authz), "\n" if $OPTS{debug}; - if (acme_client({chdir => $challenge_dir, in => $csr, out => \$x509}, @authz)) { - print STDERR "[$s] Error: Couldn't issue X.509 certificate!\n"; - $rv = 1; - next; - } - - # extract pubkeys from CSR and cert, and ensure they match - spawn({in => $csr, out => \$csr_pubkey }, qw/openssl req -inform DER -noout -pubkey/); - spawn({in => $x509, out => \$x509_pubkey}, qw/openssl x509 -inform PEM -noout -pubkey/); - unless (defined $x509_pubkey and defined $csr_pubkey and $x509_pubkey eq $csr_pubkey) { - print STDERR "[$s] Error: Received bogus X.509 certificate from ACME server!\n"; - $rv = 1; - next; - }; - - # verify certificate validity against the CA - $conf->{CAfile} //= '/usr/share/letsencrypt-tiny/lets-encrypt-x3-cross-signed.pem'; - if ($conf->{CAfile} ne '' and spawn({in => $x509}, 'openssl', 'verify', '-CAfile', $conf->{CAfile}, - qw/-purpose sslserver -x509_strict/)) { - print STDERR "[$s] Error: Received invalid X.509 certificate from ACME server!\n"; - $rv = 1; - next; - } - - # install certificate - if (defined $conf->{'certificate'}) { - print STDERR "Installing X.509 certificate $conf->{'certificate'}\n"; - install_cert($conf->{'certificate'}, $x509); - } - if (defined $conf->{'certificate-chain'}) { - print STDERR "Installing X.509 certificate chain $conf->{'certificate-chain'}\n"; - install_cert($conf->{'certificate-chain'}, $x509, $conf->{CAfile}); - } - - if (defined $conf->{chown}) { - my ($user, $group) = split /:/, $conf->{chown}, 2; - my $uid = getpwnam($user) // die "Can't getpwnam($user): $!"; - my $gid = defined $group ? (getgrnam($group) // die "Can't getgrnam($group): $!") : -1; - foreach (grep defined, @$conf{qw/certificate certificate-chain/}) { - chown($uid, $gid, $_) or die "Can't chown: $!"; - } - } - if (defined $conf->{chmod}) { - my $mode = oct($conf->{chmod}) // die; - foreach (grep defined, @$conf{qw/certificate certificate-chain/}) { - chmod($mode, $_) or die "Can't chown: $!"; - } - } - - my @certopts = join ',', qw/no_header no_version no_pubkey no_sigdump/; - open my $fh, '|-', qw/openssl x509 -noout -fingerprint -sha256 -text -certopt/, @certopts - or die "fork: $!"; - print $fh $x509; - close $fh or die $! ? - "Can't close: $!" : - "Error: x509(1ssl) exited with value ".($? >> 8)."\n"; - - if (defined $conf->{notify}) { - print STDERR "Running notification command `$conf->{notify}`\n"; - if (system($conf->{notify}) != 0) { - print STDERR "Warning: notification command exited with value ".($? >> 8)."\n"; - $rv = 1; - } - } - } - undef $challenge_dir; - exit $rv; -} - - -############################################################################# -# revoke-cert FILE [FILE ..] -# -elsif ($COMMAND eq 'revoke-cert') { - die "Nothing to revoke\n" unless @ARGV; - my $rv = 0; - foreach my $filename (@ARGV) { - print STDERR "Revoking $filename\n"; - - # conversion PEM -> DER - open my $fh, '-|', qw/openssl x509 -outform DER -in/, $filename or die "fork: $!"; - my $der = do { local $/ = undef; <$fh> }; - close $fh or die $! ? - "Can't close: $!" : - "Error: x509(1ssl) exited with value ".($? >> 8)."\n"; - - my @certopts = join ',', qw/no_header no_version no_pubkey no_sigdump no_extensions/; - open my $fh2, '|-', qw/openssl x509 -inform DER -noout -fingerprint -sha256 -text -certopt/, @certopts - or die "fork: $!"; - print $fh2 $der; - close $fh2 or die $! ? - "Can't close: $!" : - "Error: x509(1ssl) exited with value ".($? >> 8)."\n"; - - if (acme_client({in => $der})) { - print STDERR "Warning: Couldn't revoke $filename\n"; - $rv = 1; - } - } - exit $rv; -} - - -############################################################################# -# -else { - die "Unknown command $COMMAND" -} - - -END { - local $?; - $_->() foreach reverse @CLEANUP; -} diff --git a/letsencrypt-accountd b/letsencrypt-accountd deleted file mode 100755 index ffc5619..0000000 --- a/letsencrypt-accountd +++ /dev/null @@ -1,202 +0,0 @@ -#!/usr/bin/perl -T - -#---------------------------------------------------------------------- -# Let's Encrypt ACME client (account key manager) -# Copyright © 2016 Guilhem Moulin -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -#---------------------------------------------------------------------- - -use strict; -use warnings; - -our $VERSION = '0.0.1'; -my $PROTOCOL_VERSION = 1; -my $NAME = 'letsencrypt-accountd'; - -use Errno 'EINTR'; -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/; - -use Config::Tiny (); -use JSON (); - -# Clean up PATH -$ENV{PATH} = join ':', qw{/usr/bin /bin}; -delete @ENV{qw/IFS CDPATH ENV BASH_ENV/}; - -my ($SOCKNAME, $S, %OPTS); -$SIG{$_} = sub() { exit } foreach qw/INT TERM/; # run the END block upon SIGINT/SIGTERM - - -############################################################################# -# Parse and validate configuration -# -sub usage(;$$) { - my $rv = shift // 0; - if ($rv) { - my $msg = shift; - print STDERR $msg."\n" if defined $msg; - print STDERR "Try '$NAME --help' or consult the manpage for more information.\n"; - } - else { - print STDERR "Usage: $NAME [--config=FILE] [--privkey=ARG] [--socket=PATH] [--quiet]\n" - ."Consult the manpage for more information.\n"; - } - exit $rv; -} -usage(1) unless GetOptions(\%OPTS, qw/config=s privkey=s socket=s quiet|q debug help|h/); -usage(0) if $OPTS{help}; - -do { - my $conffile = $OPTS{config} // first { -f $_ } - ( "./$NAME.conf" - , ($ENV{XDG_CONFIG_HOME} // "$ENV{HOME}/.config")."/letsencrypt-tiny/$NAME.conf" - , "/etc/letsencrypt-tiny/$NAME.conf" - ); - die "Error: Can't find configuration file\n" unless defined $conffile; - print STDERR "Using configuration file: $conffile\n" if $OPTS{debug}; - - my $h = Config::Tiny::->read($conffile) or die Config::Tiny::->errstr()."\n"; - my $h2 = delete $h->{_} // {}; - die "Invalid section(s): ".join(', ', keys %$h)."\n" if %$h; - my %h = map { $_ => delete $h2->{$_} } qw/privkey gpg socket quiet/; - die "Unknown option(s): ".join(', ', keys %$h2)."\n" if %$h2; - $h{quiet} = lc $h{quiet} eq 'yes' ? 1 : 0 if defined $h{quiet}; - $OPTS{$_} //= $h{$_} foreach grep {defined $h{$_}} keys %h; - - $OPTS{quiet} = 0 if $OPTS{debug}; - die "Error: 'privkey' is not specified\n" unless defined $OPTS{privkey}; -}; - - -############################################################################# -# Build the JSON Web Key (RFC 7517) from the account key's public parameters, -# and determine the signing method $SIGN. -# -my ($JWK, $SIGN); -if ($OPTS{privkey} =~ /\A(file|gpg):(\p{Print}+)\z/) { - my ($method, $filename) = ($1,$2); - my ($fh, @command); - if ($method eq 'file') { - # generate with `openssl genrsa 4096 | install --mode=0600 /dev/stdin /tmp/privkey` - open $fh, '<', $filename or die "Error: Can't open $filename: $!\n"; - } - elsif ($method eq 'gpg') { - @command = split /\s+/, ($OPTS{gpg} // 'gpg --quiet'); - open $fh, '-|', @command, qw/-o - --decrypt --/, $filename or die "fork: $!"; - } - else { - die; # impossible - } - - my $str = do {local $/ = undef; <$fh>}; - close $fh or die $! ? - "Can't close: $!" : - "Error: $command[0] exited with value ".($? >> 8)."\n"; - - require 'Crypt/OpenSSL/RSA.pm'; - my $rsa = Crypt::OpenSSL::RSA->new_private_key($str); - undef $str; - - die "Error: $filename: Not a private key\n" unless $rsa->is_private(); - die "Error: $filename: Invalid key\n" unless $rsa->check_key(); - $rsa->use_sha256_hash(); - - require 'Crypt/OpenSSL/Bignum.pm'; - my ($n, $e) = $rsa->get_key_parameters(); # don't include private params! - $_ = encode_base64url($_->to_bin()) foreach ($n, $e); - - $JWK = { kty => 'RSA', n => $n, e => $e }; - $SIGN = sub($) { $rsa->sign($_[0]) }; -} -else { - die "Error: unsupported method: $OPTS{privkey}\n"; -} -$JWK = JSON::->new->encode($JWK); - - -############################################################################# -# Create the server UNIX socket and bind(2) against it. -# NOTE: We don't use the abstract namespace so we can rely on the file -# permissions to keep other users out. (Also, OpenSSH 7.1 doesn't seem -# to support the abstract namespace.) The downside is that we have to -# delete the file manually. -# -do { - my $sockname = $OPTS{socket} // (defined $ENV{XDG_RUNTIME_DIR} ? "$ENV{XDG_RUNTIME_DIR}/S.letsencrypt" : undef); - die "Missing socket option\n" unless defined $sockname; - $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; - my @stat = stat($dirname) or die "Can't stat $dirname: $!"; - die "Error: insecure permissions on $dirname\n" if ($stat[2] & 0022) != 0; - - my $umask = umask(0177) // die "umask: $!"; - - print STDERR "Starting Let's Encrypt Account Key Manager at $sockname\n" unless $OPTS{quiet}; - socket(my $sock, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!"; - my $sockaddr = Socket::sockaddr_un($sockname) // die; - bind($sock, $sockaddr) or die "bind: $!"; - - ($SOCKNAME, $S) = ($sockname, $sock); - listen($S, 1) or die "listen: $!"; - - umask($umask) // die "umask: $!"; -}; - - -############################################################################# -# 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}; - - $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}; - 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: $!"; -} - - -############################################################################# -# -END { - if (defined $SOCKNAME and -S $SOCKNAME) { - print STDERR "Unlinking $SOCKNAME\n" if $OPTS{debug}; - unlink $SOCKNAME or print STDERR "Can't unlink $SOCKNAME: $!\n"; - } - if (defined $S) { - print STDERR "Shutting down and closing Let's Encrypt Account Key Manager\n" unless $OPTS{quiet}; - shutdown($S, SHUT_RDWR) or warn "shutdown: $!"; - close $S or print STDERR "Can't close: $!\n"; - } -} diff --git a/letsencrypt-accountd.1 b/letsencrypt-accountd.1 deleted file mode 100644 index a06cdcc..0000000 --- a/letsencrypt-accountd.1 +++ /dev/null @@ -1,153 +0,0 @@ -.TH LETSENCRYPT\-ACCOUNTD "1" "MARCH 2016" "Tiny Let's Encrypt ACME client (account key manager)" "User Commands" - -.SH NAME -letsencrypt\-accountd \- Tiny Let's Encrypt ACME client (account key manager) - -.SH SYNOPSIS -.B letsencrypt\-accountd\fR [\fB\-\-config=\fIFILENAME\fR] -[\fB\-\-privkey=\fIARG\fR] [\fB\-\-socket=\fIPATH\fR] [\fB\-\-quiet\fR] - - -.SH DESCRIPTION -.PP -.B letsencrypt\-accountd\fR is the account key manager component of -\fIletsencrypt\fR(1), a tiny ACME client written with process isolation -and minimal privileges in mind. No other \fIletsencrypt\fR(1) component -need access to the account key; in fact the account key could also be -stored on a smartcard. - -.B letsencrypt\-accountd\fR binds to a UNIX\-domain socket (specified -with \fB\-\-socket=\fR), which ACME clients can connect to in order to -request data signatures. -As a consequence, \fBletsencrypt\-accountd\fR needs to be up and running -before using \fIletsencrypt\fR(1) to issue ACME commands. -Also, the process does not automatically terminate after the last -signature request: instead, one sends an \fIINT\fR or \fITERM\fR signal -to bring the server down. - -Furthermore, one can use the UNIX\-domain socket forwarding facility of -OpenSSH 6.7 and later to run \fBletsencrypt\-accountd\fR and -\fIletsencrypt\fR(1) on different hosts. For instance one could store -the account key on a machine that is not exposed to the internet. See -the \fBEXAMPLES\fR section below. - - -.SH OPTIONS -.TP -.B \-\-config=\fIfilename\fR -Use \fIfilename\fR as configuration file. See the \fBCONFIGURATION -FILE\fR section below for the configuration options. - -.TP -.B \-\-privkey=\fIarg\fR -Specify the (private) account key to use for signing requests. -Currently supported \fIarg\fRuments are: - -.RS -.IP \[bu] 2 -file:\fIFILE\fR, to specify an encrypted private key (in PEM format); and -.IP \[bu] -gpg:\fIFILE\fR, to specify a \fIgpg\fR(1)\-encrypted private key (in PEM format). - -.PP -The following command can be used to generate a new 4096\-bits RSA key in -PEM format with mode 0600: - -.nf - openssl genrsa 4096 | install -m0600 /dev/stdin /path/to/priv.key -.fi -.RE - -.TP -.B \-\-socket=\fIpath\fR -Use \fIpath\fR as the UNIX\-domain socket to bind against for signature -requests from the ACME client. \fBletsencrypt\-accountd\fR aborts if -\fIpath\fR exists or if its parent directory is writable by other users. - -.TP -.B \-?\fR, \fB\-\-help\fR -Display a brief help and exit. - -.TP -.B \-q\fR, \fB\-\-quiet\fR -Be quiet. - -.TP -.B \-\-debug -Turn on debug mode. - - -.SH CONFIGURATION FILE -If \fB\-\-config=\fR is not given, \fBletsencrypt\-accountd\fR uses the -first existing configuration file among -\fI./letsencrypt\-accountd.conf\fR, -\fI$XDG_CONFIG_HOME/letsencrypt\-tiny/letsencrypt\-accountd.conf\fR (or -\fI~/.config/letsencrypt\-tiny/letsencrypt\-accountd.conf\fR if the -XDG_CONFIG_HOME environment variable is not set), and -\fI/etc/letsencrypt\-tiny/letsencrypt\-accountd.conf\fR. - -When given on the command line, the \fB\-\-privkey=\fR, -\fB\-\-socket=\fR and \fB\-\-quiet\fR options take precedence over their -counterpart (without leading \(lq\-\-\(rq) in the configuration file. -Valid options are: - -.TP -.I privkey -See \fB\-\-privkey=\fR. -This option is required when \fB\-\-privkey=\fR is not specified on the -command line. - -.TP -.I gpg -For a \fIgpg\fR(1)\-encrypted private account key, specify the binary -\fIgpg\fR(1) to use, as well as some default options. -Default: \(lqgpg \-\-quiet\(rq. - -.TP -.I socket -See \fB\-\-socket=\fR. -Default: \(lq$XDG_RUNTIME_DIR/S.letsencrypt\(rq if the XDG_RUNTIME_DIR -environment variable is set. - -.TP -.I quiet -Be quiet. Possible values: \(lqYes\(rq/\(lqNo\(rq. - - -.SH EXAMPLES - -Run \fBletsencrypt\-accountd\fR in a first terminal: - -.nf - ~$ letsencrypt\-accountd \-\-privkey=file:/path/to/priv.key \-\-socket=/run/user/1000/S.letsencrypt -.fi - -Then, while \fBletsencrypt\-accountd\fR is running, execute locally -\fIletsencrypt\fR(1) in another terminal: - -.nf - ~$ sudo letsencrypt \-\-socket=/run/user/1000/S.letsencrypt new\-cert -.fi - -Alternatively, use \fIssh\fR(1) to forward the socket and execute -\fIletsencrypt\fR(1) remotely: - -.nf - ~$ ssh -oExitOnForwardFailure=yes -tt -R /path/to/remote.sock:/run/user/1000/S.letsencrypt user@example.org \\ - sudo letsencrypt --socket=/path/to/remote.sock new-cert -.fi - - -.SH SEE ALSO -\fBletsencrypt\fR(1), \fBssh\fR(1) - -.SH AUTHOR -.ie \n[www-html] \{\ - Written by -. MTO guilhem@fripost.org "Guilhem Moulin" . -\} -.el \{\ - Written by Guilhem Moulin -. MT guilhem@fripost.org -. ME . -\} diff --git a/letsencrypt.1 b/letsencrypt.1 deleted file mode 100644 index 1c4b0db..0000000 --- a/letsencrypt.1 +++ /dev/null @@ -1,370 +0,0 @@ -.TH LETSENCRYPT "1" "MARCH 2016" "Tiny Let's Encrypt ACME client" "User Commands" - -.SH NAME -letsencrypt \- Tiny Let's Encrypt ACME client - -.SH SYNOPSIS -.B letsencrypt\fR [\fB\-\-config=\fIFILENAME\fR] -[\fB\-\-socket=\fIPATH\fR] [\fIOPTION\fR ...] \fICOMMAND\fR -[\fIARGUMENT\fR ...] - - -.SH DESCRIPTION -.PP -.B letsencrypt\fR is a tiny ACME client written with process isolation -and minimal privileges in mind. -It is divided into four components, each with its own executable: - -.IP \[bu] 4 -A \fIletsencrypt\-accountd\fR(1) process to manage the account key and -issue SHA\-256 signatures needed for each ACME command. -(This process binds to a UNIX\-domain socket to reply to signature -requests from the ACME client.) -One can use the UNIX\-domain socket forwarding facility of OpenSSH 6.7 -and later to run \fIletsencrypt\-accountd\fR(1) and \fBletsencrypt\fR on -different hosts. - -.IP \[bu] 4 -A \(lqmaster\(rq \fBletsencrypt\fR process, which runs as root and is -the only component with access to the private key material of the server -keys. -It is used to fork the ACME client (and optionally the ACME webserver) -after dropping root privileges. -For certificate issuances (\fBnew\-cert\fR command), it also generates -Certificate Signing Requests, then verifies the validity of the issued -certificate, and optionally reloads or restarts services when the -\fInotify\fR option is set. - -.IP \[bu] 4 -An actual ACME client (specified with the \fIcommand\fR option of the -\(lq[client]\(rq section of the configuration file), which builds ACME -commands and dialogues with the remote ACME server. -Since ACME commands need to be signed with the account key, the -\(lqmaster\(rq \fBletsencrypt\fR process passes the -\fIletsencrypt\-accountd\fR(1) UNIX\-domain socket to the ACME client: -data signatures are requested by writing the data to be signed to the -socket. - -.IP \[bu] 4 -For certificate issuances (\fBnew\-cert\fR command), an optional -webserver (specified with the \fIcommand\fR option of the -\(lq[webserver]\(rq section of the configuration file), which is spawned -by the \(lqmaster\(rq \fBletsencrypt\fR process when no service is -listening on the HTTP port. -(The only challenge type currently supported by \fBletsencrypt\fR is -\(lqhttp\-01\(rq, which requires a webserver to answer challenges.) -That webserver only processes GET and HEAD requests under the -\(lq/.well\-known/acme\-challenge/\(rq URI. -By default some \fIiptables\fR(1) rules are automatically installed to -open the HTTP port, and removed afterwards. - -.SH COMMANDS -.TP -.B letsencrypt \fR[\fB\-\-agreement\-uri=\fIURI\fR]\fB \fBnew\-reg \fR[\fICONTACT\fR ...] -Register the account key managed by \fIletsencrypt\-accountd\fR(1). A -list of \fICONTACT\fR information (such as \(lqmaito:\(rq -URIs) can be specified in order for the server to contact the client for -issues related to this registration (such as notifications about -server\-initiated revocations). - -\fB\-\-agreement\-uri=\fR can be used to specify a \fIURI\fR referring -to a subscriber agreement or terms of service provided by the server; -adding this options indicates the client's agreement with the referenced -terms. Note that the server might require the client to agree to -subscriber agreement before performing any further actions. - -If the account key is already registered, \fBletsencrypt\fR prints the -URI of the existing registration and aborts. - -.TP -.B letsencrypt \fR[\fB\-\-agreement\-uri=\fIURI\fR]\fB \fBreg=\fIURI\fR \fR[\fICONTACT\fR ...] - -Dump or edit the registration \fIURI\fR (relative to the ACME server URI, -which is specified with the \fIserver\fR option of the \(lq[client]\(rq -section of the configuration file). - -When specified, the list of \fICONTACT\fR information and the agreement -\fIURI\fR are sent to the server to replace the existing values. - -.TP -.B letsencrypt \fR[\fB\-\-config\-certs=\fIFILE\fR]\fB \fBnew\-cert \fR[\fISECTION\fR ...] - -Read the certificate configuration \fIFILE\fR (see the \fBCERTIFICATE -CONFIGURATION FILE\fR section below for the configuration options), and -request new Certificate Issuance for each of its sections (or the given -list of \fISECTION\fRs). - -.TP -.B letsencrypt \fBrevoke\-cert \fIFILE\fR [\fIFILE\fR ...] - -Request that the given certificate(s) \fIFILE\fR(s) be revoked. For -this command, \fIletsencrypt\-accountd\fR(1) can be pointed to either -the account key or the server's private key. - - -.SH GENERIC OPTIONS -.TP -.B \-\-config=\fIfilename\fR -Use \fIfilename\fR as configuration file. See the \fBCONFIGURATION -FILE\fR section below for the configuration options. - -.TP -.B \-\-socket=\fIpath\fR -Use \fIpath\fR as the \fIletsencrypt\-accountd\fR(1) UNIX\-domain socket -to connect to for signature requests from the ACME client. -\fBletsencrypt\fR aborts if \fIpath\fR is readable or writable by -other users, or if its parent directory is writable by other users. -This overrides the \fIsocket\fR option of the \(lq[client]\(rq section -of the configuration file. - -.TP -.B \-?\fR, \fB\-\-help\fR -Display a brief help and exit. - -.TP -.B \-\-debug -Turn on debug mode. - - -.SH CONFIGURATION FILE -If \fB\-\-config=\fR is not given, \fBletsencrypt\fR uses the first -existing configuration file among -\fI./letsencrypt.conf\fR, -\fI$XDG_CONFIG_HOME/letsencrypt\-tiny/letsencrypt.conf\fR (or -\fI~/.config/letsencrypt\-tiny/letsencrypt.conf\fR if the -XDG_CONFIG_HOME environment variable is not set), and -\fI/etc/letsencrypt\-tiny/letsencrypt.conf\fR. -Valid options are: - -.TP -Default section -.RS -.TP -.I config\-certs -For certificate issuances (\fBnew\-cert\fR command), specify the -certificate configuration file to use (see the \fBCERTIFICATE -CONFIGURATION FILE\fR section below for the configuration options). -.RE - -.TP -\(lq[client]\(rq section -This section is used for configuring the ACME client (which takes care -of ACME commands and dialogues with the remote ACME server). - -.RS -.TP -.I socket -See \fB\-\-socket=\fR. -Default: \(lq$XDG_RUNTIME_DIR/S.letsencrypt\(rq if the XDG_RUNTIME_DIR -environment variable is set. - -.TP -.I user -The username to drop privileges to (setting both effective and real -uid). -Preserve root privileges if the value is empty (not recommended). -Default: \(lqnobody\(rq. - -.TP -.I 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 (not -recommended). -Default: \(lqnogroup\(rq. - -.TP -.I command -Path to the ACME client executable. -Default: \(lq/usr/lib/letsencrypt\-tiny/client\(rq. - -.TP -.I server -Root URI of the ACME server. -Default: \(lqhttps://acme\-v01.api.letsencrypt.org/\(rq. - -.TP -.I timeout -Timeout in seconds after which the client stops polling the ACME server -and considers the request failed. -Default: \(lq10\(rq. - -.TP -.I SSL_verify -Whether to verify the server certificate chain. -Default: \(lqYes\(rq. - -.TP -.I SSL_version -Specify the version of the SSL protocol used to transmit data. - -.TP -.I SSL_cipher_list -Specify the cipher list for the connection. -.RE - -.TP -\(lq[webserver]\(rq section -This section is used for configuring the ACME webserver. - -.RS -.TP -.I listen -Specify the local address to listen on, in the form -\fIADDRESS\fR[:\fIPORT\fR]. -If \fIADDRESS\fR is enclosed with brackets \(oq[\(cq/\(oq]\(cq then it -denotes an IPv6; an empty \fIADDRESS\fR means \(oq0.0.0.0\(cq. -Default: \(lq:80\(rq. - -.TP -.I challenge\-directory -If a webserver is already running, specify a non\-existent directory -under which the webserver is configured to serve GET requests for -challenge files under \(lq/.well\-known/acme\-challenge/\(rq (for each -virtual hosts requiring authorization) as static files. -Default: \(lq/var/www/acme\-challenge\(rq. - -.TP -.I user -The username to drop privileges to (setting both effective and real -uid). -Preserve root privileges if the value is empty (not recommended). -Default: \(lqwww\-data\(rq. - -.TP -.I 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 (not -recommended). -Default: \(lqwww\-data\(rq. - -.TP -.I command -Path to the ACME webserver executable. -Default: \(lq/usr/lib/letsencrypt\-tiny/webserver\(rq. - -.TP -.I iptables -Whether to automatically install \fIiptables\fR(1) rules to open the -\fIADDRESS\fR[:\fIPORT\fR] specified with \fIlisten\fR. -Theses rules are automatically removed once \fBletsencrypt\fR exits. -Default: \(lqYes\(rq. -.RE - - -.SH CERTIFICATE CONFIGURATION FILE -For certificate issuances (\fBnew\-cert\fR 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 \fB\-\-config\-certs=\fR is not given, and if the -\fIconfig\-certs\fR configuration option is absent, -then \fBletsencrypt\fR uses the first existing configuration file among -\fI./letsencrypt\-certs.conf\fR, -\fI$XDG_CONFIG_HOME/letsencrypt\-tiny/letsencrypt\-certs.conf\fR (or -\fI~/.config/letsencrypt\-tiny/letsencrypt\-certs.conf\fR if the -XDG_CONFIG_HOME environment variable is not set), and -\fI/etc/letsencrypt\-tiny/letsencrypt\-certs.conf\fR. -Each section denotes a separate certificate issuance. -Valid options are: - -.TP -.I certificate -Where to store the issued certificate (in PEM format). -At least one of \fIcertificate\fR or \fIcertificate\-chain\fR is -required. - -.TP -.I certificate\-chain -Where to store the issued certificate, concatenated with the content of -the file specified specified with the \fICAfile\fR option (in PEM -format). -At least one of \fIcertificate\fR or \fIcertificate\-chain\fR is -required. - -.TP -.I certificate\-key -Path the service's private key. This option is required. The following -command can be used to generate a new 4096\-bits RSA key in PEM format -with mode 0600: - -.nf - openssl genrsa 4096 | install -m0600 /dev/stdin /path/to/priv.key -.fi - -.TP -.I min\-days -For an existing certificate, the minimum number of days before its -expiration date the section is considered for re\-issuance. -Default: \(lq10\(rq. - - -.TP -.I CAfile -Path to the issuer's certificate. This is used for -\fIcertificate\-chain\fR and to verify the validity of each issued -certificate. -Specifying an empty value skip certificate validation. -Default: \(lq/usr/share/letsencrypt\-tiny/lets\-encrypt\-x3\-cross\-signed.pem\(rq. - -.TP -.I hash -Message digest to sign the Certificate Signing Request with. - -.TP -.I keyUsage -Comma\-separated list of Key Usages, see \fIx509v3_config\fR(5ssl). - -.TP -.I subject -Subject field of the Certificate Signing Request, in the form -\fR/\fItype0\fR=\fIvalue0\fR/\fItype1\fR=\fIvalue1\fR/\fItype2\fR=... -This option is required. - -.TP -.I subjectAltName -Comma\-separated list of Subject Alternative Names, in the form -\fItype0\fR:\fIvalue1\fR,\fItype1\fR:\fIvalue1\fR,\fItype2\fR:... -The only \fItype\fR currently supported is \(lqDNS\(rq, to specify an -alternative domain name. - -.TP -.I chown -An optional \fIusername\fR[:\fIgroupname\fR] to chown the issued -\fIcertificate\fR and \fIcertificate\-chain\fR with. - -.TP -.I chmod -An optional octal mode to chmod the issued \fIcertificate\fR and -\fIcertificate\-chain\fR with. - -.TP -.I notify -Command to pass the the system's command shell (\(lq/bin/sh \-c\(rq) -after successful installation of the \fIcertificate\fR and/or -\fIcertificate\-chain\fR. - - -.SH EXAMPLES - -.nf - ~$ sudo letsencrypt new-reg mailto:noreply@example.com - ~$ sudo letsencrypt reg=/acme/reg/137760 --agreement-uri=https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf - ~$ sudo letsencrypt new-cert - ~$ sudo letsencrypt revoke-cert /path/to/server/certificate.pem -.fi - - -.SH SEE ALSO -\fBletsencrypt\-accountd\fR(1) - -.SH AUTHOR -.ie \n[www-html] \{\ - Written by -. MTO guilhem@fripost.org "Guilhem Moulin" . -\} -.el \{\ - Written by Guilhem Moulin -. MT guilhem@fripost.org -. ME . -\} -- cgit v1.2.3