Hi tech@,

(super-slightly revised mail/patch compared to the one from October 1st, mainly indent and line-wrap [1])

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 contains 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 but I was not able to do that. I would appreciate feedback on it. 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

[1] https://marc.info/?l=openbsd-tech&m=150688390625866&w=2
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    5 Dec 2017 11:16:44 -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.23
diff -u -p -r1.23 acme-client.1
--- acme-client.1       17 Oct 2017 22:47:58 -0000      1.23
+++ acme-client.1       5 Dec 2017 11:16:44 -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.9
diff -u -p -r1.9 extern.h
--- extern.h    27 Nov 2017 01:58:52 -0000      1.9
+++ extern.h    5 Dec 2017 11:16:44 -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,
@@ -176,6 +186,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);
@@ -183,7 +194,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);
 
Index: main.c
===================================================================
RCS file: /cvs/src/usr.sbin/acme-client/main.c,v
retrieving revision 1.36
diff -u -p -r1.36 main.c
--- main.c      27 Nov 2017 01:58:52 -0000      1.36
+++ main.c      5 Dec 2017 11:16:44 -0000
@@ -42,7 +42,7 @@ main(int argc, char *argv[])
        char             *chngdir = NULL, *auth = 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;
@@ -238,6 +241,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. */
 
@@ -254,9 +259,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);
                free(alts);
@@ -269,6 +279,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. */
 
@@ -284,6 +295,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);
@@ -306,6 +318,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);
        }
@@ -325,6 +338,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);
        }
@@ -342,6 +356,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);
        }
@@ -359,6 +374,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);
                /*
@@ -379,6 +395,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);
        }
@@ -392,6 +409,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,
@@ -402,6 +420,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) {
@@ -423,10 +462,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.14
diff -u -p -r1.14 netproc.c
--- netproc.c   27 Nov 2017 01:58:52 -0000      1.14
+++ netproc.c   5 Dec 2017 11:16:44 -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)
 {
@@ -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.9
diff -u -p -r1.9 parse.h
--- parse.h     27 Nov 2017 16:53:04 -0000      1.9
+++ parse.h     5 Dec 2017 11:16:44 -0000
@@ -61,6 +61,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  5 Dec 2017 11:16:44 -0000
@@ -0,0 +1,108 @@
+/*
+ * 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) {
+                       if (errno != ESRCH) {
+                               warn("%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.10
diff -u -p -r1.10 util.c
--- util.c      27 Nov 2017 16:53:04 -0000      1.10
+++ util.c      5 Dec 2017 11:16:44 -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