Hi tech@,

I'm using acme-client(1) to handle my certificates on a bunch of mailservers 
(smtps, imaps, pops) and a dedicated syslogd(8) server with tls. My daily cron 
on these machines contain a line like this:
rcctl -f start httpd >/dev/null && (acme-client foo.netsend.nl; rcctl stop 
httpd >/dev/null)

I think it would be nicer if acme-client is able to start and stop httpd(8) 
itself with the config mentioned in acme-client(5) so users on non-webservers 
don't have to be bothered with setting up a web server themselves.

The attached patch makes this possible by adding a "-l" switch to acme-client 
and simplifies the aforementioned cron entry to the following:
acme-client -l foo.netsend.nl

On first use, the code creates a new acme-client specific httpd.conf(5) in 
/etc/httpd.acme.conf but I'm not entirely happy with that. I think it would be 
nicer if /etc/ could be left untouched. Other options could be to extend httpd 
so that it can read configuration files from stdin, so there is no fiddling 
with any config files at all. Or to create the config file in a temporary 
location each time acme-client starts.

Another possible improvement might be to automatically and temporarily open up 
port 80 in pf, again I'm not sure how desirable that would be.

Feedback and directions are very welcome. :)

Kind regards,

Tim
Index: Makefile
===================================================================
RCS file: /cvs/src/usr.sbin/acme-client/Makefile,v
retrieving revision 1.8
diff -u -p -r1.8 Makefile
--- Makefile    3 Jul 2017 22:21:47 -0000       1.8
+++ Makefile    1 Oct 2017 18:23:28 -0000
@@ -2,7 +2,7 @@
 PROG=          acme-client
 SRCS=          acctproc.c base64.c certproc.c chngproc.c dbg.c dnsproc.c
 SRCS+=         fileproc.c http.c jsmn.c json.c keyproc.c main.c netproc.c
-SRCS+=         parse.y revokeproc.c rsa.c util.c
+SRCS+=         parse.y revokeproc.c rsa.c servproc.c util.c
 
 MAN=           acme-client.1 acme-client.conf.5
 
Index: acme-client.1
===================================================================
RCS file: /cvs/src/usr.sbin/acme-client/acme-client.1,v
retrieving revision 1.22
diff -u -p -r1.22 acme-client.1
--- acme-client.1       22 Mar 2017 11:14:14 -0000      1.22
+++ acme-client.1       1 Oct 2017 18:23:28 -0000
@@ -41,6 +41,8 @@ Create a new RSA domain key if one does 
 Force updating the certificate signature even if it's too soon.
 .It Fl f Ar configfile
 Specify an alternative configuration file.
+.It Fl l
+Listen: let acme-client start a web server internally.
 .It Fl n
 No operation: check and print configuration.
 .It Fl r
