'tis the season to be jolly...

I think it's time to kick the tires on this one.

I don't like the "exec" keyword, we should find something better.
Also, should the user be optional?
Oh, and it's not enforcing that exec is present in the config.

sthen pointed me in the direction of dehydrated
https://github.com/dehydrated-io/dehydrated/blob/master/docs/dns-verification.md
and uacme
https://github.com/ndilieto/uacme
as examples for acme clients that implement hooks for (dns) challenges

I implemented the uacme api since I find that less ugly. It should be
trivial to transmogrify it with a shell one-liner to support
dehydrated.

Comments, tests?

diff --git acme-client.conf.5 acme-client.conf.5
index 3c5fd1c2362..e580a365c51 100644
--- acme-client.conf.5
+++ acme-client.conf.5
@@ -170,11 +170,55 @@ in one file, and is required by most browsers.
 This is optional if
 .Ar domain certificate
 is specified.
-.It Ic sign with Ar authority
+.It Ic sign with Ar authority Op Ic challenge Ar type
 The certificate authority (as declared above in the
 .Sx AUTHORITIES
 section) to use.
 If this setting is absent, the first authority specified is used.
+.Ar type
+can be
+.Cm http
+or
+.Cm dns .
+It defaults to
+.Cm http .
+.It Ic exec Ar script Ic as Ar user
+Run
+.Ar script
+as user
+.Ar user
+for each
+.Cm dns
+challenge.
+This is required when using the
+.Cm dns
+challenge type.
+The script is called with five arguments:
+.Bl -tag -width Ds
+.It Ar method
+.Cm begin ,
+.Cm done ,
+or
+.Cm failed .
+.Cm begin
+indicates that a DNS record should be created and
+.Cm done
+or
+.Cm failed
+indicate that a DNS record should be removed.
+.It Ar type
+.Cm dns-01 .
+.It Ar ident
+The domain.
+.It Ar token
+Unused, for compatibility with existing hook scripts.
+.It Ar auth
+The challenge response.
+.El
+.Pp
+The script needs to create a DNS record of the form
+.Dl _acme-challenge.ident 30 IN TXT auth
+and exit once it has propagated to all name servers.
 .It Ic challengedir Ar path
 The directory in which the challenge file will be stored.
 If it is not specified, a default of
diff --git chngproc.c chngproc.c
index 476daed3416..deef61fccc6 100644
--- chngproc.c
+++ chngproc.c
@@ -15,10 +15,13 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
+#include <sys/wait.h>
+
 #include <assert.h>
 #include <err.h>
 #include <errno.h>
 #include <fcntl.h>
+#include <pwd.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
@@ -26,8 +29,38 @@
 
 #include "extern.h"
 
-int
-chngproc(int netsock, const char *root)
+static int
+do_exec(const char *exec, const char *method, const char *type,
+    const char *ident, const char *token, const char *auth)
+{
+       pid_t            pid;
+       int              status;
+
+       switch (pid = fork()) {
+       case -1:
+               warn("fork");
+               return -1;
+       case 0: /* child */
+               /* XXX close netproc fd */
+               if (pledge("stdio rpath exec", NULL) == -1) {
+                       warn("pledge");
+                       return -1;
+               }
+               execl(exec, exec, method, type, ident, token, auth, NULL);
+               warn("execl %s", exec);
+               _exit(1);
+       }
+       if (waitpid(pid, &status, 0) == -1) {
+               warn("waitpid");
+               return -1;
+       }
+       if (WEXITSTATUS(status) == 0)
+               return 0;
+       return -1;
+}
+
+static int
+chngproc_http(int netsock, const char *root)
 {
        char             *tok = NULL, *th = NULL, *fmt = NULL, **fs = NULL;
        size_t            i, fsz = 0;
@@ -153,3 +186,114 @@ out:
        free(tok);
        return rc;
 }
