diff options
author | Florian Obser <florian@cvs.openbsd.org> | 2016-08-31 22:01:43 +0000 |
---|---|---|
committer | Florian Obser <florian@cvs.openbsd.org> | 2016-08-31 22:01:43 +0000 |
commit | b079979c58ecf59474cdfd312647eea6408870ef (patch) | |
tree | 793fccb82966d239250ce0c7023c1c421be082dd /usr.sbin/acme-client | |
parent | 70677c65a4fa7b624d3f5a75e33afd3ad0dbefc7 (diff) |
Import Kristaps' letskencrypt and call it acme-client in tree.
OK to get it in deraadt@ (and probably beck@)
At least deraadt@, beck@ and otto@ are fine with the name and the
disagreements stopped.
Diffstat (limited to 'usr.sbin/acme-client')
25 files changed, 7854 insertions, 0 deletions
diff --git a/usr.sbin/acme-client/ChangeLog b/usr.sbin/acme-client/ChangeLog new file mode 100644 index 00000000000..51a88c65bb4 --- /dev/null +++ b/usr.sbin/acme-client/ChangeLog @@ -0,0 +1,1186 @@ +2016-08-19 07:17 kristaps + + * letskencrypt.1 (tags: VERSION_0_1_10): Merge + https://github.com/kristapsdz/letskencrypt-portable/pull/6 -- + thanks, Bernard! + +2016-08-06 12:45 kristaps + + * Makefile (tags: VERSION_0_1_10): Allow a fake-install prefix. + From https://github.com/kristapsdz/letskencrypt/pull/16 . + +2016-08-06 12:38 kristaps + + * letskencrypt.1: Document -a. + +2016-08-06 12:37 kristaps + + * main.c (tags: VERSION_0_1_10): Allow overriding agreement and + also update usage message. + +2016-08-06 12:37 kristaps + + * extern.h, netproc.c (utags: VERSION_0_1_10): Adding override for + agreement with -a flag. Noted (and partially patched) in + https://github.com/kristapsdz/letskencrypt/pull/15 and by + ``pasta'' over e-mail -- thanks! + +2016-07-28 08:37 kristaps + + * letskencrypt.1: Document backing up. + +2016-07-28 08:32 kristaps + + * extern.h, fileproc.c (tags: VERSION_0_1_10), main.c: Initial + backing-up of certificates. Suggested by + https://github.com/kristapsdz/letskencrypt/issues/12 and + https://github.com/kristapsdz/letskencrypt/issues/9 + +2016-07-16 05:59 kristaps + + * acctproc.c (tags: VERSION_0_1_10), keyproc.c (tags: + VERSION_0_1_10), main.c (utags: VERSION_0_1_9): Properly check -n + and -N existence in main.c, allowing them to propogate to netproc + and so on. This reverts a prior change that was incomplete. + +2016-07-16 05:11 kristaps + + * extern.h (tags: VERSION_0_1_9): Allow PATH_VAR_EMPTY to be + overridden. Apparently not all systems have this directory, so + let them provide their own. + +2016-07-16 05:10 kristaps + + * acctproc.c, keyproc.c, letskencrypt.1 (tags: VERSION_0_1_9), + main.c: When using -N or -n, try to open the key-file first, then + only create it if it doesn't exist. This allows using -nN even + after first creating the files. From + https://github.com/kristapsdz/letskencrypt/issues/8 + +2016-07-12 23:51 kristaps + + * letskencrypt.1: Fix example and reorder exit status + documentation. + +2016-07-12 23:12 kristaps + + * letskencrypt.1: Note new exit codes, change "mkdir -m" for mkdir + and chmod (not all systems have the -m flag), and use the return + codes in the example. + +2016-07-12 23:06 kristaps + + * main.c: Fix usage message, fix error message to be a bit more + useful (as noted in + https://github.com/kristapsdz/letskencrypt-portable/issues/3 ) + and finally change the error code to be "2" if nothing changed on + the disc, otherwise 0 on success (the certificates updated) and 1 + on failure. + +2016-07-12 23:04 kristaps + + * fileproc.c (tags: VERSION_0_1_9): Return a special error code + when we update certificates. + +2016-07-12 23:02 kristaps + + * extern.h, util.c (tags: VERSION_0_1_10, VERSION_0_1_9): Add check + for extended error code (i.e., exit status of 2). + +2016-07-12 00:11 kristaps + + * util.c (tags: VERSION_0_1_8): Silence a coverity issue. No + logical change. + +2016-07-11 23:42 kristaps + + * main.c (tags: VERSION_0_1_8): Fix access invocation. + +2016-07-11 23:22 kristaps + + * letskencrypt.1 (tags: VERSION_0_1_8): Add manual bits for -N, + domain key registration. + +2016-07-11 23:19 kristaps + + * main.c: Turn on domain key creation. + +2016-07-11 23:18 kristaps + + * keyproc.c (tags: VERSION_0_1_8): Note domain key, not account + key. + +2016-07-11 23:18 kristaps + + * Makefile (tags: VERSION_0_1_9, VERSION_0_1_8), keyproc.c: Adding + key creation to keyproc. + +2016-07-11 23:08 kristaps + + * Makefile, acctproc.c (tags: VERSION_0_1_8), rsa.c (tags: + VERSION_0_1_10, VERSION_0_1_9, VERSION_0_1_8), rsa.h (tags: + VERSION_0_1_10, VERSION_0_1_9, VERSION_0_1_8): Move rsa key + creation and loading into their own file (and header) for usage + (current) by acctproc and (pending) keyproc. + +2016-07-11 23:07 kristaps + + * extern.h (tags: VERSION_0_1_8): Cosmetic fix. + +2016-07-11 22:40 kristaps + + * extern.h, keyproc.c, main.c: Beginning of code to let the keyproc + create a new RSA domain key. This was prompted by + https://github.com/kristapsdz/letskencrypt/issues/7 + +2016-07-09 22:34 kristaps + + * letskencrypt.1: Add some example usage. + +2016-07-02 04:24 kristaps + + * http.c (tags: VERSION_0_1_10, VERSION_0_1_9, VERSION_0_1_8): On + OpenBSD 5.7, tls_read and family behave strangely: account for + that. + +2016-07-02 02:59 kristaps + + * http.c: OpenBSD 5.7 needs stdint.h for uintptr_t. + +2016-07-02 00:19 kristaps + + * main.c: Use isalnum instead of isalpha for domain name + validation. Submitted by Remco and as + https://github.com/kristapsdz/letskencrypt/pull/5 + +2016-06-27 23:25 kristaps + + * acctproc.c (tags: VERSION_0_1_7): Have creation of account key be + properly umasked. From a patch by Remco---thanks! + +2016-06-27 22:51 kristaps + + * README.md (tags: VERSION_0_1_10, VERSION_0_1_9, VERSION_0_1_8, + VERSION_0_1_7): Update to note NetBSD. + +2016-06-25 04:45 kristaps + + * acctproc.c: Also move the key creation into an RSA-specific + format, directly from a patch by Remco---thanks! + +2016-06-25 04:38 kristaps + + * acctproc.c: Split out more RSA-specific functions, from a + modified patch by Remco---thanks! + +2016-06-25 04:22 kristaps + + * acctproc.c, extern.h (tags: VERSION_0_1_7), json.c (tags: + VERSION_0_1_10, VERSION_0_1_9, VERSION_0_1_8, VERSION_0_1_7): + Note RSA-specific functions as patched by Remco---thanks! + +2016-06-25 04:03 kristaps + + * main.c (tags: VERSION_0_1_7): Memory leak in error path. + +2016-06-25 03:59 kristaps + + * dnsproc.c (tags: VERSION_0_1_10, VERSION_0_1_9, VERSION_0_1_8, + VERSION_0_1_7), main.c: Initialise variable and also downgrade + "cached" message to trace mode. + +2016-06-25 00:57 kristaps + + * letskencrypt.1 (tags: VERSION_0_1_7): Document multi-domain + setup. + +2016-06-25 00:50 kristaps + + * netproc.c (tags: VERSION_0_1_9, VERSION_0_1_8, VERSION_0_1_7): + Don't modify the input buffer when tracing! + +2016-06-25 00:18 kristaps + + * main.c: Introduce -m (not documented while I test it) that + appends the initial domain to all paths. This makes it easier to + use in systems where one's invoking letskencrypt multiple times. + Also add some simple validation of the domain names to prevent + them from (1) trampling the directory structure and (2) being + bogus in general. + +2016-06-24 23:16 kristaps + + * netproc.c: Do what the documentation says regarding -v -v and + dump buffers. + +2016-06-03 05:02 kristaps + + * letskencrypt.1 (tags: VERSION_0_1_6): Be more specific about + RSA-ness. + +2016-06-03 05:00 kristaps + + * keyproc.c (tags: VERSION_0_1_7, VERSION_0_1_6): Remove dependency + on RSA for the domain key. This completely lifts the + restrictions, as the certificate creation is opaque. This is + from a patch by Remco--thanks! + +2016-06-03 04:58 kristaps + + * acctproc.c (tags: VERSION_0_1_6): Require key to be RSA (for + now). This builds on a patch submitted by Remco--thank you! + +2016-06-03 04:50 kristaps + + * acctproc.c: Remove dependency on RSA for account key. This is + only the first step, and makes the key extraction be generic. It + will need more interoperability with the signing process to + actually work. + +2016-06-03 03:27 kristaps + + * extern.h (tags: VERSION_0_1_6): Fix typo noted by @kAworu in + https://github.com/kristapsdz/letskencrypt/pull/3 -- thanks! + +2016-06-02 05:06 kristaps + + * keyproc.c: Fix an error-path memory leak and make more specific + notes as to why I'm not touching the memory (right now) of this + mystery function. This raised by @kAworu in pull/2 -- thanks! + +2016-06-02 04:42 kristaps + + * json.c (tags: VERSION_0_1_6): Handle zero-length arrays and mark + file-scoped function as static. The former from a patch by + @kAworu in pull/2 -- thanks! + +2016-06-02 04:38 kristaps + + * json.c: Correct allocation size as noted by @kAworu in pull/2. + +2016-06-02 04:02 kristaps + + * http.c (tags: VERSION_0_1_7, VERSION_0_1_6): Protect against + zero-length read (EOF) freeing with the realloc. Found by + Remco--thanks! + +2016-06-01 15:54 kristaps + + * acctproc.c, certproc.c (tags: VERSION_0_1_10, VERSION_0_1_9, + VERSION_0_1_8, VERSION_0_1_7, VERSION_0_1_6), chngproc.c (tags: + VERSION_0_1_10, VERSION_0_1_9, VERSION_0_1_8, VERSION_0_1_7, + VERSION_0_1_6), dnsproc.c (tags: VERSION_0_1_6), fileproc.c + (tags: VERSION_0_1_8, VERSION_0_1_7, VERSION_0_1_6), keyproc.c, + main.c (tags: VERSION_0_1_6), netproc.c (tags: VERSION_0_1_6), + revokeproc.c (tags: VERSION_0_1_10, VERSION_0_1_9, VERSION_0_1_8, + VERSION_0_1_7, VERSION_0_1_6): Kick out a lot of old header files + we don't use any more. + +2016-06-01 15:29 kristaps + + * letskencrypt.1: Remove implementation notes. Suggested by + deraadt@ among others. + +2016-06-01 15:03 kristaps + + * acctproc.c, certproc.c, dnsproc.c, extern.h, keyproc.c, + letskencrypt.1, main.c, netproc.c, revokeproc.c: Prune out + setting a user: this is no longer necessary as all procs not + fully pledged need the root user for chroot. + +2016-06-01 15:01 kristaps + + * util-pledge.c (tags: VERSION_0_1_10, VERSION_0_1_9, + VERSION_0_1_8, VERSION_0_1_7, VERSION_0_1_6): Fix compilation. + +2016-06-01 14:56 kristaps + + * acctproc.c, certproc.c, dnsproc.c, extern.h, keyproc.c, main.c, + netproc.c, revokeproc.c, util-pledge.c: The user-drop now occurs + only in the -portable code, so we don't want to carry around all + sorts of extra information we don't use. + +2016-06-01 14:19 kristaps + + * util-pledge.c: Re-add chroot(2) for file process and challenge + process. This is because the whitepath doesn't exist (yet) for + the former, and the challenge process has unknown files in a + known path. + +2016-06-01 14:18 kristaps + + * sandbox-pledge.c (tags: VERSION_0_1_10, VERSION_0_1_9, + VERSION_0_1_8, VERSION_0_1_7, VERSION_0_1_6): Remove comment + about rename(2) (ack'd by deraadt@) and remove rpath from inet + pledge, which was a holdover from libcurl. + +2016-06-01 13:33 kristaps + + * main.c: Fix usage. patch by Caspar Schutijser--thanks! + +2016-05-26 08:38 kristaps + + * Makefile (tags: VERSION_0_1_7, VERSION_0_1_6), chroot-pledge.c, + util-pledge.c: Rename chroot-pledge into util-pledge, which is + more appropriate. + +2016-05-26 08:30 kristaps + + * Makefile, chroot-pledge.c, dnsproc.c: Make the compiler happy. + +2016-05-26 08:25 kristaps + + * chroot-pledge.c, util.c (tags: VERSION_0_1_7, VERSION_0_1_6): + What's the point of dropping root privileges if root can't do + anything? + +2016-05-26 08:19 kristaps + + * Makefile, chroot-pledge.c, extern.h, main.c, util.c: "Embrace the + pledge". + +2016-05-25 04:46 kristaps + + * http.c (tags: VERSION_0_1_5): Try to close connection properly. + +2016-05-25 04:35 kristaps + + * http.c: Figure out what tls_close does with the socket. + +2016-05-25 04:34 kristaps + + * revokeproc.c (tags: VERSION_0_1_5): Fix scanning ahead for SAN + DNS entries. + +2016-05-25 03:47 kristaps + + * netproc.c (tags: VERSION_0_1_5): Have sreq and nreq return the + HTTP error or -1, remove some debugging messages, use the new + http_get members instead of calling functions. "Clean-up." + +2016-05-25 03:46 kristaps + + * http.c, http.h (tags: VERSION_0_1_10, VERSION_0_1_9, + VERSION_0_1_8, VERSION_0_1_7, VERSION_0_1_6, VERSION_0_1_5): Make + sure the connection is closed as soon as the body is read and + also put the head and body buffer pointers into http_get. + +2016-05-25 03:43 kristaps + + * dnsproc.c (tags: VERSION_0_1_5): Cache the last DNS response and + return that, if the subsequent request is the same. + +2016-05-24 17:17 kristaps + + * README.md (tags: VERSION_0_1_6, VERSION_0_1_5): Clean up the + readme notes. + +2016-05-24 17:10 kristaps + + * http.c: Change feature test. + +2016-05-24 14:32 kristaps + + * http.c: Needed for compat glue. + +2016-05-24 13:32 kristaps + + * http.c: For the time being, allow tls_read/write to be ifdef'd + for two different versions of the API. + +2016-05-24 13:03 kristaps + + * Makefile (tags: VERSION_0_1_5): Fix lib. + +2016-05-24 11:04 kristaps + + * netproc.c: Don't use strdup() for bodies--they can be binary. + +2016-05-24 10:39 kristaps + + * netproc.c: Initial [working] removal of curl from netproc. This + replaces the nreq() and sreq() functions with those using http.h. + There is still a fair amount of superfluous debugging going on, + and the system isn't "optimum" regarding memory at all. + +2016-05-24 10:38 kristaps + + * http.c: Weaken TLS validation. Is this necessary? + +2016-05-24 10:37 kristaps + + * Makefile: Stop using curl, and instead use the home-grown http.c + and -tls. + +2016-05-24 10:36 kristaps + + * extern.h, util.c (utags: VERSION_0_1_5): Max the maximum DNS + entries be globally known and also add the new dnsproc comm + identifiers. + +2016-05-24 10:35 kristaps + + * dnsproc.c: Have dnsproc transfer both the IP/IPv6 to netproc and + also the family itself. Move the maximum number of queried + servers into extern.h. + +2016-05-24 09:42 kristaps + + * http.c, http.h: Continue fleshing out http.h implementation. + +2016-05-24 04:48 kristaps + + * http.c: Missing header for compilation. + +2016-05-24 04:42 kristaps + + * http.c: Start to kick out libcurl with a small HTTP client + originally inspired by https://github.com/snimmagadda/http. + +2016-05-22 12:04 kristaps + + * README.md (tags: VERSION_0_1_4): Note use of MIT license in + JSMN's files. + +2016-05-22 12:03 kristaps + + * Makefile, extern.h, json.c (tags: VERSION_0_1_5), netproc.c + (utags: VERSION_0_1_4): Kick out json-c in favour of jsmn (with + an array->tree wrapper). + +2016-05-22 11:55 kristaps + + * jsmn.c, jsmn.h (utags: VERSION_0_1_10, VERSION_0_1_4, + VERSION_0_1_5, VERSION_0_1_6, VERSION_0_1_7, VERSION_0_1_8, + VERSION_0_1_9): Put license directly into jsmn.c and jsmn.h, just + to be clear about it. + +2016-05-22 11:54 kristaps + + * jsmn.c, jsmn.h: Add JSMN: https://github.com/zserge/jsmn. + +2016-05-22 11:53 kristaps + + * certproc.c (tags: VERSION_0_1_5), revokeproc.c (utags: + VERSION_0_1_4): It's not clear whether the lengths returned by + the BIO are nil-terminated (and valgrind suggests they aren't), + so make sure that they are always nil-terminated. + +2016-05-20 13:09 kristaps + + * extern.h, main.c (tags: VERSION_0_1_5, VERSION_0_1_4), + revokeproc.c: When we start up, check that the domains listed on + the command-line are those on the certificate, if found. NOTE: + what if there's no SAN entry at all? + +2016-05-20 08:35 kristaps + + * Makefile: Forgotten PREFIX variable. + +2016-05-20 08:28 kristaps + + * keyproc.c (tags: VERSION_0_1_5, VERSION_0_1_4): Fix erroneous + check of realloc return value. + +2016-05-20 08:07 kristaps + + * letskencrypt.1 (tags: VERSION_0_1_5, VERSION_0_1_4, + VERSION_0_1_3): Fix typo. + +2016-05-20 08:07 kristaps + + * acctproc.c (tags: VERSION_0_1_5, VERSION_0_1_4), certproc.c, + chngproc.c (tags: VERSION_0_1_5, VERSION_0_1_4), dnsproc.c (tags: + VERSION_0_1_4), fileproc.c (tags: VERSION_0_1_5, VERSION_0_1_4), + keyproc.c, main.c, netproc.c, revokeproc.c (utags: + VERSION_0_1_3): No need to have the dropfs, dropprivs, or sandbox + functions double-report their error. + +2016-05-20 08:05 kristaps + + * main.c: Pre-check that the files exist: no need to fork if we + don't need to. Also, the console can get spammed by multiple + procs writing into stderr. + +2016-05-20 05:49 kristaps + + * Makefile (tags: VERSION_0_1_3), README.md (tags: VERSION_0_1_3), + letskencrypt.dot: Strip out dot-file and www rule: this all goes + into the letskencrypt-www repo. Strip down the README.md file to + only what's necessary. + +2016-05-19 17:02 kristaps + + * chngproc.c: Convert chngproc to ignore reader failure. + +2016-05-19 17:00 kristaps + + * certproc.c: Convert certproc to ignore reader failure. + +2016-05-19 16:49 kristaps + + * keyproc.c: Rename label to "out" (consistency) and allow for + reader failure. + +2016-05-19 16:40 kristaps + + * acctproc.c: Have acctproc properly handle reader termination. + +2016-05-19 16:33 kristaps + + * revokeproc.c: Last nit: make writestr also be ok if the reader + has exited. + +2016-05-19 16:31 kristaps + + * revokeproc.c: Have revokeproc properly handle the case where the + reader fails. + +2016-05-19 16:29 kristaps + + * util.c (tags: VERSION_0_1_4, VERSION_0_1_3): Have the writer + functions notify us whether the reader has exited. + +2016-05-19 15:56 kristaps + + * acctproc.c, certproc.c, chngproc.c, dnsproc.c, keyproc.c, + netproc.c, revokeproc.c: Have writeop, writestr, and writebuf all + return -1 on failure, 0 on end of file (epipe), and 1 on success. + This addresses all the callers. + +2016-05-19 09:01 kristaps + + * README.md (tags: VERSION_0_0_5): We're no longer just using the + staging server. Here we go! + +2016-05-19 09:01 kristaps + + * letskencrypt.1, main.c (utags: VERSION_0_0_5): Document the -s + flag. + +2016-05-19 08:59 kristaps + + * extern.h (tags: VERSION_0_1_3, VERSION_0_0_5), main.c, netproc.c + (tags: VERSION_0_0_5): Flip on real versus staging servers. + bsd.lv is now eating its dogfood. + +2016-05-19 08:58 kristaps + + * keyproc.c (tags: VERSION_0_0_5): Fix how SAN is registered with + the key. In prior versions, we were having one SAN entry per + domain. However, apparently this is not allowed; instead we now + have a single SAN extension entry with the full list. + +2016-05-19 07:58 kristaps + + * certproc.c, fileproc.c (utags: VERSION_0_0_4, VERSION_0_0_5): + Make filenames in debug messages more meaningful. + +2016-05-19 07:22 kristaps + + * netproc.c (tags: VERSION_0_0_4): Forgot to close revokeproc + channel. + +2016-05-19 06:10 kristaps + + * README.md (tags: VERSION_0_0_4): Remove the coverity note (that's + going into the -portable version). + +2016-05-19 06:09 kristaps + + * extern.h (tags: VERSION_0_0_4), json.c (tags: VERSION_0_1_3, + VERSION_0_0_5, VERSION_0_0_4), netproc.c: Significantly clean up + the handling of HTTP document bodies: first, only invoke the JSON + functions locally, within a doXXXX function; second, don't read + into the JSON parser, but into an intermediary buffer (allowing + us to dump it on error); third, move some fetch bodies from the + main netproc() function into their own functions; and lastly, + store the CA nonce agency as a variable (we'll use this later + when using other servers). + +2016-05-19 05:51 kristaps + + * main.c (tags: VERSION_0_0_4): Remove debugging message. + +2016-05-19 05:09 kristaps + + * netproc.c: Push communication-related parameters into struct + conn. Makes the code a bit more readable. + +2016-05-19 05:08 kristaps + + * main.c: No functional change: just order getopt() parameters for + easier search. + +2016-05-18 16:03 kristaps + + * letskencrypt.1 (tags: VERSION_0_0_4), main.c: Allow overriding + the priv-drop user. + +2016-05-18 15:32 kristaps + + * util.c (tags: VERSION_0_0_5, VERSION_0_0_4): Use strsignal() + instead of a hack. + +2016-05-18 15:22 kristaps + + * dbg.c (tags: VERSION_0_1_10, VERSION_0_1_9, VERSION_0_1_8, + VERSION_0_1_7, VERSION_0_1_6, VERSION_0_1_5, VERSION_0_1_4, + VERSION_0_1_3, VERSION_0_0_5, VERSION_0_0_4), extern.h, main.c, + util.c: Remove all logging in favour of warnx et al. Remove + wrong-headed attempt at setproctitle. + +2016-05-18 13:01 kristaps + + * acctproc.c (tags: VERSION_0_0_5, VERSION_0_0_4), certproc.c, + chngproc.c (tags: VERSION_0_0_5, VERSION_0_0_4), dbg.c, extern.h, + fileproc.c, json.c, keyproc.c (tags: VERSION_0_0_4), netproc.c, + revokeproc.c (tags: VERSION_0_0_5, VERSION_0_0_4), + sandbox-pledge.c (tags: VERSION_0_1_5, VERSION_0_1_4, + VERSION_0_1_3, VERSION_0_0_5, VERSION_0_0_4), util.c: Kick out + dowarn in favour of warn. + +2016-05-18 12:53 kristaps + + * acctproc.c, certproc.c, chngproc.c, dbg.c, dnsproc.c (tags: + VERSION_0_0_5, VERSION_0_0_4), extern.h, fileproc.c, keyproc.c, + netproc.c, revokeproc.c, util.c: + Replace dowarnx() with warnx(). + +2016-05-18 12:35 kristaps + + * main.c: Start using setproctitle(). First step in kicking out + dbg.c. + +2016-05-18 11:44 kristaps + + * util.c: Move setresuid goop into -portable. + +2016-05-18 07:18 kristaps + + * README.md: Fix broken link. + +2016-05-18 07:10 kristaps + + * README.md (tags: VERSION_0_0_3): Note -portable and FreeBSD. + +2016-05-18 06:37 kristaps + + * dnsproc.c (tags: VERSION_0_0_3): FreeBSD nit. This will be + smoothed out in subsequent improvement of -portable. + +2016-05-18 06:36 kristaps + + * README.md: Note that we now do revocation. + +2016-05-18 06:19 kristaps + + * letskencrypt.1 (tags: VERSION_0_0_3): Update the manpage with + revocation instructions. + +2016-05-18 05:54 kristaps + + * README.md, sandbox-pledge.c (tags: VERSION_0_0_3): Require + OpenBSD >= 5.9. + +2016-05-18 05:49 kristaps + + * util.c (tags: VERSION_0_0_3): Catch buffers larger than BUFSIZ + bytes. + +2016-05-18 05:32 kristaps + + * sandbox-pledge.c: Add forgotten break statement. + +2016-05-18 05:30 kristaps + + * Makefile (tags: VERSION_0_0_5, VERSION_0_0_4, VERSION_0_0_3): + Clean up the Makefile now that we have less cruft. + +2016-05-18 05:25 kristaps + + * Makefile, README.md, config.h, main.c (tags: VERSION_0_0_3), + sandbox-pledge.c: Start stripping out compatibility, which is now + in letskencrypt-portable. + +2016-05-18 05:00 kristaps + + * Makefile, acctproc.c (tags: VERSION_0_0_3), certproc.c (tags: + VERSION_0_0_3), chngproc.c (tags: VERSION_0_0_3), dnsproc.c, + extern.h (tags: VERSION_0_0_3), fileproc.c (tags: VERSION_0_0_3), + keyproc.c (tags: VERSION_0_0_3), netproc.c (tags: VERSION_0_0_3), + revokeproc.c (tags: VERSION_0_0_3), sandbox-pledge.c: Split all + sandbox operations into their own file. This is part of the + ongoing re-structure into the main and -portable branch. + +2016-05-18 04:31 kristaps + + * Makefile, acctproc.c, base64.c (tags: VERSION_0_1_10, + VERSION_0_1_9, VERSION_0_1_8, VERSION_0_1_7, VERSION_0_1_6, + VERSION_0_1_5, VERSION_0_1_4, VERSION_0_1_3, VERSION_0_0_5, + VERSION_0_0_4, VERSION_0_0_3), certproc.c, chngproc.c, config.h, + dbg.c (tags: VERSION_0_0_3), dnsproc.c, extern.h, fileproc.c, + json.c (tags: VERSION_0_0_3), keyproc.c, main.c, netproc.c, + revokeproc.c, util.c: Finish the revocation function. Also, + start to lay the groundwork for a -portable and OpenBSD version + of the software with a guarded config.h inclusion. + +2016-05-17 08:06 kristaps + + * letskencrypt.1: Add note on revokeproc to manpage. + +2016-05-17 08:04 kristaps + + * main.c: Plug still-open fd. + +2016-05-17 07:55 kristaps + + * netproc.c: Re-add accidentally-removed check for certificate + non-expiration. + +2016-05-17 07:52 kristaps + + * README.md, extern.h, letskencrypt.1, letskencrypt.dot (tags: + VERSION_0_0_5, VERSION_0_0_4, VERSION_0_0_3), main.c, netproc.c, + revokeproc.c: Check for expiration date of certificate, if found. + This makes it possible to simply run letskencrypt as a cronjob + without worrying about overloading the ACME server. + +2016-05-17 07:51 kristaps + + * chngproc.c: Have chngproc's magic testing phase (which isn't an + official option) not have files made in the challengedir at all. + +2016-05-17 05:47 kristaps + + * Makefile, dbg.c, revokeproc.c: Add in the initial framework for + checking certificate revocation times. + +2016-05-17 05:46 kristaps + + * netproc.c: Continue cleaning up operations (in netproc). + +2016-05-17 05:45 kristaps + + * acctproc.c, certproc.c, chngproc.c, dnsproc.c, extern.h, + fileproc.c, main.c, util.c: Continue making operations more + semantically meaningful. Continue building in revocation + facility. + +2016-05-16 17:21 kristaps + + * certproc.c, extern.h, json.c, main.c, netproc.c: Initial steps of + revocation. This is pretty straightforward. + +2016-05-16 15:48 kristaps + + * README.md: Continue to polish the documentation. + +2016-05-16 15:25 kristaps + + * acctproc.c, certproc.c, chngproc.c, fileproc.c: When closing out, + close sockets first to cause depending processes to bail earlier. + +2016-05-16 15:25 kristaps + + * netproc.c: Failing start-up for account or key proc doesn't error + us. + +2016-05-16 15:24 kristaps + + * letskencrypt.1: Calm down people afraid of root. + +2016-05-16 15:14 kristaps + + * acctproc.c, certproc.c, extern.h, keyproc.c, netproc.c, util.c: + Have the keyproc and acctproc notify the netproc when they've + started, and have the netproc wait til they have: there's no + point in talking to Let's Encrypt if these services haven't + started. + +2016-05-16 12:07 kristaps + + * Makefile, README.md, letskencrypt.1, letskencrypt.dot: Add some + media for the GH site and fix a mistake in the manpage. + +2016-05-16 10:10 kristaps + + * README.md: Update README a bit. + +2016-05-16 10:06 kristaps + + * letskencrypt.1: Be more terse in the implementation notes. + +2016-05-16 09:58 kristaps + + * chngproc.c, extern.h, main.c: Add a secret and undocumented + feature that allows me to create files in the challenge directory + on another system. Don't use this. + +2016-05-16 09:57 kristaps + + * dnsproc.c: Have dnsproc properly return (and not exit) like the + other processes. + +2016-05-16 09:51 kristaps + + * netproc.c: Fix CID 111099. + +2016-05-16 09:47 kristaps + + * util.c: Fix CID 111100. + +2016-05-16 09:38 kristaps + + * README.md, letskencrypt.1: More documentation notes on why Linux + and Mac OS X are a bad idea. + +2016-05-16 09:24 kristaps + + * letskencrypt.1: Update notes on dnsproc. + +2016-05-16 09:21 kristaps + + * README.md: Clean up the README. + +2016-05-16 09:20 kristaps + + * chngproc.c: Don't let the testing code make it out. + +2016-05-16 09:19 kristaps + + * netproc.c: We don't close any fds before the out, so don't check + them against -1. + +2016-05-16 09:19 kristaps + + * netproc.c: Remove comment that no longer belongs. + +2016-05-16 09:18 kristaps + + * acctproc.c: Account key doesn't need read permissions. + +2016-05-16 09:16 kristaps + + * main.c: Fix an off-by-one and also fix closing the DNS file + descriptor. + +2016-05-16 09:15 kristaps + + * netproc.c: Push the DNS resolution into one function for clarity. + Prune a lot of unused variables. + +2016-05-16 08:27 kristaps + + * dnsproc.c: Add the dnsproc manager. This was noted by deraadt@. + This does nothing but looks up addresses as used by netproc. + +2016-05-16 04:51 kristaps + + * Makefile, chngproc.c, dbg.c, extern.h, main.c, netproc.c, util.c: + Initial check-in of a separate process for DNS management. + +2016-05-16 03:57 kristaps + + * letskencrypt.1: Fix typo found by Anthony Bentley--thanks! + +2016-05-15 13:31 kristaps + + * main.c: Fix for https://github.com/kristapsdz/letskencrypt/pull/1 + posted by https://github.com/pozdnychev -- thanks! + +2016-05-15 11:47 kristaps + + * README.md, letskencrypt.1: Documentation on Linux. + +2016-05-15 11:44 kristaps + + * fileproc.c, netproc.c: Catch __attribute__ warnings. + +2016-05-15 11:39 kristaps + + * netproc.c: Fully demonstrate that chroot doesn't work on Linux + for netproc. + +2016-05-15 11:38 kristaps + + * chngproc.c: Linux compatibility. + +2016-05-15 11:37 kristaps + + * util.c: Drop privs in the correct order and make sure sys_signame + is not used on Linux. + +2016-05-15 11:00 kristaps + + * chngproc.c, fileproc.c: Continue minimising the pledges. + +2016-05-15 10:39 kristaps + + * chngproc.c: Reduce the number of pledges in chngproc. + +2016-05-15 10:24 kristaps + + * certproc.c, chngproc.c, extern.h, fileproc.c, keyproc.c, main.c, + netproc.c, util.c: Add forgotten waitpid for COMP_FILE and add + some readops to make sure that netproc failing doesn't cause + short reads and exits. + +2016-05-15 09:53 kristaps + + * extern.h, json.c: Function attributes for messages. + +2016-05-15 08:06 kristaps + + * acctproc.c, util.c: Fix a segfault. + +2016-05-15 07:41 kristaps + + * json.c, netproc.c: Properly catch when the challenge has been + verified. + +2016-05-15 07:35 kristaps + + * README.md, letskencrypt.1: More documentation. + +2016-05-15 06:58 kristaps + + * json.c: Add more documentation. + +2016-05-15 06:57 kristaps + + * keyproc.c: Catch error return code. + +2016-05-15 06:56 kristaps + + * acctproc.c: Documents key bits. + +2016-05-15 06:56 kristaps + + * letskencrypt.1: Consistency in naming "Let's Encrypt". + +2016-05-15 06:48 kristaps + + * acctproc.c, certproc.c, chngproc.c, extern.h, fileproc.c, + keyproc.c, main.c, netproc.c, util.c: Look again at the + relinquishing of privilege. Move privilege-dropping before the + pledge just for consistency. + +2016-05-14 21:39 kristaps + + * README.md: Again, fix links. + +2016-05-14 21:37 kristaps + + * README.md: Fix links. + +2016-05-14 21:31 kristaps + + * README.md: Small notes. + +2016-05-14 21:01 kristaps + + * certproc.c: De-constify. + +2016-05-14 21:00 kristaps + + * certproc.c, extern.h, fileproc.c, letskencrypt.1, main.c, util.c: + Cleanup and abstractions. + +2016-05-14 20:33 kristaps + + * certproc.c, extern.h, fileproc.c, letskencrypt.1, main.c, + netproc.c, util.c: Fully download chain and fullchain. + +2016-05-14 20:32 kristaps + + * dbg.c: Fix stdout/stderr in debugging messages. + +2016-05-14 18:34 kristaps + + * certproc.c, netproc.c: Minor formatting. + +2016-05-14 18:05 kristaps + + * Makefile, certproc.c, dbg.c, extern.h, fileproc.c, main.c, + util.c: Split out reading and verifying the certificate from + writing to the file. + +2016-05-14 17:46 kristaps + + * acctproc.c, certproc.c, chngproc.c, keyproc.c, main.c, netproc.c: + Move proccomp setting into main. + +2016-05-14 16:43 kristaps + + * acctproc.c, extern.h, json.c: Move more JSON into json.c. + +2016-05-14 16:26 kristaps + + * chngproc.c, dbg.c, extern.h, json.c, netproc.c: Move all JSON + things into json.c. + +2016-05-14 15:42 kristaps + + * acctproc.c, certproc.c, extern.h, keyproc.c, main.c: Clean-ups + for better readability. + +2016-05-14 10:39 kristaps + + * Makefile: Have BSD form by the default in the Makefile. + +2016-05-14 10:38 kristaps + + * README.md: Document compilation on Liinux. + +2016-05-14 10:38 kristaps + + * Makefile, acctproc.c, json.c, keyproc.c, main.c, netproc.c, + util.c: Compiling on Linux. + +2016-05-14 10:25 kristaps + + * LICENSE.md (tags: VERSION_0_1_10, VERSION_0_1_9, VERSION_0_1_8, + VERSION_0_1_7, VERSION_0_1_6, VERSION_0_1_5, VERSION_0_1_4, + VERSION_0_1_3, VERSION_0_0_5, VERSION_0_0_4, VERSION_0_0_3), + README.md: GitHub files. + +2016-05-14 10:14 kristaps + + * letskencrypt.1 (tags: VERSION_0_0_2): Naming. + +2016-05-14 10:13 kristaps + + * main.c, netproc.c (utags: VERSION_0_0_2): Set temporary directory + permissions. + +2016-05-14 10:06 kristaps + + * letskencrypt.1: More documentation. + +2016-05-14 10:03 kristaps + + * acctproc.c (tags: VERSION_0_0_2), certproc.c (tags: + VERSION_0_0_2), chngproc.c (tags: VERSION_0_0_2), extern.h (tags: + VERSION_0_0_2), keyproc.c (tags: VERSION_0_0_2), letskencrypt.1, + main.c, netproc.c: Priv dropping and more documentation. + +2016-05-14 09:38 kristaps + + * base64.c (tags: VERSION_0_0_2), dbg.c (tags: VERSION_0_0_2), + extern.h, main.c, netproc.c, util.c (tags: VERSION_0_0_2): + Privilege dropping (beginning). + +2016-05-14 09:12 kristaps + + * letskencrypt.1: More documentation. + +2016-05-14 07:18 kristaps + + * chngproc.c, extern.h, json.c (tags: VERSION_0_0_2), + letskencrypt.1, main.c, netproc.c, util.c: Fully-working cycle + with SAN enabled. + +2016-05-13 18:16 kristaps + + * Makefile (tags: VERSION_0_0_2), acctproc.c, extern.h, keyproc.c, + letskencrypt.1, main.c, netproc.c: Add manpage, continue working + in SAN. + +2016-05-13 16:48 kristaps + + * certproc.c, dbg.c, extern.h, json.c, keyproc.c, main.c, + netproc.c, util.c: Add initial support for SAN. + +2016-05-13 12:23 kristaps + + * acctproc.c, certproc.c: First fully-working version. + +2016-05-13 11:49 kristaps + + * Makefile, acctproc.c, certproc.c, chngproc.c, dbg.c, extern.h, + keyproc.c, main.c, netproc.c, util.c: Add certificate process to + manage certificates. Lots of cleanup w/r/t logging and process + titles. + +2016-05-13 11:08 kristaps + + * base64.c, extern.h, keyproc.c, netproc.c, util.c: Certificate + submission and download. + +2016-05-13 10:46 kristaps + + * acctproc.c, chngproc.c, extern.h, keyproc.c, netproc.c, util.c: + Fully working submission of certificate to CA. + +2016-05-13 09:59 kristaps + + * Makefile, acctproc.c, base64.c, chngproc.c, dbg.c, extern.h, + json.c, keyproc.c, main.c, netproc.c, util.c: Split out JSON + handling code. + +2016-05-13 09:44 kristaps + + * netproc.c, util.c: Clean some bugs found with scan-build. + +2016-05-13 09:40 kristaps + + * chngproc.c, netproc.c: Full challenge-request-response cycle in + place. + +2016-05-13 09:14 kristaps + + * acctproc.c, chngproc.c, keyproc.c, main.c, netproc.c, util.c: + Compiling on OpenBSD (prior to pledge). + +2016-05-13 09:06 kristaps + + * netproc.c: Retrying for challenge. + +2016-05-13 08:52 kristaps + + * acctproc.c, chngproc.c, extern.h, main.c, netproc.c, util.c: + Cleaning up writing/reading. + +2016-05-13 08:20 kristaps + + * Makefile, acctproc.c, chngproc.c, extern.h, keyproc.c, main.c, + netproc.c, util.c: Moving on to functionality of the + challenge-responds. + +2016-05-13 04:29 kristaps + + * acctproc.c, base64.c, extern.h, main.c, netproc.c, util.c: New + keys are now properly submitted. + +2016-05-12 18:55 kristaps + + * acctproc.c, dbg.c, netproc.c: Full connection to acme, fixing + syntax errors. + +2016-05-12 17:33 kristaps + + * Makefile, acctproc.c, netproc.c: Parsing of JSON directory. + +2016-05-12 17:09 kristaps + + * Makefile, acctproc.c, base64.c, dbg.c, extern.h, keyproc.c, + main.c, netproc.c, util.c: Still being built: push signing into + acctproc and have netproc properly start to handle the CA + interaction. + +2016-05-12 08:34 kristaps + + * acctproc.c, keyproc.c, netproc.c: Full creation of request + thumbprint. + +2016-05-12 08:07 kristaps + + * Makefile, acctproc.c, dbg.c, extern.h, keyproc.c, main.c, + netproc.c (utags: VERSION_0): Import first. + +2016-05-12 08:07 kristaps + + * Makefile, acctproc.c, dbg.c, extern.h, keyproc.c, main.c, + netproc.c: Initial revision + diff --git a/usr.sbin/acme-client/Makefile b/usr.sbin/acme-client/Makefile new file mode 100644 index 00000000000..b1633c2996e --- /dev/null +++ b/usr.sbin/acme-client/Makefile @@ -0,0 +1,40 @@ +PREFIX = /usr/local +CFLAGS += -g -W -Wall +OBJS = acctproc.o \ + base64.o \ + certproc.o \ + chngproc.o \ + dbg.o \ + dnsproc.o \ + fileproc.o \ + http.o \ + jsmn.o \ + json.o \ + keyproc.o \ + main.o \ + netproc.o \ + revokeproc.o \ + rsa.o \ + sandbox-pledge.o \ + util.o \ + util-pledge.o + +letskencrypt: $(OBJS) + $(CC) -o $@ $(OBJS) -ltls -lssl -lcrypto + +rsa.o acctproc.o keyproc.o: rsa.h + +jsmn.o json.o: jsmn.h + +http.o netproc.o: http.h + +install: letskencrypt + mkdir -p $(DESTDIR)$(PREFIX)/bin + mkdir -p $(DESTDIR)$(PREFIX)/man/man1 + install -m 0755 letskencrypt $(DESTDIR)$(PREFIX)/bin + install -m 0644 letskencrypt.1 $(DESTDIR)$(PREFIX)/man/man1 + +$(OBJS): extern.h + +clean: + rm -f letskencrypt $(OBJS) diff --git a/usr.sbin/acme-client/acctproc.c b/usr.sbin/acme-client/acctproc.c new file mode 100644 index 00000000000..9c0a04dfce3 --- /dev/null +++ b/usr.sbin/acme-client/acctproc.c @@ -0,0 +1,440 @@ +/* $Id: acctproc.c,v 1.1 2016/08/31 22:01:42 florian Exp $ */ +/* + * Copyright (c) 2016 Kristaps Dzonsons <kristaps@bsd.lv> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include <sys/stat.h> + +#include <err.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include <openssl/pem.h> +#include <openssl/rsa.h> +#include <openssl/rand.h> +#include <openssl/err.h> + +#include "extern.h" +#include "rsa.h" + +/* + * Converts a BIGNUM to the form used in JWK. + * This is essentially a base64-encoded big-endian binary string + * representation of the number. + */ +static char * +bn2string(const BIGNUM *bn) +{ + int len; + char *buf, *bbuf; + + /* Extract big-endian representation of BIGNUM. */ + + len = BN_num_bytes(bn); + if (NULL == (buf = malloc(len))) { + warn("malloc"); + return(NULL); + } else if (len != BN_bn2bin(bn, (unsigned char *)buf)) { + warnx("BN_bn2bin"); + free(buf); + return(NULL); + } + + /* Convert to base64url. */ + + if (NULL == (bbuf = base64buf_url(buf, len))) { + warnx("base64buf_url"); + free(buf); + return(NULL); + } + + free(buf); + return(bbuf); +} + +/* + * Extract the relevant RSA components from the key and create the JSON + * thumbprint from them. + */ +static char * +op_thumb_rsa(EVP_PKEY *pkey) +{ + char *exp, *mod, *json; + RSA *r; + + exp = mod = json = NULL; + + if (NULL == (r = EVP_PKEY_get1_RSA(pkey))) + warnx("EVP_PKEY_get1_RSA"); + else if (NULL == (mod = bn2string(r->n))) + warnx("bn2string"); + else if (NULL == (exp = bn2string(r->e))) + warnx("bn2string"); + else if (NULL == (json = json_fmt_thumb_rsa(exp, mod))) + warnx("json_fmt_thumb_rsa"); + + free(exp); + free(mod); + return(json); +} + +/* + * The thumbprint operation is used for the challenge sequence. + */ +static int +op_thumbprint(int fd, EVP_PKEY *pkey) +{ + char *thumb, *dig64; + int rc; + unsigned int digsz; + unsigned char *dig; + + EVP_MD_CTX *ctx; + + rc = 0; + thumb = dig64 = NULL; + dig = NULL; + ctx = NULL; + + /* Construct the thumbprint input itself. */ + + switch (EVP_PKEY_type(pkey->type)) { + case EVP_PKEY_RSA: + if (NULL != (thumb = op_thumb_rsa(pkey))) + break; + goto out; + default: + warnx("EVP_PKEY_type: unknown key type"); + goto out; + } + + /* + * Compute the SHA256 digest of the thumbprint then + * base64-encode the digest itself. + * If the reader is closed when we write, ignore it (we'll pick + * it up in the read loop). + */ + + if (NULL == (dig = malloc(EVP_MAX_MD_SIZE))) { + warn("malloc"); + goto out; + } else if (NULL == (ctx = EVP_MD_CTX_create())) { + warnx("EVP_MD_CTX_create"); + goto out; + } else if ( ! EVP_DigestInit_ex(ctx, EVP_sha256(), NULL)) { + warnx("EVP_SignInit_ex"); + goto out; + } else if ( ! EVP_DigestUpdate(ctx, thumb, strlen(thumb))) { + warnx("EVP_SignUpdate"); + goto out; + } else if ( ! EVP_DigestFinal_ex(ctx, dig, &digsz)) { + warnx("EVP_SignFinal"); + goto out; + } else if (NULL == (dig64 = base64buf_url((char *)dig, digsz))) { + warnx("base64buf_url"); + goto out; + } else if (writestr(fd, COMM_THUMB, dig64) < 0) + goto out; + + rc = 1; +out: + if (NULL != ctx) + EVP_MD_CTX_destroy(ctx); + + free(thumb); + free(dig); + free(dig64); + return(rc); +} + +static int +op_sign_rsa(char **head, char **prot, EVP_PKEY *pkey, const char *nonce) +{ + RSA *r; + char *exp, *mod; + int rc; + + *head = *prot = exp = mod = NULL; + rc = 0; + + /* + * First, extract relevant portions of our private key. + * Then construct the public header. + * Finally, format the header combined with the nonce. + */ + + if (NULL == (r = EVP_PKEY_get1_RSA(pkey))) + warnx("EVP_PKEY_get1_RSA"); + else if (NULL == (mod = bn2string(r->n))) + warnx("bn2string"); + else if (NULL == (exp = bn2string(r->e))) + warnx("bn2string"); + else if (NULL == (*head = json_fmt_header_rsa(exp, mod))) + warnx("json_fmt_header_rsa"); + else if (NULL == (*prot = json_fmt_protected_rsa(exp, mod, nonce))) + warnx("json_fmt_protected_rsa"); + else + rc = 1; + + free(exp); + free(mod); + return(rc); +} + +/* + * Operation to sign a message with the account key. + * This requires the sender ("fd") to provide the payload and a nonce. + */ +static int +op_sign(int fd, EVP_PKEY *pkey) +{ + char *nonce, *pay, + *pay64, *prot, *prot64, *head, + *sign, *dig64, *fin; + int cc, rc; + unsigned int digsz; + unsigned char *dig; + EVP_MD_CTX *ctx; + + rc = 0; + pay = nonce = head = fin = + sign = prot = prot64 = pay64 = dig64 = NULL; + dig = NULL; + ctx = NULL; + + /* Read our payload and nonce from the requestor. */ + + if (NULL == (pay = readstr(fd, COMM_PAY))) + goto out; + else if (NULL == (nonce = readstr(fd, COMM_NONCE))) + goto out; + + /* Base64-encode the payload. */ + + if (NULL == (pay64 = base64buf_url(pay, strlen(pay)))) { + warnx("base64buf_url"); + goto out; + } + + switch (EVP_PKEY_type(pkey->type)) { + case EVP_PKEY_RSA: + if ( ! op_sign_rsa(&head, &prot, pkey, nonce)) + goto out; + break; + default: + warnx("EVP_PKEY_type"); + goto out; + } + + /* The header combined with the nonce, base64. */ + + if (NULL == (prot64 = base64buf_url(prot, strlen(prot)))) { + warnx("base64buf_url"); + goto out; + } + + /* Now the signature material. */ + + cc = asprintf(&sign, "%s.%s", prot64, pay64); + if (-1 == cc) { + warn("asprintf"); + sign = NULL; + goto out; + } + + if (NULL == (dig = malloc(EVP_PKEY_size(pkey)))) { + warn("malloc"); + goto out; + } + + /* + * Here we go: using our RSA key as merged into the envelope, + * sign a SHA256 digest of our message. + */ + + if (NULL == (ctx = EVP_MD_CTX_create())) { + warnx("EVP_MD_CTX_create"); + goto out; + } else if ( ! EVP_SignInit_ex(ctx, EVP_sha256(), NULL)) { + warnx("EVP_SignInit_ex"); + goto out; + } else if ( ! EVP_SignUpdate(ctx, sign, strlen(sign))) { + warnx("EVP_SignUpdate"); + goto out; + } else if ( ! EVP_SignFinal(ctx, dig, &digsz, pkey)) { + warnx("EVP_SignFinal"); + goto out; + } else if (NULL == (dig64 = base64buf_url((char *)dig, digsz))) { + warnx("base64buf_url"); + goto out; + } + + /* + * Write back in the correct JSON format. + * If the reader is closed, just ignore it (we'll pick it up + * when we next enter the read loop). + */ + + if (NULL == (fin = json_fmt_signed(head, prot64, pay64, dig64))) { + warnx("json_fmt_signed"); + goto out; + } else if (writestr(fd, COMM_REQ, fin) < 0) + goto out; + + rc = 1; +out: + if (NULL != ctx) + EVP_MD_CTX_destroy(ctx); + + free(pay); + free(sign); + free(pay64); + free(nonce); + free(head); + free(prot); + free(prot64); + free(dig); + free(dig64); + free(fin); + return(rc); +} + +int +acctproc(int netsock, const char *acctkey, int newacct) +{ + FILE *f; + EVP_PKEY *pkey; + long lval; + enum acctop op; + unsigned char rbuf[64]; + int rc, cc; + mode_t prev; + + f = NULL; + pkey = NULL; + rc = 0; + + /* + * First, open our private key file read-only or write-only if + * we're creating from scratch. + * Set our umask to be maximally restrictive. + */ + + prev = umask((S_IWUSR | S_IXUSR) | S_IRWXG | S_IRWXO); + f = fopen(acctkey, newacct ? "wx" : "r"); + umask(prev); + + if (NULL == f) { + warn("%s", acctkey); + goto out; + } + + /* File-system, user, and sandbox jailing. */ + + if ( ! sandbox_before()) + goto out; + + ERR_load_crypto_strings(); + + if ( ! dropfs(PATH_VAR_EMPTY)) + goto out; + else if ( ! dropprivs()) + goto out; + else if ( ! sandbox_after()) + goto out; + + /* + * Seed our PRNG with data from arc4random(). + * Do this until we're told it's ok and use increments of 64 + * bytes (arbitrarily). + */ + + while (0 == RAND_status()) { + arc4random_buf(rbuf, sizeof(rbuf)); + RAND_seed(rbuf, sizeof(rbuf)); + } + + if (newacct) { + if (NULL == (pkey = rsa_key_create(f, acctkey))) + goto out; + dodbg("%s: generated RSA account key", acctkey); + } else { + if (NULL == (pkey = rsa_key_load(f, acctkey))) + goto out; + doddbg("%s: loaded RSA account key", acctkey); + } + + fclose(f); + f = NULL; + + /* Notify the netproc that we've started up. */ + + if (0 == (cc = writeop(netsock, COMM_ACCT_STAT, ACCT_READY))) + rc = 1; + if (cc <= 0) + goto out; + + /* + * Now we wait for requests from the network-facing process. + * It might ask us for our thumbprint, for example, or for us to + * sign a message. + */ + + for (;;) { + op = ACCT__MAX; + if (0 == (lval = readop(netsock, COMM_ACCT))) + op = ACCT_STOP; + else if (ACCT_SIGN == lval || ACCT_THUMBPRINT == lval) + op = lval; + + if (ACCT__MAX == op) { + warnx("unknown operation from netproc"); + goto out; + } else if (ACCT_STOP == op) + break; + + switch (op) { + case (ACCT_SIGN): + if (op_sign(netsock, pkey)) + break; + warnx("op_sign"); + goto out; + case (ACCT_THUMBPRINT): + if (op_thumbprint(netsock, pkey)) + break; + warnx("op_thumbprint"); + goto out; + default: + abort(); + } + } + + rc = 1; +out: + close(netsock); + if (NULL != f) + fclose(f); + if (NULL != pkey) + EVP_PKEY_free(pkey); + ERR_print_errors_fp(stderr); + ERR_free_strings(); + return(rc); +} + diff --git a/usr.sbin/acme-client/base64.c b/usr.sbin/acme-client/base64.c new file mode 100644 index 00000000000..6f4364ddaa5 --- /dev/null +++ b/usr.sbin/acme-client/base64.c @@ -0,0 +1,130 @@ +/* $Id: base64.c,v 1.1 2016/08/31 22:01:42 florian Exp $ */ +/* + * Copyright (c) 2016 Kristaps Dzonsons <kristaps@bsd.lv> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include <sys/types.h> + +#include <assert.h> +#include <stdarg.h> +#include <stdlib.h> + +#include "extern.h" + +static const char b64[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + +/* + * Compute the maximum buffer required for a base64 encoded string of + * length "len". + */ +size_t +base64len(size_t len) +{ + + return(((len + 2) / 3 * 4) + 1); +} + +/* + * Base64 computation. + * This is heavily "assert"-d because Coverity complains. + */ +size_t +base64buf(char *enc, const char *str, size_t len) +{ + size_t i, val; + char *p; + + p = enc; + + for (i = 0; i < len - 2; i += 3) { + val = (str[i] >> 2) & 0x3F; + assert(val < sizeof(b64)); + *p++ = b64[val]; + + val = ((str[i] & 0x3) << 4) | + ((int)(str[i + 1] & 0xF0) >> 4); + assert(val < sizeof(b64)); + *p++ = b64[val]; + + val = ((str[i + 1] & 0xF) << 2) | + ((int)(str[i + 2] & 0xC0) >> 6); + assert(val < sizeof(b64)); + *p++ = b64[val]; + + val = str[i + 2] & 0x3F; + assert(val < sizeof(b64)); + *p++ = b64[val]; + } + + if (i < len) { + val = (str[i] >> 2) & 0x3F; + assert(val < sizeof(b64)); + *p++ = b64[val]; + + if (i == (len - 1)) { + val = ((str[i] & 0x3) << 4); + assert(val < sizeof(b64)); + *p++ = b64[val]; + *p++ = '='; + } else { + val = ((str[i] & 0x3) << 4) | + ((int)(str[i + 1] & 0xF0) >> 4); + assert(val < sizeof(b64)); + *p++ = b64[val]; + + val = ((str[i + 1] & 0xF) << 2); + assert(val < sizeof(b64)); + *p++ = b64[val]; + } + *p++ = '='; + } + + *p++ = '\0'; + return(p - enc); +} + +/* + * Pass a stream of bytes to be base64 encoded, then converted into + * base64url format. + * Returns NULL on allocation failure (not logged). + */ +char * +base64buf_url(const char *data, size_t len) +{ + size_t i, sz; + char *buf; + + sz = base64len(len); + if (NULL == (buf = malloc(sz))) + return(NULL); + + base64buf(buf, data, len); + + for (i = 0; i < sz; i++) + if ('+' == buf[i]) + buf[i] = '-'; + else if ('/' == buf[i]) + buf[i] = '_'; + else if ('=' == buf[i]) + buf[i] = '\0'; + + return(buf); +} diff --git a/usr.sbin/acme-client/certproc.c b/usr.sbin/acme-client/certproc.c new file mode 100644 index 00000000000..9f9e88300c4 --- /dev/null +++ b/usr.sbin/acme-client/certproc.c @@ -0,0 +1,261 @@ +/* $Id: certproc.c,v 1.1 2016/08/31 22:01:42 florian Exp $ */ +/* + * Copyright (c) 2016 Kristaps Dzonsons <kristaps@bsd.lv> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include <err.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include <openssl/pem.h> +#include <openssl/x509.h> +#include <openssl/x509v3.h> +#include <openssl/err.h> + +#include "extern.h" + +#define MARKER "-----BEGIN CERTIFICATE-----" + +/* + * Convert an X509 certificate to a buffer of "sz". + * We don't guarantee that it's nil-terminated. + * Returns NULL on failure. + */ +static char * +x509buf(X509 *x, size_t *sz) +{ + BIO *bio; + char *p; + int ssz; + + /* Convert X509 to PEM in BIO. */ + + if (NULL == (bio = BIO_new(BIO_s_mem()))) { + warnx("BIO_new"); + return(NULL); + } else if ( ! PEM_write_bio_X509(bio, x)) { + warnx("PEM_write_bio_X509"); + BIO_free(bio); + return(NULL); + } + + /* + * Now convert bio to string. + * Make into nil-terminated, just in case. + */ + + if (NULL == (p = calloc(1, bio->num_write + 1))) { + warn("calloc"); + BIO_free(bio); + return(NULL); + } + + ssz = BIO_read(bio, p, bio->num_write); + if (ssz < 0 || (unsigned)ssz != bio->num_write) { + warnx("BIO_read"); + BIO_free(bio); + return(NULL); + } + + *sz = ssz; + BIO_free(bio); + return(p); +} + +int +certproc(int netsock, int filesock) +{ + char *csr, *chain, *url; + unsigned char *csrcp, *chaincp; + size_t csrsz, chainsz; + int i, rc, idx, cc; + enum certop op; + long lval; + X509 *x, *chainx; + X509_EXTENSION *ext; + X509V3_EXT_METHOD *method; + void *entries; + STACK_OF(CONF_VALUE) *val; + CONF_VALUE *nval; + + ext = NULL; + idx = -1; + method = NULL; + chain = csr = url = NULL; + rc = 0; + x = chainx = NULL; + + /* File-system and sandbox jailing. */ + + if ( ! sandbox_before()) + goto out; + + ERR_load_crypto_strings(); + + if ( ! dropfs(PATH_VAR_EMPTY)) + goto out; + else if ( ! dropprivs()) + goto out; + else if ( ! sandbox_after()) + goto out; + + /* Read what the netproc wants us to do. */ + + op = CERT__MAX; + if (0 == (lval = readop(netsock, COMM_CSR_OP))) + op = CERT_STOP; + else if (CERT_REVOKE == lval || CERT_UPDATE == lval) + op = lval; + + if (CERT_STOP == op) { + rc = 1; + goto out; + } else if (CERT__MAX == op) { + warnx("unknown operation from netproc"); + goto out; + } + + /* + * Pass revocation right through to fileproc. + * If the reader is terminated, ignore it. + */ + + if (CERT_REVOKE == op) { + if (writeop(filesock, COMM_CHAIN_OP, FILE_REMOVE) >= 0) + rc = 1; + goto out; + } + + /* + * Wait until we receive the DER encoded (signed) certificate + * from the network process. + * Then convert the DER encoding into an X509 certificate. + */ + + if (NULL == (csr = readbuf(netsock, COMM_CSR, &csrsz))) + goto out; + + csrcp = (u_char *)csr; + x = d2i_X509(NULL, (const u_char **)&csrcp, csrsz); + if (NULL == x) { + warnx("d2i_X509"); + goto out; + } + + /* + * Extract the CA Issuers from its NID. + * TODO: I have no idea what I'm doing. + */ + + idx = X509_get_ext_by_NID(x, NID_info_access, idx); + if (idx >= 0 && NULL != (ext = X509_get_ext(x, idx))) + method = (X509V3_EXT_METHOD *)X509V3_EXT_get(ext); + + entries = X509_get_ext_d2i(x, NID_info_access, 0, 0); + if (NULL != method && NULL != entries) { + val = method->i2v(method, entries, 0); + for (i = 0; i < sk_CONF_VALUE_num(val); i++) { + nval = sk_CONF_VALUE_value(val, i); + if (strcmp(nval->name, "CA Issuers - URI")) + continue; + url = strdup(nval->value); + if (NULL == url) { + warn("strdup"); + goto out; + } + break; + } + } + + if (NULL == url) { + warnx("no CA issuer registered with certificate"); + goto out; + } + + /* Write the CA issuer to the netsock. */ + + if (writestr(netsock, COMM_ISSUER, url) <= 0) + goto out; + + /* Read the full-chain back from the netsock. */ + + if (NULL == (chain = readbuf(netsock, COMM_CHAIN, &chainsz))) + goto out; + + /* + * Then check if the chain is PEM-encoded by looking to see if + * it begins with the PEM marker. + * If so, ship it as-is; otherwise, convert to a PEM encoded + * buffer and ship that. + * FIXME: if PEM, re-parse it. + */ + + if (chainsz <= strlen(MARKER) || + strncmp(chain, MARKER, strlen(MARKER))) { + chaincp = (u_char *)chain; + chainx = d2i_X509(NULL, + (const u_char **)&chaincp, chainsz); + if (NULL == chainx) { + warnx("d2i_X509"); + goto out; + } + free(chain); + if (NULL == (chain = x509buf(chainx, &chainsz))) + goto out; + } + + /* Allow reader termination to just push us out. */ + + if (0 == (cc = writeop(filesock, COMM_CHAIN_OP, FILE_CREATE))) + rc = 1; + if (cc <= 0) + goto out; + if (0 == (cc = writebuf(filesock, COMM_CHAIN, chain, chainsz))) + rc = 1; + if (cc <= 0) + goto out; + + /* + * Next, convert the X509 to a buffer and send that. + * Reader failure doesn't change anything. + */ + + free(chain); + if (NULL == (chain = x509buf(x, &chainsz))) + goto out; + if (writebuf(filesock, COMM_CSR, chain, chainsz) < 0) + goto out; + + rc = 1; +out: + close(netsock); + close(filesock); + if (NULL != x) + X509_free(x); + if (NULL != chainx) + X509_free(chainx); + free(csr); + free(url); + free(chain); + ERR_print_errors_fp(stderr); + ERR_free_strings(); + return(rc); +} + diff --git a/usr.sbin/acme-client/chngproc.c b/usr.sbin/acme-client/chngproc.c new file mode 100644 index 00000000000..a868572a7fc --- /dev/null +++ b/usr.sbin/acme-client/chngproc.c @@ -0,0 +1,174 @@ +/* $Id: chngproc.c,v 1.1 2016/08/31 22:01:42 florian Exp $ */ +/* + * Copyright (c) 2016 Kristaps Dzonsons <kristaps@bsd.lv> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include <assert.h> +#include <err.h> +#include <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "extern.h" + +int +chngproc(int netsock, const char *root, int remote) +{ + int rc; + long lval; + enum chngop op; + char *tok, *th, *fmt; + char **fs; + size_t i, fsz; + void *pp; + int fd, cc; + + rc = 0; + th = tok = fmt = NULL; + fd = -1; + fs = NULL; + fsz = 0; + + /* File-system and sandbox jailing. */ + + if ( ! sandbox_before()) + goto out; + else if ( ! dropfs(root)) + goto out; + else if ( ! sandbox_after()) + goto out; + + /* + * Loop while we wait to get a thumbprint and token. + * We'll get this for each SAN request. + */ + + for (;;) { + op = CHNG__MAX; + if (0 == (lval = readop(netsock, COMM_CHNG_OP))) + op = CHNG_STOP; + else if (CHNG_SYN == lval) + op = lval; + + if (CHNG__MAX == op) { + warnx("unknown operation from netproc"); + goto out; + } else if (CHNG_STOP == op) + break; + + assert(CHNG_SYN == op); + + /* + * Read the thumbprint and token. + * The token is the filename, so store that in a vector + * of tokens that we'll later clean up. + */ + + if (NULL == (th = readstr(netsock, COMM_THUMB))) + goto out; + else if (NULL == (tok = readstr(netsock, COMM_TOK))) + goto out; + + /* Vector appending... */ + + pp = realloc(fs, (fsz + 1) * sizeof(char *)); + if (NULL == pp) { + warn("realloc"); + goto out; + } + fs = pp; + fs[fsz] = tok; + tok = NULL; + fsz++; + + if (-1 == asprintf(&fmt, "%s.%s", fs[fsz - 1], th)) { + warn("asprintf"); + goto out; + } + + /* + * I use this for testing when letskencrypt is being run + * on machines apart from where I'm hosting the + * challenge directory. + * DON'T DEPEND ON THIS FEATURE. + */ + if (remote) { + puts("RUN THIS IN THE CHALLENGE DIRECTORY"); + puts("YOU HAVE 20 SECONDS..."); + printf("doas sh -c \"echo %s > %s\"\n", + fmt, fs[fsz - 1]); + sleep(20); + puts("TIME'S UP."); + } else { + /* + * Create and write to our challenge file. + * Note: we use file descriptors instead of FILE + * because we want to minimise our pledges. + */ + fd = open(fs[fsz - 1], + O_WRONLY|O_EXCL|O_CREAT, 0444); + if (-1 == fd) { + warn("%s", fs[fsz - 1]); + goto out; + } if (-1 == write(fd, fmt, strlen(fmt))) { + warn("%s", fs[fsz - 1]); + goto out; + } else if (-1 == close(fd)) { + warn("%s", fs[fsz - 1]); + goto out; + } + fd = -1; + } + + free(th); + free(fmt); + th = fmt = NULL; + + dodbg("%s/%s: created", root, fs[fsz - 1]); + + /* + * Write our acknowledgement. + * Ignore reader failure. + */ + + cc = writeop(netsock, COMM_CHNG_ACK, CHNG_ACK); + if (0 == cc) + break; + if (cc < 0) + goto out; + } + + rc = 1; +out: + close(netsock); + if (-1 != fd) + close(fd); + for (i = 0; i < fsz; i++) { + if (-1 == unlink(fs[i]) && ENOENT != errno) + warn("%s", fs[i]); + free(fs[i]); + } + free(fs); + free(fmt); + free(th); + free(tok); + return(rc); +} diff --git a/usr.sbin/acme-client/dbg.c b/usr.sbin/acme-client/dbg.c new file mode 100644 index 00000000000..d3910fdcf4c --- /dev/null +++ b/usr.sbin/acme-client/dbg.c @@ -0,0 +1,51 @@ +/* $Id: dbg.c,v 1.1 2016/08/31 22:01:42 florian Exp $ */ +/* + * Copyright (c) 2016 Kristaps Dzonsons <kristaps@bsd.lv> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include <err.h> +#include <stdarg.h> +#include <stdlib.h> + +#include "extern.h" + +void +doddbg(const char *fmt, ...) +{ + va_list ap; + + if (verbose < 2) + return; + + va_start(ap, fmt); + vwarnx(fmt, ap); + va_end(ap); +} + +void +dodbg(const char *fmt, ...) +{ + va_list ap; + + if ( ! verbose) + return; + + va_start(ap, fmt); + vwarnx(fmt, ap); + va_end(ap); +} diff --git a/usr.sbin/acme-client/dnsproc.c b/usr.sbin/acme-client/dnsproc.c new file mode 100644 index 00000000000..e00c3454af3 --- /dev/null +++ b/usr.sbin/acme-client/dnsproc.c @@ -0,0 +1,206 @@ +/* $Id: dnsproc.c,v 1.1 2016/08/31 22:01:42 florian Exp $ */ +/* + * Copyright (c) 2016 Kristaps Dzonsons <kristaps@bsd.lv> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include <sys/socket.h> +#include <arpa/inet.h> + +#include <err.h> +#include <netdb.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "extern.h" + +struct addr { + int family; /* 4 for PF_INET, 6 for PF_INET6 */ + char ip[INET6_ADDRSTRLEN]; +}; + +/* + * This is a modified version of host_dns in config.c of OpenBSD's ntpd. + */ +/* + * Copyright (c) 2003, 2004 Henning Brauer <henning@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ +static ssize_t +host_dns(const char *s, struct addr *vec) +{ + struct addrinfo hints, *res0, *res; + int error; + ssize_t vecsz; + struct sockaddr *sa; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; /* DUMMY */ + /* ntpd MUST NOT use AI_ADDRCONFIG here */ + + error = getaddrinfo(s, NULL, &hints, &res0); + + if (error == EAI_AGAIN || + /* FIXME */ +#ifndef __FreeBSD__ + error == EAI_NODATA || +#endif + error == EAI_NONAME) + return(0); + + if (error) { + warnx("%s: parse error: %s", + s, gai_strerror(error)); + return(-1); + } + + for (vecsz = 0, res = res0; + NULL != res && vecsz < MAX_SERVERS_DNS; + res = res->ai_next) { + if (res->ai_family != AF_INET && + res->ai_family != AF_INET6) + continue; + + sa = res->ai_addr; + + if (AF_INET == res->ai_family) { + vec[vecsz].family = 4; + inet_ntop(AF_INET, + &(((struct sockaddr_in *)sa)->sin_addr), + vec[vecsz].ip, INET6_ADDRSTRLEN); + } else { + vec[vecsz].family = 6; + inet_ntop(AF_INET6, + &(((struct sockaddr_in6 *)sa)->sin6_addr), + vec[vecsz].ip, INET6_ADDRSTRLEN); + } + + dodbg("%s: DNS: %s", s, vec[vecsz].ip); + vecsz++; + break; + } + + freeaddrinfo(res0); + return(vecsz); +} + +int +dnsproc(int nfd) +{ + int rc, cc; + char *look, *last; + struct addr v[MAX_SERVERS_DNS]; + long lval; + size_t i; + ssize_t vsz; + enum dnsop op; + + rc = 0; + look = last = NULL; + vsz = 0; + + /* + * Why don't we chroot() here? + * On OpenBSD, the pledge(2) takes care of our constraining the + * environment to DNS resolution only, so the chroot(2) is + * unnecessary. + * On Mac OS X, we can't chroot(2): we'd need to have an mdns + * responder thing in each jail. + * On Linux, forget it. getaddrinfo(2) pulls on all sorts of + * mystery meat. + */ + + if ( ! sandbox_before()) + goto out; + else if ( ! dropprivs()) + goto out; + else if ( ! sandbox_after()) + goto out; + + /* + * This is simple: just loop on a request operation, and each + * time we write back zero or more entries. + * Also do a simple trick and cache the last lookup. + */ + + for (;;) { + op = DNS__MAX; + if (0 == (lval = readop(nfd, COMM_DNS))) + op = DNS_STOP; + else if (DNS_LOOKUP == lval) + op = lval; + + if (DNS__MAX == op) { + warnx("unknown operation from netproc"); + goto out; + } else if (DNS_STOP == op) + break; + + if (NULL == (look = readstr(nfd, COMM_DNSQ))) + goto out; + + /* + * Check if we're asked to repeat the lookup. + * If not, request it from host_dns(). + */ + + if (NULL == last || strcmp(look, last)) { + if ((vsz = host_dns(look, v)) < 0) + goto out; + + free(last); + last = look; + look = NULL; + } else { + doddbg("%s: cached", look); + free(look); + look = NULL; + } + + if (0 == (cc = writeop(nfd, COMM_DNSLEN, vsz))) + break; + else if (cc < 0) + goto out; + for (i = 0; i < (size_t)vsz; i++) { + if (writeop(nfd, COMM_DNSF, v[i].family) <= 0) + goto out; + if (writestr(nfd, COMM_DNSA, v[i].ip) <= 0) + goto out; + } + } + + rc = 1; +out: + close(nfd); + free(look); + free(last); + return(rc); +} diff --git a/usr.sbin/acme-client/extern.h b/usr.sbin/acme-client/extern.h new file mode 100644 index 00000000000..10f4323aa99 --- /dev/null +++ b/usr.sbin/acme-client/extern.h @@ -0,0 +1,267 @@ +/* $Id: extern.h,v 1.1 2016/08/31 22:01:42 florian Exp $ */ +/* + * Copyright (c) 2016 Kristaps Dzonsons <kristaps@bsd.lv> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ +#ifndef EXTERN_H +#define EXTERN_H + +#ifndef PATH_VAR_EMPTY +#define PATH_VAR_EMPTY "/var/empty" +#endif + +#define MAX_SERVERS_DNS 8 + +#define CERT_PEM "cert.pem" +#define CERT_BAK "cert.pem~" +#define CHAIN_PEM "chain.pem" +#define CHAIN_BAK "chain.pem~" +#define FCHAIN_PEM "fullchain.pem" +#define FCHAIN_BAK "fullchain.pem~" + + +/* + * Requests to and from acctproc. + */ +enum acctop { + ACCT_STOP = 0, + ACCT_READY, + ACCT_SIGN, + ACCT_THUMBPRINT, + ACCT__MAX +}; + +/* + * Requests to and from chngproc. + */ +enum chngop { + CHNG_STOP = 0, + CHNG_SYN, + CHNG_ACK, + CHNG__MAX +}; + +/* + * Requests to keyproc. + */ +enum keyop { + KEY_STOP = 0, + KEY_READY, + KEY__MAX +}; + +/* + * Requests to certproc. + */ +enum certop { + CERT_STOP = 0, + CERT_REVOKE, + CERT_UPDATE, + CERT__MAX +}; + +/* + * Requests to fileproc. + */ +enum fileop { + FILE_STOP = 0, + FILE_REMOVE, + FILE_CREATE, + FILE__MAX +}; + +/* + * Requests to dnsproc. + */ +enum dnsop { + DNS_STOP = 0, + DNS_LOOKUP, + DNS__MAX +}; + +enum revokeop { + REVOKE_STOP = 0, + REVOKE_CHECK, + REVOKE_EXP, + REVOKE_OK, + REVOKE__MAX +}; + +/* + * Our components. + * Each one of these is in a separated, isolated process. + */ +enum comp { + COMP_NET, /* network-facing (to ACME) */ + COMP_KEY, /* handles domain keys */ + COMP_CERT, /* handles domain certificates */ + COMP_ACCOUNT, /* handles account key */ + COMP_CHALLENGE, /* handles challenges */ + COMP_FILE, /* handles writing certs */ + COMP_DNS, /* handles DNS lookups */ + COMP_REVOKE, /* checks X509 expiration */ + COMP__MAX +}; + +/* + * Inter-process communication labels. + * This is purely for looking at debugging. + */ +enum comm { + COMM_REQ, + COMM_THUMB, + COMM_CERT, + COMM_PAY, + COMM_NONCE, + COMM_TOK, + COMM_CHNG_OP, + COMM_CHNG_ACK, + COMM_ACCT, + COMM_ACCT_STAT, + COMM_CSR, + COMM_CSR_OP, + COMM_ISSUER, + COMM_CHAIN, + COMM_CHAIN_OP, + COMM_DNS, + COMM_DNSQ, + COMM_DNSA, + COMM_DNSF, + COMM_DNSLEN, + COMM_KEY_STAT, + COMM_REVOKE_OP, + COMM_REVOKE_CHECK, + COMM_REVOKE_RESP, + COMM__MAX +}; + +/* + * This contains the URI and token of an ACME-issued challenge. + * A challenge consists of a token, which we must present on the + * (presumably!) local machine to an ACME connection; and a URI, to + * which we must connect to verify the token. + */ +struct chng { + char *uri; /* uri on ACME server */ + char *token; /* token we must offer */ + size_t retry; /* how many times have we tried */ + int status; /* challenge accepted? */ +}; + +/* + * This consists of the services offered by the CA. + * They must all be filled in. + */ +struct capaths { + char *newauthz; /* new authorisation */ + char *newcert; /* sign certificate */ + char *newreg; /* new acme account */ + char *revokecert; /* revoke certificate */ +}; + +struct jsmnn; + +__BEGIN_DECLS + +/* + * Start with our components. + * These are all isolated and talk to each other using sockets. + */ +int acctproc(int, const char *, int); +int certproc(int, int); +int chngproc(int, const char *, int); +int dnsproc(int); +int revokeproc(int, const char *, + int, int, const char *const *, size_t); +int fileproc(int, int, const char *); +int keyproc(int, const char *, + const char **, size_t, int); +int netproc(int, int, int, int, int, int, int, int, int, + const char *const *, size_t, const char *); + +/* + * Debugging functions. + * These just route to warnx according to the verbosity. + */ +void dodbg(const char *, ...) + __attribute__((format(printf, 1, 2))); +void doddbg(const char *, ...) + __attribute__((format(printf, 1, 2))); + +/* + * Read and write things from the wire. + * The readers behave differently with respect to EOF. + */ +long readop(int, enum comm); +char *readbuf(int, enum comm, size_t *); +char *readstr(int, enum comm); +int writebuf(int, enum comm, const void *, size_t); +int writestr(int, enum comm, const char *); +int writeop(int, enum comm, long); + +int checkexit(pid_t, enum comp); +int checkexit_ext(int *, pid_t, enum comp); + +/* + * Base64 and URL encoding. + * Returns a buffer or NULL on allocation error. + */ +size_t base64buf(char *, const char *, size_t); +size_t base64len(size_t); +char *base64buf_url(const char *, size_t); + +/* + * JSON parsing routines. + * Keep this all in on place, though it's only used by one file. + */ +struct jsmnn *json_parse(const char *, size_t); +void json_free(struct jsmnn *); +int json_parse_response(struct jsmnn *); +void json_free_challenge(struct chng *); +int json_parse_challenge(struct jsmnn *, struct chng *); +void json_free_capaths(struct capaths *); +int json_parse_capaths(struct jsmnn *, struct capaths *); + +char *json_fmt_challenge(const char *, const char *); +char *json_fmt_newauthz(const char *); +char *json_fmt_newcert(const char *); +char *json_fmt_newreg(const char *); +char *json_fmt_protected_rsa(const char *, + const char *, const char *); +char *json_fmt_revokecert(const char *); +char *json_fmt_header_rsa(const char *, const char *); +char *json_fmt_thumb_rsa(const char *, const char *); +char *json_fmt_signed(const char *, + const char *, const char *, const char *); + +int dropprivs(void); +int dropfs(const char *); +int checkprivs(void); + +int sandbox_after(void); +int sandbox_before(void); + +/* + * Should we print debugging messages? + */ +int verbose; + +/* + * What component is the process within (COMP__MAX for none)? + */ +enum comp proccomp; + +__END_DECLS + +#endif /* ! EXTERN_H */ diff --git a/usr.sbin/acme-client/fileproc.c b/usr.sbin/acme-client/fileproc.c new file mode 100644 index 00000000000..33f15418a97 --- /dev/null +++ b/usr.sbin/acme-client/fileproc.c @@ -0,0 +1,221 @@ +/* $Id: fileproc.c,v 1.1 2016/08/31 22:01:42 florian Exp $ */ +/* + * Copyright (c) 2016 Kristaps Dzonsons <kristaps@bsd.lv> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include <err.h> +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#include "extern.h" + +static int +serialise(const char *tmp, const char *real, + const char *v, size_t vsz, + const char *v2, size_t v2sz) +{ + int fd; + + /* + * Write into backup location, overwriting. + * Then atomically (?) do the rename. + */ + + fd = open(tmp, O_WRONLY|O_CREAT|O_TRUNC, 0444); + if (-1 == fd) { + warn("%s", tmp); + return(0); + } else if ((ssize_t)vsz != write(fd, v, vsz)) { + warnx("%s", tmp); + close(fd); + return(0); + } else if (NULL != v2 && (ssize_t)v2sz != write(fd, v2, v2sz)) { + warnx("%s", tmp); + close(fd); + return(0); + } else if (-1 == close(fd)) { + warn("%s", tmp); + return(0); + } else if (-1 == rename(tmp, real)) { + warn("%s", real); + return(0); + } + + return(1); +} + +int +fileproc(int certsock, int backup, const char *certdir) +{ + char *csr, *ch; + size_t chsz, csz; + int rc; + long lval; + enum fileop op; + time_t t; + char file[PATH_MAX]; + + csr = ch = NULL; + rc = 0; + + /* File-system and sandbox jailing. */ + + if ( ! sandbox_before()) + goto out; + else if ( ! dropfs(certdir)) + goto out; + else if ( ! sandbox_after()) + goto out; + + /* Read our operation. */ + + op = FILE__MAX; + if (0 == (lval = readop(certsock, COMM_CHAIN_OP))) + op = FILE_STOP; + else if (FILE_CREATE == lval || FILE_REMOVE == lval) + op = lval; + + if (FILE_STOP == op) { + rc = 1; + goto out; + } else if (FILE__MAX == op) { + warnx("unknown operation from certproc"); + goto out; + } + + /* + * If we're backing up, then copy all files (found) by linking + * them to the file followed by the epoch in seconds. + * If we're going to remove, the unlink(2) will cause the + * original to go away. + * If we're going to update, the rename(2) will replace the + * certificate, leaving the backup as the only one. + */ + + if (backup) { + t = time(NULL); + snprintf(file, sizeof(file), + "cert-%llu.pem", (unsigned long long)t); + if (-1 == link(CERT_PEM, file) && ENOENT != errno) { + warnx("%s/%s", certdir, CERT_PEM); + goto out; + } else + dodbg("%s/%s: linked to %s", + certdir, CERT_PEM, file); + + snprintf(file, sizeof(file), + "chain-%llu.pem", (unsigned long long)t); + if (-1 == link(CHAIN_PEM, file) && ENOENT != errno) { + warnx("%s/%s", certdir, CHAIN_PEM); + goto out; + } else + dodbg("%s/%s: linked to %s", + certdir, CHAIN_PEM, file); + + snprintf(file, sizeof(file), + "fullchain-%llu.pem", (unsigned long long)t); + if (-1 == link(FCHAIN_PEM, file) && ENOENT != errno) { + warnx("%s/%s", certdir, FCHAIN_PEM); + goto out; + } else + dodbg("%s/%s: linked to %s", + certdir, FCHAIN_PEM, file); + } + + /* + * If revoking certificates, just unlink the files. + * We return the special error code of 2 to indicate that the + * certificates were removed. + */ + + if (FILE_REMOVE == op) { + if (-1 == unlink(CERT_PEM) && ENOENT != errno) { + warn("%s/%s", certdir, CERT_PEM); + goto out; + } else + dodbg("%s/%s: unlinked", certdir, CERT_PEM); + + if (-1 == unlink(CHAIN_PEM) && ENOENT != errno) { + warn("%s/%s", certdir, CHAIN_PEM); + goto out; + } else + dodbg("%s/%s: unlinked", certdir, CHAIN_PEM); + + if (-1 == unlink(FCHAIN_PEM) && ENOENT != errno) { + warn("%s/%s", certdir, FCHAIN_PEM); + goto out; + } else + dodbg("%s/%s: unlinked", certdir, FCHAIN_PEM); + + rc = 2; + goto out; + } + + /* + * Start by downloading the chain PEM as a buffer. + * This is not nil-terminated, but we're just going to guess + * that it's well-formed and not actually touch the data. + * Once downloaded, dump it into CHAIN_BAK. + */ + + if (NULL == (ch = readbuf(certsock, COMM_CHAIN, &chsz))) + goto out; + if ( ! serialise(CHAIN_BAK, CHAIN_PEM, ch, chsz, NULL, 0)) + goto out; + + dodbg("%s/%s: created", certdir, CHAIN_PEM); + + /* + * Next, wait until we receive the DER encoded (signed) + * certificate from the network process. + * This comes as a stream of bytes: we don't know how many, so + * just keep downloading. + */ + + if (NULL == (csr = readbuf(certsock, COMM_CSR, &csz))) + goto out; + if ( ! serialise(CERT_BAK, CERT_PEM, csr, csz, NULL, 0)) + goto out; + + dodbg("%s/%s: created", certdir, CERT_PEM); + + /* + * Finally, create the full-chain file. + * This is just the concatenation of the certificate and chain. + * We return the special error code 2 to indicate that the + * on-file certificates were changed. + */ + + if ( ! serialise(FCHAIN_BAK, FCHAIN_PEM, csr, csz, ch, chsz)) + goto out; + + dodbg("%s/%s: created", certdir, FCHAIN_PEM); + + rc = 2; +out: + close(certsock); + free(csr); + free(ch); + return(rc); +} diff --git a/usr.sbin/acme-client/http.c b/usr.sbin/acme-client/http.c new file mode 100644 index 00000000000..8838af44c2c --- /dev/null +++ b/usr.sbin/acme-client/http.c @@ -0,0 +1,851 @@ +/* $Id: http.c,v 1.1 2016/08/31 22:01:42 florian Exp $ */ +/* + * Copyright (c) 2016 Kristaps Dzonsons <kristaps@bsd.lv> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include <sys/socket.h> +#include <sys/param.h> +#include <arpa/inet.h> + +#include <assert.h> +#include <ctype.h> +#include <err.h> +#include <limits.h> +#include <netdb.h> +#include <stdio.h> +#include <stdint.h> +#include <stdlib.h> +#include <string.h> +#include <tls.h> +#include <unistd.h> + +#include "http.h" +#include "extern.h" + +/* + * A buffer for transferring HTTP/S data. + */ +struct httpxfer { + char *hbuf; /* header transfer buffer */ + size_t hbufsz; /* header buffer size */ + int headok; /* header has been parsed */ + char *bbuf; /* body transfer buffer */ + size_t bbufsz; /* body buffer size */ + int bodyok; /* body has been parsed */ + char *headbuf; /* lookaside buffer for headers */ + struct httphead *head; /* parsed headers */ + size_t headsz; /* number of headers */ +}; + +/* + * An HTTP/S connection object. + */ +struct http { + int fd; /* connected socket */ + short port; /* port number */ + struct source src; /* endpoint (raw) host */ + char *path; /* path to request */ + char *host; /* name of endpoint host */ + struct tls_config *cfg; /* if TLS */ + struct tls *ctx; /* if TLS */ + writefp writer; /* write function */ + readfp reader; /* read function */ +}; + +static ssize_t +dosysread(char *buf, size_t sz, const struct http *http) +{ + ssize_t rc; + + rc = read(http->fd, buf, sz); + if (rc < 0) + warn("%s: read", http->src.ip); + return(rc); +} + +static ssize_t +dosyswrite(const void *buf, size_t sz, const struct http *http) +{ + ssize_t rc; + + rc = write(http->fd, buf, sz); + if (rc < 0) + warn("%s: write", http->src.ip); + return(rc); +} + +#if defined(TLS_READ_AGAIN) && defined(TLS_WRITE_AGAIN) +/* + * Old-style libtls calls. + * These changed between 5.8 and 5.9. + */ +static ssize_t +dotlsread(char *buf, size_t sz, const struct http *http) +{ + size_t out, tot = 0; + int rc; + + for (;;) { + out = 0; + rc = tls_read(http->ctx, buf, sz, &out); + if (out > 0) { + buf += out; + assert(sz >= out); + sz -= out; + tot += out; + } + if (TLS_READ_AGAIN == rc) + continue; + else if (0 == out || 0 == sz || 0 == rc) + break; + warnx("%s: tls_read: %s", + http->src.ip, tls_error(http->ctx)); + return(-1); + } + + return(tot); +} + +static ssize_t +dotlswrite(const void *buf, size_t sz, const struct http *http) +{ + size_t out, tot = 0; + int rc; + + for (;;) { + out = 0; + rc = tls_write(http->ctx, buf, sz, &out); + if (out > 0) { + buf += out; + assert(sz >= out); + sz -= out; + tot += out; + } + if (TLS_WRITE_AGAIN == rc) + continue; + else if (0 == out || 0 == rc || 0 == rc) + break; + warnx("%s: tls_write: %s", + http->src.ip, tls_error(http->ctx)); + return(-1); + } + + return(tot); +} +#else +/* + * New-style libtls calls. + */ +static ssize_t +dotlsread(char *buf, size_t sz, const struct http *http) +{ + ssize_t rc; + + do + rc = tls_read(http->ctx, buf, sz); + while (TLS_WANT_POLLIN == rc || + TLS_WANT_POLLOUT == rc); + + if (rc < 0) + warnx("%s: tls_read: %s", + http->src.ip, + tls_error(http->ctx)); + return(rc); +} + +static ssize_t +dotlswrite(const void *buf, size_t sz, const struct http *http) +{ + ssize_t rc; + + do + rc = tls_write(http->ctx, buf, sz); + while (TLS_WANT_POLLIN == rc || + TLS_WANT_POLLOUT == rc); + + if (rc < 0) + warnx("%s: tls_write: %s", + http->src.ip, + tls_error(http->ctx)); + return(rc); +} +#endif + +static ssize_t +http_read(char *buf, size_t sz, const struct http *http) +{ + ssize_t ssz, xfer; + + xfer = 0; + do { + if ((ssz = http->reader(buf, sz, http)) < 0) + return(-1); + if (0 == ssz) + break; + xfer += ssz; + sz -= ssz; + buf += ssz; + } while (ssz > 0 && sz > 0); + + return(xfer); +} + +static int +http_write(const void *buf, size_t sz, const struct http *http) +{ + ssize_t ssz, xfer; + + xfer = sz; + while (sz > 0) { + if ((ssz = http->writer(buf, sz, http)) < 0) + return(-1); + sz -= ssz; + buf += ssz; + } + return(xfer); +} + +/* + * Between 5.8 and 5.9, libtls changed its semantics. + * In the old way, tls_close() will close the underlying file + * descriptors. + * In the new way, it won't. + */ +void +http_disconnect(struct http *http) +{ + + if (NULL != http->ctx) { + /* TLS connection. */ + if (-1 == tls_close(http->ctx)) + warnx("%s: tls_close: %s", + http->src.ip, + tls_error(http->ctx)); + if (NULL != http->ctx) + tls_free(http->ctx); +#if ! defined(TLS_READ_AGAIN) && ! defined(TLS_WRITE_AGAIN) + if (-1 == close(http->fd)) + warn("%s: close", http->src.ip); +#endif + } else if (-1 != http->fd) { + /* Non-TLS connection. */ + if (-1 == close(http->fd)) + warn("%s: close", http->src.ip); + } + + http->fd = -1; + http->ctx = NULL; +} + +void +http_free(struct http *http) +{ + + if (NULL == http) + return; + http_disconnect(http); + if (NULL != http->cfg) + tls_config_free(http->cfg); + free(http->host); + free(http->path); + free(http->src.ip); + free(http); +} + +struct http * +http_alloc(const struct source *addrs, size_t addrsz, + const char *host, short port, const char *path) +{ + struct sockaddr_storage ss; + int family, fd, c; + socklen_t len; + size_t cur, i = 0; + struct http *http; + + /* Do this while we still have addresses to connect. */ +again: + if (i == addrsz) + return(NULL); + cur = i++; + + /* Convert to PF_INET or PF_INET6 address from string. */ + + memset(&ss, 0, sizeof(struct sockaddr_storage)); + + if (4 == addrs[cur].family) { + family = PF_INET; + ((struct sockaddr_in *)&ss)->sin_family = AF_INET; + ((struct sockaddr_in *)&ss)->sin_port = htons(port); + c = inet_pton(AF_INET, addrs[cur].ip, + &((struct sockaddr_in *)&ss)->sin_addr); + len = sizeof(struct sockaddr_in); + } else if (6 == addrs[cur].family) { + family = PF_INET6; + ((struct sockaddr_in6 *)&ss)->sin6_family = AF_INET6; + ((struct sockaddr_in6 *)&ss)->sin6_port = htons(port); + c = inet_pton(AF_INET6, addrs[cur].ip, + &((struct sockaddr_in6 *)&ss)->sin6_addr); + len = sizeof(struct sockaddr_in6); + } else { + warnx("%s: unknown family", addrs[cur].ip); + goto again; + } + + if (c < 0) { + warn("%s: inet_ntop", addrs[cur].ip); + goto again; + } else if (0 == c) { + warnx("%s: inet_ntop", addrs[cur].ip); + goto again; + } + + /* Create socket and connect. */ + + fd = socket(family, SOCK_STREAM, 0); + if (-1 == fd) { + warn("%s: socket", addrs[cur].ip); + goto again; + } else if (-1 == connect(fd, (struct sockaddr *)&ss, len)) { + warn("%s: connect", addrs[cur].ip); + close(fd); + goto again; + } + + /* Allocate the communicator. */ + + http = calloc(1, sizeof(struct http)); + if (NULL == http) { + warn("calloc"); + close(fd); + return(NULL); + } + http->fd = fd; + http->port = port; + http->src.family = addrs[cur].family; + http->src.ip = strdup(addrs[cur].ip); + http->host = strdup(host); + http->path = strdup(path); + if (NULL == http->src.ip || + NULL == http->host || + NULL == http->path) { + warn("strdup"); + goto err; + } + + /* If necessary, do our TLS setup. */ + + if (443 != port) { + http->writer = dosyswrite; + http->reader = dosysread; + return(http); + } + + http->writer = dotlswrite; + http->reader = dotlsread; + + if (-1 == tls_init()) { + warn("tls_init"); + goto err; + } + + http->cfg = tls_config_new(); + if (NULL == http->cfg) { + warn("tls_config_new"); + goto err; + } + + tls_config_set_protocols(http->cfg, TLS_PROTOCOLS_ALL); + + /* FIXME: is this necessary? */ + tls_config_insecure_noverifycert(http->cfg); + + if (-1 == tls_config_set_ciphers(http->cfg, "compat")) { + warn("tls_config_set_ciphers"); + goto err; + } else if (NULL == (http->ctx = tls_client())) { + warn("tls_client"); + goto err; + } else if (-1 == tls_configure(http->ctx, http->cfg)) { + warnx("%s: tls_configure: %s", + http->src.ip, tls_error(http->ctx)); + goto err; + } + + if (0 != tls_connect_socket + (http->ctx, http->fd, http->host)) { + warnx("%s: tls_connect_socket: %s, %s", + http->src.ip, http->host, + tls_error(http->ctx)); + goto err; + } + + return(http); +err: + http_free(http); + return(NULL); +} + +struct httpxfer * +http_open(const struct http *http, const void *p, size_t psz) +{ + char *req; + int c; + struct httpxfer *trans; + + if (NULL == p) { + c = asprintf(&req, + "GET %s HTTP/1.0\r\n" + "Host: %s\r\n" + "\r\n", + http->path, http->host); + } else { + c = asprintf(&req, + "POST %s HTTP/1.0\r\n" + "Host: %s\r\n" + "Content-Length: %zu\r\n" + "\r\n", + http->path, http->host, psz); + } + if (-1 == c) { + warn("asprintf"); + return(NULL); + } else if ( ! http_write(req, c, http)) { + free(req); + return(NULL); + } else if (NULL != p && ! http_write(p, psz, http)) { + free(req); + return(NULL); + } + + free(req); + + trans = calloc(1, sizeof(struct httpxfer)); + if (NULL == trans) + warn("calloc"); + return(trans); +} + +void +http_close(struct httpxfer *x) +{ + + if (NULL == x) + return; + free(x->hbuf); + free(x->bbuf); + free(x->headbuf); + free(x->head); + free(x); +} + +/* + * Read the HTTP body from the wire. + * If invoked multiple times, this will return the same pointer with the + * same data (or NULL, if the original invocation returned NULL). + * Returns NULL if read or allocation errors occur. + * You must not free the returned pointer. + */ +char * +http_body_read(const struct http *http, + struct httpxfer *trans, size_t *sz) +{ + char buf[BUFSIZ]; + ssize_t ssz; + void *pp; + size_t szp; + + if (NULL == sz) + sz = &szp; + + /* Have we already parsed this? */ + + if (trans->bodyok > 0) { + *sz = trans->bbufsz; + return(trans->bbuf); + } else if (trans->bodyok < 0) + return(NULL); + + *sz = 0; + trans->bodyok = -1; + + do { + /* If less than sizeof(buf), at EOF. */ + if ((ssz = http_read(buf, sizeof(buf), http)) < 0) + return(NULL); + else if (0 == ssz) + break; + pp = realloc(trans->bbuf, trans->bbufsz + ssz); + if (NULL == pp) { + warn("realloc"); + return(NULL); + } + trans->bbuf = pp; + memcpy(trans->bbuf + trans->bbufsz, buf, ssz); + trans->bbufsz += ssz; + } while (sizeof(buf) == ssz); + + trans->bodyok = 1; + *sz = trans->bbufsz; + return(trans->bbuf); +} + +struct httphead * +http_head_get(const char *v, struct httphead *h, size_t hsz) +{ + size_t i; + + for (i = 0; i < hsz; i++) { + if (strcmp(h[i].key, v)) + continue; + return(&h[i]); + } + return(NULL); +} + +/* + * Look through the headers and determine our HTTP code. + * This will return -1 on failure, otherwise the code. + */ +int +http_head_status(const struct http *http, + struct httphead *h, size_t sz) +{ + int rc; + unsigned int code; + struct httphead *st; + + if (NULL == (st = http_head_get("Status", h, sz))) { + warnx("%s: no status header", http->src.ip); + return(-1); + } + + rc = sscanf(st->val, "%*s %u %*s", &code); + if (rc < 0) { + warn("sscanf"); + return(-1); + } else if (1 != rc) { + warnx("%s: cannot convert status header", + http->src.ip); + return(-1); + } + return(code); +} + +/* + * Parse headers from the transfer. + * Malformed headers are skipped. + * A special "Status" header is added for the HTTP status line. + * This can only happen once http_head_read has been called with + * success. + * This can be invoked multiple times: it will only parse the headers + * once and after that it will just return the cache. + * You must not free the returned pointer. + * If the original header parse failed, or if memory allocation fails + * internally, this returns NULL. + */ +struct httphead * +http_head_parse(const struct http *http, + struct httpxfer *trans, size_t *sz) +{ + size_t hsz, szp; + struct httphead *h; + char *cp, *ep, *ccp, *buf; + + if (NULL == sz) + sz = &szp; + + /* + * If we've already parsed the headers, return the + * previously-parsed buffer now. + * If we have errors on the stream, return NULL now. + */ + + if (NULL != trans->head) { + *sz = trans->headsz; + return(trans->head); + } else if (trans->headok <= 0) + return(NULL); + + if (NULL == (buf = strdup(trans->hbuf))) { + warn("strdup"); + return(NULL); + } + hsz = 0; + cp = buf; + + do { + if (NULL != (cp = strstr(cp, "\r\n"))) + cp += 2; + hsz++; + } while (NULL != cp); + + /* + * Allocate headers, then step through the data buffer, parsing + * out headers as we have them. + * We know at this point that the buffer is nil-terminated in + * the usual way. + */ + + h = calloc(hsz, sizeof(struct httphead)); + if (NULL == h) { + warn("calloc"); + free(buf); + return(NULL); + } + + *sz = hsz; + hsz = 0; + cp = buf; + + do { + if (NULL != (ep = strstr(cp, "\r\n"))) { + *ep = '\0'; + ep += 2; + } + if (0 == hsz) { + h[hsz].key = "Status"; + h[hsz++].val = cp; + continue; + } + + /* Skip bad headers. */ + if (NULL == (ccp = strchr(cp, ':'))) { + warnx("%s: header without separator", + http->src.ip); + continue; + } + + *ccp++ = '\0'; + while (isspace((int)*ccp)) + ccp++; + h[hsz].key = cp; + h[hsz++].val = ccp; + } while (NULL != (cp = ep)); + + trans->headbuf = buf; + trans->head = h; + trans->headsz = hsz; + return(h); +} + +/* + * Read the HTTP headers from the wire. + * If invoked multiple times, this will return the same pointer with the + * same data (or NULL, if the original invocation returned NULL). + * Returns NULL if read or allocation errors occur. + * You must not free the returned pointer. + */ +char * +http_head_read(const struct http *http, + struct httpxfer *trans, size_t *sz) +{ + char buf[BUFSIZ]; + ssize_t ssz; + char *ep; + void *pp; + size_t szp; + + if (NULL == sz) + sz = &szp; + + /* Have we already parsed this? */ + + if (trans->headok > 0) { + *sz = trans->hbufsz; + return(trans->hbuf); + } else if (trans->headok < 0) + return(NULL); + + *sz = 0; + ep = NULL; + trans->headok = -1; + + /* + * Begin by reading by BUFSIZ blocks until we reach the header + * termination marker (two CRLFs). + * We might read into our body, but that's ok: we'll copy out + * the body parts into our body buffer afterward. + */ + + do { + /* If less than sizeof(buf), at EOF. */ + if ((ssz = http_read(buf, sizeof(buf), http)) < 0) + return(NULL); + else if (0 == ssz) + break; + pp = realloc(trans->hbuf, trans->hbufsz + ssz); + if (NULL == pp) { + warn("realloc"); + return(NULL); + } + trans->hbuf = pp; + memcpy(trans->hbuf + trans->hbufsz, buf, ssz); + trans->hbufsz += ssz; + /* Search for end of headers marker. */ + ep = memmem(trans->hbuf, trans->hbufsz, "\r\n\r\n", 4); + } while (NULL == ep && sizeof(buf) == ssz); + + if (NULL == ep) { + warnx("%s: partial transfer", http->src.ip); + return(NULL); + } + *ep = '\0'; + + /* + * The header data is invalid if it has any binary characters in + * it: check that now. + * This is important because we want to guarantee that all + * header keys and pairs are properly nil-terminated. + */ + + if (strlen(trans->hbuf) != (uintptr_t)(ep - trans->hbuf)) { + warnx("%s: binary data in header", http->src.ip); + return(NULL); + } + + /* + * Copy remaining buffer into body buffer. + */ + + ep += 4; + trans->bbufsz = (trans->hbuf + trans->hbufsz) - ep; + trans->bbuf = malloc(trans->bbufsz); + if (NULL == trans->bbuf) { + warn("malloc"); + return(NULL); + } + memcpy(trans->bbuf, ep, trans->bbufsz); + + trans->headok = 1; + *sz = trans->hbufsz; + return(trans->hbuf); +} + +void +http_get_free(struct httpget *g) +{ + + if (NULL == g) + return; + http_close(g->xfer); + http_free(g->http); + free(g); +} + +struct httpget * +http_get(const struct source *addrs, size_t addrsz, + const char *domain, short port, const char *path, + const void *post, size_t postsz) +{ + struct http *h; + struct httpxfer *x; + struct httpget *g; + struct httphead *head; + size_t headsz, bodsz, headrsz; + int code; + char *bod, *headr; + + h = http_alloc(addrs, addrsz, domain, port, path); + if (NULL == h) + return(NULL); + + if (NULL == (x = http_open(h, post, postsz))) { + http_free(h); + return(NULL); + } else if (NULL == (headr = http_head_read(h, x, &headrsz))) { + http_close(x); + http_free(h); + return(NULL); + } else if (NULL == (bod = http_body_read(h, x, &bodsz))) { + http_close(x); + http_free(h); + return(NULL); + } + + http_disconnect(h); + + if (NULL == (head = http_head_parse(h, x, &headsz))) { + http_close(x); + http_free(h); + return(NULL); + } else if ((code = http_head_status(h, head, headsz)) < 0) { + http_close(x); + http_free(h); + return(NULL); + } + + if (NULL == (g = calloc(1, sizeof(struct httpget)))) { + warn("calloc"); + http_close(x); + http_free(h); + return(NULL); + } + + g->headpart = headr; + g->headpartsz = headrsz; + g->bodypart = bod; + g->bodypartsz = bodsz; + g->head = head; + g->headsz = headsz; + g->code = code; + g->xfer = x; + g->http = h; + return(g); +} + +#if 0 +int +main(void) +{ + struct httpget *g; + struct httphead *httph; + size_t i, httphsz; + struct source addrs[2]; + size_t addrsz; + +#if 0 + addrs[0].ip = "127.0.0.1"; + addrs[0].family = 4; + addrsz = 1; +#else + addrs[0].ip = "2a00:1450:400a:806::2004"; + addrs[0].family = 6; + addrs[1].ip = "193.135.3.123"; + addrs[1].family = 4; + addrsz = 2; +#endif + +#if 0 + g = http_get(addrs, addrsz, "localhost", 80, "/index.html"); +#else + g = http_get(addrs, addrsz, "www.google.ch", 80, "/index.html", NULL, 0); +#endif + + if (NULL == g) + errx(EXIT_FAILURE, "http_get"); + + httph = http_head_parse(g->http, g->xfer, &httphsz); + warnx("code: %d", g->code); + + for (i = 0; i < httphsz; i++) + warnx("head: [%s]=[%s]", httph[i].key, httph[i].val); + + http_get_free(g); + return(EXIT_SUCCESS); +} +#endif diff --git a/usr.sbin/acme-client/http.h b/usr.sbin/acme-client/http.h new file mode 100644 index 00000000000..9ed1d163595 --- /dev/null +++ b/usr.sbin/acme-client/http.h @@ -0,0 +1,94 @@ +/* $Id: http.h,v 1.1 2016/08/31 22:01:42 florian Exp $ */ +/* + * Copyright (c) 2016 Kristaps Dzonsons <kristaps@bsd.lv> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ +#ifndef HTTP_H +#define HTTP_H + +struct source { + int family; /* 4 (PF_INET) or 6 (PF_INET6) */ + char *ip; /* IPV4 or IPV6 address */ +}; + +struct http; + +/* + * Write and read callbacks to allow HTTP and HTTPS. + * Both of these return the number of bytes read (or written) or -1 on + * failure. + * 0 bytes read means that the connection has closed. + */ +typedef ssize_t (*writefp)(const void *, size_t, const struct http *); +typedef ssize_t (*readfp)(char *, size_t, const struct http *); + +/* + * HTTP/S header pair. + * There's also a cooked-up pair, "Status", with the status code. + * Both strings are nil-terminated. + */ +struct httphead { + const char *key; + const char *val; +}; + +/* + * Grab all information from a transfer. + * DO NOT free any parts of this, and editing the parts (e.g., changing + * the underlying strings) will persist; so in short, don't. + * All of these values will be set upon http_get() success. + */ +struct httpget { + struct httpxfer *xfer; /* underlying transfer */ + struct http *http; /* underlying connection */ + int code; /* return code */ + struct httphead *head; /* headers */ + size_t headsz; /* number of headers */ + char *headpart; /* header buffer */ + size_t headpartsz; /* size of headpart */ + char *bodypart; /* body buffer */ + size_t bodypartsz; /* size of bodypart */ +}; + +__BEGIN_DECLS + +/* Convenience functions. */ +struct httpget *http_get(const struct source *, size_t, + const char *, short, const char *, + const void *, size_t); +void http_get_free(struct httpget *); + +/* Allocation and release. */ +struct http *http_alloc(const struct source *, size_t, + const char *, short, const char *); +void http_free(struct http *); +struct httpxfer *http_open(const struct http *, const void *, size_t); +void http_close(struct httpxfer *); +void http_disconnect(struct http *); + +/* Access. */ +char *http_head_read(const struct http *, + struct httpxfer *, size_t *); +struct httphead *http_head_parse(const struct http *, + struct httpxfer *, size_t *); +char *http_body_read(const struct http *, + struct httpxfer *, size_t *); +int http_head_status(const struct http *, + struct httphead *, size_t); +struct httphead *http_head_get(const char *, + struct httphead *, size_t); + +__END_DECLS + +#endif /* HTTP_H */ diff --git a/usr.sbin/acme-client/jsmn.c b/usr.sbin/acme-client/jsmn.c new file mode 100644 index 00000000000..26676fa7b2f --- /dev/null +++ b/usr.sbin/acme-client/jsmn.c @@ -0,0 +1,332 @@ +/* + Copyright (c) 2010 Serge A. Zaitsev + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE.* + */ +#include "jsmn.h" + +/** + * Allocates a fresh unused token from the token pull. + */ +static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser, + jsmntok_t *tokens, size_t num_tokens) { + jsmntok_t *tok; + if (parser->toknext >= num_tokens) { + return NULL; + } + tok = &tokens[parser->toknext++]; + tok->start = tok->end = -1; + tok->size = 0; +#ifdef JSMN_PARENT_LINKS + tok->parent = -1; +#endif + return tok; +} + +/** + * Fills token type and boundaries. + */ +static void jsmn_fill_token(jsmntok_t *token, jsmntype_t type, + int start, int end) { + token->type = type; + token->start = start; + token->end = end; + token->size = 0; +} + +/** + * Fills next available token with JSON primitive. + */ +static int jsmn_parse_primitive(jsmn_parser *parser, const char *js, + size_t len, jsmntok_t *tokens, size_t num_tokens) { + jsmntok_t *token; + int start; + + start = parser->pos; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + switch (js[parser->pos]) { +#ifndef JSMN_STRICT + /* In strict mode primitive must be followed by "," or "}" or "]" */ + case ':': +#endif + case '\t' : case '\r' : case '\n' : case ' ' : + case ',' : case ']' : case '}' : + goto found; + } + if (js[parser->pos] < 32 || js[parser->pos] >= 127) { + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } +#ifdef JSMN_STRICT + /* In strict mode primitive must be followed by a comma/object/array */ + parser->pos = start; + return JSMN_ERROR_PART; +#endif + +found: + if (tokens == NULL) { + parser->pos--; + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos); +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + parser->pos--; + return 0; +} + +/** + * Fills next token with JSON string. + */ +static int jsmn_parse_string(jsmn_parser *parser, const char *js, + size_t len, jsmntok_t *tokens, size_t num_tokens) { + jsmntok_t *token; + + int start = parser->pos; + + parser->pos++; + + /* Skip starting quote */ + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + char c = js[parser->pos]; + + /* Quote: end of string */ + if (c == '\"') { + if (tokens == NULL) { + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_STRING, start+1, parser->pos); +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + return 0; + } + + /* Backslash: Quoted symbol expected */ + if (c == '\\' && parser->pos + 1 < len) { + int i; + parser->pos++; + switch (js[parser->pos]) { + /* Allowed escaped symbols */ + case '\"': case '/' : case '\\' : case 'b' : + case 'f' : case 'r' : case 'n' : case 't' : + break; + /* Allows escaped symbol \uXXXX */ + case 'u': + parser->pos++; + for(i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0'; i++) { + /* If it isn't a hex character we have an error */ + if(!((js[parser->pos] >= 48 && js[parser->pos] <= 57) || /* 0-9 */ + (js[parser->pos] >= 65 && js[parser->pos] <= 70) || /* A-F */ + (js[parser->pos] >= 97 && js[parser->pos] <= 102))) { /* a-f */ + parser->pos = start; + return JSMN_ERROR_INVAL; + } + parser->pos++; + } + parser->pos--; + break; + /* Unexpected symbol */ + default: + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } + } + parser->pos = start; + return JSMN_ERROR_PART; +} + +/** + * Parse JSON string and fill tokens. + */ +int jsmn_parse(jsmn_parser *parser, const char *js, size_t len, + jsmntok_t *tokens, unsigned int num_tokens) { + int r; + int i; + jsmntok_t *token; + int count = parser->toknext; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + char c; + jsmntype_t type; + + c = js[parser->pos]; + switch (c) { + case '{': case '[': + count++; + if (tokens == NULL) { + break; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) + return JSMN_ERROR_NOMEM; + if (parser->toksuper != -1) { + tokens[parser->toksuper].size++; +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + } + token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY); + token->start = parser->pos; + parser->toksuper = parser->toknext - 1; + break; + case '}': case ']': + if (tokens == NULL) + break; + type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY); +#ifdef JSMN_PARENT_LINKS + if (parser->toknext < 1) { + return JSMN_ERROR_INVAL; + } + token = &tokens[parser->toknext - 1]; + for (;;) { + if (token->start != -1 && token->end == -1) { + if (token->type != type) { + return JSMN_ERROR_INVAL; + } + token->end = parser->pos + 1; + parser->toksuper = token->parent; + break; + } + if (token->parent == -1) { + break; + } + token = &tokens[token->parent]; + } +#else + for (i = parser->toknext - 1; i >= 0; i--) { + token = &tokens[i]; + if (token->start != -1 && token->end == -1) { + if (token->type != type) { + return JSMN_ERROR_INVAL; + } + parser->toksuper = -1; + token->end = parser->pos + 1; + break; + } + } + /* Error if unmatched closing bracket */ + if (i == -1) return JSMN_ERROR_INVAL; + for (; i >= 0; i--) { + token = &tokens[i]; + if (token->start != -1 && token->end == -1) { + parser->toksuper = i; + break; + } + } +#endif + break; + case '\"': + r = jsmn_parse_string(parser, js, len, tokens, num_tokens); + if (r < 0) return r; + count++; + if (parser->toksuper != -1 && tokens != NULL) + tokens[parser->toksuper].size++; + break; + case '\t' : case '\r' : case '\n' : case ' ': + break; + case ':': + parser->toksuper = parser->toknext - 1; + break; + case ',': + if (tokens != NULL && parser->toksuper != -1 && + tokens[parser->toksuper].type != JSMN_ARRAY && + tokens[parser->toksuper].type != JSMN_OBJECT) { +#ifdef JSMN_PARENT_LINKS + parser->toksuper = tokens[parser->toksuper].parent; +#else + for (i = parser->toknext - 1; i >= 0; i--) { + if (tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT) { + if (tokens[i].start != -1 && tokens[i].end == -1) { + parser->toksuper = i; + break; + } + } + } +#endif + } + break; +#ifdef JSMN_STRICT + /* In strict mode primitives are: numbers and booleans */ + case '-': case '0': case '1' : case '2': case '3' : case '4': + case '5': case '6': case '7' : case '8': case '9': + case 't': case 'f': case 'n' : + /* And they must not be keys of the object */ + if (tokens != NULL && parser->toksuper != -1) { + jsmntok_t *t = &tokens[parser->toksuper]; + if (t->type == JSMN_OBJECT || + (t->type == JSMN_STRING && t->size != 0)) { + return JSMN_ERROR_INVAL; + } + } +#else + /* In non-strict mode every unquoted value is a primitive */ + default: +#endif + r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens); + if (r < 0) return r; + count++; + if (parser->toksuper != -1 && tokens != NULL) + tokens[parser->toksuper].size++; + break; + +#ifdef JSMN_STRICT + /* Unexpected char in strict mode */ + default: + return JSMN_ERROR_INVAL; +#endif + } + } + + if (tokens != NULL) { + for (i = parser->toknext - 1; i >= 0; i--) { + /* Unmatched opened object or array */ + if (tokens[i].start != -1 && tokens[i].end == -1) { + return JSMN_ERROR_PART; + } + } + } + + return count; +} + +/** + * Creates a new parser based over a given buffer with an array of tokens + * available. + */ +void jsmn_init(jsmn_parser *parser) { + parser->pos = 0; + parser->toknext = 0; + parser->toksuper = -1; +} + diff --git a/usr.sbin/acme-client/jsmn.h b/usr.sbin/acme-client/jsmn.h new file mode 100644 index 00000000000..0a3c5d7265e --- /dev/null +++ b/usr.sbin/acme-client/jsmn.h @@ -0,0 +1,97 @@ +/* + Copyright (c) 2010 Serge A. Zaitsev + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE.* + */ +#ifndef __JSMN_H_ +#define __JSMN_H_ + +#include <stddef.h> + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * JSON type identifier. Basic types are: + * o Object + * o Array + * o String + * o Other primitive: number, boolean (true/false) or null + */ +typedef enum { + JSMN_UNDEFINED = 0, + JSMN_OBJECT = 1, + JSMN_ARRAY = 2, + JSMN_STRING = 3, + JSMN_PRIMITIVE = 4 +} jsmntype_t; + +enum jsmnerr { + /* Not enough tokens were provided */ + JSMN_ERROR_NOMEM = -1, + /* Invalid character inside JSON string */ + JSMN_ERROR_INVAL = -2, + /* The string is not a full JSON packet, more bytes expected */ + JSMN_ERROR_PART = -3 +}; + +/** + * JSON token description. + * @param type type (object, array, string etc.) + * @param start start position in JSON data string + * @param end end position in JSON data string + */ +typedef struct { + jsmntype_t type; + int start; + int end; + int size; +#ifdef JSMN_PARENT_LINKS + int parent; +#endif +} jsmntok_t; + +/** + * JSON parser. Contains an array of token blocks available. Also stores + * the string being parsed now and current position in that string + */ +typedef struct { + unsigned int pos; /* offset in the JSON string */ + unsigned int toknext; /* next token to allocate */ + int toksuper; /* superior token node, e.g parent object or array */ +} jsmn_parser; + +/** + * Create JSON parser over an array of tokens + */ +void jsmn_init(jsmn_parser *parser); + +/** + * Run JSON parser. It parses a JSON data string into and array of tokens, each describing + * a single JSON object. + */ +int jsmn_parse(jsmn_parser *parser, const char *js, size_t len, + jsmntok_t *tokens, unsigned int num_tokens); + +#ifdef __cplusplus +} +#endif + +#endif /* __JSMN_H_ */ diff --git a/usr.sbin/acme-client/json.c b/usr.sbin/acme-client/json.c new file mode 100644 index 00000000000..0c23f5490d7 --- /dev/null +++ b/usr.sbin/acme-client/json.c @@ -0,0 +1,632 @@ +/* $Id: json.c,v 1.1 2016/08/31 22:01:42 florian Exp $ */ +/* + * Copyright (c) 2016 Kristaps Dzonsons <kristaps@bsd.lv> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include <assert.h> +#include <err.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "jsmn.h" +#include "extern.h" + +struct jsmnp; + +/* + * A node in the JSMN parse tree. + * Each of this corresponds to an object in the original JSMN token + * list, although the contents have been extracted properly. + */ +struct jsmnn { + struct parse *p; /* parser object */ + union { + char *str; /* JSMN_PRIMITIVE, JSMN_STRING */ + struct jsmnp *obj; /* JSMN_OBJECT */ + struct jsmnn **array; /* JSMN_ARRAY */ + } d; + size_t fields; /* entries in "d" */ + jsmntype_t type; /* type of node */ +}; + +/* + * Objects consist of node pairs: the left-hand side (before the colon) + * and the right-hand side---the data. + */ +struct jsmnp { + struct jsmnn *lhs; /* left of colon */ + struct jsmnn *rhs; /* right of colon */ +}; + +/* + * Object for converting the JSMN token array into a tree. + */ +struct parse { + struct jsmnn *nodes; /* all nodes */ + size_t cur; /* current number */ + size_t max; /* nodes in "nodes" */ +}; + +/* + * Recursive part for convertin a JSMN token array into a tree. + * See "example/jsondump.c" for its construction (it's the same except + * for how it handles allocation errors). + */ +static ssize_t +build(struct parse *parse, struct jsmnn **np, + jsmntok_t *t, const char *js, size_t sz) +{ + size_t i, j; + struct jsmnn *n; + ssize_t tmp; + + if (0 == sz) + return(0); + + assert(parse->cur < parse->max); + n = *np = &parse->nodes[parse->cur++]; + n->p = parse; + n->type = t->type; + + switch (t->type) { + case (JSMN_STRING): + /* FALLTHROUGH */ + case (JSMN_PRIMITIVE): + n->fields = 1; + n->d.str = strndup + (js + t->start, + t->end - t->start); + if (NULL == n->d.str) + break; + return(1); + case (JSMN_OBJECT): + n->fields = t->size; + n->d.obj = calloc(n->fields, + sizeof(struct jsmnp)); + if (NULL == n->d.obj) + break; + for (i = j = 0; i < (size_t)t->size; i++) { + tmp = build(parse, + &n->d.obj[i].lhs, + t + 1 + j, js, sz - j); + if (tmp < 0) + break; + j += tmp; + tmp = build(parse, + &n->d.obj[i].rhs, + t + 1 + j, js, sz - j); + if (tmp < 0) + break; + j += tmp; + } + if (i < (size_t)t->size) + break; + return(j + 1); + case (JSMN_ARRAY): + n->fields = t->size; + n->d.array = calloc(n->fields, + sizeof(struct jsmnn *)); + if (NULL == n->d.array) + break; + for (i = j = 0; i < (size_t)t->size; i++) { + tmp = build(parse, + &n->d.array[i], + t + 1 + j, js, sz - j); + if (tmp < 0) + break; + j += tmp; + } + if (i < (size_t)t->size) + break; + return(j + 1); + default: + break; + } + + return(-1); +} + +/* + * Fully free up a parse sequence. + * This handles all nodes sequentially, not recursively. + */ +static void +jsmnparse_free(struct parse *p) +{ + size_t i; + + if (NULL == p) + return; + for (i = 0; i < p->max; i++) + if (JSMN_ARRAY == p->nodes[i].type) + free(p->nodes[i].d.array); + else if (JSMN_OBJECT == p->nodes[i].type) + free(p->nodes[i].d.obj); + else if (JSMN_PRIMITIVE == p->nodes[i].type) + free(p->nodes[i].d.str); + else if (JSMN_STRING == p->nodes[i].type) + free(p->nodes[i].d.str); + free(p->nodes); + free(p); +} + +/* + * Allocate a tree representation of "t". + * This returns NULL on allocation failure or when sz is zero, in which + * case all resources allocated along the way are freed already. + */ +static struct jsmnn * +jsmntree_alloc(jsmntok_t *t, const char *js, size_t sz) +{ + struct jsmnn *first; + struct parse *p; + + if (0 == sz) + return(NULL); + + p = calloc(1, sizeof(struct parse)); + if (NULL == p) + return(NULL); + + p->max = sz; + p->nodes = calloc(p->max, sizeof(struct jsmnn)); + if (NULL == p->nodes) { + free(p); + return(NULL); + } + + if (build(p, &first, t, js, sz) < 0) { + jsmnparse_free(p); + first = NULL; + } + + return(first); +} + +/* + * Call through to free parse contents. + */ +void +json_free(struct jsmnn *first) +{ + + if (NULL != first) + jsmnparse_free(first->p); +} + +/* + * Just check that the array object is in fact an object. + */ +static struct jsmnn * +json_getarrayobj(struct jsmnn *n) +{ + + return (JSMN_OBJECT != n->type ? NULL : n); +} + +/* + * Extract an array from the returned JSON object, making sure that it's + * the correct type. + * Returns NULL on failure. + */ +static struct jsmnn * +json_getarray(struct jsmnn *n, const char *name) +{ + size_t i; + + if (JSMN_OBJECT != n->type) + return(NULL); + for (i = 0; i < n->fields; i++) { + if (JSMN_STRING != n->d.obj[i].lhs->type && + JSMN_PRIMITIVE != n->d.obj[i].lhs->type) + continue; + else if (strcmp(name, n->d.obj[i].lhs->d.str)) + continue; + break; + } + if (i == n->fields) + return(NULL); + if (JSMN_ARRAY != n->d.obj[i].rhs->type) + return(NULL); + return(n->d.obj[i].rhs); +} + +/* + * Extract a single string from the returned JSON object, making sure + * that it's the correct type. + * Returns NULL on failure. + */ +static char * +json_getstr(struct jsmnn *n, const char *name) +{ + size_t i; + char *cp; + + if (JSMN_OBJECT != n->type) + return(NULL); + for (i = 0; i < n->fields; i++) { + if (JSMN_STRING != n->d.obj[i].lhs->type && + JSMN_PRIMITIVE != n->d.obj[i].lhs->type) + continue; + else if (strcmp(name, n->d.obj[i].lhs->d.str)) + continue; + break; + } + if (i == n->fields) + return(NULL); + if (JSMN_STRING != n->d.obj[i].rhs->type && + JSMN_PRIMITIVE != n->d.obj[i].rhs->type) + return(NULL); + + cp = strdup(n->d.obj[i].rhs->d.str); + if (NULL == cp) + warn("strdup"); + return(cp); +} + +/* + * Completely free the challenge response body. + */ +void +json_free_challenge(struct chng *p) +{ + + free(p->uri); + free(p->token); + p->uri = p->token = NULL; +} + +/* + * Parse the response from the ACME server when we're waiting to see + * whether the challenge has been ok. + */ +int +json_parse_response(struct jsmnn *n) +{ + char *resp; + int rc; + + if (NULL == n) + return(-1); + if (NULL == (resp = json_getstr(n, "status"))) + return(-1); + + if (0 == strcmp(resp, "valid")) + rc = 1; + else if (0 == strcmp(resp, "pending")) + rc = 0; + else + rc = -1; + + free(resp); + return(rc); +} + +/* + * Parse the response from a new-authz, which consists of challenge + * information, into a structure. + * We only care about the HTTP-01 response. + */ +int +json_parse_challenge(struct jsmnn *n, struct chng *p) +{ + struct jsmnn *array, *obj; + size_t i; + int rc; + char *type; + + if (NULL == n) + return(0); + + array = json_getarray(n, "challenges"); + if (NULL == array) + return(0); + + for (i = 0; i < array->fields; i++) { + obj = json_getarrayobj(array->d.array[i]); + if (NULL == obj) + continue; + type = json_getstr(obj, "type"); + if (NULL == type) + continue; + rc = strcmp(type, "http-01"); + free(type); + if (rc) + continue; + p->uri = json_getstr(obj, "uri"); + p->token = json_getstr(obj, "token"); + return(NULL != p->uri && + NULL != p->token); + } + + return(0); +} + +/* + * Extract the CA paths from the JSON response object. + * Return zero on failure, non-zero on success. + */ +int +json_parse_capaths(struct jsmnn *n, struct capaths *p) +{ + + if (NULL == n) + return(0); + + p->newauthz = json_getstr(n, "new-authz"); + p->newcert = json_getstr(n, "new-cert"); + p->newreg = json_getstr(n, "new-reg"); + p->revokecert = json_getstr(n, "revoke-cert"); + + return(NULL != p->newauthz && + NULL != p->newcert && + NULL != p->newreg && + NULL != p->revokecert); +} + +/* + * Free up all of our CA-noted paths (which may all be NULL). + */ +void +json_free_capaths(struct capaths *p) +{ + + free(p->newauthz); + free(p->newcert); + free(p->newreg); + free(p->revokecert); + memset(p, 0, sizeof(struct capaths)); +} + +/* + * Parse an HTTP response body from a buffer of size "sz". + * Returns an opaque pointer on success, otherwise NULL on error. + */ +struct jsmnn * +json_parse(const char *buf, size_t sz) +{ + struct jsmnn *n; + jsmn_parser p; + jsmntok_t *tok; + int r; + size_t tokcount; + + jsmn_init(&p); + tokcount = 128; + + /* Do this until we don't need any more tokens. */ +again: + tok = calloc(tokcount, sizeof(jsmntok_t)); + if (NULL == tok) { + warn("calloc"); + return(NULL); + } + + /* Actually try to parse the JSON into the tokens. */ + + r = jsmn_parse(&p, buf, sz, tok, tokcount); + if (r < 0 && JSMN_ERROR_NOMEM == r) { + tokcount *= 2; + free(tok); + goto again; + } else if (r < 0) { + warnx("jsmn_parse: %d", r); + free(tok); + return(NULL); + } + + /* Now parse the tokens into a tree. */ + + n = jsmntree_alloc(tok, buf, r); + free(tok); + return(n); +} + +/* + * Format the "new-reg" resource request. + */ +char * +json_fmt_newreg(const char *license) +{ + int c; + char *p; + + c = asprintf(&p, "{" + "\"resource\": \"new-reg\", " + "\"agreement\": \"%s\"" + "}", license); + if (-1 == c) { + warn("asprintf"); + p = NULL; + } + return(p); +} + +/* + * Format the "new-authz" resource request. + */ +char * +json_fmt_newauthz(const char *domain) +{ + int c; + char *p; + + c = asprintf(&p, "{" + "\"resource\": \"new-authz\", " + "\"identifier\": " + "{\"type\": \"dns\", \"value\": \"%s\"}" + "}", domain); + if (-1 == c) { + warn("asprintf"); + p = NULL; + } + return(p); +} + +/* + * Format the "challenge" resource request. + */ +char * +json_fmt_challenge(const char *token, const char *thumb) +{ + int c; + char *p; + + c = asprintf(&p, "{" + "\"resource\": \"challenge\", " + "\"keyAuthorization\": \"%s.%s\"" + "}", token, thumb); + if (-1 == c) { + warn("asprintf"); + p = NULL; + } + return(p); +} + +/* + * Format the "new-cert" resource request. + */ +char * +json_fmt_revokecert(const char *cert) +{ + int c; + char *p; + + c = asprintf(&p, "{" + "\"resource\": \"revoke-cert\", " + "\"certificate\": \"%s\"" + "}", cert); + if (-1 == c) { + warn("asprintf"); + p = NULL; + } + return(p); +} + +/* + * Format the "new-cert" resource request. + */ +char * +json_fmt_newcert(const char *cert) +{ + int c; + char *p; + + c = asprintf(&p, "{" + "\"resource\": \"new-cert\", " + "\"csr\": \"%s\"" + "}", cert); + if (-1 == c) { + warn("asprintf"); + p = NULL; + } + return(p); +} + +/* + * Header component of json_fmt_signed(). + */ +char * +json_fmt_header_rsa(const char *exp, const char *mod) +{ + int c; + char *p; + + c = asprintf(&p, "{" + "\"alg\": \"RS256\", " + "\"jwk\": " + "{\"e\": \"%s\", \"kty\": \"RSA\", \"n\": \"%s\"}" + "}", exp, mod); + if (-1 == c) { + warn("asprintf"); + p = NULL; + } + return(p); +} + +/* + * Protected component of json_fmt_signed(). + */ +char * +json_fmt_protected_rsa(const char *exp, const char *mod, const char *nce) +{ + int c; + char *p; + + c = asprintf(&p, "{" + "\"alg\": \"RS256\", " + "\"jwk\": " + "{\"e\": \"%s\", \"kty\": \"RSA\", \"n\": \"%s\"}, " + "\"nonce\": \"%s\"" + "}", exp, mod, nce); + if (-1 == c) { + warn("asprintf"); + p = NULL; + } + return(p); +} + +/* + * Signed message contents for the CA server. + */ +char * +json_fmt_signed(const char *header, const char *protected, + const char *payload, const char *digest) +{ + int c; + char *p; + + c = asprintf(&p, "{" + "\"header\": %s, " + "\"protected\": \"%s\", " + "\"payload\": \"%s\", " + "\"signature\": \"%s\"" + "}", header, protected, payload, digest); + if (-1 == c) { + warn("asprintf"); + p = NULL; + } + return(p); +} + +/* + * Produce thumbprint input. + * This isn't technically a JSON string--it's the input we'll use for + * hashing and digesting. + * However, it's in the form of a JSON string, so do it here. + */ +char * +json_fmt_thumb_rsa(const char *exp, const char *mod) +{ + int c; + char *p; + + /*NOTE: WHITESPACE IS IMPORTANT. */ + + c = asprintf(&p, + "{\"e\":\"%s\",\"kty\":\"RSA\",\"n\":\"%s\"}", + exp, mod); + if (-1 == c) { + warn("asprintf"); + p = NULL; + } + return(p); +} diff --git a/usr.sbin/acme-client/keyproc.c b/usr.sbin/acme-client/keyproc.c new file mode 100644 index 00000000000..5f728919b84 --- /dev/null +++ b/usr.sbin/acme-client/keyproc.c @@ -0,0 +1,293 @@ +/* $Id: keyproc.c,v 1.1 2016/08/31 22:01:42 florian Exp $ */ +/* + * Copyright (c) 2016 Kristaps Dzonsons <kristaps@bsd.lv> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include <sys/stat.h> + +#include <err.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include <openssl/pem.h> +#include <openssl/err.h> +#include <openssl/rand.h> +#include <openssl/x509.h> +#include <openssl/x509v3.h> + +#include "extern.h" +#include "rsa.h" + +/* + * This was lifted more or less directly from demos/x509/mkreq.c of the + * OpenSSL source code. + */ +static int +add_ext(STACK_OF(X509_EXTENSION) *sk, int nid, const char *value) +{ + X509_EXTENSION *ex; + char *cp; + + /* + * XXX: I don't like this at all. + * There's no documentation for X509V3_EXT_conf_nid, so I'm not + * sure if the "value" parameter is ever written to, touched, + * etc. + * The 'official' examples suggest not (they use a string + * literal as the input), but to be safe, I'm doing an + * allocation here and just letting it go. + * This leaks memory, but bounded to the number of SANs. + */ + + if (NULL == (cp = strdup(value))) { + warn("strdup"); + return(0); + } + ex = X509V3_EXT_conf_nid(NULL, NULL, nid, cp); + if (NULL == ex) { + warnx("X509V3_EXT_conf_nid"); + free(cp); + return(0); + } + sk_X509_EXTENSION_push(sk, ex); + return(1); +} + +/* + * Create an X509 certificate from the private key we have on file. + * To do this, we first open the key file, then jail ourselves. + * We then use the crypto library to create the certificate within the + * jail and, on success, ship it to "netsock" as an X509 request. + */ +int +keyproc(int netsock, const char *keyfile, + const char **alts, size_t altsz, int newkey) +{ + char *der64, *der, *dercp, *sans, *san; + FILE *f; + size_t i, sansz; + void *pp; + EVP_PKEY *pkey; + X509_REQ *x; + X509_NAME *name; + unsigned char rbuf[64]; + int len, rc, cc, nid; + mode_t prev; + STACK_OF(X509_EXTENSION) *exts; + + x = NULL; + pkey = NULL; + name = NULL; + der = der64 = sans = san = NULL; + rc = 0; + exts = NULL; + + /* + * First, open our private key file read-only or write-only if + * we're creating from scratch. + * Set our umask to be maximally restrictive. + */ + + prev = umask((S_IWUSR | S_IXUSR) | S_IRWXG | S_IRWXO); + f = fopen(keyfile, newkey ? "wx" : "r"); + umask(prev); + + if (NULL == f) { + warn("%s", keyfile); + goto out; + } + + /* File-system, user, and sandbox jail. */ + + if ( ! sandbox_before()) + goto out; + + ERR_load_crypto_strings(); + + if ( ! dropfs(PATH_VAR_EMPTY)) + goto out; + else if ( ! dropprivs()) + goto out; + else if ( ! sandbox_after()) + goto out; + + /* + * Seed our PRNG with data from arc4random(). + * Do this until we're told it's ok and use increments of 64 + * bytes (arbitrarily). + * TODO: is this sufficient as a RAND source? + */ + + while (0 == RAND_status()) { + arc4random_buf(rbuf, sizeof(rbuf)); + RAND_seed(rbuf, sizeof(rbuf)); + } + + if (newkey) { + if (NULL == (pkey = rsa_key_create(f, keyfile))) + goto out; + dodbg("%s: generated RSA domain key", keyfile); + } else { + if (NULL == (pkey = rsa_key_load(f, keyfile))) + goto out; + doddbg("%s: loaded RSA domain key", keyfile); + } + + fclose(f); + f = NULL; + + /* + * Generate our certificate from the EVP public key. + * Then set it as the X509 requester's key. + */ + + if (NULL == (x = X509_REQ_new())) { + warnx("X509_new"); + goto out; + } else if ( ! X509_REQ_set_pubkey(x, pkey)) { + warnx("X509_set_pubkey"); + goto out; + } + + /* Now specify the common name that we'll request. */ + + if (NULL == (name = X509_NAME_new())) { + warnx("X509_NAME_new"); + goto out; + } else if ( ! X509_NAME_add_entry_by_txt(name, "CN", + MBSTRING_ASC, (u_char *)alts[0], -1, -1, 0)) { + warnx("X509_NAME_add_entry_by_txt: CN=%s", alts[0]); + goto out; + } else if ( ! X509_REQ_set_subject_name(x, name)) { + warnx("X509_req_set_issuer_name"); + goto out; + } + + /* + * Now add the SAN extensions. + * This was lifted more or less directly from demos/x509/mkreq.c + * of the OpenSSL source code. + * (The zeroth altname is the domain name.) + * TODO: is this the best way of doing this? + */ + + if (altsz > 1) { + nid = NID_subject_alt_name; + if (NULL == (exts = sk_X509_EXTENSION_new_null())) { + warnx("sk_X509_EXTENSION_new_null"); + goto out; + } + /* Initialise to empty string. */ + if (NULL == (sans = strdup(""))) { + warn("strdup"); + goto out; + } + sansz = strlen(sans) + 1; + + /* + * For each SAN entry, append it to the string. + * We need a single SAN entry for all of the SAN + * domains: NOT an entry per domain! + */ + + for (i = 1; i < altsz; i++) { + cc = asprintf(&san, "%sDNS:%s", + i > 1 ? "," : "", alts[i]); + if (-1 == cc) { + warn("asprintf"); + goto out; + } + pp = realloc(sans, sansz + strlen(san)); + if (NULL == pp) { + warn("realloc"); + goto out; + } + sans = pp; + sansz += strlen(san); + strlcat(sans, san, sansz); + free(san); + san = NULL; + } + + if ( ! add_ext(exts, nid, sans)) { + warnx("add_ext"); + goto out; + } else if ( ! X509_REQ_add_extensions(x, exts)) { + warnx("X509_REQ_add_extensions"); + goto out; + } + sk_X509_EXTENSION_pop_free + (exts, X509_EXTENSION_free); + } + + /* Sign the X509 request using SHA256. */ + + if ( ! X509_REQ_sign(x, pkey, EVP_sha256())) { + warnx("X509_sign"); + goto out; + } + + /* Now, serialise to DER, then base64. */ + + if ((len = i2d_X509_REQ(x, NULL)) < 0) { + warnx("i2d_X509"); + goto out; + } else if (NULL == (der = dercp = malloc(len))) { + warn("malloc"); + goto out; + } else if (len != i2d_X509_REQ(x, (u_char **)&dercp)) { + warnx("i2d_X509"); + goto out; + } else if (NULL == (der64 = base64buf_url(der, len))) { + warnx("base64buf_url"); + goto out; + } + + /* + * Write that we're ready, then write. + * We ignore reader-closed failure, as we're just going to roll + * into the exit case anyway. + */ + + if (writeop(netsock, COMM_KEY_STAT, KEY_READY) < 0) + goto out; + if (writestr(netsock, COMM_CERT, der64) < 0) + goto out; + + rc = 1; +out: + close(netsock); + if (NULL != f) + fclose(f); + free(der); + free(der64); + free(sans); + free(san); + if (NULL != x) + X509_REQ_free(x); + if (NULL != name) + X509_NAME_free(name); + if (NULL != pkey) + EVP_PKEY_free(pkey); + ERR_print_errors_fp(stderr); + ERR_free_strings(); + return(rc); +} + diff --git a/usr.sbin/acme-client/letskencrypt.1 b/usr.sbin/acme-client/letskencrypt.1 new file mode 100644 index 00000000000..72aa22cf319 --- /dev/null +++ b/usr.sbin/acme-client/letskencrypt.1 @@ -0,0 +1,323 @@ +.Dd $Mdocdate: August 31 2016 $ +.Dt LETSKENCRYPT 1 +.Os +.Sh NAME +.Nm letskencrypt +.Nd secure Let's Encrypt client +.\" .Sh LIBRARY +.\" For sections 2, 3, and 9 only. +.\" Not used in OpenBSD. +.Sh SYNOPSIS +.Nm letskencrypt +.Op Fl bFmnNrsv +.Op Fl a Ar agreement +.Op Fl C Ar challengedir +.Op Fl c Ar certdir +.Op Fl f Ar accountkey +.Op Fl k Ar domainkey +.Ar domain +.Op Ar altnames... +.Sh DESCRIPTION +The +.Nm +utility submits an X509 certificate for +.Ar domain +and its alternate DNS names +.Ar altnames +to a +.Dq Let's Encrypt +server for automated signing. +It can also revoke previously-submitted signatures. +It must be run as root. +(Why? +.Xr chroot 2 . ) +.Pp +By default, it uses +.Pa /var/www/letsencrypt +for responding to challenges +.Pq Fl C , +.Pa /etc/ssl/letsencrypt +for the public certificate directory +.Pq Fl c , +.Pa /etc/ssl/letsencrypt/private/privkey.pem +for the domain private key +.Pq Fl k , +and +.Pa /etc/letsencrypt/privkey.pem +for the account private key +.Pq Fl f . +All of these must exist unless you use +.Fl n +and/or +.Fl N , +which will generate the account and domain private keys, respectively. +Its arguments are as follows: +.Bl -tag -width Ds +.It Fl b +Back up all +.Sx Certificates +in the certificate directory. +This will only back up if something will be done to them (remove or +replace). +The backups are called +.Pa cert-NNNNN.pem , +.Pa chain-NNNNN.pem , +and +.Pa fullchain-NNNNN.pem , +where +.Li NNNNN +is the current UNIX epoch. +Any given backup effort will use the same epoch time for all three +certificates. +If there are no certificates in place, this does nothing. +.It Fl F +Force updating the certificate signature even if it's too soon. +.It Fl m +Append +.Ar domain +to all default paths except the challenge path +.Pq i.e., those that are overriden by Fl c , k , f . +Thus, +.Ar foo.com +as the initial domain would make the default domain private key into +.Pa /etc/ssl/letsencrypt/private/foo.com/privkey.pem . +This is useful in setups with multiple domain sets. +.It Fl n +Create a new 4096-bit RSA account key if one does not already exist. +.It Fl N +Create a new 4096-bit RSA domain key if one does not already exist. +.It Fl r +Revoke the X509 certificate found in +.Sx Certificates . +.It Fl s +Use the +.Dq Let's Encrypt +staging server instead of the real thing. +.It Fl v +Verbose operation. +Specify twice to also trace communication and data transfers. +.It Fl a Ar agreement +Use an alternative agreement URL. +The default uses the current one, but it may be out of date. +.It Fl C Ar challengedir +Where to register challenges. +See +.Sx Challenges +for details. +.It Fl c Ar certdir +Where to put public certificates. +See +.Sx Certificates +for details. +.It Fl f Ar accountkey +The account private key. +This was either made with a previous +.Dq Let's Encrypt +client or with +.Fl n . +.It Fl k Ar domainkey +The private key for the domain. +This may also be created with +.Fl N . +.It Ar domain +The domain name. +The only difference between this and the +.Ar altnames +is that it's put into the certificate's +.Li CN +field and is use the +.Dq main +domain when specifying +.Fl m . +.It Ar altnames +Alternative names +.Pq Dq SAN +for the domain name. +The number of SAN entries is limited by +.Dq Let's Encrypt +to 100 or so. +.El +.Pp +The process by which +.Nm +obtains signed certificates is roughly as follows. +In this, the +.Dq CA +is the ACME server for Let's Encrypt. +.Bl -enum +.It +Access the CA (unauthenticated) and requests its list of resources. +.It +Optionally create and register a new RSA account key. +.It +Read and process the RSA account key. +This is used to authenticate each subsequent communication to the CA. +.It +For each domain name, +.Bl -enum +.It +submit a challenge for authentication to the CA, +.It +create a challenge response file, +.It +wait until the CA has verified the challenge. +.El +.It +Read and extract the domain key. +.It +Create an X509 request from the doman key for the domain and its +alternative names. +.It +Submit a request for signature to the CA. +.It +Download the signed X509 certificate. +.It +Extract the CA issuer from the X509 certificate. +.It +Download the certificate chain from the issuer. +.El +.Pp +The revocation sequence is similar: +.Bl -enum +.It +Request list of resources, manage RSA account key as in the case for +signing. +.It +Read and extract the X509 certificate (if found). +.It +Create an X509 revocation request. +.It +Submit a request for revocation to the CA. +.It +Remove the certificate, the chain, and the full-chain. +.El +.Ss Challenges +Let's Encrypt uses challenges to verify that the submitter has access to +the registered domains. +.Nm +implements only the +.Dq http-01 +challenge type, where a file is created within a directory accessible by +a locally-run web server configured for the requested domain. +For example, for the domain +.Dq foo.com +and alternate +.Dq www.foo.com +and the default challenge directory, an Apache configuration snippet +might be as follows: +.Bd -literal +<VirtualHost *:80> + [...] + ServerName foo.com + ServerAlias www.foo.com + Alias /.well-known/acme-challenge /var/www/letsencrypt + <Directory /var/www/letsencrypt> + Options None + AllowOverride None + Order allow,deny + Allow from all + </Directory> +</VirtualHost> +.Ed +.Pp +This way, the files placed in +.Pa /var/www/letsencrypt +will be properly mapped by the web server when the Let's Encrypt +responds to a challenge. +.Ss Certificates +Public certificates (domain certificate, chain, and the full-chain) are +placed by default in +.Pa /etc/ssl/letsencrypt +as +.Pa cert.pem , +.Pa chain.pem , +and +.Pa fullchain.pem , +respectively. +These are all created as the root user with mode 444. +.Pp +An nginx configuration using these might be as follows: +.Bd -literal +server { + listen 443; + server_name foo.com www.foo.com; + [...] + ssl_certificate /etc/ssl/letsencrypt/fullchain.pem; + ssl_certificate_key /etc/ssl/letsencrypt/private/privkey.pem; +} +.Ed +.Pp +The +.Pa cert.pem +file, if found, is checked for its expiration: if more than 30 days from +expiring, +.Nm +will not attempt to refresh the signature. +.\" .Sh CONTEXT +.\" For section 9 functions only. +.\" .Sh IMPLEMENTATION NOTES +.\" Not used in OpenBSD. +.\" .Sh RETURN VALUES +.\" For sections 2, 3, and 9 function return values only. +.\" .Sh ENVIRONMENT +.\" For sections 1, 6, 7, and 8 only. +.\" .Sh FILES +.Sh EXIT STATUS +.Nm +returns 1 on failure, 2 if the certificates didn't change (up to date), +or 0 if certificates were changed (revoked or updated). +.\" For sections 1, 6, and 8 only. +.Sh EXAMPLES +To create and submit a new key for a single domain, assuming that the +web server has already been configured to map the challenge directory +as in the +.Sx Challenges +section: +.Bd -literal +# mkdir /var/www/letsencrypt +# mkdir /etc/ssl/letsencrypt +# mkdir /etc/ssl/letsencrypt/private /etc/letsencrypt +# chmod 0700 /etc/ssl/letsencrypt/private /etc/letsencrypt +# letskencrypt -vNn foo.com www.foo.com smtp.foo.com +.Ed +.Pp +After generating the necessary directories, the above will create all +keys and submit them to the server. +You'll then probably want to restart your web server to pick up the new +certificates. +.Pp +You can then keep your certificates fresh with a daily +.Xr cron 8 +invocation running the following: +.Bd -literal +#! /bin/sh + +letskencrypt foo.com www.foo.com smtp.foo.com + +if [ $? -eq 0 ] +then + /etc/rc.d/httpd reload +fi +.Ed +.Pp +You'll need to replace the httpd-reload statement with the correct +script to have your web server reload its certificates. +.\" .Sh DIAGNOSTICS +.\" For sections 1, 4, 6, 7, 8, and 9 printf/stderr messages only. +.\" .Sh ERRORS +.\" For sections 2, 3, 4, and 9 errno settings only. +.Sh SEE ALSO +.Xr openssl 1 +.\" .Sh STANDARDS +.\" .Sh HISTORY +.\" .Sh AUTHORS +.\" .Sh CAVEATS +.Sh BUGS +The challenge and certificate processes currently retain their (root) +privileges. +.Pp +For the time being, +.Nm +only supports RSA as an account key format. +.\" .Sh SECURITY CONSIDERATIONS +.\" Not used in OpenBSD. diff --git a/usr.sbin/acme-client/main.c b/usr.sbin/acme-client/main.c new file mode 100644 index 00000000000..ee20c4afaf4 --- /dev/null +++ b/usr.sbin/acme-client/main.c @@ -0,0 +1,485 @@ +/* $Id: main.c,v 1.1 2016/08/31 22:01:42 florian Exp $ */ +/* + * Copyright (c) 2016 Kristaps Dzonsons <kristaps@bsd.lv> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include <sys/socket.h> + +#include <ctype.h> +#include <err.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "extern.h" + +#define AGREEMENT "https://letsencrypt.org" \ + "/documents/LE-SA-v1.1.1-August-1-2016.pdf" +#define SSL_DIR "/etc/ssl/letsencrypt" +#define SSL_PRIV_DIR "/etc/ssl/letsencrypt/private" +#define ETC_DIR "/etc/letsencrypt" +#define WWW_DIR "/var/www/letsencrypt" +#define PRIVKEY_FILE "privkey.pem" + +/* + * This isn't RFC1035 compliant, but does the bare minimum in making + * sure that we don't get bogus domain names on the command line, which + * might otherwise screw up our directory structure. + * Returns zero on failure, non-zero on success. + */ +static int +domain_valid(const char *cp) +{ + + for ( ; '\0' != *cp; cp++) + if (!('.' == *cp || '-' == *cp || + '_' == *cp || isalnum((int)*cp))) + return(0); + return(1); +} + +/* + * Wrap around asprintf(3), which sometimes nullifies the input values, + * sometimes not, but always returns <0 on error. + * Returns NULL on failure or the pointer on success. + */ +static char * +doasprintf(const char *fmt, ...) +{ + int c; + char *cp; + va_list ap; + + va_start(ap, fmt); + c = vasprintf(&cp, fmt, ap); + va_end(ap); + return(c < 0 ? NULL : cp); +} + +int +main(int argc, char *argv[]) +{ + const char *domain, *agreement; + char *certdir, *acctkey, *chngdir, *keyfile; + int key_fds[2], acct_fds[2], chng_fds[2], + cert_fds[2], file_fds[2], dns_fds[2], + rvk_fds[2]; + pid_t pids[COMP__MAX]; + int c, rc, newacct, remote, revoke, force, + staging, multidir, newkey, backup; + extern int verbose; + extern enum comp proccomp; + size_t i, altsz, ne; + const char **alts; + + alts = NULL; + newacct = remote = revoke = verbose = force = + multidir = staging = newkey = backup = 0; + certdir = keyfile = acctkey = chngdir = NULL; + agreement = AGREEMENT; + + while (-1 != (c = getopt(argc, argv, "bFmnNrstva:f:c:C:k:"))) + switch (c) { + case ('a'): + agreement = optarg; + break; + case ('b'): + backup = 1; + break; + case ('c'): + free(certdir); + if (NULL == (certdir = strdup(optarg))) + err(EXIT_FAILURE, "strdup"); + break; + case ('C'): + free(chngdir); + if (NULL == (chngdir = strdup(optarg))) + err(EXIT_FAILURE, "strdup"); + break; + case ('f'): + free(acctkey); + if (NULL == (acctkey = strdup(optarg))) + err(EXIT_FAILURE, "strdup"); + break; + case ('F'): + force = 1; + break; + case ('k'): + free(keyfile); + if (NULL == (keyfile = strdup(optarg))) + err(EXIT_FAILURE, "strdup"); + break; + case ('m'): + multidir = 1; + break; + case ('n'): + newacct = 1; + break; + case ('N'): + newkey = 1; + break; + case ('r'): + revoke = 1; + break; + case ('s'): + staging = 1; + break; + case ('t'): + /* + / Undocumented feature. + * Don't use it. + */ + remote = 1; + break; + case ('v'): + verbose = verbose ? 2 : 1; + break; + default: + goto usage; + } + + argc -= optind; + argv += optind; + if (0 == argc) + goto usage; + + /* Make sure that the domains are sane. */ + + for (i = 0; i < (size_t)argc; i++) { + if (domain_valid(argv[i])) + continue; + errx(EXIT_FAILURE, "%s: bad domain syntax", argv[i]); + } + + domain = argv[0]; + argc--; + argv++; + + if ( ! checkprivs()) + errx(EXIT_FAILURE, "must be run as root"); + + /* + * Now we allocate our directories and file paths IFF we haven't + * specified them on the command-line. + * If we're in "multidir" (-m) mode, we use our initial domain + * name when specifying the prefixes. + * Otherwise, we put them all in a known location. + */ + + if (NULL == certdir) + certdir = multidir ? + doasprintf(SSL_DIR "/%s", domain) : + strdup(SSL_DIR); + if (NULL == keyfile) + keyfile = multidir ? + doasprintf(SSL_PRIV_DIR "/%s/" + PRIVKEY_FILE, domain) : + strdup(SSL_PRIV_DIR "/" PRIVKEY_FILE); + if (NULL == acctkey) + acctkey = multidir ? + doasprintf(ETC_DIR "/%s/" + PRIVKEY_FILE, domain) : + strdup(ETC_DIR "/" PRIVKEY_FILE); + if (NULL == chngdir) + chngdir = strdup(WWW_DIR); + + if (NULL == certdir || NULL == keyfile || + NULL == acctkey || NULL == chngdir) + err(EXIT_FAILURE, "strdup"); + + /* + * Do some quick checks to see if our paths exist. + * This will be done in the children, but we might as well check + * now before the fork. + */ + + ne = 0; + + if (-1 == access(certdir, R_OK)) { + warnx("%s: -c directory must exist", certdir); + ne++; + } + + if ( ! newkey && -1 == access(keyfile, R_OK)) { + warnx("%s: -k file must exist", keyfile); + ne++; + } else if (newkey && -1 != access(keyfile, R_OK)) { + dodbg("%s: domain key exists " + "(not creating)", keyfile); + newkey = 0; + } + + if (-1 == access(chngdir, R_OK)) { + warnx("%s: -C directory must exist", chngdir); + ne++; + } + + if ( ! newacct && -1 == access(acctkey, R_OK)) { + warnx("%s: -f file must exist", acctkey); + ne++; + } else if (newacct && -1 != access(acctkey, R_OK)) { + dodbg("%s: account key exists " + "(not creating)", acctkey); + newacct = 0; + } + + if (ne > 0) + exit(EXIT_FAILURE); + + /* Set the zeroth altname as our domain. */ + + altsz = argc + 1; + alts = calloc(altsz, sizeof(char *)); + if (NULL == alts) + err(EXIT_FAILURE, "calloc"); + alts[0] = domain; + for (i = 0; i < (size_t)argc; i++) + alts[i + 1] = argv[i]; + + /* + * Open channels between our components. + */ + + if (-1 == socketpair(AF_UNIX, SOCK_STREAM, 0, key_fds)) + err(EXIT_FAILURE, "socketpair"); + if (-1 == socketpair(AF_UNIX, SOCK_STREAM, 0, acct_fds)) + err(EXIT_FAILURE, "socketpair"); + if (-1 == socketpair(AF_UNIX, SOCK_STREAM, 0, chng_fds)) + err(EXIT_FAILURE, "socketpair"); + if (-1 == socketpair(AF_UNIX, SOCK_STREAM, 0, cert_fds)) + err(EXIT_FAILURE, "socketpair"); + if (-1 == socketpair(AF_UNIX, SOCK_STREAM, 0, file_fds)) + err(EXIT_FAILURE, "socketpair"); + if (-1 == socketpair(AF_UNIX, SOCK_STREAM, 0, dns_fds)) + err(EXIT_FAILURE, "socketpair"); + if (-1 == socketpair(AF_UNIX, SOCK_STREAM, 0, rvk_fds)) + err(EXIT_FAILURE, "socketpair"); + + /* Start with the network-touching process. */ + + if (-1 == (pids[COMP_NET] = fork())) + err(EXIT_FAILURE, "fork"); + + if (0 == pids[COMP_NET]) { + proccomp = COMP_NET; + close(key_fds[0]); + close(acct_fds[0]); + close(chng_fds[0]); + close(cert_fds[0]); + close(file_fds[0]); + close(file_fds[1]); + close(dns_fds[0]); + close(rvk_fds[0]); + c = netproc(key_fds[1], acct_fds[1], + chng_fds[1], cert_fds[1], + dns_fds[1], rvk_fds[1], + newacct, revoke, staging, + (const char *const *)alts, altsz, + agreement); + free(alts); + exit(c ? EXIT_SUCCESS : EXIT_FAILURE); + } + + close(key_fds[1]); + close(acct_fds[1]); + close(chng_fds[1]); + close(cert_fds[1]); + close(dns_fds[1]); + close(rvk_fds[1]); + + /* Now the key-touching component. */ + + if (-1 == (pids[COMP_KEY] = fork())) + err(EXIT_FAILURE, "fork"); + + if (0 == pids[COMP_KEY]) { + proccomp = COMP_KEY; + close(cert_fds[0]); + close(dns_fds[0]); + close(rvk_fds[0]); + close(acct_fds[0]); + close(chng_fds[0]); + close(file_fds[0]); + close(file_fds[1]); + c = keyproc(key_fds[0], keyfile, + (const char **)alts, altsz, newkey); + free(alts); + exit(c ? EXIT_SUCCESS : EXIT_FAILURE); + } + + close(key_fds[0]); + + /* The account-touching component. */ + + if (-1 == (pids[COMP_ACCOUNT] = fork())) + err(EXIT_FAILURE, "fork"); + + if (0 == pids[COMP_ACCOUNT]) { + proccomp = COMP_ACCOUNT; + free(alts); + close(cert_fds[0]); + close(dns_fds[0]); + close(rvk_fds[0]); + close(chng_fds[0]); + close(file_fds[0]); + close(file_fds[1]); + c = acctproc(acct_fds[0], acctkey, newacct); + exit(c ? EXIT_SUCCESS : EXIT_FAILURE); + } + + close(acct_fds[0]); + + /* The challenge-accepting component. */ + + if (-1 == (pids[COMP_CHALLENGE] = fork())) + err(EXIT_FAILURE, "fork"); + + if (0 == pids[COMP_CHALLENGE]) { + proccomp = COMP_CHALLENGE; + free(alts); + close(cert_fds[0]); + close(dns_fds[0]); + close(rvk_fds[0]); + close(file_fds[0]); + close(file_fds[1]); + c = chngproc(chng_fds[0], chngdir, remote); + exit(c ? EXIT_SUCCESS : EXIT_FAILURE); + } + + close(chng_fds[0]); + + /* The certificate-handling component. */ + + if (-1 == (pids[COMP_CERT] = fork())) + err(EXIT_FAILURE, "fork"); + + if (0 == pids[COMP_CERT]) { + proccomp = COMP_CERT; + free(alts); + close(dns_fds[0]); + close(rvk_fds[0]); + close(file_fds[1]); + c = certproc(cert_fds[0], file_fds[0]); + exit(c ? EXIT_SUCCESS : EXIT_FAILURE); + } + + close(cert_fds[0]); + close(file_fds[0]); + + /* The certificate-handling component. */ + + if (-1 == (pids[COMP_FILE] = fork())) + err(EXIT_FAILURE, "fork"); + + if (0 == pids[COMP_FILE]) { + proccomp = COMP_FILE; + free(alts); + close(dns_fds[0]); + close(rvk_fds[0]); + c = fileproc(file_fds[1], backup, certdir); + /* + * This is different from the other processes in that it + * can return 2 if the certificates were updated. + */ + exit(c > 1 ? 2 : + (c ? EXIT_SUCCESS : EXIT_FAILURE)); + } + + close(file_fds[1]); + + /* The DNS lookup component. */ + + if (-1 == (pids[COMP_DNS] = fork())) + err(EXIT_FAILURE, "fork"); + + if (0 == pids[COMP_DNS]) { + proccomp = COMP_DNS; + free(alts); + close(rvk_fds[0]); + c = dnsproc(dns_fds[0]); + exit(c ? EXIT_SUCCESS : EXIT_FAILURE); + } + + close(dns_fds[0]); + + /* The expiration component. */ + + if (-1 == (pids[COMP_REVOKE] = fork())) + err(EXIT_FAILURE, "fork"); + + if (0 == pids[COMP_REVOKE]) { + proccomp = COMP_REVOKE; + c = revokeproc(rvk_fds[0], certdir, + force, revoke, + (const char *const *)alts, altsz); + free(alts); + exit(c ? EXIT_SUCCESS : EXIT_FAILURE); + } + + close(rvk_fds[0]); + + /* Jail: sandbox, file-system, user. */ + + if ( ! sandbox_before()) + exit(EXIT_FAILURE); + else if ( ! dropfs(PATH_VAR_EMPTY)) + exit(EXIT_FAILURE); + else if ( ! dropprivs()) + exit(EXIT_FAILURE); + else if ( ! sandbox_after()) + exit(EXIT_FAILURE); + + /* + * Collect our subprocesses. + * Require that they both have exited cleanly. + */ + + rc = checkexit(pids[COMP_KEY], COMP_KEY) + + checkexit(pids[COMP_CERT], COMP_CERT) + + checkexit(pids[COMP_NET], COMP_NET) + + checkexit_ext(&c, pids[COMP_FILE], COMP_FILE) + + checkexit(pids[COMP_ACCOUNT], COMP_ACCOUNT) + + checkexit(pids[COMP_CHALLENGE], COMP_CHALLENGE) + + checkexit(pids[COMP_DNS], COMP_DNS) + + checkexit(pids[COMP_REVOKE], COMP_REVOKE); + + free(certdir); + free(keyfile); + free(acctkey); + free(chngdir); + free(alts); + return(COMP__MAX != rc ? EXIT_FAILURE : + (2 == c ? EXIT_SUCCESS : 2)); +usage: + fprintf(stderr, "usage: %s " + "[-bFmnNrsv] " + "[-a agreement] " + "[-C challengedir] " + "[-c certdir] " + "[-f accountkey] " + "[-k domainkey] " + "domain [altnames...]\n", + getprogname()); + free(certdir); + free(keyfile); + free(acctkey); + free(chngdir); + return(EXIT_FAILURE); +} diff --git a/usr.sbin/acme-client/netproc.c b/usr.sbin/acme-client/netproc.c new file mode 100644 index 00000000000..039f51a8205 --- /dev/null +++ b/usr.sbin/acme-client/netproc.c @@ -0,0 +1,811 @@ +/* $Id: netproc.c,v 1.1 2016/08/31 22:01:42 florian Exp $ */ +/* + * Copyright (c) 2016 Kristaps Dzonsons <kristaps@bsd.lv> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include <assert.h> +#include <ctype.h> +#include <err.h> +#include <errno.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "http.h" +#include "extern.h" + +#define URL_REAL_CA "https://acme-v01.api.letsencrypt.org/directory" +#define URL_STAGE_CA "https://acme-staging.api.letsencrypt.org/directory" + +#define RETRY_DELAY 5 +#define RETRY_MAX 10 + +/* + * Buffer used when collecting the results of a CURL transfer. + */ +struct buf { + char *buf; /* binary buffer */ + size_t sz; /* length of buffer */ +}; + +/* + * Used for CURL communications. + */ +struct conn { + const char *na; /* nonce authority */ + int fd; /* acctproc handle */ + int dfd; /* dnsproc handle */ + struct buf buf; /* transfer buffer */ +}; + +/* + * If something goes wrong (or we're tracing output), we dump the + * current transfer's data as a debug message. + * Make sure that print all non-printable characters as question marks + * so that we don't spam the console. + * Also, consolidate white-space. + * This of course will ruin string literals, but the intent here is just + * to show the message, not to replicate it. + */ +static void +buf_dump(const struct buf *buf) +{ + size_t i; + int j; + char *nbuf; + + if (0 == buf->sz) + return; + if (NULL == (nbuf = malloc(buf->sz))) + err(EXIT_FAILURE, "malloc"); + + for (j = 0, i = 0; i < buf->sz; i++) + if (isspace((int)buf->buf[i])) { + nbuf[j++] = ' '; + while (isspace((int)buf->buf[i])) + i++; + i--; + } else + nbuf[j++] = isprint((int)buf->buf[i]) ? + buf->buf[i] : '?'; + dodbg("transfer buffer: [%.*s] (%zu bytes)", j, nbuf, buf->sz); + free(nbuf); +} + +/* + * Extract the domain and port from a URL. + * The url must be formatted as schema://address[/stuff]. + * This returns NULL on failure. + */ +static char * +url2host(const char *host, short *port, char **path) +{ + char *url, *ep; + + /* We only understand HTTP and HTTPS. */ + + if (0 == strncmp(host, "https://", 8)) { + *port = 443; + if (NULL == (url = strdup(host + 8))) { + warn("strdup"); + return(NULL); + } + } else if (0 == strncmp(host, "http://", 7)) { + *port = 80; + if (NULL == (url = strdup(host + 7))) { + warn("strdup"); + return(NULL); + } + } else { + warnx("%s: unknown schema", host); + return(NULL); + } + + /* Terminate path part. */ + + if (NULL != (ep = strchr(url, '/'))) { + *path = strdup(ep); + *ep = '\0'; + } else + *path = strdup(""); + + if (NULL == *path) { + warn("strdup"); + free(url); + return(NULL); + } + + return(url); +} + +/* + * Contact dnsproc and resolve a host. + * Place the answers in "v" and return the number of answers, which can + * be at most MAX_SERVERS_DNS. + * Return <0 on failure. + */ +static ssize_t +urlresolve(int fd, const char *host, struct source *v) +{ + char *addr; + size_t i, sz; + long lval; + + if (writeop(fd, COMM_DNS, DNS_LOOKUP) <= 0) + return(-1); + else if (writestr(fd, COMM_DNSQ, host) <= 0) + return(-1); + else if ((lval = readop(fd, COMM_DNSLEN)) < 0) + return(-1); + + sz = lval; + assert(sz <= MAX_SERVERS_DNS); + + for (i = 0; i < sz; i++) { + memset(&v[i], 0, sizeof(struct source)); + if ((lval = readop(fd, COMM_DNSF)) < 0) + goto err; + else if (4 != lval && 6 != lval) + goto err; + else if (NULL == (addr = readstr(fd, COMM_DNSA))) + goto err; + v[i].family = lval; + v[i].ip = addr; + } + + return(sz); +err: + for (i = 0; i < sz; i++) + free(v[i].ip); + return(-1); +} + +/* + * Send a "regular" HTTP GET message to "addr" and stuff the response + * into the connection buffer. + * Return the HTTP error code or <0 on failure. + */ +static long +nreq(struct conn *c, const char *addr) +{ + struct httpget *g; + struct source src[MAX_SERVERS_DNS]; + char *host, *path; + short port; + size_t srcsz; + ssize_t ssz; + long code; + + if (NULL == (host = url2host(addr, &port, &path))) + return(-1); + + if ((ssz = urlresolve(c->dfd, host, src)) < 0) { + free(host); + free(path); + return(-1); + } + srcsz = ssz; + + g = http_get(src, srcsz, host, port, path, NULL, 0); + free(host); + free(path); + if (NULL == g) + return(-1); + + code = g->code; + + /* Copy the body part into our buffer. */ + + free(c->buf.buf); + c->buf.sz = g->bodypartsz; + c->buf.buf = malloc(c->buf.sz); + memcpy(c->buf.buf, g->bodypart, c->buf.sz); + http_get_free(g); + if (NULL == c->buf.buf) { + warn("malloc"); + return(-1); + } + return(code); +} + +/* + * Create and send a signed communication to the ACME server. + * Stuff the response into the communication buffer. + * Return <0 on failure on the HTTP error code otherwise. + */ +static long +sreq(struct conn *c, const char *addr, const char *req) +{ + struct httpget *g; + struct source src[MAX_SERVERS_DNS]; + char *host, *path, *nonce, *reqsn; + short port; + struct httphead *h; + ssize_t ssz; + long code; + + if (NULL == (host = url2host(c->na, &port, &path))) + return(-1); + + if ((ssz = urlresolve(c->dfd, host, src)) < 0) { + free(host); + free(path); + return(-1); + } + + g = http_get(src, (size_t)ssz, host, port, path, NULL, 0); + free(host); + free(path); + if (NULL == g) + return(-1); + + h = http_head_get("Replay-Nonce", g->head, g->headsz); + if (NULL == h) { + warnx("%s: no replay nonce", c->na); + http_get_free(g); + return(-1); + } else if (NULL == (nonce = strdup(h->val))) { + warn("strdup"); + http_get_free(g); + return(-1); + } + http_get_free(g); + + /* + * Send the nonce and request payload to the acctproc. + * This will create the proper JSON object we need. + */ + + if (writeop(c->fd, COMM_ACCT, ACCT_SIGN) <= 0) { + free(nonce); + return(-1); + } else if (writestr(c->fd, COMM_PAY, req) <= 0) { + free(nonce); + return(-1); + } else if (writestr(c->fd, COMM_NONCE, nonce) <= 0) { + free(nonce); + return(-1); + } + free(nonce); + + /* Now read back the signed payload. */ + + if (NULL == (reqsn = readstr(c->fd, COMM_REQ))) + return(-1); + + /* Now send the signed payload to the CA. */ + + if (NULL == (host = url2host(addr, &port, &path))) { + free(reqsn); + return(-1); + } else if ((ssz = urlresolve(c->dfd, host, src)) < 0) { + free(host); + free(path); + free(reqsn); + return(-1); + } + + g = http_get(src, (size_t)ssz, host, + port, path, reqsn, strlen(reqsn)); + + free(host); + free(path); + free(reqsn); + if (NULL == g) + return(-1); + + /* Stuff response into parse buffer. */ + + code = g->code; + + free(c->buf.buf); + c->buf.sz = g->bodypartsz; + c->buf.buf = malloc(c->buf.sz); + memcpy(c->buf.buf, g->bodypart, c->buf.sz); + http_get_free(g); + if (NULL == c->buf.buf) { + warn("malloc"); + return(-1); + } + return(code); +} + +/* + * Send to the CA that we want to authorise a new account. + * This only happens once for a new account key. + * Returns non-zero on success. + */ +static int +donewreg(struct conn *c, const char *agreement, + const struct capaths *p) +{ + int rc; + char *req; + long lc; + + rc = 0; + dodbg("%s: new-reg", p->newreg); + + if (NULL == (req = json_fmt_newreg(agreement))) + warnx("json_fmt_newreg"); + else if ((lc = sreq(c, p->newreg, req)) < 0) + warnx("%s: bad comm", p->newreg); + else if (200 != lc && 201 != lc) + warnx("%s: bad HTTP: %ld", p->newreg, lc); + else if (NULL == c->buf.buf || 0 == c->buf.sz) + warnx("%s: empty response", p->newreg); + else + rc = 1; + + if (0 == rc || verbose > 1) + buf_dump(&c->buf); + free(req); + return(rc); +} + +/* + * Request a challenge for the given domain name. + * This must happen for each name "alt". + * On non-zero exit, fills in "chng" with the challenge. + */ +static int +dochngreq(struct conn *c, const char *alt, + struct chng *chng, const struct capaths *p) +{ + int rc; + char *req; + long lc; + struct jsmnn *j; + + j = NULL; + rc = 0; + dodbg("%s: req-auth: %s", p->newauthz, alt); + + if (NULL == (req = json_fmt_newauthz(alt))) + warnx("json_fmt_newauthz"); + else if ((lc = sreq(c, p->newauthz, req)) < 0) + warnx("%s: bad comm", p->newauthz); + else if (200 != lc && 201 != lc) + warnx("%s: bad HTTP: %ld", p->newauthz, lc); + else if (NULL == (j = json_parse(c->buf.buf, c->buf.sz))) + warnx("%s: bad JSON object", p->newauthz); + else if ( ! json_parse_challenge(j, chng)) + warnx("%s: bad challenge", p->newauthz); + else + rc = 1; + + if (0 == rc || verbose > 1) + buf_dump(&c->buf); + json_free(j); + free(req); + return(rc); +} + +/* + * Note to the CA that a challenge response is in place. + */ +static int +dochngresp(struct conn *c, const struct chng *chng, const char *th) +{ + int rc; + long lc; + char *req; + + rc = 0; + dodbg("%s: challenge", chng->uri); + + if (NULL == (req = json_fmt_challenge(chng->token, th))) + warnx("json_fmt_challenge"); + else if ((lc = sreq(c, chng->uri, req)) < 0) + warnx("%s: bad comm", chng->uri); + else if (200 != lc && 201 != lc && 202 != lc) + warnx("%s: bad HTTP: %ld", chng->uri, lc); + else + rc = 1; + + if (0 == rc || verbose > 1) + buf_dump(&c->buf); + free(req); + return(rc); +} + +/* + * Check with the CA whether a challenge has been processed. + * Note: we'll only do this a limited number of times, and pause for a + * time between checks, but this happens in the caller. + */ +static int +dochngcheck(struct conn *c, struct chng *chng) +{ + int cc; + long lc; + struct jsmnn *j; + + dodbg("%s: status", chng->uri); + + if ((lc = nreq(c, chng->uri)) < 0) { + warnx("%s: bad comm", chng->uri); + return(0); + } else if (200 != lc && 201 != lc && 202 != lc) { + warnx("%s: bad HTTP: %ld", chng->uri, lc); + buf_dump(&c->buf); + return(0); + } else if (NULL == (j = json_parse(c->buf.buf, c->buf.sz))) { + warnx("%s: bad JSON object", chng->uri); + buf_dump(&c->buf); + return(0); + } else if (-1 == (cc = json_parse_response(j))) { + warnx("%s: bad response", chng->uri); + buf_dump(&c->buf); + json_free(j); + return(0); + } else if (cc > 0) + chng->status = 1; + + json_free(j); + return(1); +} + +static int +dorevoke(struct conn *c, const char *addr, const char *cert) +{ + char *req; + int rc; + long lc; + + lc = 0; + rc = 0; + dodbg("%s: revocation", addr); + + if (NULL == (req = json_fmt_revokecert(cert))) + warnx("json_fmt_revokecert"); + else if ((lc = sreq(c, addr, req)) < 0) + warnx("%s: bad comm", addr); + else if (200 != lc && 201 != lc && 409 != lc) + warnx("%s: bad HTTP: %ld", addr, lc); + else + rc = 1; + + if (409 == lc) + warnx("%s: already revoked", addr); + + if (0 == rc || verbose > 1) + buf_dump(&c->buf); + free(req); + return(rc); +} + +/* + * Submit our certificate to the CA. + * This, upon success, will return the signed CA. + */ +static int +docert(struct conn *c, const char *addr, const char *cert) +{ + char *req; + int rc; + long lc; + + rc = 0; + dodbg("%s: certificate", addr); + + if (NULL == (req = json_fmt_newcert(cert))) + warnx("json_fmt_newcert"); + else if ((lc = sreq(c, addr, req)) < 0) + warnx("%s: bad comm", addr); + else if (200 != lc && 201 != lc) + warnx("%s: bad HTTP: %ld", addr, lc); + else if (0 == c->buf.sz || NULL == c->buf.buf) + warnx("%s: empty response", addr); + else + rc = 1; + + if (0 == rc || verbose > 1) + buf_dump(&c->buf); + free(req); + return(rc); +} + +/* + * Look up directories from the certificate authority. + */ +static int +dodirs(struct conn *c, const char *addr, struct capaths *paths) +{ + struct jsmnn *j; + long lc; + int rc; + + j = NULL; + rc = 0; + dodbg("%s: directories", addr); + + if ((lc = nreq(c, addr)) < 0) + warnx("%s: bad comm", addr); + else if (200 != lc && 201 != lc) + warnx("%s: bad HTTP: %ld", addr, lc); + else if (NULL == (j = json_parse(c->buf.buf, c->buf.sz))) + warnx("json_parse"); + else if ( ! json_parse_capaths(j, paths)) + warnx("%s: bad CA paths", addr); + else + rc = 1; + + if (0 == rc || verbose > 1) + buf_dump(&c->buf); + json_free(j); + return(rc); +} + +/* + * Request the full chain certificate. + */ +static int +dofullchain(struct conn *c, const char *addr) +{ + int rc; + long lc; + + rc = 0; + dodbg("%s: full chain", addr); + + if ((lc = nreq(c, addr)) < 0) + warnx("%s: bad comm", addr); + else if (200 != lc && 201 != lc) + warnx("%s: bad HTTP: %ld", addr, lc); + else + rc = 1; + + if (0 == rc || verbose > 1) + buf_dump(&c->buf); + return(rc); +} + +/* + * Here we communicate with the letsencrypt server. + * For this, we'll need the certificate we want to upload and our + * account key information. + */ +int +netproc(int kfd, int afd, int Cfd, int cfd, int dfd, int rfd, + int newacct, int revoke, int staging, + const char *const *alts, size_t altsz, const char *agreement) +{ + int rc; + size_t i; + char *cert, *thumb, *url; + struct conn c; + struct capaths paths; + struct chng *chngs; + long lval; + + rc = 0; + memset(&paths, 0, sizeof(struct capaths)); + memset(&c, 0, sizeof(struct conn)); + url = cert = thumb = NULL; + chngs = NULL; + + /* File-system, user, and sandbox jail. */ + + if ( ! sandbox_before()) + goto out; + else if ( ! dropfs(PATH_VAR_EMPTY)) + goto out; + else if ( ! dropprivs()) + goto out; + else if ( ! sandbox_after()) + goto out; + + /* + * Wait until the acctproc, keyproc, and revokeproc have started + * up and are ready to serve us data. + * There's no point in running if these don't work. + * Then check whether revokeproc indicates that the certificate + * on file (if any) can be updated. + */ + + if (0 == (lval = readop(afd, COMM_ACCT_STAT))) { + rc = 1; + goto out; + } else if (ACCT_READY != lval) { + warnx("unknown operation from acctproc"); + goto out; + } + + if (0 == (lval = readop(kfd, COMM_KEY_STAT))) { + rc = 1; + goto out; + } else if (KEY_READY != lval) { + warnx("unknown operation from keyproc"); + goto out; + } + + if (0 == (lval = readop(rfd, COMM_REVOKE_RESP))) { + rc = 1; + goto out; + } else if (REVOKE_EXP != lval && REVOKE_OK != lval) { + warnx("unknown operation from revokeproc"); + goto out; + } + + /* If our certificate is up-to-date, return now. */ + + if (REVOKE_OK == lval) { + rc = 1; + goto out; + } + + /* Allocate main state. */ + + chngs = calloc(altsz, sizeof(struct chng)); + if (NULL == chngs) { + warn("calloc"); + goto out; + } + + c.dfd = dfd; + c.fd = afd; + c.na = staging ? URL_STAGE_CA : URL_REAL_CA; + + /* + * Look up the domain of the ACME server. + * We'll use this ourselves instead of having libcurl do the DNS + * resolution itself. + */ + if ( ! dodirs(&c, c.na, &paths)) + goto out; + + /* + * If we're meant to revoke, then wait for revokeproc to send us + * the certificate (if it's found at all). + * Following that, submit the request to the CA then notify the + * certproc, which will in turn notify the fileproc. + */ + + if (revoke) { + if (NULL == (cert = readstr(rfd, COMM_CSR))) + goto out; + if ( ! dorevoke(&c, paths.revokecert, cert)) + goto out; + else if (writeop(cfd, COMM_CSR_OP, CERT_REVOKE) > 0) + rc = 1; + goto out; + } + + /* If new, register with the CA server. */ + + if (newacct && ! donewreg(&c, agreement, &paths)) + goto out; + + /* Pre-authorise all domains with CA server. */ + + for (i = 0; i < altsz; i++) + if ( ! dochngreq(&c, alts[i], &chngs[i], &paths)) + goto out; + + /* + * We now have our challenges. + * We need to ask the acctproc for the thumbprint. + * We'll combine this to the challenge to create our response, + * which will be orchestrated by the chngproc. + */ + + if (writeop(afd, COMM_ACCT, ACCT_THUMBPRINT) <= 0) + goto out; + else if (NULL == (thumb = readstr(afd, COMM_THUMB))) + goto out; + + /* We'll now ask chngproc to build the challenge. */ + + for (i = 0; i < altsz; i++) { + if (writeop(Cfd, COMM_CHNG_OP, CHNG_SYN) <= 0) + goto out; + else if (writestr(Cfd, COMM_THUMB, thumb) <= 0) + goto out; + else if (writestr(Cfd, COMM_TOK, chngs[i].token) <= 0) + goto out; + + /* Read that the challenge has been made. */ + + if (CHNG_ACK != readop(Cfd, COMM_CHNG_ACK)) + goto out; + + /* Write to the CA that it's ready. */ + + if ( ! dochngresp(&c, &chngs[i], thumb)) + goto out; + } + + /* + * We now wait on the ACME server for each domain. + * Connect to the server (assume it's the same server) once + * every five seconds. + */ + + for (i = 0; i < altsz; i++) { + if (1 == chngs[i].status) + continue; + + if (chngs[i].retry++ >= RETRY_MAX) { + warnx("%s: too many tries", chngs[i].uri); + goto out; + } + + /* Sleep before every attempt. */ + sleep(RETRY_DELAY); + if ( ! dochngcheck(&c, &chngs[i])) + goto out; + } + + /* + * Write our acknowledgement that the challenges are over. + * The challenge process will remove all of the files. + */ + + if (writeop(Cfd, COMM_CHNG_OP, CHNG_STOP) <= 0) + goto out; + + /* Wait to receive the certificate itself. */ + + if (NULL == (cert = readstr(kfd, COMM_CERT))) + goto out; + + /* + * Otherwise, submit the CA for signing, download the signed + * copy, and ship that into the certificate process for copying. + */ + + if ( ! docert(&c, paths.newcert, cert)) + goto out; + else if (writeop(cfd, COMM_CSR_OP, CERT_UPDATE) <= 0) + goto out; + else if (writebuf(cfd, COMM_CSR, c.buf.buf, c.buf.sz) <= 0) + goto out; + + /* + * Read back the issuer from the certproc. + * Then contact the issuer to get the certificate chain. + * Write this chain directly back to the certproc. + */ + + if (NULL == (url = readstr(cfd, COMM_ISSUER))) + goto out; + else if ( ! dofullchain(&c, url)) + goto out; + else if (writebuf(cfd, COMM_CHAIN, c.buf.buf, c.buf.sz) <= 0) + goto out; + + rc = 1; +out: + close(cfd); + close(kfd); + close(afd); + close(Cfd); + close(dfd); + close(rfd); + free(cert); + free(url); + free(thumb); + free(c.buf.buf); + if (NULL != chngs) + for (i = 0; i < altsz; i++) + json_free_challenge(&chngs[i]); + free(chngs); + json_free_capaths(&paths); + return(rc); +} diff --git a/usr.sbin/acme-client/revokeproc.c b/usr.sbin/acme-client/revokeproc.c new file mode 100644 index 00000000000..159442b3143 --- /dev/null +++ b/usr.sbin/acme-client/revokeproc.c @@ -0,0 +1,375 @@ +/* $Id: revokeproc.c,v 1.1 2016/08/31 22:01:42 florian Exp $ */ +/* + * Copyright (c) 2016 Kristaps Dzonsons <kristaps@bsd.lv> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include <assert.h> +#include <ctype.h> +#include <err.h> +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include <openssl/pem.h> +#include <openssl/x509.h> +#include <openssl/x509v3.h> +#include <openssl/err.h> + +#include "extern.h" + +#define RENEW_ALLOW (30 * 24 * 60 * 60) + +/* + * Convert the X509's expiration time (which is in ASN1_TIME format) + * into a time_t value. + * There are lots of suggestions on the Internet on how to do this and + * they're really, really unsafe. + * Adapt those poor solutions to a safe one. + */ +static time_t +X509expires(X509 *x) +{ + ASN1_TIME *time; + struct tm t; + unsigned char *str; + size_t i = 0; + + time = X509_get_notAfter(x); + str = time->data; + memset(&t, 0, sizeof(t)); + + /* Account for 2 and 4-digit time. */ + + if (time->type == V_ASN1_UTCTIME) { + if (time->length <= 2) { + warnx("invalid ASN1_TIME"); + return((time_t)-1); + } + t.tm_year = + (str[0] - '0') * 10 + + (str[1] - '0'); + if (t.tm_year < 70) + t.tm_year += 100; + i = 2; + } else if (time->type == V_ASN1_GENERALIZEDTIME) { + if (time->length <= 4) { + warnx("invalid ASN1_TIME"); + return((time_t)-1); + } + t.tm_year = + (str[0] - '0') * 1000 + + (str[1] - '0') * 100 + + (str[2] - '0') * 10 + + (str[3] - '0'); + t.tm_year -= 1900; + i = 4; + } + + /* Now the post-year parts. */ + + if (time->length <= (int)i + 10) { + warnx("invalid ASN1_TIME"); + return((time_t)-1); + } + + t.tm_mon = ((str[i + 0] - '0') * 10 + (str[i + 1] - '0')) - 1; + t.tm_mday = (str[i + 2] - '0') * 10 + (str[i + 3] - '0'); + t.tm_hour = (str[i + 4] - '0') * 10 + (str[i + 5] - '0'); + t.tm_min = (str[i + 6] - '0') * 10 + (str[i + 7] - '0'); + t.tm_sec = (str[i + 8] - '0') * 10 + (str[i + 9] - '0'); + + return(mktime(&t)); +} + +int +revokeproc(int fd, const char *certdir, int force, int revoke, + const char *const *alts, size_t altsz) +{ + int rc, cc, i, extsz, ssz; + long lval; + FILE *f; + size_t *found; + char *path, *der, *dercp, *der64, *san, *str, *tok; + X509 *x; + enum revokeop op, rop; + time_t t; + int len; + X509_EXTENSION *ex; + ASN1_OBJECT *obj; + BIO *bio; + size_t j; + + found = NULL; + bio = NULL; + der = der64 = NULL; + rc = 0; + f = NULL; + path = NULL; + san = NULL; + x = NULL; + + /* + * First try to open the certificate before we drop privileges + * and jail ourselves. + * We allow "f" to be NULL IFF the cert doesn't exist yet. + */ + + if (-1 == asprintf(&path, "%s/%s", certdir, CERT_PEM)) { + warn("asprintf"); + goto out; + } else if (NULL == (f = fopen(path, "r")) && ENOENT != errno) { + warn("%s", path); + goto out; + } + + /* File-system and sandbox jailing. */ + + if ( ! sandbox_before()) + goto out; + + ERR_load_crypto_strings(); + + if ( ! dropfs(PATH_VAR_EMPTY)) + goto out; + else if ( ! dropprivs()) + goto out; + else if ( ! sandbox_after()) + goto out; + + /* + * If we couldn't open the certificate, it doesn't exist so we + * haven't submitted it yet, so obviously we can mark that it + * has expired and we should renew it. + * If we're revoking, however, then that's an error! + * Ignore if the reader isn't reading in either case. + */ + + if (NULL == f && revoke) { + warnx("%s/%s: no certificate found", + certdir, CERT_PEM); + (void)writeop(fd, COMM_REVOKE_RESP, REVOKE_OK); + goto out; + } else if (NULL == f && ! revoke) { + if (writeop(fd, COMM_REVOKE_RESP, REVOKE_EXP) >= 0) + rc = 1; + goto out; + } + + if (NULL == (x = PEM_read_X509(f, NULL, NULL, NULL))) { + warnx("PEM_read_X509"); + goto out; + } + + /* Read out the expiration date. */ + + if ((time_t)-1 == (t = X509expires(x))) { + warnx("X509expires"); + goto out; + } + + /* + * Next, the long process to make sure that the SAN entries + * listed with the certificate fully cover those passed on the + * comamnd line. + */ + + extsz = NULL != x->cert_info->extensions ? + sk_X509_EXTENSION_num(x->cert_info->extensions) : 0; + + /* Scan til we find the SAN NID. */ + + for (i = 0; i < extsz; i++) { + ex = sk_X509_EXTENSION_value + (x->cert_info->extensions, i); + assert(NULL != ex); + obj = X509_EXTENSION_get_object(ex); + assert(NULL != obj); + if (NID_subject_alt_name != OBJ_obj2nid(obj)) + continue; + + if (NULL != san) { + warnx("%s/%s: two SAN entries", + certdir, CERT_PEM); + goto out; + } + + bio = BIO_new(BIO_s_mem()); + if (NULL == bio) { + warnx("BIO_new"); + goto out; + } else if ( ! X509V3_EXT_print(bio, ex, 0, 0)) { + warnx("X509V3_EXT_print"); + goto out; + } else if (NULL == (san = calloc(1, bio->num_write + 1))) { + warn("calloc"); + goto out; + } + ssz = BIO_read(bio, san, bio->num_write); + if (ssz < 0 || (unsigned)ssz != bio->num_write) { + warnx("BIO_read"); + goto out; + } + } + + if (NULL == san) { + warnx("%s/%s: does not have a SAN entry", certdir, CERT_PEM); + goto out; + } + + /* An array of buckets: the number of entries found. */ + + if (NULL == (found = calloc(altsz, sizeof(size_t)))) { + warn("calloc"); + goto out; + } + + /* + * Parse the SAN line. + * Make sure that all of the domains are represented only once. + */ + + str = san; + while (NULL != (tok = strsep(&str, ","))) { + if ('\0' == *tok) + continue; + while (isspace((int)*tok)) + tok++; + if (strncmp(tok, "DNS:", 4)) + continue; + tok += 4; + for (j = 0; j < altsz; j++) + if (0 == strcmp(tok, alts[j])) + break; + if (j == altsz) { + warnx("%s/%s: unknown SAN entry: %s", + certdir, CERT_PEM, tok); + goto out; + } + if (found[j]++) { + warnx("%s/%s: duplicate SAN entry: %s", + certdir, CERT_PEM, tok); + goto out; + } + } + + for (j = 0; j < altsz; j++) { + if (found[j]) + continue; + warnx("%s/%s: domain not listed: %s", + certdir, CERT_PEM, alts[j]); + goto out; + } + + /* + * If we're going to revoke, write the certificate to the + * netproc in DER and base64-encoded format. + * Then exit: we have nothing left to do. + */ + + if (revoke) { + dodbg("%s/%s: revocation", certdir, CERT_PEM); + + /* + * First, tell netproc we're online. + * If they're down, then just exit without warning. + */ + + cc = writeop(fd, COMM_REVOKE_RESP, REVOKE_EXP); + if (0 == cc) + rc = 1; + if (cc <= 0) + goto out; + + if ((len = i2d_X509(x, NULL)) < 0) { + warnx("i2d_X509"); + goto out; + } else if (NULL == (der = dercp = malloc(len))) { + warn("malloc"); + goto out; + } else if (len != i2d_X509(x, (u_char **)&dercp)) { + warnx("i2d_X509"); + goto out; + } else if (NULL == (der64 = base64buf_url(der, len))) { + warnx("base64buf_url"); + goto out; + } else if (writestr(fd, COMM_CSR, der64) >= 0) + rc = 1; + + goto out; + } + + rop = time(NULL) >= (t - RENEW_ALLOW) ? REVOKE_EXP : REVOKE_OK; + + if (REVOKE_EXP == rop) + dodbg("%s/%s: certificate renewable: %lld days left", + certdir, CERT_PEM, + (long long)(t - time(NULL)) / 24 / 60 / 60); + else + dodbg("%s/%s: certificate valid: %lld days left", + certdir, CERT_PEM, + (long long)(t - time(NULL)) / 24 / 60 / 60); + + if (REVOKE_OK == rop && force) { + warnx("%s/%s: forcing renewal", certdir, CERT_PEM); + rop = REVOKE_EXP; + } + + /* + * We can re-submit it given RENEW_ALLOW time before. + * If netproc is down, just exit. + */ + + if (0 == (cc = writeop(fd, COMM_REVOKE_RESP, rop))) + rc = 1; + if (cc <= 0) + goto out; + + op = REVOKE__MAX; + if (0 == (lval = readop(fd, COMM_REVOKE_OP))) + op = REVOKE_STOP; + else if (REVOKE_CHECK == lval) + op = lval; + + if (REVOKE__MAX == op) { + warnx("unknown operation from netproc"); + goto out; + } else if (REVOKE_STOP == op) { + rc = 1; + goto out; + } + + rc = 1; +out: + close(fd); + if (NULL != f) + fclose(f); + if (NULL != x) + X509_free(x); + if (NULL != bio) + BIO_free(bio); + free(san); + free(path); + free(der); + free(found); + free(der64); + ERR_print_errors_fp(stderr); + ERR_free_strings(); + return(rc); +} diff --git a/usr.sbin/acme-client/rsa.c b/usr.sbin/acme-client/rsa.c new file mode 100644 index 00000000000..54d987e5576 --- /dev/null +++ b/usr.sbin/acme-client/rsa.c @@ -0,0 +1,97 @@ +/* $Id: rsa.c,v 1.1 2016/08/31 22:01:42 florian Exp $ */ +/* + * Copyright (c) 2016 Kristaps Dzonsons <kristaps@bsd.lv> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include <err.h> +#include <stdlib.h> +#include <unistd.h> + +#include <openssl/evp.h> +#include <openssl/pem.h> +#include <openssl/rsa.h> + +#include "rsa.h" + +/* + * Default number of bits when creating a new key. + */ +#define KBITS 4096 + +/* + * Create an RSA key with the default KBITS number of bits. + */ +EVP_PKEY * +rsa_key_create(FILE *f, const char *fname) +{ + EVP_PKEY_CTX *ctx; + EVP_PKEY *pkey; + + ctx = NULL; + pkey = NULL; + + /* First, create the context and the key. */ + + if (NULL == (ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL))) { + warnx("EVP_PKEY_CTX_new_id"); + goto err; + } else if (EVP_PKEY_keygen_init(ctx) <= 0) { + warnx("EVP_PKEY_keygen_init"); + goto err; + } else if (EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, KBITS) <= 0) { + warnx("EVP_PKEY_set_rsa_keygen_bits"); + goto err; + } else if (EVP_PKEY_keygen(ctx, &pkey) <= 0) { + warnx("EVP_PKEY_keygen"); + goto err; + } + + /* Serialise the key to the disc. */ + + if (PEM_write_PrivateKey(f, pkey, NULL, NULL, 0, NULL, NULL)) + goto out; + + warnx("%s: PEM_write_PrivateKey", fname); +err: + if (NULL != pkey) + EVP_PKEY_free(pkey); + pkey = NULL; +out: + if (NULL != ctx) + EVP_PKEY_CTX_free(ctx); + return(pkey); +} + + +EVP_PKEY * +rsa_key_load(FILE *f, const char *fname) +{ + EVP_PKEY *pkey; + + pkey = PEM_read_PrivateKey(f, NULL, NULL, NULL); + if (NULL == pkey) { + warnx("%s: PEM_read_PrivateKey", fname); + return(NULL); + } else if (EVP_PKEY_RSA == EVP_PKEY_type(pkey->type)) + return(pkey); + + warnx("%s: unsupported key type", fname); + EVP_PKEY_free(pkey); + return(NULL); +} + diff --git a/usr.sbin/acme-client/rsa.h b/usr.sbin/acme-client/rsa.h new file mode 100644 index 00000000000..8e4162808ca --- /dev/null +++ b/usr.sbin/acme-client/rsa.h @@ -0,0 +1,23 @@ +/* $Id: rsa.h,v 1.1 2016/08/31 22:01:42 florian Exp $ */ +/* + * Copyright (c) 2016 Kristaps Dzonsons <kristaps@bsd.lv> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ +#ifndef RSA_H +#define RSA_H + +EVP_PKEY *rsa_key_create(FILE *, const char *); +EVP_PKEY *rsa_key_load(FILE *, const char *); + +#endif /* ! RSA_H */ diff --git a/usr.sbin/acme-client/sandbox-pledge.c b/usr.sbin/acme-client/sandbox-pledge.c new file mode 100644 index 00000000000..ece973d638a --- /dev/null +++ b/usr.sbin/acme-client/sandbox-pledge.c @@ -0,0 +1,83 @@ +/* $Id: sandbox-pledge.c,v 1.1 2016/08/31 22:01:42 florian Exp $ */ +/* + * Copyright (c) 2016 Kristaps Dzonsons <kristaps@bsd.lv> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include <err.h> +#include <errno.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "extern.h" + +int +sandbox_before(void) +{ + + return(1); +} + +int +sandbox_after(void) +{ + + switch (proccomp) { + case (COMP_ACCOUNT): + case (COMP_CERT): + case (COMP_KEY): + case (COMP_REVOKE): + case (COMP__MAX): + if (-1 == pledge("stdio", NULL)) { + warn("pledge"); + return(0); + } + break; + case (COMP_CHALLENGE): + if (-1 == pledge("stdio cpath wpath", NULL)) { + warn("pledge"); + return(0); + } + break; + case (COMP_DNS): + if (-1 == pledge("stdio dns", NULL)) { + warn("pledge"); + return(0); + } + break; + case (COMP_FILE): + /* + * Rpath and cpath for rename, wpath and cpath for + * writing to the temporary. + */ + if (-1 == pledge("stdio cpath wpath rpath", NULL)) { + warn("pledge"); + return(0); + } + break; + case (COMP_NET): + if (-1 == pledge("stdio inet", NULL)) { + warn("pledge"); + return(0); + } + break; + } + return(1); +} diff --git a/usr.sbin/acme-client/util-pledge.c b/usr.sbin/acme-client/util-pledge.c new file mode 100644 index 00000000000..6b5e78d9b34 --- /dev/null +++ b/usr.sbin/acme-client/util-pledge.c @@ -0,0 +1,65 @@ +/* $Id: util-pledge.c,v 1.1 2016/08/31 22:01:42 florian Exp $ */ +/* + * Copyright (c) 2016 Kristaps Dzonsons <kristaps@bsd.lv> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include <err.h> +#include <unistd.h> + +#include "extern.h" + +int +dropfs(const char *path) +{ + + /* + * Only the challenge and file processes touch files within the + * pledge, so only these need to be chrooted. + */ + + if (COMP_CHALLENGE != proccomp && + COMP_FILE != proccomp) + return(1); + + if (-1 == chroot(path)) + warn("%s: chroot", path); + else if (-1 == chdir("/")) + warn("/: chdir"); + else + return(1); + + return(0); +} + +int +checkprivs(void) +{ + + /* Needed for chroot(2) calls in dropfs(). */ + + return(0 == getuid()); +} + +int +dropprivs(void) +{ + + /* Don't need to drop privileges like this. */ + + return(1); +} diff --git a/usr.sbin/acme-client/util.c b/usr.sbin/acme-client/util.c new file mode 100644 index 00000000000..d7e5b6322c6 --- /dev/null +++ b/usr.sbin/acme-client/util.c @@ -0,0 +1,317 @@ +/* $Id: util.c,v 1.1 2016/08/31 22:01:42 florian Exp $ */ +/* + * Copyright (c) 2016 Kristaps Dzonsons <kristaps@bsd.lv> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include <sys/wait.h> + +#include <assert.h> +#include <err.h> +#include <errno.h> +#include <limits.h> +#include <signal.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <stdint.h> +#include <string.h> +#include <unistd.h> + +#include "extern.h" + +static volatile sig_atomic_t sig; + +static const char *const comps[COMP__MAX] = { + "netproc", /* COMP_NET */ + "keyproc", /* COMP_KEY */ + "certproc", /* COMP_CERT */ + "acctproc", /* COMP_ACCOUNT */ + "challengeproc", /* COMP_CHALLENGE */ + "fileproc", /* COMP_FILE */ + "dnsproc", /* COMP_DNS */ + "revokeproc", /* COMP_REVOKE */ +}; + +static const char *const comms[COMM__MAX] = { + "req", /* COMM_REQ */ + "thumbprint", /* COMM_THUMB */ + "cert", /* COMM_CERT */ + "payload", /* COMM_PAY */ + "nonce", /* COMM_NONCE */ + "token", /* COMM_TOK */ + "challenge-op", /* COMM_CHNG_OP */ + "challenge-ack", /* COMM_CHNG_ACK */ + "account", /* COMM_ACCT */ + "acctpro-status", /* COMM_ACCT_STAT */ + "csr", /* COMM_CSR */ + "csr-op", /* COMM_CSR_OP */ + "issuer", /* COMM_ISSUER */ + "chain", /* COMM_CHAIN */ + "chain-op", /* COMM_CHAIN_OP */ + "dns", /* COMM_DNS */ + "dnsq", /* COMM_DNSQ */ + "dns-address", /* COMM_DNSA */ + "dns-family", /* COMM_DNSF */ + "dns-length", /* COMM_DNSLEN */ + "keyproc-status", /* COMM_KEY_STAT */ + "revoke-op", /* COMM_REVOKE_OP */ + "revoke-check", /* COMM_REVOKE_CHECK */ + "revoke-response", /* COMM_REVOKE_RESP */ +}; + +static void +sigpipe(int code) +{ + + (void)code; + sig = 1; +} + +/* + * This will read a long-sized operation. + * Operations are usually enums, so this should be alright. + * We return 0 on EOF and LONG_MAX on failure. + */ +long +readop(int fd, enum comm comm) +{ + ssize_t ssz; + long op; + + ssz = read(fd, &op, sizeof(long)); + if (ssz < 0) { + warn("read: %s", comms[comm]); + return(LONG_MAX); + } else if (ssz && ssz != sizeof(long)) { + warnx("short read: %s", comms[comm]); + return(LONG_MAX); + } else if (0 == ssz) + return(0); + + return(op); +} + +char * +readstr(int fd, enum comm comm) +{ + size_t sz; + + return(readbuf(fd, comm, &sz)); +} + +/* + * Read a buffer from the sender. + * This consists of two parts: the lenght of the buffer, and the buffer + * itself. + * We allow the buffer to be binary, but nil-terminate it anyway. + */ +char * +readbuf(int fd, enum comm comm, size_t *sz) +{ + ssize_t ssz; + size_t rsz, lsz; + char *p; + + p = NULL; + + if ((ssz = read(fd, sz, sizeof(size_t))) < 0) { + warn("read: %s length", comms[comm]); + return(NULL); + } else if ((size_t)ssz != sizeof(size_t)) { + warnx("short read: %s length", comms[comm]); + return(NULL); + } else if (*sz > SIZE_MAX - 1) { + warnx("integer overflow"); + return(NULL); + } else if (NULL == (p = calloc(1, *sz + 1))) { + warn("malloc"); + return(NULL); + } + + /* Catch this over several reads. */ + + rsz = 0; + lsz = *sz; + while (lsz) { + if ((ssz = read(fd, p + rsz, lsz)) < 0) { + warn("read: %s", comms[comm]); + break; + } else if (ssz > 0) { + assert((size_t)ssz <= lsz); + rsz += (size_t)ssz; + lsz -= (size_t)ssz; + } + } + + if (lsz) { + warnx("couldn't read buffer: %s", comms[comm]); + free(p); + return(NULL); + } + + return(p); +} + +/* + * Wring a long-value to a communication pipe. + * Returns 0 if the reader has terminated, -1 on error, 1 on success. + */ +int +writeop(int fd, enum comm comm, long op) +{ + void (*sigfp)(int); + ssize_t ssz; + int er; + + sigfp = signal(SIGPIPE, sigpipe); + + if ((ssz = write(fd, &op, sizeof(long))) < 0) { + if (EPIPE != (er = errno)) + warn("write: %s", comms[comm]); + signal(SIGPIPE, sigfp); + return(EPIPE == er ? 0 : -1); + } + + signal(SIGPIPE, sigfp); + + if ((size_t)ssz != sizeof(long)) { + warnx("short write: %s", comms[comm]); + return(-1); + } + + return(1); +} + +/* + * Fully write the given buffer. + * Returns 0 if the reader has terminated, -1 on error, 1 on success. + */ +int +writebuf(int fd, enum comm comm, const void *v, size_t sz) +{ + ssize_t ssz; + int er, rc; + void (*sigfp)(int); + + rc = -1; + + /* + * First, try to write the length. + * If the other end of the pipe has closed, we allow the short + * write to propogate as a return value of zero. + * To detect this, catch SIGPIPE. + */ + + sigfp = signal(SIGPIPE, sigpipe); + + if ((ssz = write(fd, &sz, sizeof(size_t))) < 0) { + if (EPIPE != (er = errno)) + warn("write: %s length", comms[comm]); + signal(SIGPIPE, sigfp); + return(EPIPE == er ? 0 : -1); + } + + /* Now write errors cause us to bail. */ + + if ((size_t)ssz != sizeof(size_t)) + warnx("short write: %s length", comms[comm]); + else if ((ssz = write(fd, v, sz)) < 0) + warn("write: %s", comms[comm]); + else if ((size_t)ssz != sz) + warnx("short write: %s", comms[comm]); + else + rc = 1; + + signal(SIGPIPE, sigfp); + return(rc); +} + +int +writestr(int fd, enum comm comm, const char *v) +{ + + return(writebuf(fd, comm, v, strlen(v))); +} + +/* + * Make sure that the given process exits properly, i.e., properly + * exiting with EXIT_SUCCESS. + * Returns non-zero on success and zero on failure. + */ +int +checkexit(pid_t pid, enum comp comp) +{ + int c, cc; + const char *cp; + + if (-1 == waitpid(pid, &c, 0)) { + warn("waitpid"); + return(0); + } else if ( ! WIFEXITED(c) && WIFSIGNALED(c)) { + cp = strsignal(WTERMSIG(c)); + warnx("signal: %s(%u): %s", comps[comp], pid, cp); + return(0); + } else if ( ! WIFEXITED(c)) { + warnx("did not exit: %s(%u)", comps[comp], pid); + return(0); + } else if (EXIT_SUCCESS != WEXITSTATUS(c)) { + cc = WEXITSTATUS(c); + dodbg("bad exit: %s(%u): %d", comps[comp], pid, cc); + return(0); + } + + return(1); +} + +/* + * Make sure that the given process exits properly, i.e., properly + * exiting with EXIT_SUCCESS *or* 2. + * Returns non-zero on success and zero on failure and sets the "rc" + * value to be the exit status. + */ +int +checkexit_ext(int *rc, pid_t pid, enum comp comp) +{ + int c; + const char *cp; + + *rc = EXIT_FAILURE; + + if (-1 == waitpid(pid, &c, 0)) { + warn("waitpid"); + return(0); + } + + if ( ! WIFEXITED(c) && WIFSIGNALED(c)) { + cp = strsignal(WTERMSIG(c)); + warnx("signal: %s(%u): %s", comps[comp], pid, cp); + return(0); + } else if ( ! WIFEXITED(c)) { + warnx("did not exit: %s(%u)", comps[comp], pid); + return(0); + } + + /* Now check extended status. */ + + if (EXIT_SUCCESS != (*rc = WEXITSTATUS(c)) && 2 != *rc) { + dodbg("bad exit: %s(%u): %d", comps[comp], pid, *rc); + return(0); + } + return(1); +} + |