Index: extern.h
===================================================================
RCS file: /cvs/src/usr.sbin/acme-client/extern.h,v
retrieving revision 1.8
diff -u -p -r1.8 extern.h
--- extern.h    21 Jan 2017 08:54:26 -0000      1.8
+++ extern.h    1 Oct 2017 18:23:28 -0000
@@ -37,6 +37,14 @@ enum acctop {
 };
 
 /*
+ * Requests to servproc.
+ */
+enum   servop {
+       SERV_STOP = 0,
+       SERV__MAX
+};
+
+/*
  * Requests to and from chngproc.
  */
 enum   chngop {
@@ -105,6 +113,7 @@ enum        comp {
        COMP_FILE, /* handles writing certs */
        COMP_DNS, /* handles DNS lookups */
        COMP_REVOKE, /* checks X509 expiration */
+       COMP_SERV, /* httpd */
        COMP__MAX
 };
 
@@ -119,6 +128,7 @@ enum        comm {
        COMM_PAY,
        COMM_NONCE,
        COMM_TOK,
+       COMM_SERV_OP,
        COMM_CHNG_OP,
        COMM_CHNG_ACK,
        COMM_ACCT,
@@ -175,6 +185,7 @@ __BEGIN_DECLS
 int             acctproc(int, const char *, int);
 int             certproc(int, int);
 int             chngproc(int, const char *);
+int             servproc(int);
 int             dnsproc(int);
 int             revokeproc(int, const char *, const char *,
                        int, int, const char *const *, size_t);
@@ -182,7 +193,7 @@ int          fileproc(int, const char *, const 
                        const char *);
 int             keyproc(int, const char *,
                        const char **, size_t, int);
-int             netproc(int, int, int, int, int, int, int, int,
+int             netproc(int, int, int, int, int, int, int, int, int,
                        struct authority_c *, const char *const *,
                        size_t, const char *);
 
Index: main.c
===================================================================
RCS file: /cvs/src/usr.sbin/acme-client/main.c,v
retrieving revision 1.35
diff -u -p -r1.35 main.c
--- main.c      27 May 2017 08:31:08 -0000      1.35
+++ main.c      1 Oct 2017 18:23:28 -0000
@@ -42,7 +42,7 @@ main(int argc, char *argv[])
        char             *chngdir = NULL, *auth = NULL, *agreement = NULL;
        char             *conffile = CONF_FILE;
        int               key_fds[2], acct_fds[2], chng_fds[2], cert_fds[2];
-       int               file_fds[2], dns_fds[2], rvk_fds[2];
+       int               file_fds[2], dns_fds[2], rvk_fds[2], serv_fds[2];
        int               force = 0;
        int               c, rc, revocate = 0;
        int               popts = 0;
@@ -56,7 +56,7 @@ main(int argc, char *argv[])
        struct domain_c         *domain = NULL;
        struct altname_c        *ac;
 
-       while ((c = getopt(argc, argv, "FADrvnf:")) != -1)
+       while ((c = getopt(argc, argv, "FADlrvnf:")) != -1)
                switch (c) {
                case 'f':
                        if ((conffile = strdup(optarg)) == NULL)
@@ -71,6 +71,9 @@ main(int argc, char *argv[])
                case 'D':
                        popts |= ACME_OPT_NEWDKEY;
                        break;
+               case 'l':
+                       popts |= ACME_OPT_LISTEN;
+                       break;
                case 'r':
                        revocate = 1;
                        break;
@@ -239,6 +242,8 @@ main(int argc, char *argv[])
                err(EXIT_FAILURE, "socketpair");
        if (socketpair(AF_UNIX, SOCK_STREAM, 0, rvk_fds) == -1)
                err(EXIT_FAILURE, "socketpair");
+       if (socketpair(AF_UNIX, SOCK_STREAM, 0, serv_fds) == -1)
+               err(EXIT_FAILURE, "socketpair");
 
        /* Start with the network-touching process. */
 
@@ -255,9 +260,14 @@ main(int argc, char *argv[])
                close(file_fds[1]);
                close(dns_fds[0]);
                close(rvk_fds[0]);
+               close(serv_fds[0]);
+               if ((popts & ACME_OPT_LISTEN) == 0) {
+                       close(serv_fds[1]);
+                       serv_fds[1] = -1;
+               }
                c = netproc(key_fds[1], acct_fds[1],
                    chng_fds[1], cert_fds[1],
-                   dns_fds[1], rvk_fds[1],
+                   dns_fds[1], rvk_fds[1], serv_fds[1],
                    (popts & ACME_OPT_NEWACCT), revocate, authority,
                    (const char *const *)alts, altsz,
                    agreement);
@@ -271,6 +281,7 @@ main(int argc, char *argv[])
        close(cert_fds[1]);
        close(dns_fds[1]);
        close(rvk_fds[1]);
+       close(serv_fds[1]);
 
        /* Now the key-touching component. */
 
@@ -286,6 +297,7 @@ main(int argc, char *argv[])
                close(chng_fds[0]);
                close(file_fds[0]);
                close(file_fds[1]);
+               close(serv_fds[0]);
                c = keyproc(key_fds[0], domain->key,
                    (const char **)alts, altsz, (popts & ACME_OPT_NEWDKEY));
                free(alts);
@@ -308,6 +320,7 @@ main(int argc, char *argv[])
                close(chng_fds[0]);
                close(file_fds[0]);
                close(file_fds[1]);
+               close(serv_fds[0]);
                c = acctproc(acct_fds[0], acctkey, (popts & ACME_OPT_NEWACCT));
                exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
        }
@@ -327,6 +340,7 @@ main(int argc, char *argv[])
                close(rvk_fds[0]);
                close(file_fds[0]);
                close(file_fds[1]);
+               close(serv_fds[0]);
                c = chngproc(chng_fds[0], chngdir);
                exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
        }
@@ -344,6 +358,7 @@ main(int argc, char *argv[])
                close(dns_fds[0]);
                close(rvk_fds[0]);
                close(file_fds[1]);
+               close(serv_fds[0]);
                c = certproc(cert_fds[0], file_fds[0]);
                exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
        }
@@ -361,6 +376,7 @@ main(int argc, char *argv[])
                free(alts);
                close(dns_fds[0]);
                close(rvk_fds[0]);
+               close(serv_fds[0]);
                c = fileproc(file_fds[1], certdir, certfile, chainfile,
                    fullchainfile);
                /*
@@ -381,6 +397,7 @@ main(int argc, char *argv[])
                proccomp = COMP_DNS;
                free(alts);
                close(rvk_fds[0]);
+               close(serv_fds[0]);
                c = dnsproc(dns_fds[0]);
                exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
        }
@@ -394,6 +411,7 @@ main(int argc, char *argv[])
 
        if (pids[COMP_REVOKE] == 0) {
                proccomp = COMP_REVOKE;
+               close(serv_fds[0]);
                c = revokeproc(rvk_fds[0], certdir,
                    certfile != NULL ? certfile : fullchainfile,
                    force, revocate,
@@ -404,6 +422,27 @@ main(int argc, char *argv[])
 
        close(rvk_fds[0]);
 
+       /* Optional server component. */
+
+       pids[COMP_SERV] = -1;
+
+       if (popts & ACME_OPT_LISTEN) {
+
+               /* Start a web server ourselves. */
+
+               if ((pids[COMP_SERV] = fork()) == -1)
+                       err(EXIT_FAILURE, "fork");
+
+               if (pids[COMP_SERV] == 0) {
+                       proccomp = COMP_SERV;
+                       free(alts);
+                       c = servproc(serv_fds[0]);
+                       exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
+               }
+       }
+
+       close(serv_fds[0]);
+
        /* Jail: sandbox, file-system, user. */
 
        if (pledge("stdio", NULL) == -1) {
@@ -425,10 +464,15 @@ main(int argc, char *argv[])
            checkexit(pids[COMP_DNS], COMP_DNS) +
            checkexit(pids[COMP_REVOKE], COMP_REVOKE);
 
+       if (pids[COMP_SERV] != -1)
+           rc += checkexit(pids[COMP_SERV], COMP_SERV);
+       else
+           rc++;
+
        free(alts);
        return rc != COMP__MAX ? EXIT_FAILURE : (c == 2 ? EXIT_SUCCESS : 2);
 usage:
        fprintf(stderr,
-           "usage: acme-client [-ADFnrv] [-f configfile] domain\n");
+           "usage: acme-client [-ADFlnrv] [-f configfile] domain\n");
        return EXIT_FAILURE;
 }