+
+static int
+chngproc_dns(int netsock, const char* exec, const char *exec_as)
+{
+       struct passwd   *pw;
+       size_t           i, identsz = 0;
+       long             lval;
+       int              rc = 0, cc;
+       enum chngop      op;
+       char            *ident = NULL, *tok = NULL, *auth = NULL;
+       char            **idents = NULL;
+       void            *pp;
+
+       if ((pw = getpwnam(exec_as)) == NULL) {
+               warn("getpwnam");
+               goto out;
+       }
+       if (setgroups(1, &pw->pw_gid) ||
+           setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) ||
+           setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid)) {
+               warnx("can't drop privileges");
+               goto out;
+       }
+
+       if (unveil(exec, "x") == -1) {
+               warn("unveil");
+               goto out;
+       }
+
+       if (pledge("stdio rpath proc exec", NULL) == -1) {
+               warn("pledge");
+               goto out;
+       }
+
+       for (;;) {
+               op = CHNG__MAX;
+               if ((lval = readop(netsock, COMM_CHNG_OP)) == 0)
+                       op = CHNG_STOP;
+               else if (lval == CHNG_SYN)
+                       op = lval;
+
+               if (op == CHNG__MAX) {
+                       warnx("unknown operation from netproc");
+                       goto out;
+               } else if (op == CHNG_STOP)
+                       break;
+
+               assert(op == CHNG_SYN);
+
+               if ((ident = readstr(netsock, COMM_IDENT)) == NULL)
+                       goto out;
+               if ((tok = readstr(netsock, COMM_TOK)) == NULL)
+                       goto out;
+               else if ((auth = readstr(netsock, COMM_AUTH)) == NULL)
+                   goto out;
+
+               pp = reallocarray(idents, (identsz + 1), sizeof(char *));
+               if (pp == NULL) {
+                       warn("realloc");
+                       goto out;
+               }
+               idents = pp;
+               if ((idents[identsz] = strdup(ident)) == NULL) {
+                       warn("strdup");
+                       goto out;
+               }
+               identsz++;
+               if (do_exec(exec, "begin", "dns-01", ident, tok, auth) == -1) {
+                       warnx("exec failed");
+                       goto out;
+               }
+               free(ident);
+               ident = NULL;
+               free(tok);
+               tok = NULL;
+               free(auth);
+               auth = NULL;
+
+               cc = writeop(netsock, COMM_CHNG_ACK, CHNG_ACK);
+               if (cc == 0)
+                       break;
+               if (cc < 0)
+                       goto out;
+       }
+
+       rc = 1;
+out:
+       close(netsock);
+       for (i = 0; i < identsz; i++) {
+               if (do_exec(exec, rc == 1 ? "done" : "failed", "dns-01",
+                   idents[i], "", "") == -1)
+                       warnx("exec failed");
+       }
+       free(ident);
+       free(tok);
+       free(auth);
+       return rc;
+}
+
+int
+chngproc(int netsock, enum chngtype chngtype, const char *root, const char
+    *exec, const char *exec_as)
+{
+       switch (chngtype) {
+       case CHNG_HTTP:
+               return chngproc_http(netsock, root);
+       case CHNG_DNS:
+               return chngproc_dns(netsock, exec, exec_as);
+       }
+       return 0;
+}
diff --git extern.h extern.h
index 4b43b6ef4ac..ad78b636bf7 100644
--- extern.h
+++ extern.h
@@ -122,6 +122,8 @@ enum        comm {
        COMM_KID,
        COMM_URL,
        COMM_TOK,
+       COMM_IDENT,
+       COMM_AUTH,
        COMM_CHNG_OP,
        COMM_CHNG_ACK,
        COMM_ACCT,
@@ -161,6 +163,7 @@ struct      chng {
        char            *token; /* token we must offer */
        char            *error; /* "detail" field in case of error */
        size_t           retry; /* how many times have we tried */
+       char            *identifier; /* the domain */
        enum chngstatus  status; /* challenge accepted? */
 };
 
@@ -202,7 +205,8 @@ __BEGIN_DECLS
  */
 int             acctproc(int, const char *, enum keytype);
 int             certproc(int, int);
-int             chngproc(int, const char *);
+int             chngproc(int, enum chngtype, const char *, const char*,
+                       const char*);
 int             dnsproc(int);
 int             revokeproc(int, const char *, int, int, const char *const *,
                        size_t);
@@ -212,7 +216,7 @@ int          keyproc(int, const char *, const char **, 
size_t,
                        enum keytype);
 int             netproc(int, int, int, int, int, int, int,
                        struct authority_c *, const char *const *,
-                       size_t);
+                       size_t, enum chngtype);
 
 /*
  * Debugging functions.
@@ -253,7 +257,8 @@ struct jsmnn        *json_parse(const char *, size_t);
 void            json_free(struct jsmnn *);
 int             json_parse_response(struct jsmnn *);
 void            json_free_challenge(struct chng *);
-int             json_parse_challenge(struct jsmnn *, struct chng *);
+int             json_parse_challenge(struct jsmnn *, struct chng *,
+                    enum chngtype);
 void            json_free_order(struct order *);
 int             json_parse_order(struct jsmnn *, struct order *);
 int             json_parse_upd_order(struct jsmnn *, struct order *);
diff --git json.c json.c
index 92e087b2ec7..1e382ba26c7 100644
--- json.c
+++ json.c
@@ -371,7 +371,7 @@ json_parse_response(struct jsmnn *n)
  * We only care about the HTTP-01 response.
  */
 int
