1 /*
2  * Copyright (C) 2019-2021 Nicola Di Lieto <nicola.dilieto@gmail.com>
3  *
4  * This file is part of uacme.
5  *
6  * uacme is free software: you can redistribute it and/or modify it
7  * under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * uacme is distributed in the hope that it will be useful, but
12  * WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program.  If not, see
18  * <http://www.gnu.org/licenses/>.
19  */
20 
21 #include "config.h"
22 
23 #include <ctype.h>
24 #include <err.h>
25 #include <errno.h>
26 #include <fcntl.h>
27 #include <getopt.h>
28 #include <libgen.h>
29 #include <locale.h>
30 #include <regex.h>
31 #include <stdarg.h>
32 #include <stdbool.h>
33 #include <stdio.h>
34 #include <stdlib.h>
35 #include <string.h>
36 #include <sys/stat.h>
37 #include <sys/wait.h>
38 #include <unistd.h>
39 
40 #include "base64.h"
41 #include "curlwrap.h"
42 #include "crypto.h"
43 #include "json.h"
44 #include "msg.h"
45 
46 #define PRODUCTION_URL "https://acme-v02.api.letsencrypt.org/directory"
47 #define STAGING_URL "https://acme-staging-v02.api.letsencrypt.org/directory"
48 #define DEFAULT_CONFDIR SYSCONFDIR "/ssl/uacme"
49 
50 typedef struct acme {
51     privkey_t key;
52     json_value_t *json;
53     json_value_t *account;
54     json_value_t *dir;
55     json_value_t *order;
56     char *nonce;
57     char *kid;
58     char *headers;
59     char *body;
60     char *type;
61     unsigned char alt_fp[32];
62     size_t alt_fp_len;
63     size_t alt_n;
64     const char *eab_keyid;
65     const char *eab_key;
66     const char *directory;
67     const char *hook;
68     const char *email;
69     char *keyprefix;
70     char *certprefix;
71 } acme_t;
72 
73 #if !HAVE_STRCASESTR
strcasestr(const char * haystack,const char * needle)74 char *strcasestr(const char *haystack, const char *needle)
75 {
76     char *ret = NULL;
77     char *_haystack = strdup(haystack);
78     char *_needle = strdup(needle);
79 
80     if (!_haystack || !_needle)
81         warn("strcasestr: strdup failed");
82     else {
83         char *p;
84         for (p = _haystack; *p; p++)
85             *p = tolower(*p);
86         for (p = _needle; *p; p++)
87             *p = tolower(*p);
88         ret = strstr(_haystack, _needle);
89         if (ret)
90             ret = (char *)haystack + (ret - _haystack);
91     }
92     free(_haystack);
93     free(_needle);
94     return ret;
95 }
96 #endif
97 
find_header(const char * headers,const char * name)98 char *find_header(const char *headers, const char *name)
99 {
100     char *regex = NULL;
101     if (asprintf(&regex, "^%s:[ \t]*(.*)\r\n", name) < 0) {
102         warnx("find_header: asprintf failed");
103         return NULL;
104     }
105     char *ret = NULL;
106     regex_t reg;
107     if (regcomp(&reg, regex, REG_EXTENDED | REG_ICASE | REG_NEWLINE)) {
108         warnx("find_header: regcomp failed");
109     } else {
110         regmatch_t m[2];
111         if (regexec(&reg, headers, 2, m, 0) == 0) {
112             ret = strndup(headers + m[1].rm_so, m[1].rm_eo - m[1].rm_so);
113             if (!ret)
114                 warn("find_header: strndup failed");
115         }
116     }
117     free(regex);
118     regfree(&reg);
119     return ret;
120 }
121 
acme_get(acme_t * a,const char * url)122 int acme_get(acme_t *a, const char *url)
123 {
124     int ret = 0;
125 
126     json_free(a->json);
127     a->json = NULL;
128     free(a->headers);
129     a->headers = NULL;
130     free(a->body);
131     a->body = NULL;
132     free(a->type);
133     a->type = NULL;
134 
135     if (!url) {
136         warnx("acme_get: invalid URL");
137         goto out;
138     }
139     if (g_loglevel > 1)
140         warnx("acme_get: url=%s", url);
141     curldata_t *c = curl_get(url);
142     if (!c) {
143         warnx("acme_get: curl_get failed");
144         goto out;
145     }
146     free(a->nonce);
147     a->nonce = find_header(c->headers, "Replay-Nonce");
148     a->type = find_header(c->headers, "Content-Type");
149     if (a->type && strcasestr(a->type, "json"))
150         a->json = json_parse(c->body, c->body_len);
151     a->headers = c->headers;
152     c->headers = NULL;
153     a->body = c->body;
154     c->body = NULL;
155     ret = c->code;
156     curldata_free(c);
157 out:
158     if (g_loglevel > 2) {
159         if (a->headers)
160             warnx("acme_get: HTTP headers\n%s", a->headers);
161         if (a->body)
162             warnx("acme_get: HTTP body\n%s", a->body);
163     }
164     if (g_loglevel > 1) {
165         if (a->json) {
166             warnx("acme_get: return code %d, json=", ret);
167             json_dump(stderr, a->json);
168         } else
169             warnx("acme_get: return code %d", ret);
170     }
171     if (!a->headers)
172         a->headers = strdup("");
173     if (!a->body)
174         a->body = strdup("");
175     if (!a->type)
176         a->type = strdup("");
177     return ret;
178 }
179 
acme_post(acme_t * a,const char * url,const char * format,...)180 int acme_post(acme_t *a, const char *url, const char *format, ...)
181 {
182     int ret = 0;
183     char *payload = NULL;
184     char *protected = NULL;
185     char *jws = NULL;
186 
187     if (!url) {
188         warnx("acme_post: invalid URL");
189         return 0;
190     }
191 
192     if (!a->nonce) {
193         warnx("acme_post: need a nonce first");
194         return 0;
195     }
196 
197     va_list ap;
198     va_start(ap, format);
199     if (vasprintf(&payload, format, ap) < 0)
200         payload = NULL;
201     va_end(ap);
202     if (!payload) {
203         warnx("acme_post: vasprintf failed");
204         return 0;
205     }
206 
207     for (int retry = 0; a->nonce && retry < 3; retry++) {
208         if (retry > 0)
209             msg(1, "acme_post: server rejected nonce, retrying");
210 
211         json_free(a->json);
212         a->json = NULL;
213         free(a->headers);
214         a->headers = NULL;
215         free(a->body);
216         a->body = NULL;
217         free(a->type);
218         a->type = NULL;
219 
220         protected = (a->kid && *a->kid) ?
221             jws_protected_kid(a->nonce, url, a->kid, a->key) :
222             jws_protected_jwk(a->nonce, url, a->key);
223         if (!protected) {
224             warnx("acme_post: jws_protected_xxx failed");
225             goto out;
226         }
227         jws = jws_encode(protected, payload, a->key);
228         if (!jws) {
229             warnx("acme_post: jws_encode failed");
230             goto out;
231         }
232         if (g_loglevel > 2)
233             warnx("acme_post: url=%s payload=%s protected=%s jws=%s",
234                     url, payload, protected, jws);
235         else if (g_loglevel > 1)
236             warnx("acme_post: url=%s payload=%s", url, payload);
237         curldata_t *c = curl_post(url, jws, strlen(jws),
238                 "Content-Type: application/jose+json", NULL);
239         if (!c) {
240             warnx("acme_post: curl_post failed");
241             goto out;
242         }
243         free(a->nonce);
244         a->nonce = find_header(c->headers, "Replay-Nonce");
245         a->type = find_header(c->headers, "Content-Type");
246         if (a->type && strcasestr(a->type, "json"))
247             a->json = json_parse(c->body, c->body_len);
248         a->headers = c->headers;
249         c->headers = NULL;
250         a->body = c->body;
251         c->body = NULL;
252         ret = c->code;
253         curldata_free(c);
254         if (g_loglevel > 2) {
255             if (a->headers)
256                 warnx("acme_post: HTTP headers:\n%s", a->headers);
257             if (a->body)
258                 warnx("acme_post: HTTP body:\n%s", a->body);
259         }
260         if (g_loglevel > 1) {
261             if (a->json) {
262                 warnx("acme_post: return code %d, json=", ret);
263                 json_dump(stderr, a->json);
264             } else
265                 warnx("acme_post: return code %d", ret);
266         }
267         if (ret != 400 || !a->type || !a->nonce || !a->json ||
268                 !strcasestr(a->type, "application/problem+json") ||
269                 json_compare_string(a->json, "type",
270                     "urn:ietf:params:acme:error:badNonce") != 0)
271             break;
272     }
273 out:
274     free(payload);
275     free(protected);
276     free(jws);
277     if (!a->headers)
278         a->headers = strdup("");
279     if (!a->body)
280         a->body = strdup("");
281     if (!a->type)
282         a->type = strdup("");
283     return ret;
284 }
285 
hook_run(const char * prog,const char * method,const char * type,const char * ident,const char * token,const char * auth)286 int hook_run(const char *prog, const char *method, const char *type,
287         const char *ident, const char *token, const char *auth)
288 {
289     int ret = -1;
290     pid_t pid = fork();
291     if (pid < 0)
292         warn("hook_run: fork failed");
293     else if (pid > 0) { // parent
294         int status;
295         if (waitpid(pid, &status, 0) < 0)
296             warn("hook_run: waitpid failed");
297         else if (WIFEXITED(status))
298             ret = WEXITSTATUS(status);
299         else
300             warnx("hook_run: %s terminated abnormally", prog);
301     } else { // child
302         if (execl(prog, prog, method, type, ident, token, auth,
303                     (char *)NULL) < 0) {
304             warn("hook_run: failed to execute %s", prog);
305             abort();
306         }
307     }
308     return ret;
309 }
310 
check_or_mkdir(bool allow_create,const char * dir,mode_t mode)311 bool check_or_mkdir(bool allow_create, const char *dir, mode_t mode)
312 {
313     bool ret = false;
314     char *tmp = strdup(dir);
315     if (!tmp) {
316         warnx("check_or_mkdir: strdup failed");
317         goto out;
318     }
319     for (size_t i = strlen(tmp); i > 1; i--) {
320         if (tmp[i - 1] != '/')
321             break;
322         tmp[i - 1] = 0;
323     }
324     if (access(dir, F_OK) < 0) {
325         if (!allow_create) {
326             warnx("failed to access %s", dir);
327             goto out;
328         }
329         if (mkdir(dir, mode) < 0) {
330             warn("failed to create %s", dir);
331             goto out;
332         }
333         msg(1, "created directory %s", dir);
334     }
335     struct stat st;
336     if (stat(dir, &st) != 0) {
337         warn("failed to stat %s", dir);
338         goto out;
339     }
340     if (!S_ISDIR(st.st_mode)) {
341         warnx("%s is not a directory", dir);
342         goto out;
343     }
344     ret = true;
345 out:
346     free(tmp);
347     return ret;
348 }
349 
identifiers(char * const * names)350 char *identifiers(char * const *names)
351 {
352     char *ids = NULL;
353     char *tmp = NULL;
354     if (asprintf(&tmp, "{\"identifiers\":[") < 0) {
355         warnx("identifiers: asprintf failed");
356         return NULL;
357     }
358     while (names && *names) {
359         if (asprintf(&ids, "%s{\"type\":\"%s\",\"value\":\"%s\"},", tmp,
360                     is_ip(*names, NULL, NULL) ? "ip" : "dns", *names) < 0) {
361             warnx("identifiers: asprintf failed");
362             free(tmp);
363             return NULL;
364         }
365         free(tmp);
366         tmp = ids;
367         ids = NULL;
368         names++;
369     }
370     tmp[strlen(tmp)-1] = 0;
371     if (asprintf(&ids, "%s]}", tmp) < 0) {
372         warnx("identifiers: asprintf failed");
373         ids = NULL;
374     }
375     free(tmp);
376     return ids;
377 }
378 
acme_error(acme_t * a)379 bool acme_error(acme_t *a)
380 {
381     if (!a->json) return false;
382 
383     if (a->type && strcasestr(a->type, "application/problem+json")) {
384         warnx("the server reported the following error:");
385         json_dump(stderr, a->json);
386         return true;
387     }
388 
389     const json_value_t *e = json_find(a->json, "error");
390     if (e && e->type == JSON_OBJECT) {
391         warnx("the server reported the following error:");
392         json_dump(stderr, e);
393         return true;
394     }
395 
396     return false;
397 }
398 
acme_bootstrap(acme_t * a)399 bool acme_bootstrap(acme_t *a)
400 {
401     msg(1, "fetching directory at %s", a->directory);
402     if (acme_get(a, a->directory) != 200) {
403         warnx("failed to fetch directory at %s", a->directory);
404         acme_error(a);
405         return false;
406     } else if (acme_error(a))
407         return false;
408 
409     a->dir = a->json;
410     a->json = NULL;
411 
412     const char *url = json_find_string(a->dir, "newNonce");
413     if (!url)
414     {
415         warnx("failed to find newNonce URL in directory");
416         return false;
417     }
418 
419     msg(2, "fetching new nonce at %s", url);
420     if (acme_get(a, url) != 204) {
421         warnx("failed to fetch new nonce at %s", url);
422         acme_error(a);
423         return false;
424     } else if (acme_error(a))
425         return false;
426 
427     return true;
428 }
429 
eab_encode(acme_t * a,const char * url)430 char *eab_encode(acme_t *a, const char *url)
431 {
432     char *protected = NULL;
433     char *payload = NULL;
434     char *jws = NULL;
435 
436     protected = jws_protected_eab(256, a->eab_keyid, url);
437     if (!protected) {
438         warnx("eab_encode: jws_protected_eab failed");
439         goto out;
440     }
441 
442     payload = jws_jwk(a->key, NULL, NULL);
443     if (!payload) {
444         warnx("eab_encode: jws_jwk failed");
445         goto out;
446     }
447 
448     jws = jws_encode_hmac(protected, payload, 256, a->eab_key);
449     if (!jws) {
450         warnx("eab_encode: jws_encode_hmac failed");
451         goto out;
452     }
453 
454     if (g_loglevel > 2)
455         warnx("eab_encode: payload=%s protected=%s jws=%s",
456                 payload, protected, jws);
457 
458 out:
459     free(protected);
460     free(payload);
461     return jws;
462 }
463 
account_new(acme_t * a,bool yes)464 bool account_new(acme_t *a, bool yes)
465 {
466     const char *url = json_find_string(a->dir, "newAccount");
467     if (!url) {
468         warnx("failed to find newAccount URL in directory");
469         return false;
470     }
471 
472     msg(1, "creating new account at %s", url);
473     switch (acme_post(a, url, "{\"onlyReturnExisting\":true}")) {
474         case 200:
475             if (!(a->kid = find_header(a->headers, "Location")))
476             {
477                 warnx("account exists but location not found");
478                 return false;
479             }
480             warnx("Account already exists at %s", a->kid);
481             return false;
482 
483         case 400:
484             if (a->json && a->type &&
485                     strcasestr(a->type, "application/problem+json") &&
486                     json_compare_string(a->json, "type",
487                       "urn:ietf:params:acme:error:accountDoesNotExist") == 0) {
488                 const json_value_t *meta = json_find(a->dir, "meta");
489                 const char *ext = json_find_value(meta,
490                         "externalAccountRequired");
491                 if (ext && strcasecmp(ext, "true") == 0 && !a->eab_key) {
492                     msg(0, "this ACME server requires external credentials, "
493                            "please supply them with -e KEYID:KEY");
494                     return false;
495                 }
496                 const char *terms = json_find_string(meta, "termsOfService");
497                 if (terms) {
498                     if (yes)
499                         msg(0, "terms at %s autoaccepted (-y)", terms);
500                     else {
501                         char c = 0;
502                         msg(0, "type 'y' to accept the terms at %s", terms);
503                         if (scanf(" %c", &c) != 1 || tolower(c) != 'y') {
504                             warnx("terms not agreed to, aborted");
505                             return false;
506                         }
507                     }
508                 }
509                 char *payload = strdup("\"termsOfServiceAgreed\":true");
510                 if (!payload) {
511                     warn("account_new: strdup failed");
512                     return false;
513                 }
514                 if (a->email && strlen(a->email)) {
515                     char *tmp = NULL;
516                     if (asprintf(&tmp, "%s,\"contact\": [\"mailto:%s\"]",
517                                 payload, a->email) < 0) {
518                         warnx("account_new: asprintf failed");
519                         free(tmp);
520                         free(payload);
521                         return false;
522                     }
523                     free(payload);
524                     payload = tmp;
525                 }
526                 if (a->eab_key) {
527                     char *tmp = NULL;
528                     char *eab = eab_encode(a, url);
529                     if (!eab) {
530                         warnx("account_new: eab_encode failed");
531                         free(payload);
532                         return false;
533                     }
534                     if (asprintf(&tmp, "%s,\"externalAccountBinding\": %s",
535                                 payload, eab) < 0) {
536                         warnx("account_new: asprintf failed");
537                         free(tmp);
538                         free(eab);
539                         free(payload);
540                         return false;
541                     }
542                     free(eab);
543                     free(payload);
544                     payload = tmp;
545                 }
546                 int r = acme_post(a, url, "{%s}", payload);
547                 free(payload);
548                 if (r == 201) {
549                     if (acme_error(a))
550                         return false;
551                     if (json_compare_string(a->json, "status", "valid")) {
552                         const char *st = json_find_string(a->json, "status");
553                         warnx("account created but status is not valid (%s)",
554                                 st ? st : "unknown");
555                         return false;
556                     }
557                     if (!(a->kid = find_header(a->headers, "Location"))) {
558                         warnx("account created but location not found");
559                         return false;
560                     }
561                     msg(1, "account created at %s", a->kid);
562                     return true;
563                 }
564             }
565             // intentional fallthrough
566         default:
567             warnx("failed to create account at %s", url);
568             acme_error(a);
569             return false;
570     }
571 }
572 
account_retrieve(acme_t * a)573 bool account_retrieve(acme_t *a)
574 {
575     const char *url = json_find_string(a->dir, "newAccount");
576     if (!url) {
577         warnx("failed to find newAccount URL in directory");
578         return false;
579     }
580     msg(1, "retrieving account at %s", url);
581     switch (acme_post(a, url, "{\"onlyReturnExisting\":true}")) {
582         case 200:
583             if (acme_error(a))
584                 return false;
585             break;
586 
587         case 400:
588             if (a->json && a->type &&
589                     strcasestr(a->type, "application/problem+json") &&
590                     json_compare_string(a->json, "type",
591                       "urn:ietf:params:acme:error:accountDoesNotExist") == 0) {
592                 warnx("no account associated with %s/key.pem found at %s. "
593                         "Consider trying 'new'", a->keyprefix, url);
594                 return false;
595             }
596             // intentional fallthrough
597         default:
598             warnx("failed to retrieve account at %s", url);
599             acme_error(a);
600             return false;
601     }
602     const char *status = json_find_string(a->json, "status");
603     if (status && strcmp(status, "valid")) {
604         warnx("invalid account status (%s)", status);
605         return false;
606     }
607     if (!(a->kid = find_header(a->headers, "Location"))) {
608         warnx("account location not found");
609         return false;
610     }
611     msg(1, "account location: %s", a->kid);
612     a->account = a->json;
613     a->json = NULL;
614     return true;
615 }
616 
account_update(acme_t * a)617 bool account_update(acme_t *a)
618 {
619     bool email_update = false;
620     const json_value_t *contacts = json_find(a->account, "contact");
621     if (contacts && contacts->type != JSON_ARRAY) {
622         warnx("failed to parse account contacts");
623         return false;
624     }
625     if (a->email && strlen(a->email) > 0) {
626         if (!contacts || contacts->v.array.size == 0)
627             email_update = true;
628         else for (size_t i=0; i<contacts->v.array.size; i++) {
629             if (contacts->v.array.values[i].type != JSON_STRING ||
630                     strcasestr(contacts->v.array.values[i].v.value,
631                         "mailto:") != contacts->v.array.values[i].v.value) {
632                 warnx("failed to parse account contacts");
633                 return false;
634             }
635             if (strcasecmp(contacts->v.array.values[i].v.value
636                         + strlen("mailto:"), a->email))
637                 email_update = true;
638         }
639     }
640     else if (contacts && contacts->v.array.size > 0)
641         email_update = true;
642     if (email_update) {
643         int ret = 0;
644         if (a->email && strlen(a->email) > 0) {
645             msg(1, "updating account email to %s at %s", a->email, a->kid);
646             ret = acme_post(a, a->kid, "{\"contact\": [\"mailto:%s\"]}",
647                     a->email);
648         } else {
649             msg(1, "removing account email at %s", a->kid);
650             ret = acme_post(a, a->kid, "{\"contact\": []}");
651         }
652         if (ret != 200) {
653             warnx("failed to update account email at %s", a->kid);
654             acme_error(a);
655             return false;
656         } else if (acme_error(a))
657             return false;
658         msg(1, "account at %s updated", a->kid);
659     }
660     else
661         msg(1, "email is already up to date for account at %s", a->kid);
662     return true;
663 }
664 
account_keychange(acme_t * a,bool never,keytype_t type,int bits)665 bool account_keychange(acme_t *a, bool never, keytype_t type, int bits)
666 {
667     bool success = false;
668     privkey_t newkey = NULL;
669     char *newkeyfile = NULL;
670     char *keyfile = NULL;
671     char *bakfile = NULL;
672     char *protected = NULL;
673     char *payload = NULL;
674     char *jwk = NULL;
675     char *jws = NULL;
676     const char *url = json_find_string(a->dir, "keyChange");
677     if (!url) {
678         warnx("account_keychange: failed to find keyChange URL in directory");
679         goto out;
680     }
681 
682     if (asprintf(&keyfile, "%s/key.pem", a->keyprefix) < 0) {
683         warnx("account_keychange: asprintf failed");
684         keyfile = NULL;
685         goto out;
686     }
687 
688     if (asprintf(&bakfile, "%s/key-%llu.pem", a->keyprefix,
689                 (unsigned long long)time(NULL)) < 0) {
690         warnx("account_keychange: asprintf failed");
691         bakfile = NULL;
692         goto out;
693     }
694 
695     if (asprintf(&newkeyfile, "%s/newkey.pem", a->keyprefix) < 0) {
696         warnx("account_keychange: asprintf failed");
697         newkeyfile = NULL;
698         goto out;
699     }
700 
701     newkey = key_load(never ? PK_NONE : type, bits, newkeyfile);
702     if (!newkey)
703         goto out;
704 
705     protected = jws_protected_jwk(NULL, url, newkey);
706     if (!protected) {
707         warnx("account_keychange: jws_protected_jwk failed");
708         goto out;
709     }
710 
711     jwk = jws_jwk(a->key, NULL, NULL);
712     if (!jwk) {
713         warnx("account_keychange: jws_jwk failed");
714         goto out;
715     }
716 
717     if (asprintf(&payload, "{\"account\":\"%s\",\"oldKey\":%s}",
718                 a->kid, jwk) < 0) {
719         warnx("account_keychange: asprintf failed");
720         payload = NULL;
721         goto out;
722     }
723 
724     jws = jws_encode(protected, payload, newkey);
725     if (!jws) {
726         warnx("account_keychange: jws_encode failed");
727         goto out;
728     }
729 
730     if (g_loglevel > 2)
731         warnx("account_keychange: url=%s payload=%s protected=%s jws=%s",
732                 url, payload, protected, jws);
733     else if (g_loglevel > 1)
734         warnx("account_keychange: url=%s payload=%s", url, payload);
735 
736     msg(1, "changing account key at %s", url);
737     if (acme_post(a, url, jws) != 200) {
738         warnx("failed to change account key at %s", url);
739         acme_error(a);
740         goto out;
741     } else if (acme_error(a))
742         goto out;
743 
744     msg(1, "backing up %s as %s", keyfile, bakfile);
745     if (link(keyfile, bakfile) < 0)
746         warn("failed to link %s to %s", bakfile, keyfile);
747     else {
748         msg(1, "renaming %s to %s", newkeyfile, keyfile);
749         if (rename(newkeyfile, keyfile) < 0) {
750             warn("failed to rename %s to %s", newkeyfile, keyfile);
751             unlink(bakfile);
752         } else {
753             msg(1, "account key changed");
754             success = true;
755         }
756     }
757     if (!success) {
758         warnx("WARNING: account key changed but %s NOT replaced by %s",
759                 keyfile, newkeyfile);
760         goto out;
761     }
762 out:
763     if (newkey)
764         privkey_deinit(newkey);
765     free(newkeyfile);
766     free(keyfile);
767     free(bakfile);
768     free(protected);
769     free(payload);
770     free(jwk);
771     free(jws);
772     return success;
773 }
774 
account_deactivate(acme_t * a)775 bool account_deactivate(acme_t *a)
776 {
777     msg(1, "deactivating account at %s", a->kid);
778     if (acme_post(a, a->kid, "{\"status\": \"deactivated\"}") != 200) {
779         warnx("failed to deactivate account at %s", a->kid);
780         acme_error(a);
781         return false;
782     } else if (acme_error(a))
783         return false;
784     msg(1, "account at %s deactivated", a->kid);
785     return true;
786 }
787 
authorize(acme_t * a)788 bool authorize(acme_t *a)
789 {
790     bool success = false;
791     char *thumbprint = NULL;
792     json_value_t *auth = NULL;
793     const json_value_t *auths = json_find(a->order, "authorizations");
794     if (!auths || auths->type != JSON_ARRAY) {
795         warnx("failed to parse authorizations URL");
796         goto out;
797     }
798 
799     thumbprint = jws_thumbprint(a->key);
800     if (!thumbprint)
801         goto out;
802 
803     for (size_t i=0; i<auths->v.array.size; i++) {
804         if (auths->v.array.values[i].type != JSON_STRING) {
805             warnx("failed to parse authorizations URL");
806             goto out;
807         }
808         msg(1, "retrieving authorization at %s",
809                 auths->v.array.values[i].v.value);
810         if (acme_post(a, auths->v.array.values[i].v.value, "") != 200) {
811             warnx("failed to retrieve auth %s",
812                     auths->v.array.values[i].v.value);
813             acme_error(a);
814             goto out;
815         }
816         const char *status = json_find_string(a->json, "status");
817         if (status && strcmp(status, "valid") == 0)
818             continue;
819         if (!status || strcmp(status, "pending") != 0) {
820             warnx("unexpected auth status (%s) at %s",
821                 status ? status : "unknown",
822                 auths->v.array.values[i].v.value);
823             acme_error(a);
824             goto out;
825         }
826         const json_value_t *ident = json_find(a->json, "identifier");
827         const char *ident_type = json_find_string(ident, "type");
828         if (!ident_type || (strcmp(ident_type, "dns") != 0 &&
829                 strcmp(ident_type, "ip") != 0)) {
830             warnx("no valid identifier in auth %s",
831                     auths->v.array.values[i].v.value);
832             goto out;
833         }
834         const char *ident_value = json_find_string(ident, "value");
835         if (!ident_value || strlen(ident_value) <= 0) {
836             warnx("no valid identifier in auth %s",
837                     auths->v.array.values[i].v.value);
838             goto out;
839         }
840         const json_value_t *chlgs = json_find(a->json, "challenges");
841         if (!chlgs || chlgs->type != JSON_ARRAY) {
842             warnx("no challenges in auth %s",
843                     auths->v.array.values[i].v.value);
844             goto out;
845         }
846         json_free(auth);
847         auth = a->json;
848         a->json = NULL;
849 
850         bool chlg_done = false;
851         for (size_t j=0; j<chlgs->v.array.size && !chlg_done; j++) {
852             const char *status = json_find_string(
853                     chlgs->v.array.values+j, "status");
854             if (status && (strcmp(status, "pending") == 0
855                         || strcmp(status, "processing") == 0)) {
856                 const char *url = json_find_string(
857                         chlgs->v.array.values+j, "url");
858                 const char *type = json_find_string(
859                         chlgs->v.array.values+j, "type");
860                 const char *token = json_find_string(
861                         chlgs->v.array.values+j, "token");
862                 char *key_auth = NULL;
863                 if (!type || !url || !token) {
864                     warnx("failed to parse challenge");
865                     goto out;
866                 }
867                 if (strcmp(type, "dns-01") == 0 ||
868                         strcmp(type, "tls-alpn-01") == 0)
869                     key_auth = sha2_base64url(256, "%s.%s", token, thumbprint);
870                 else if (asprintf(&key_auth, "%s.%s", token, thumbprint) < 0)
871                     key_auth = NULL;
872                 if (!key_auth) {
873                     warnx("failed to generate authorization key");
874                     goto out;
875                 }
876                 if (a->hook && strlen(a->hook) > 0) {
877                     msg(2, "type=%s", type);
878                     msg(2, "ident=%s", ident_value);
879                     msg(2, "token=%s", token);
880                     msg(2, "key_auth=%s", key_auth);
881                     msg(1, "running %s %s %s %s %s %s", a->hook, "begin",
882                             type, ident_value, token, key_auth);
883                     int r = hook_run(a->hook, "begin", type, ident_value, token,
884                             key_auth);
885                     msg(2, "hook returned %d", r);
886                     if (r < 0) {
887                         free(key_auth);
888                         goto out;
889                     } else if (r > 0) {
890                         msg(1, "challenge %s declined", type);
891                         free(key_auth);
892                         continue;
893                     }
894                 } else {
895                     char c = 0;
896                     msg(0, "challenge=%s ident=%s token=%s key_auth=%s",
897                         type, ident_value, token, key_auth);
898                     msg(0, "type 'y' followed by a newline to accept challenge"
899                             ", anything else to skip");
900                     if (scanf(" %c", &c) != 1 || tolower(c) != 'y') {
901                         free(key_auth);
902                         continue;
903                     }
904                 }
905 
906                 msg(1, "starting challenge at %s", url);
907                 if (acme_post(a, url, "{}") != 200) {
908                     warnx("failed to start challenge at %s", url);
909                     acme_error(a);
910                 } else while (!chlg_done) {
911                     msg(1, "polling challenge status at %s", url);
912                     if (acme_post(a, url, "") != 200) {
913                         warnx("failed to poll challenge status at %s", url);
914                         acme_error(a);
915                         break;
916                     }
917                     const char *status = json_find_string(a->json, "status");
918                     if (status && strcmp(status, "valid") == 0)
919                         chlg_done = true;
920                     else if (!status || (strcmp(status, "processing") != 0 &&
921                             strcmp(status, "pending") != 0)) {
922                         warnx("challenge %s failed with status %s",
923                                 url, status ? status : "unknown");
924                         acme_error(a);
925                         break;
926                     } else {
927                         msg(2, "challenge %s, waiting 5 seconds", status);
928                         sleep(5);
929                     }
930                 }
931                 if (a->hook && strlen(a->hook) > 0) {
932                     const char *method = chlg_done ? "done" : "failed";
933                     msg(1, "running %s %s %s %s %s %s", a->hook, method,
934                             type, ident_value, token, key_auth);
935                     hook_run(a->hook, method, type, ident_value, token,
936                             key_auth);
937                 }
938                 free(key_auth);
939                 if (!chlg_done)
940                     goto out;
941             }
942         }
943         if (!chlg_done) {
944             warnx("no challenge completed");
945             goto out;
946         }
947     }
948 
949     success = true;
950 
951 out:
952     json_free(auth);
953     free(thumbprint);
954     return success;
955 }
956 
cert_issue(acme_t * a,char * const * names,const char * csr)957 bool cert_issue(acme_t *a, char * const *names, const char *csr)
958 {
959     bool success = false;
960     char *orderurl = NULL;
961     char *certfile = NULL;
962     char *bakfile = NULL;
963     char *tmpfile = NULL;
964     char *cert = NULL;
965     time_t t = time(NULL);
966     int fd = -1;
967     char *ids = identifiers(names);
968     if (!ids) {
969         warnx("failed to process alternate names");
970         goto out;
971     }
972 
973     const char *url = json_find_string(a->dir, "newOrder");
974     if (!url) {
975         warnx("failed to find newOrder URL in directory");
976         goto out;
977     }
978 
979     msg(1, "creating new order at %s", url);
980     if (acme_post(a, url, ids) != 201)
981     {
982         warnx("failed to create new order at %s", url);
983         acme_error(a);
984         goto out;
985     }
986     const char *status = json_find_string(a->json, "status");
987     if (!status || (strcmp(status, "pending") && strcmp(status, "ready"))) {
988         warnx("invalid order status (%s)", status ? status : "unknown");
989         acme_error(a);
990         goto out;
991     }
992     orderurl = find_header(a->headers, "Location");
993     if (!orderurl) {
994         warnx("order location not found");
995         goto out;
996     }
997     msg(1, "order location: %s", orderurl);
998     a->order = a->json;
999     a->json = NULL;
1000 
1001     if (strcmp(status, "ready") != 0) {
1002         if (!authorize(a)) {
1003             warnx("failed to authorize order at %s", orderurl);
1004             goto out;
1005         }
1006         while (1) {
1007             msg(1, "polling order status at %s", orderurl);
1008             if (acme_post(a, orderurl, "") != 200) {
1009                 warnx("failed to poll order status at %s", orderurl);
1010                 acme_error(a);
1011                 goto out;
1012             }
1013             status = json_find_string(a->json, "status");
1014             if (status && strcmp(status, "ready") == 0) {
1015                 json_free(a->order);
1016                 a->order = a->json;
1017                 a->json = NULL;
1018                 break;
1019             }
1020             else if (!status || strcmp(status, "pending") != 0) {
1021                 warnx("unexpected order status (%s) at %s",
1022                         status ? status : "unknown", orderurl);
1023                 acme_error(a);
1024                 goto out;
1025             } else {
1026                 msg(2, "order pending, waiting 5 seconds");
1027                 sleep(5);
1028             }
1029         }
1030     }
1031 
1032     const char *finalize = json_find_string(a->order, "finalize");
1033     if (!finalize) {
1034         warnx("failed to find finalize URL");
1035         goto out;
1036     }
1037 
1038     msg(1, "finalizing order at %s", finalize);
1039     if (acme_post(a, finalize, "{\"csr\": \"%s\"}", csr) != 200) {
1040         warnx("failed to finalize order at %s", finalize);
1041         acme_error(a);
1042         goto out;
1043     } else if (acme_error(a))
1044         goto out;
1045 
1046     while (1) {
1047         msg(1, "polling order status at %s", orderurl);
1048         if (acme_post(a, orderurl, "") != 200) {
1049             warnx("failed to poll order status at %s", orderurl);
1050             acme_error(a);
1051             goto out;
1052         }
1053         status = json_find_string(a->json, "status");
1054         if (status && strcmp(status, "valid") == 0) {
1055             json_free(a->order);
1056             a->order = a->json;
1057             a->json = NULL;
1058             break;
1059         } else if (!status || strcmp(status, "processing") != 0) {
1060             warnx("unexpected order status (%s) at %s",
1061                     status ? status : "unknown", orderurl);
1062             acme_error(a);
1063             goto out;
1064         } else {
1065             msg(2, "order processing, waiting 5 seconds");
1066             sleep(5);
1067         }
1068     }
1069 
1070     const char *certurl = json_find_string(a->order, "certificate");
1071     if (!certurl) {
1072         warnx("failed to parse certificate url");
1073         goto out;
1074     }
1075 
1076     msg(1, "retrieving certificate at %s", certurl);
1077     if (acme_post(a, certurl, "") != 200) {
1078         warnx("failed to retrieve certificate at %s", certurl);
1079         acme_error(a);
1080         goto out;
1081     } else if (acme_error(a)) {
1082         goto out;
1083     }
1084     cert = a->body;
1085     a->body = NULL;
1086 
1087     if (a->alt_n) {
1088         const char *regex = "^Link:[ \t]*<(.*)>[ \t]*.*;[ \t]*"
1089             "rel[ \t]*=[ \t]*\"?alternate(\"|[ \t]*;).*\r\n";
1090         regex_t reg;
1091         if (regcomp(&reg, regex, REG_EXTENDED | REG_ICASE | REG_NEWLINE)) {
1092             warnx("cert_issue: regcomp failed");
1093             goto out;
1094         } else {
1095             size_t i;
1096             regmatch_t m[2];
1097             char *alt_cert = NULL;
1098             char *h = NULL;
1099             char *headers = strdup(a->headers);
1100             if (!headers) {
1101                 warn("cert_issue: strdup failed");
1102                 goto out;
1103             }
1104             a->headers = NULL;
1105             h = headers;
1106             for (i = 0; i < a->alt_n && regexec(&reg, h, 2, m, 0) == 0; i++) {
1107                 if (a->alt_fp_len > 0 || i == a->alt_n - 1) {
1108                     char *alturl = strndup(h + m[1].rm_so,
1109                             m[1].rm_eo - m[1].rm_so);
1110                     if (!alturl) {
1111                         warn("cert_issue: strndup failed");
1112                         break;
1113                     }
1114                     msg(1, "retrieving alternate certificate at %s", alturl);
1115                     if (acme_post(a, alturl, "") != 200) {
1116                         warnx("failed to retrieve alternate certificate at %s",
1117                                 alturl);
1118                         acme_error(a);
1119                         free(alturl);
1120                         break;
1121                     }
1122                     free(alturl);
1123                     if (cert_match(a->body, a->alt_fp, a->alt_fp_len)) {
1124                         alt_cert = a->body;
1125                         a->body = NULL;
1126                         break;
1127                     }
1128                 }
1129                 h += m[1].rm_eo;
1130             }
1131             free(headers);
1132             regfree(&reg);
1133             if (alt_cert) {
1134                 free(cert);
1135                 cert = alt_cert;
1136             } else
1137                 warnx("no matching alternate certificate found, "
1138                         "falling back to default");
1139         }
1140     }
1141 
1142     if (asprintf(&certfile, "%scert.pem", a->certprefix) < 0) {
1143         certfile = NULL;
1144         warnx("cert_issue: asprintf failed");
1145         goto out;
1146     }
1147 
1148     if (asprintf(&tmpfile, "%scert.pem.tmp", a->certprefix) < 0) {
1149         tmpfile = NULL;
1150         warnx("cert_issue: asprintf failed");
1151         goto out;
1152     }
1153 
1154     if (asprintf(&bakfile, "%scert-%llu.pem", a->certprefix,
1155                 (unsigned long long)t) < 0) {
1156         bakfile = NULL;
1157         warnx("cert_issue: asprintf failed");
1158         goto out;
1159     }
1160 
1161     msg(1, "saving certificate to %s", tmpfile);
1162     fd = open(tmpfile, O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IRGRP|S_IROTH);
1163     if (fd < 0) {
1164         warn("failed to create %s", tmpfile);
1165         goto out;
1166     }
1167 
1168     if (write(fd, cert, strlen(cert)) != (ssize_t)strlen(cert)) {
1169         warn("failed to write to %s", tmpfile);
1170         goto out;
1171     }
1172 
1173     if (close(fd) < 0) {
1174         warn("failed to close %s", tmpfile);
1175         goto out;
1176     } else
1177         fd = -1;
1178 
1179     if (link(certfile, bakfile) < 0) {
1180         if (errno != ENOENT) {
1181             warn("failed to link %s to %s", bakfile, certfile);
1182             goto out;
1183         }
1184     } else
1185         msg(1, "backed up %s as %s", certfile, bakfile);
1186 
1187     msg(1, "renaming %s to %s", tmpfile, certfile);
1188     if (rename(tmpfile, certfile) < 0) {
1189         warn("failed to rename %s to %s", tmpfile, certfile);
1190         unlink(bakfile);
1191         goto out;
1192     }
1193 
1194     success = true;
1195 out:
1196     if (fd >= 0)
1197         close(fd);
1198     free(bakfile);
1199     free(tmpfile);
1200     free(certfile);
1201     free(ids);
1202     free(orderurl);
1203     free(cert);
1204     return success;
1205 }
1206 
cert_revoke(acme_t * a,const char * certfile,int reason_code)1207 bool cert_revoke(acme_t *a, const char *certfile, int reason_code)
1208 {
1209     bool success = false;
1210     char *certfiledup = NULL;
1211     char *revokedfile = NULL;
1212     const char *url = NULL;
1213     char *crt = cert_der_base64url(certfile);
1214     if (!crt) {
1215         warnx("failed to load %s", certfile);
1216         goto out;
1217     }
1218 
1219     url = json_find_string(a->dir, "revokeCert");
1220     if (!url) {
1221         warnx("failed to find revokeCert URL in directory");
1222         goto out;
1223     }
1224 
1225     msg(1, "revoking %s at %s", certfile, url);
1226     if (acme_post(a, url, "{\"certificate\":\"%s\",\"reason\":%d}", crt,
1227             reason_code) != 200) {
1228         warnx("failed to revoke %s at %s", certfile, url);
1229         acme_error(a);
1230         goto out;
1231     } else if (acme_error(a))
1232         goto out;
1233 
1234     msg(1, "revoked %s", certfile);
1235     certfiledup = strdup(certfile);
1236     if (!certfiledup) {
1237         warnx("cert_revoke: strdup failed");
1238         certfiledup = NULL;
1239         goto out;
1240     }
1241     if (asprintf(&revokedfile, "%s/revoked-%llu.pem", dirname(certfiledup),
1242                 (unsigned long long)time(NULL)) < 0) {
1243         warnx("cert_revoke: asprintf failed");
1244         revokedfile = NULL;
1245         goto out;
1246     }
1247     msg(1, "renaming %s to %s", certfile, revokedfile);
1248     if (rename(certfile, revokedfile) < 0)
1249         warn("failed to rename %s to %s", certfile, revokedfile);
1250 
1251     success = true;
1252 out:
1253     free(crt);
1254     free(revokedfile);
1255     free(certfiledup);
1256     return success;
1257 }
1258 
validate_identifier_str(const char * s)1259 bool validate_identifier_str(const char *s)
1260 {
1261     int dots = 0;
1262     size_t len = 0;
1263     if (is_ip(s, 0, 0))
1264         return true;
1265     for (size_t j = 0; j < strlen(s); j++) {
1266         switch (s[j]) {
1267             case '.':
1268                 if (j == 0) {
1269                     warnx("'.' not allowed at beginning in %s", s);
1270                     return false;
1271                 }
1272                 dots++;
1273                 // intentional fallthrough
1274             case '_':
1275             case '-':
1276                 len++;
1277                 continue;
1278             case '*':
1279                 if (j != 0 || s[1] != '.') {
1280                     warnx("'*.' only allowed at beginning in %s", s);
1281                     return false;
1282                 }
1283                 break;
1284             default:
1285                 if (!isupper(s[j]) && !islower(s[j]) && !isdigit(s[j])) {
1286                     warnx("invalid character '%c' in %s", s[j], s);
1287                     return false;
1288                 }
1289                 len++;
1290         }
1291     }
1292     if (len == 0) {
1293         warnx("empty identifier is not allowed");
1294         return false;
1295     }
1296     if (dots == 0) {
1297         warnx("identifier '%s' has no dots", s);
1298         return false;
1299     }
1300     return true;
1301 }
1302 
eab_parse(acme_t * a,char * eab)1303 bool eab_parse(acme_t *a, char *eab)
1304 {
1305     regmatch_t m[3];
1306     regex_t reg;
1307     if (regcomp(&reg, "^([^:]+):([-_A-Za-z0-9]+)$",
1308                 REG_EXTENDED | REG_NEWLINE)) {
1309         warnx("eab_parse: regcomp failed");
1310         return false;
1311     }
1312 
1313     int r = regexec(&reg, eab, sizeof(m)/sizeof(m[0]), m, 0);
1314     regfree(&reg);
1315 
1316     if (r) {
1317         warnx("-e credentials must be specified as 'KEYID:KEY', "
1318                 "with KEY base64url encoded");
1319         return false;
1320     }
1321 
1322     eab[m[1].rm_eo] = 0;
1323     a->eab_keyid = eab + m[1].rm_so;
1324 
1325     eab[m[2].rm_eo] = 0;
1326     a->eab_key = eab + m[2].rm_so;
1327 
1328     return true;
1329 }
1330 
alt_parse(acme_t * a,char * alt)1331 bool alt_parse(acme_t *a, char *alt)
1332 {
1333     char *endptr;
1334     long l = strtol(alt, &endptr, 0);
1335     if (*endptr == 0 && l > 0 && l < UINT_MAX) {
1336         a->alt_n = l;
1337         return true;
1338     }
1339 
1340     size_t len = 0;
1341     char *tok = strtok(alt, ":");
1342     while (tok && len < sizeof(a->alt_fp)) {
1343         if (strlen(tok) != 2 || !isxdigit(tok[0]) || !isxdigit(tok[1]))
1344             break;
1345         a->alt_fp[len++] = strtol(tok, NULL, 16);
1346         tok = strtok(NULL, ":");
1347     }
1348     if (!tok && len > 1) {
1349         a->alt_fp_len = len;
1350         a->alt_n = UINT_MAX;
1351         return true;
1352     }
1353 
1354     warnx("-l requires a positive argument or a SHA256 fingerprint");
1355     return false;
1356 }
1357 
usage(const char * progname)1358 void usage(const char *progname)
1359 {
1360     fprintf(stderr,
1361         "usage: %s [-a|--acme-url URL] [-b|--bits BITS] [-c|--confdir DIR]\n"
1362         "\t[-d|--days DAYS] [-e|--eab KEYID:KEY] [-f|--force] [-h|--hook PROG]\n"
1363         "\t[-l|--alternate [N | SHA256]] [-m|--must-staple] [-n|--never-create]\n"
1364         "\t[-o|--no-ocsp] [-s|--staging] [-t|--type RSA | EC]\n"
1365         "\t[-v|--verbose ...] [-V|--version] [-y|--yes] [-?|--help]\n"
1366         "\tnew [EMAIL] | update [EMAIL] | deactivate | newkey |\n"
1367         "\tissue IDENTIFIER [ALTNAME ...]] | issue CSRFILE |\n"
1368         "\trevoke CERTFILE [CERTKEYFILE]\n", progname);
1369 }
1370 
version(const char * progname)1371 void version(const char *progname)
1372 {
1373     fprintf(stderr, "%s: version " PACKAGE_VERSION "\n"
1374             "Copyright (C) 2019-2021 Nicola Di Lieto\n\n"
1375             "%s is free software: you can redistribute and/or modify\n"
1376             "it under the terms of the GNU General Public License as\n"
1377             "published by the Free Software Foundation, either version 3\n"
1378             "of the License, or (at your option) any later version.\n\n"
1379             "%s is distributed in the hope that it will be useful, but\n"
1380             "WITHOUT ANY WARRANTY; without even the implied warranty of\n"
1381             "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n\n"
1382             "See https://www.gnu.org/licenses/gpl.html for more details.\n",
1383             progname, progname, progname);
1384 }
1385 
main(int argc,char ** argv)1386 int main(int argc, char **argv)
1387 {
1388     static struct option options[] = {
1389         {"acme-url",     required_argument, NULL, 'a'},
1390         {"bits",         required_argument, NULL, 'b'},
1391         {"confdir",      required_argument, NULL, 'c'},
1392         {"days",         required_argument, NULL, 'd'},
1393         {"eab",          required_argument, NULL, 'e'},
1394         {"force",        no_argument,       NULL, 'f'},
1395         {"help",         no_argument,       NULL, '?'},
1396         {"hook",         required_argument, NULL, 'h'},
1397         {"alternate",    required_argument, NULL, 'l'},
1398         {"must-staple",  no_argument,       NULL, 'm'},
1399         {"never-create", no_argument,       NULL, 'n'},
1400         {"no-ocsp",      no_argument,       NULL, 'o'},
1401         {"staging",      no_argument,       NULL, 's'},
1402         {"type",         required_argument, NULL, 't'},
1403         {"verbose",      no_argument,       NULL, 'v'},
1404         {"version",      no_argument,       NULL, 'V'},
1405         {"yes",          no_argument,       NULL, 'y'},
1406         {NULL,           0,                 NULL, 0}
1407     };
1408 
1409     int ret = 2;
1410     bool never = false;
1411     bool force = false;
1412     bool yes = false;
1413     bool staging = false;
1414     bool custom_directory = false;
1415     bool status_req = false;
1416     bool status_check = true;
1417     int days = 30;
1418     int bits = 0;
1419     keytype_t type = PK_RSA;
1420     const char *ident = NULL;
1421     char *filename = NULL;
1422     char *csr = NULL;
1423     char **names = NULL;
1424     const char *confdir = DEFAULT_CONFDIR;
1425     char *keyprefix = NULL;
1426     privkey_t key = NULL;
1427     acme_t a;
1428     memset(&a, 0, sizeof(a));
1429     a.directory = PRODUCTION_URL;
1430 
1431     if (argc < 2) {
1432         usage(basename(argv[0]));
1433         return ret;
1434     }
1435 
1436 #if LIBCURL_VERSION_NUM < 0x072600
1437 #error libcurl version 7.38.0 or later is required
1438 #endif
1439     const curl_version_info_data *cvid = curl_version_info(CURLVERSION_NOW);
1440     if (!cvid || cvid->version_num < 0x072600) {
1441         warnx("libcurl version 7.38.0 or later is required");
1442         return ret;
1443     }
1444 
1445     if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) {
1446         warnx("failed to initialize libcurl");
1447         return ret;
1448     }
1449 
1450     if (!crypto_init()) {
1451         warnx("failed to initialize crypto library");
1452         curl_global_cleanup();
1453         return ret;
1454     }
1455 
1456     while (1) {
1457         char *endptr;
1458         int option_index;
1459         int c = getopt_long(argc, argv, "a:b:c:d:e:f?h:l:mnost:vVy",
1460                 options, &option_index);
1461         if (c == -1) break;
1462         switch (c) {
1463             case 'a':
1464                 if (staging) {
1465                     warnx("-a,--acme-url is incompatible with -s,--staging");
1466                     goto out;
1467                 }
1468                 custom_directory = true;
1469                 a.directory = optarg;
1470                 break;
1471 
1472             case 'b':
1473                 bits = strtol(optarg, &endptr, 10);
1474                 if (*endptr != 0 || bits <= 0) {
1475                     warnx("BITS must be a positive integer");
1476                     goto out;
1477                 }
1478                 break;
1479 
1480             case 'c':
1481                 confdir = optarg;
1482                 break;
1483 
1484             case 'd':
1485                 days = strtol(optarg, &endptr, 10);
1486                 if (*endptr != 0 || days <= 0) {
1487                     warnx("DAYS must be a positive integer");
1488                     goto out;
1489                 }
1490                 break;
1491 
1492             case 'e':
1493                 if (!eab_parse(&a, optarg))
1494                     goto out;
1495                 break;
1496 
1497             case 'f':
1498                 force = true;
1499                 break;
1500 
1501             case 'h':
1502                 a.hook = optarg;
1503                 break;
1504 
1505             case 'l':
1506                 if (!alt_parse(&a, optarg))
1507                     goto out;
1508                 break;
1509 
1510             case 'm':
1511                 status_req = true;
1512                 break;
1513 
1514             case 'n':
1515                 never = true;
1516                 break;
1517 
1518             case 'o':
1519                 status_check = false;
1520                 break;
1521 
1522             case 'v':
1523                 g_loglevel++;
1524                 break;
1525 
1526             case 's':
1527                 if (custom_directory) {
1528                     warnx("-s,--staging is incompatible with -a,--acme-url");
1529                     goto out;
1530                 }
1531                 staging = true;
1532                 a.directory = STAGING_URL;
1533                 break;
1534 
1535             case 't':
1536                 if (strcasecmp(optarg, "RSA") == 0)
1537                     type = PK_RSA;
1538                 else if (strcasecmp(optarg, "EC") == 0)
1539                     type = PK_EC;
1540                 else {
1541                     warnx("type must be either RSA or EC");
1542                     goto out;
1543                 }
1544                 break;
1545 
1546              case 'V':
1547                 version(basename(argv[0]));
1548                 goto out;
1549                 break;
1550 
1551             case 'y':
1552                 yes = true;
1553                 break;
1554 
1555             default:
1556                 usage(basename(argv[0]));
1557                 goto out;
1558         }
1559     }
1560 
1561     switch (type) {
1562         case PK_RSA:
1563             if (bits == 0)
1564                 bits = 2048;
1565             else if (bits < 2048 || bits > 8192) {
1566                 warnx("BITS must be between 2048 and 8192 for RSA keys");
1567                 goto out;
1568             }
1569             else if (bits & 7) {
1570                 warnx("BITS must be a multiple of 8 for RSA keys");
1571                 goto out;
1572             }
1573             break;
1574 
1575         case PK_EC:
1576             switch (bits) {
1577                 case 0:
1578                     bits = 256;
1579                     break;
1580 
1581                 case 256:
1582                 case 384:
1583                     break;
1584 
1585                 default:
1586                     warnx("BITS must be either 256 or 384 for EC keys");
1587                     goto out;
1588             }
1589             break;
1590 
1591         default:
1592             warnx("key type must be either RSA or EC");
1593             goto out;
1594     }
1595 
1596     if (optind == argc) {
1597         usage(basename(argv[0]));
1598         goto out;
1599     }
1600 
1601     const char *action = argv[optind++];
1602     if (strcmp(action, "new") == 0 || strcmp(action, "update") == 0) {
1603         if (optind < argc)
1604             a.email = argv[optind++];
1605         if (optind < argc) {
1606             usage(basename(argv[0]));
1607             goto out;
1608         }
1609     } else if (strcmp(action, "newkey") == 0
1610             || strcmp(action, "deactivate") == 0) {
1611         if (optind < argc) {
1612             usage(basename(argv[0]));
1613             goto out;
1614         }
1615     } else if (strcmp(action, "issue") == 0) {
1616         if (optind == argc) {
1617             usage(basename(argv[0]));
1618             goto out;
1619         }
1620         struct stat st;
1621         if (stat(argv[optind], &st)) {
1622             if (errno != ENOENT && errno != EACCES) {
1623                 warn("failed to stat %s", argv[optind]);
1624                 goto out;
1625             }
1626         } else if (S_ISREG(st.st_mode)) {
1627             filename = strdup(argv[optind++]);
1628             if (!filename) {
1629                 warn("strdup failed");
1630                 goto out;
1631             }
1632             if (optind < argc) {
1633                 usage(basename(argv[0]));
1634                 goto out;
1635             }
1636         }
1637         if (!filename) {
1638             int i = 0;
1639             for (i = 0; argv[optind + i]; i++)
1640                 if (!validate_identifier_str(argv[optind + i]))
1641                     goto out;
1642             if (i == 0) {
1643                 usage(basename(argv[0]));
1644                 goto out;
1645             }
1646             names = calloc(i + 1, sizeof(*names));
1647             if (!names) {
1648                 warn("calloc failed");
1649                 goto out;
1650             }
1651             while (i--) {
1652                 names[i] = strdup(argv[optind + i]);
1653                 if (!names[i]) {
1654                     warn("strdup failed");
1655                     goto out;
1656                 }
1657             }
1658             ident = names[0];
1659             if (ident[0] == '*' && ident[1] == '.')
1660                 ident += 2;
1661         }
1662     } else if (strcmp(action, "revoke") == 0) {
1663         if (optind == argc) {
1664             usage(basename(argv[0]));
1665             goto out;
1666         }
1667         filename = strdup(argv[optind++]);
1668         if (!filename) {
1669             warn("strdup failed");
1670             goto out;
1671         }
1672         if (access(filename, R_OK)) {
1673             warn("failed to read %s", filename);
1674             goto out;
1675         }
1676         if (optind < argc) {
1677             const char *keyfile = argv[optind++];
1678             a.key = key_load(PK_NONE, bits, keyfile);
1679             if (!a.key)
1680                 goto out;
1681         }
1682         if (optind < argc) {
1683             usage(basename(argv[0]));
1684             goto out;
1685         }
1686     } else {
1687         usage(basename(argv[0]));
1688         goto out;
1689     }
1690 
1691     time_t now = time(NULL);
1692     char buf[0x100];
1693     setlocale(LC_TIME, "C");
1694     strftime(buf, sizeof(buf), "%a, %d %b %Y %H:%M:%S %z", localtime(&now));
1695     if (strstr(PACKAGE_VERSION, "-dev-")) {
1696         warnx("development version " PACKAGE_VERSION " starting on %s", buf);
1697         warnx("please use for testing only; releases are available at "
1698                 "https://github.com/ndilieto/uacme/tree/upstream/latest");
1699     } else
1700         msg(1, "version " PACKAGE_VERSION " starting on %s", buf);
1701 
1702     if (a.hook && access(a.hook, R_OK | X_OK) < 0) {
1703         warn("%s", a.hook);
1704         goto out;
1705     }
1706 
1707     if (!a.key) {
1708         if (asprintf(&a.keyprefix, "%s/private", confdir) < 0) {
1709             a.keyprefix = NULL;
1710             warnx("asprintf failed");
1711             goto out;
1712         }
1713 
1714         if (ident) {
1715             if (asprintf(&keyprefix, "%s/private/%s", confdir, ident) < 0) {
1716                 keyprefix = NULL;
1717                 warnx("asprintf failed");
1718                 goto out;
1719             }
1720 
1721             if (asprintf(&a.certprefix, "%s/%s/", confdir, ident) < 0) {
1722                 a.certprefix = NULL;
1723                 warnx("asprintf failed");
1724                 goto out;
1725             }
1726         }
1727 
1728         bool is_new = strcmp(action, "new") == 0;
1729         if (!check_or_mkdir(is_new && !never, confdir,
1730                     S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH))
1731             goto out;
1732 
1733         if (!check_or_mkdir(is_new && !never, a.keyprefix, S_IRWXU))
1734             goto out;
1735 
1736         a.key = key_load((!is_new || never) ? PK_NONE : type, bits,
1737                 "%s/key.pem", a.keyprefix);
1738         if (!a.key)
1739             goto out;
1740     }
1741 
1742     if (strcmp(action, "new") == 0) {
1743         if (acme_bootstrap(&a) && account_new(&a, yes))
1744             ret = 0;
1745     } else if (strcmp(action, "update") == 0) {
1746         if (acme_bootstrap(&a) && account_retrieve(&a) && account_update(&a))
1747             ret = 0;
1748     } else if (strcmp(action, "newkey") == 0) {
1749         if (acme_bootstrap(&a) && account_retrieve(&a)
1750                 && account_keychange(&a, never, type, bits))
1751             ret = 0;
1752     } else if (strcmp(action, "deactivate") == 0) {
1753         if (acme_bootstrap(&a) && account_retrieve(&a)
1754                 && account_deactivate(&a))
1755             ret = 0;
1756     } else if (strcmp(action, "issue") == 0) {
1757         if (filename) {
1758             int len = strlen(filename);
1759             char *dot = strrchr(filename, '.');
1760 
1761             if (dot)
1762                 len = dot - filename;
1763 
1764             if (asprintf(&a.certprefix, "%.*s-", len, filename) < 0) {
1765                 a.certprefix = NULL;
1766                 warnx("asprintf failed");
1767                 goto out;
1768             }
1769 
1770             csr = csr_load(filename, &names);
1771             if (!csr)
1772                 goto out;
1773 
1774             if (status_req)
1775                 warnx("-m, --must-staple is ignored when issuing with a CSR");
1776         } else {
1777             if (!check_or_mkdir(!never, keyprefix, S_IRWXU))
1778                 goto out;
1779 
1780             if (!check_or_mkdir(!never, a.certprefix,
1781                         S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH))
1782                 goto out;
1783 
1784             key = key_load(never ? PK_NONE : type, bits, "%s/key.pem",
1785                     keyprefix);
1786             if (!key)
1787                 goto out;
1788         }
1789 
1790         free(filename);
1791         if (asprintf(&filename, "%scert.pem", a.certprefix) < 0) {
1792             filename = NULL;
1793             warnx("asprintf failed");
1794             goto out;
1795         }
1796 
1797         msg(1, "checking existence and expiration of %s", filename);
1798         if (cert_valid(filename, names, days, status_check)) {
1799             if (force)
1800                 msg(1, "forcing reissue of %s", filename);
1801             else {
1802                 msg(1, "skipping %s", filename);
1803                 ret = 1;
1804                 goto out;
1805             }
1806         }
1807 
1808         if (!csr) {
1809             msg(1, "generating certificate request");
1810             csr = csr_gen(names, status_req, key);
1811             if (!csr) {
1812                 warnx("failed to generate certificate request");
1813                 goto out;
1814             }
1815         }
1816 
1817         if (acme_bootstrap(&a) && account_retrieve(&a)
1818                 && cert_issue(&a, names, csr))
1819             ret = 0;
1820     } else if (strcmp(action, "revoke") == 0) {
1821         if (acme_bootstrap(&a) && (!a.keyprefix || account_retrieve(&a)) &&
1822                 cert_revoke(&a, filename, 0))
1823             ret = 0;
1824     }
1825 
1826 out:
1827     json_free(a.json);
1828     json_free(a.account);
1829     json_free(a.dir);
1830     json_free(a.order);
1831     free(a.nonce);
1832     free(a.kid);
1833     free(a.headers);
1834     free(a.body);
1835     free(a.type);
1836     free(a.keyprefix);
1837     free(a.certprefix);
1838     if (a.key)
1839         privkey_deinit(a.key);
1840     if (key)
1841         privkey_deinit(key);
1842     free(keyprefix);
1843     free(csr);
1844     free(filename);
1845     for (int i = 0; names && names[i]; i++)
1846         free(names[i]);
1847     free(names);
1848     crypto_deinit();
1849     curl_global_cleanup();
1850     exit(ret);
1851 }
1852 
1853