Index: netproc.c
===================================================================
RCS file: /cvs/src/usr.sbin/acme-client/netproc.c,v
retrieving revision 1.13
diff -u -p -r1.13 netproc.c
--- netproc.c   24 Jan 2017 13:32:55 -0000      1.13
+++ netproc.c   1 Oct 2017 18:23:28 -0000
@@ -565,7 +565,7 @@ dofullchain(struct conn *c, const char *
  * account key information.
  */
 int
-netproc(int kfd, int afd, int Cfd, int cfd, int dfd, int rfd,
+netproc(int kfd, int afd, int Cfd, int cfd, int dfd, int rfd, int sfd,
     int newacct, int revocate, struct authority_c *authority,
     const char *const *alts,size_t altsz, const char *agreement)
 {
@@ -737,13 +737,19 @@ netproc(int kfd, int afd, int Cfd, int c
        }
 
        /*
-        * Write our acknowledgement that the challenges are over.
-        * The challenge process will remove all of the files.
+        * Write our acknowledgement that the challenges are over to both the
+        * challenge process and the server process, if started.
+        * The challenge process will remove all of the files and the server 
process
+        * will stop the http server.
         */
 
        if (writeop(Cfd, COMM_CHNG_OP, CHNG_STOP) <= 0)
                goto out;
 
+       if (sfd != -1)
+               if (writeop(sfd, COMM_SERV_OP, SERV_STOP) <= 0)
+                       goto out;
+
        /* Wait to receive the certificate itself. */
 
        if ((cert = readstr(kfd, COMM_CERT)) == NULL)
@@ -782,6 +788,8 @@ out:
        close(Cfd);
        close(dfd);
        close(rfd);
+       if (sfd != -1)
+               close(sfd);
        free(cert);
        free(url);
        free(thumb);
Index: parse.h
===================================================================
RCS file: /cvs/src/usr.sbin/acme-client/parse.h,v
retrieving revision 1.7
diff -u -p -r1.7 parse.h
--- parse.h     21 Jan 2017 12:59:06 -0000      1.7
+++ parse.h     1 Oct 2017 18:23:28 -0000
@@ -62,6 +62,7 @@ struct keyfile {
 #define ACME_OPT_NEWACCT       0x00000002
 #define ACME_OPT_NEWDKEY       0x00000004
 #define ACME_OPT_CHECK         0x00000008
+#define ACME_OPT_LISTEN        0x00000016
 
 struct acme_conf {
        int                      opts;
Index: servproc.c
===================================================================
RCS file: servproc.c
diff -N servproc.c
--- /dev/null   1 Jan 1970 00:00:00 -0000
+++ servproc.c  1 Oct 2017 18:23:28 -0000
@@ -0,0 +1,104 @@
+/*
+ * Copyright (c) 2017 Tim Kuijsten <[email protected]>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "extern.h"
+
+#define HTTPDCONF "/etc/httpd.acme.conf"
+#define HTTPDBIN "/usr/sbin/httpd"
+
+int
+servproc(int netsock)
+{
+       int     rc = 0, s, fd = -1;
+       pid_t   pid;
+       char    cfg[] = "server \"acmeclient\" {\n"
+               "       listen on egress port 80\n"
+               "       location \"/.well-known/acme-challenge/*\" {\n"
+               "               root \"/acme\"\n"
+               "               root strip 2\n"
+               "       }\n"
+               "}\n";
+
+       if (pledge("stdio wpath cpath proc exec", NULL) == -1)
+               err(EXIT_FAILURE, "%s: pledge", __func__);
+
+       /*
+        * Create a minimal acme config for httpd if one does not yet exist.
+        */
+
+       if ((fd = open(HTTPDCONF, O_WRONLY | O_CREAT | O_EXCL, 0644)) == -1) {
+               if (errno != EEXIST)
+                       err(EXIT_FAILURE, "open");
+       } else {
+               s = write(fd, cfg, strlen(cfg));
+               if (s != (ssize_t)strlen(cfg))
+                       err(EXIT_FAILURE, "%s: write", __func__);
+               if (close(fd) == -1)
+                       err(EXIT_FAILURE, "%s: close", __func__);
+       }
+
+       /* Fork+exec httpd. */
+
+       if ((pid = fork()) == -1)
+               err(EXIT_FAILURE, "%s: fork", __func__);
+
+       if (pid == 0) {
+               if (verbose < 2)
+                       close(STDOUT_FILENO);
+               if (verbose < 1)
+                       close(STDERR_FILENO);
+
+               if (execle(HTTPDBIN, "httpd", "-df", HTTPDCONF, NULL, NULL) == 
-1)
+                       err(EXIT_FAILURE, "%s: execle", __func__);
+       }
+
+       if (pledge("stdio proc", NULL) == -1)
+               err(EXIT_FAILURE, "%s: pledge", __func__);
+
+       /*
+        * Wait until we get a request to stop.
+        */
+
+       if (readop(netsock, COMM_SERV_OP) != SERV_STOP) {
+               warnx("unknown operation from netproc");
+               goto out;
+       }
+
+       if (kill(pid, SIGINT) == -1) {
+               warnx("%s: kill", __func__);
+               goto out;
+       }
+
+       rc = checkexit(pid, COMP_SERV);
+out:
+       if (rc == 0) {
+               warnx("%s: killing httpd, SIGKILL", __func__);
+               if (kill(pid, SIGKILL) == -1)
+                       warnx("%s: kill", __func__);
+               rc = checkexit(pid, COMP_SERV);
+       }
+
+       close(netsock);
+       return rc;
+}
Index: util.c
===================================================================
RCS file: /cvs/src/usr.sbin/acme-client/util.c,v
retrieving revision 1.9
diff -u -p -r1.9 util.c
--- util.c      24 Jan 2017 13:32:55 -0000      1.9
+++ util.c      1 Oct 2017 18:23:28 -0000
@@ -42,6 +42,7 @@ static        const char *const comps[COMP__MAX
        "fileproc", /* COMP_FILE */
        "dnsproc", /* COMP_DNS */
        "revokeproc", /* COMP_REVOKE */
+       "servproc", /* COMP_SERV */
 };
 
 static const char *const comms[COMM__MAX] = {
@@ -51,6 +52,7 @@ static        const char *const comms[COMM__MAX
        "payload", /* COMM_PAY */
        "nonce", /* COMM_NONCE */
        "token", /* COMM_TOK */
+       "serv-op", /* COMM_SERV_OP */
        "challenge-op", /* COMM_CHNG_OP */
        "challenge-ack", /* COMM_CHNG_ACK */
        "account", /* COMM_ACCT */

Reply via email to