-json_parse_challenge(struct jsmnn *n, struct chng *p)
+json_parse_challenge(struct jsmnn *n, struct chng *p, enum chngtype chngtype)
 {
        struct jsmnn    *array, *obj, *error;
        size_t           i;
@@ -381,6 +381,12 @@ json_parse_challenge(struct jsmnn *n, struct chng *p)
        if (n == NULL)
                return 0;
 
+       obj = json_getobj(n, "identifier");
+       if (obj == NULL)
+               return 0;
+       if ((p->identifier = json_getstr(obj, "value")) == NULL)
+               return 0;
+
        array = json_getarray(n, "challenges");
        if (array == NULL)
                return 0;
@@ -392,7 +398,16 @@ json_parse_challenge(struct jsmnn *n, struct chng *p)
                type = json_getstr(obj, "type");
                if (type == NULL)
                        continue;
-               rc = strcmp(type, "http-01");
+
+               switch (chngtype) {
+               case CHNG_HTTP:
+                       rc = strcmp(type, "http-01");
+                       break;
+               case CHNG_DNS:
+                       rc = strcmp(type, "dns-01");
+                       break;
+               }
+
                free(type);
                if (rc)
                        continue;
diff --git main.c main.c
index 65ea2cf3ac3..0f05de9bce6 100644
--- main.c
+++ main.c
@@ -213,11 +213,9 @@ main(int argc, char *argv[])
                close(file_fds[1]);
                close(dns_fds[0]);
                close(rvk_fds[0]);
-               c = netproc(key_fds[1], acct_fds[1],
-                   chng_fds[1], cert_fds[1],
-                   dns_fds[1], rvk_fds[1],
-                   revocate, authority,
-                   (const char *const *)alts, altsz);
+               c = netproc(key_fds[1], acct_fds[1], chng_fds[1], cert_fds[1],
+                   dns_fds[1], rvk_fds[1], revocate, authority,
+                   (const char *const *)alts, altsz, domain->chng);
                exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
        }
 
@@ -282,7 +280,8 @@ main(int argc, char *argv[])
                close(rvk_fds[0]);
                close(file_fds[0]);
                close(file_fds[1]);
-               c = chngproc(chng_fds[0], chngdir);
+               c = chngproc(chng_fds[0], domain->chng, chngdir, domain->exec,
+                   domain->exec_as);
                exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
        }
 
