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(®ex, "^%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(®, regex, REG_EXTENDED | REG_ICASE | REG_NEWLINE)) {
108 warnx("find_header: regcomp failed");
109 } else {
110 regmatch_t m[2];
111 if (regexec(®, 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(®);
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(®, 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(®, 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(®);
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(®, "^([^:]+):([-_A-Za-z0-9]+)$",
1308 REG_EXTENDED | REG_NEWLINE)) {
1309 warnx("eab_parse: regcomp failed");
1310 return false;
1311 }
1312
1313 int r = regexec(®, eab, sizeof(m)/sizeof(m[0]), m, 0);
1314 regfree(®);
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