diff --git netproc.c netproc.c
index 7c502643acc..61a8c2e414d 100644
--- netproc.c
+++ netproc.c
@@ -25,6 +25,8 @@
 #include <tls.h>
 #include <vis.h>
 
+#include <openssl/evp.h>
+
 #include "http.h"
 #include "extern.h"
 #include "parse.h"
@@ -507,7 +509,8 @@ doupdorder(struct conn *c, struct order *order)
  * On non-zero exit, fills in "chng" with the challenge.
  */
 static int
-dochngreq(struct conn *c, const char *auth, struct chng *chng)
+dochngreq(struct conn *c, const char *auth, struct chng *chng, enum chngtype
+    chngtype)
 {
        int              rc = 0;
        long             lc;
@@ -521,7 +524,7 @@ dochngreq(struct conn *c, const char *auth, struct chng 
*chng)
                warnx("%s: bad HTTP: %ld", auth, lc);
        else if ((j = json_parse(c->buf.buf, c->buf.sz)) == NULL)
                warnx("%s: bad JSON object", auth);
-       else if (!json_parse_challenge(j, chng))
+       else if (!json_parse_challenge(j, chng, chngtype))
                warnx("%s: bad challenge", auth);
        else
                rc = 1;
@@ -666,14 +669,52 @@ dodirs(struct conn *c, const char *addr, struct capaths 
*paths)
        return rc;
 }
 
+static char *
+fmt_dns_chng(const char *token, const char *thumb)
+{
+       EVP_MD_CTX      *ctx = NULL;
+       unsigned int     digsz;
+       unsigned char   *dig = NULL;
+       char            *resp = NULL, *dig64 = NULL;
+
+       if (asprintf(&resp, "%s.%s", token, thumb) == -1) {
+               resp = NULL;
+               goto out;
+       } else if ((dig = malloc(EVP_MAX_MD_SIZE)) == NULL) {
+               warn("malloc");
+               goto out;
+       } else if ((ctx = EVP_MD_CTX_create()) == NULL) {
+               warnx("EVP_MD_CTX_create");
+               goto out;
+       } else if (!EVP_DigestInit_ex(ctx, EVP_sha256(), NULL)) {
+               warnx("EVP_SignInit_ex");
+               goto out;
+       } else if (!EVP_DigestUpdate(ctx, resp, strlen(resp))) {
+               warnx("EVP_SignUpdate");
+               goto out;
+       } else if (!EVP_DigestFinal_ex(ctx, dig, &digsz)) {
+               warnx("EVP_SignFinal");
+               goto out;
+       } else if ((dig64 = base64buf_url((char *)dig, digsz)) == NULL)
+               warnx("base64buf_url");
+out:
+       if (ctx != NULL)
+               EVP_MD_CTX_destroy(ctx);
+       free(dig);
+       free(resp);
+       return dig64;
+}
+
+
+
 /*
  * Communicate with the ACME server.
  * We need the certificate we want to upload and our account key information.
  */
 int
 netproc(int kfd, int afd, int Cfd, int cfd, int dfd, int rfd,
-    int revocate, struct authority_c *authority,
-    const char *const *alts, size_t altsz)
+    int revocate, struct authority_c *authority, const char *const *alts,
+    size_t altsz, enum chngtype chngtype)
 {
        int              rc = 0;
        size_t           i;
@@ -811,7 +852,8 @@ netproc(int kfd, int afd, int Cfd, int cfd, int dfd, int 
rfd,
                                goto out;
                        }
                        for (i = 0; i < order.authsz; i++) {
-                               if (!dochngreq(&c, order.auths[i], &chngs[i]))
+                               if (!dochngreq(&c, order.auths[i], &chngs[i],
+                                   chngtype))
                                        goto out;
 
                                dodbg("challenge, token: %s, uri: %s, status: "
@@ -822,19 +864,41 @@ netproc(int kfd, int afd, int Cfd, int cfd, int dfd, int 
rfd,
                                    chngs[i].status == CHNG_INVALID)
                                        continue;
 
+                               /* XXX not correct for dns-01? */
                                if (chngs[i].retry++ >= RETRY_MAX) {
                                        warnx("%s: too many tries",
                                            chngs[i].uri);
                                        goto out;
                                }
 
-                               if (writeop(Cfd, COMM_CHNG_OP, CHNG_SYN) <= 0)
-                                       goto out;
-                               else if (writestr(Cfd, COMM_THUMB, thumb) <= 0)
-                                       goto out;
-                               else if (writestr(Cfd, COMM_TOK,
-                                   chngs[i].token) <= 0)
-                                       goto out;
+                               switch (chngtype) {
+                               case CHNG_HTTP:
+                                       if (writeop(Cfd, COMM_CHNG_OP,
+                                           CHNG_SYN) <= 0)
+                                               goto out;
+                                       else if (writestr(Cfd, COMM_THUMB,
+                                           thumb) <= 0)
+                                               goto out;
+                                       else if (writestr(Cfd, COMM_TOK,
+                                           chngs[i].token) <= 0)
+                                               goto out;
+                                       break;
+                               case CHNG_DNS:
+                                       if (writeop(Cfd, COMM_CHNG_OP,
+                                           CHNG_SYN) <= 0)
+                                               goto out;
+                                       else if (writestr(Cfd, COMM_IDENT,
+                                           chngs[i].identifier) <= 0)
+                                               goto out;
+                                       else if (writestr(Cfd, COMM_TOK,
+                                           chngs[i].token) <= 0)
+                                               goto out;
+                                       else if (writestr(Cfd, COMM_AUTH,
+                                           fmt_dns_chng(chngs[i].token,
+                                           thumb)) <= 0)
+                                               goto out;
+                                       break;
+                               }
 
                                /* Read that the challenge has been made. */
                                if (readop(Cfd, COMM_CHNG_ACK) != CHNG_ACK)
@@ -879,7 +943,7 @@ netproc(int kfd, int afd, int Cfd, int cfd, int dfd, int 
rfd,
 
        if (order.status != ORDER_VALID) {
                for (i = 0; i < order.authsz; i++) {
-                       dochngreq(&c, order.auths[i], &chngs[i]);
+                       dochngreq(&c, order.auths[i], &chngs[i], chngtype);
                        if (chngs[i].error != NULL) {
                                if (stravis(&error, chngs[i].error, VIS_SAFE)
                                    != -1) {
diff --git parse.h parse.h
index 3954f62a0d0..435bd22102e 100644
--- parse.h
+++ parse.h
@@ -22,6 +22,14 @@
 #define AUTH_MAXLEN    120     /* max length of an authority_c name */
 #define DOMAIN_MAXLEN  255     /* max len of a domain name (rfc2181) */
 
+/*
+ * Supported challenge types.
+ */
+enum   chngtype {
+       CHNG_HTTP,
+       CHNG_DNS
+};
+
 /*
  * XXX other size limits needed?
  * limit all paths to PATH_MAX
@@ -54,6 +62,9 @@ struct domain_c {
        char                    *fullchain;
        char                    *auth;
        char                    *challengedir;
+       enum chngtype            chng;
+       char                    *exec;
+       char                    *exec_as;
 };
 
 struct altname_c {
diff --git parse.y parse.y
index 1febcb10a3a..22faef7e457 100644
--- parse.y
+++ parse.y
@@ -102,7 +102,7 @@ typedef struct {
 
 %token AUTHORITY URL API ACCOUNT CONTACT
 %token DOMAIN ALTERNATIVE NAME NAMES CERT FULL CHAIN KEY SIGN WITH CHALLENGEDIR
-%token YES NO
+%token YES NO CHALLENGE HTTP DNS EXEC AS
 %token INCLUDE
 %token ERROR
 %token RSA ECDSA
@@ -368,7 +368,7 @@ domainoptsl : ALTERNATIVE NAMES '{' altname_l '}'
                        }
                        domain->fullchain = s;
                }
-               | SIGN WITH STRING {
+               | SIGN WITH STRING optchallenge {
                        char *s;
                        if (domain->auth != NULL) {
                                yyerror("duplicate sign with");
@@ -383,6 +383,19 @@ domainoptsl        : ALTERNATIVE NAMES '{' altname_l '}'
                        }
                        domain->auth = s;
                }
+               | EXEC STRING AS STRING {
+                       char *s;
+                       if (domain->exec != NULL || domain->exec_as != NULL) {
+                               yyerror("duplicate exec");
+                               YYERROR;
+                       }
+                       if ((s = strdup($2)) == NULL)
+                               err(EXIT_FAILURE, "strdup");
+                       domain->exec = s;
+                       if ((s = strdup($4)) == NULL)
+                               err(EXIT_FAILURE, "strdup");
+                       domain->exec_as = s;
+               }
                | CHALLENGEDIR STRING {
                        char *s;
                        if (domain->challengedir != NULL) {
@@ -395,6 +408,17 @@ domainoptsl        : ALTERNATIVE NAMES '{' altname_l '}'
                }
                ;
 
+optchallenge   : CHALLENGE HTTP {
+                       domain->chng = CHNG_HTTP;
+               }
+               | CHALLENGE DNS {
+                       domain->chng = CHNG_DNS;
+               }
+               | {
+                       domain->chng = CHNG_HTTP;
+               }
+               ;
+
 altname_l      : altname comma altname_l
                | altname
                ;
@@ -458,14 +482,19 @@ lookup(char *s)
                {"account",             ACCOUNT},
                {"alternative",         ALTERNATIVE},
                {"api",                 API},
+               {"as",                  AS},
                {"authority",           AUTHORITY},
                {"certificate",         CERT},
                {"chain",               CHAIN},
+               {"challenge",           CHALLENGE},
                {"challengedir",        CHALLENGEDIR},
                {"contact",             CONTACT},
+               {"dns",                 DNS},
                {"domain",              DOMAIN},
                {"ecdsa",               ECDSA},
+               {"exec",                EXEC},
                {"full",                FULL},
+               {"http",                HTTP},
                {"include",             INCLUDE},
                {"key",                 KEY},
                {"name",                NAME},
@@ -1082,7 +1111,12 @@ print_config(struct acme_conf *xconf)
                        printf("\tdomain full chain certificate \"%s\"\n",
                            d->fullchain);
                if (d->auth != NULL)
-                       printf("\tsign with \"%s\"\n", d->auth);
+                       printf("\tsign with \"%s\" challenge %s\n", d->auth,
+                           d->chng == CHNG_HTTP ? "http" : "dns");
+               if (d->chng == CHNG_DNS && d->exec != NULL && d->exec_as
+                   != NULL)
+                       printf("\texec \"%s\" as \"%s\"\n", d->exec,
+                           d->exec_as);
                if (d->challengedir != NULL)
                        printf("\tchallengedir \"%s\"\n", d->challengedir);
                printf("}\n\n");
@@ -1099,6 +1133,8 @@ int
 domain_valid(const char *cp)
 {
 
+       if (*cp == '*' && *(cp + 1) == '.')
+               cp += 2;
        for ( ; *cp != '\0'; cp++)
                if (!(*cp == '.' || *cp == '-' ||
                    *cp == '_' || isalnum((int)*cp)))
diff --git util.c util.c
index 4da5b294163..2c980c7bac5 100644
--- util.c
+++ util.c
@@ -51,6 +51,8 @@ static        const char *const comms[COMM__MAX] = {
        "payload", /* COMM_PAY */
        "nonce", /* COMM_NONCE */
        "token", /* COMM_TOK */
+       "domain", /* COMM_IDENT */
+       "auth", /* COMM_AUTH */
        "challenge-op", /* COMM_CHNG_OP */
        "challenge-ack", /* COMM_CHNG_ACK */
        "account", /* COMM_ACCT */


-- 
I'm not entirely sure you are real.

Reply via email to