diff --git a/Makefile b/Makefile index 17de639..ab1cf18 100644 --- a/Makefile +++ b/Makefile @@ -102,6 +102,13 @@ ifeq ("$(OSNAME)", "freebsd") KORE_CURL_INC=-I/usr/local/include endif +ifneq ("$(ACME)", "") + S_SRC+=src/acme.c + CURL=1 + CFLAGS+=-DKORE_USE_ACME + FEATURES+=-DKORE_USE_ACME +endif + ifneq ("$(CURL)", "") S_SRC+=src/curl.c KORE_CURL_LIB?=$(shell curl-config --libs) diff --git a/examples/messaging/src/messaging.c b/examples/messaging/src/messaging.c index e1a548b..8c67c73 100644 --- a/examples/messaging/src/messaging.c +++ b/examples/messaging/src/messaging.c @@ -23,8 +23,8 @@ * your workers with custom callbacks defined per message ID. */ -/* Your code shouldn't use IDs < 100. */ -#define MY_MESSAGE_ID 100 +/* Your code shouldn't use IDs <= KORE_MSG_APP_BASE. */ +#define MY_MESSAGE_ID KORE_MSG_APP_BASE + 1 int init(int); int page(struct http_request *); diff --git a/include/kore/acme.h b/include/kore/acme.h new file mode 100644 index 0000000..110c682 --- /dev/null +++ b/include/kore/acme.h @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2019 Joris Vink + * + * 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. + */ + +#ifndef __H_ACME_H +#define __H_ACME_H + +#if defined(__cplusplus) +extern "C" { +#endif + +/* + * All acme paths are relative to the keymgr_root directory. + */ +#define KORE_ACME_ACCOUNT_KEY "account-key.pem" +#define KORE_ACME_CERTDIR "certificates" + +#define KORE_ACME_RSAKEY_E (KORE_MSG_ACME_BASE + 1) +#define KORE_ACME_RSAKEY_N (KORE_MSG_ACME_BASE + 2) +#define KORE_ACME_SIGN (KORE_MSG_ACME_BASE + 3) +#define KORE_ACME_SIGN_RESULT (KORE_MSG_ACME_BASE + 4) +#define KORE_ACME_PROC_READY (KORE_MSG_ACME_BASE + 5) +#define KORE_ACME_ACCOUNT_CREATE (KORE_MSG_ACME_BASE + 10) +#define KORE_ACME_ACCOUNT_RESOLVE (KORE_MSG_ACME_BASE + 11) +#define KORE_ACME_ORDER_CREATE (KORE_MSG_ACME_BASE + 12) +#define KORE_ACME_CSR_REQUEST (KORE_MSG_ACME_BASE + 13) +#define KORE_ACME_CSR_RESPONSE (KORE_MSG_ACME_BASE + 14) +#define KORE_ACME_INSTALL_CERT (KORE_MSG_ACME_BASE + 15) +#define KORE_ACME_ORDER_FAILED (KORE_MSG_ACME_BASE + 16) + +#define KORE_ACME_CHALLENGE_CERT (KORE_MSG_ACME_BASE + 20) +#define KORE_ACME_CHALLENGE_SET_CERT (KORE_MSG_ACME_BASE + 21) +#define KORE_ACME_CHALLENGE_CLEAR_CERT (KORE_MSG_ACME_BASE + 22) + +void kore_acme_init(void); +void kore_acme_run(void); +void kore_acme_setup(void); + +int kore_acme_tls_alpn(SSL *, const unsigned char **, unsigned char *, + const unsigned char *, unsigned int, void *); + +extern char *acme_provider; + +#if defined(__cplusplus) +} +#endif + +#endif diff --git a/include/kore/kore.h b/include/kore/kore.h index ac81190..5233067 100644 --- a/include/kore/kore.h +++ b/include/kore/kore.h @@ -78,6 +78,8 @@ extern int daemon(int, int); #define KORE_USE_PLATFORM_PLEDGE 1 #endif +#define KORE_RSAKEY_BITS 4096 + #define KORE_RESULT_ERROR 0 #define KORE_RESULT_OK 1 #define KORE_RESULT_RETRY 2 @@ -86,6 +88,8 @@ extern int daemon(int, int); #define KORE_TLS_VERSION_1_2 1 #define KORE_TLS_VERSION_BOTH 2 +#define KORE_BASE64_RAW 0x0001 + #define KORE_WAIT_INFINITE (u_int64_t)-1 #define KORE_RESEED_TIME (1800 * 1000) @@ -128,6 +132,9 @@ extern int daemon(int, int); #define X509_CN_LENGTH (ub_common_name + 1) +#define KORE_PEM_CERT_CHAIN 1 +#define KORE_DER_CERT_DATA 2 + /* XXX hackish. */ #if !defined(KORE_NO_HTTP) struct http_request; @@ -195,6 +202,7 @@ TAILQ_HEAD(netbuf_head, netbuf); #define CONN_CLOSE_EMPTY 0x02 #define CONN_WS_CLOSE_SENT 0x04 #define CONN_IS_BUSY 0x08 +#define CONN_ACME_CHALLENGE 0x10 #define KORE_IDLE_TIMER_MAX 5000 @@ -303,6 +311,12 @@ struct kore_domain { struct kore_buf *logbuf; struct kore_server *server; +#if defined(KORE_USE_ACME) + int acme; + int acme_challenge; + void *acme_cert; + size_t acme_cert_len; +#endif char *cafile; char *crlfile; char *certfile; @@ -431,11 +445,9 @@ struct kore_alog_header { u_int16_t loglen; } __attribute__((packed)); -#define KORE_WORKER_MAX UCHAR_MAX - struct kore_worker { - u_int8_t id; - u_int8_t cpu; + u_int16_t id; + u_int16_t cpu; #if defined(__linux__) int tracing; #endif @@ -600,7 +612,17 @@ struct kore_timer { TAILQ_ENTRY(kore_timer) list; }; -#define KORE_WORKER_KEYMGR 0 +/* + * Keymgr process is worker index 0, but id 2000. + * Acme process is worker index 1, but id 2001. + */ +#define KORE_WORKER_KEYMGR_IDX 0 +#define KORE_WORKER_ACME_IDX 1 +#define KORE_WORKER_BASE 2 +#define KORE_WORKER_KEYMGR 2000 +#define KORE_WORKER_ACME 2001 +#define KORE_WORKER_MAX UCHAR_MAX + #define KORE_WORKER_POLICY_RESTART 1 #define KORE_WORKER_POLICY_TERMINATE 2 @@ -616,6 +638,10 @@ struct kore_timer { #define KORE_MSG_CRL 9 #define KORE_MSG_ACCEPT_AVAILABLE 10 #define KORE_PYTHON_SEND_OBJ 11 +#define KORE_MSG_ACME_BASE 100 + +/* messages for applications should start at 201. */ +#define KORE_MSG_APP_BASE 200 /* Predefined message targets. */ #define KORE_MSG_PARENT 1000 @@ -630,15 +656,13 @@ struct kore_msg { struct kore_keyreq { int padding; - char domain[KORE_DOMAINNAME_LEN]; - u_int16_t domain_len; - u_int16_t data_len; + char domain[KORE_DOMAINNAME_LEN + 1]; + size_t data_len; u_int8_t data[]; }; struct kore_x509_msg { - char domain[KORE_DOMAINNAME_LEN]; - u_int16_t domain_len; + char domain[KORE_DOMAINNAME_LEN + 1]; size_t data_len; u_int8_t data[]; }; @@ -666,6 +690,8 @@ extern char *rand_file; extern int keymgr_active; extern char *keymgr_runas_user; extern char *keymgr_root_path; +extern char *acme_runas_user; +extern char *acme_root_path; extern u_int8_t nlisteners; extern u_int16_t cpu_count; @@ -696,9 +722,11 @@ void kore_worker_init(void); void kore_worker_make_busy(void); void kore_worker_shutdown(void); void kore_worker_dispatch_signal(int); -void kore_worker_spawn(u_int16_t, u_int16_t); void kore_worker_entry(struct kore_worker *); void kore_worker_privdrop(const char *, const char *); +void kore_worker_spawn(u_int16_t, u_int16_t, u_int16_t); +int kore_worker_keymgr_response_verify(struct kore_msg *, + const void *, struct kore_domain **); struct kore_worker *kore_worker_data(u_int8_t); @@ -706,13 +734,13 @@ void kore_platform_init(void); void kore_platform_sandbox(void); void kore_platform_event_init(void); void kore_platform_event_cleanup(void); -void kore_platform_proctitle(char *); void kore_platform_disable_read(int); void kore_platform_disable_write(int); void kore_platform_enable_accept(void); void kore_platform_disable_accept(void); void kore_platform_event_wait(u_int64_t); void kore_platform_event_all(int, void *); +void kore_platform_proctitle(const char *); void kore_platform_schedule_read(int, void *); void kore_platform_schedule_write(int, void *); void kore_platform_event_schedule(int, int, int, void *); @@ -830,10 +858,15 @@ long long kore_strtonum(const char *, int, long long, long long, int *); double kore_strtodouble(const char *, long double, long double, int *); int kore_base64_encode(const void *, size_t, char **); int kore_base64_decode(const char *, u_int8_t **, size_t *); +int kore_base64url_encode(const void *, size_t, char **, int); +int kore_base64url_decode(const char *, u_int8_t **, size_t *, int); void *kore_mem_find(void *, size_t, const void *, size_t); char *kore_text_trim(char *, size_t); char *kore_read_line(FILE *, char *, size_t); +EVP_PKEY *kore_rsakey_load(const char *); +EVP_PKEY *kore_rsakey_generate(const char *); + #if !defined(KORE_NO_HTTP) void kore_websocket_handshake(struct http_request *, const char *, const char *, const char *); @@ -885,7 +918,8 @@ void kore_domain_load_crl(void); void kore_domain_keymgr_init(void); void kore_domain_callback(void (*cb)(struct kore_domain *)); int kore_domain_attach(struct kore_domain *, struct kore_server *); -void kore_domain_tlsinit(struct kore_domain *, const void *, size_t); +void kore_domain_tlsinit(struct kore_domain *, int, + const void *, size_t); void kore_domain_crl_add(struct kore_domain *, const void *, size_t); #if !defined(KORE_NO_HTTP) int kore_module_handler_new(struct kore_domain *, const char *, @@ -930,6 +964,8 @@ struct kore_validator *kore_validator_lookup(const char *); void fatal(const char *, ...) __attribute__((noreturn)); void fatalx(const char *, ...) __attribute__((noreturn)); + +const char *kore_worker_name(int); void kore_debug_internal(char *, int, const char *, ...); u_int16_t net_read16(u_int8_t *); diff --git a/include/kore/seccomp.h b/include/kore/seccomp.h index d41c710..e70d9f0 100644 --- a/include/kore/seccomp.h +++ b/include/kore/seccomp.h @@ -164,8 +164,8 @@ void kore_seccomp_init(void); void kore_seccomp_drop(void); void kore_seccomp_enable(void); void kore_seccomp_traceme(void); +int kore_seccomp_trace(pid_t, int); int kore_seccomp_syscall_resolve(const char *); -int kore_seccomp_trace(struct kore_worker *, int); int kore_seccomp_filter(const char *, void *, size_t); const char *kore_seccomp_syscall_name(long); diff --git a/src/accesslog.c b/src/accesslog.c index 783b10c..f1c3ecf 100644 --- a/src/accesslog.c +++ b/src/accesslog.c @@ -208,7 +208,7 @@ kore_accesslog_gather(void *arg, u_int64_t now, int force) if (logbuf == NULL) logbuf = kore_buf_alloc(LOGBUF_SIZE); - for (id = 0; id < worker_count; id++) { + for (id = KORE_WORKER_BASE; id < worker_count; id++) { kw = kore_worker_data(id); accesslog_lock(kw); diff --git a/src/acme.c b/src/acme.c new file mode 100644 index 0000000..337b714 --- /dev/null +++ b/src/acme.c @@ -0,0 +1,1684 @@ +/* + * Copyright (c) 2019 Joris Vink + * + * 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. + */ + +/* + * ACMEv2 protocol implementation. + * + * The acme process is responsible for talking to the acme servers, parsing + * their JSON responses and requesting signed data / a csr from the keymgr + * process. + * + * The acme process does not hold your account or domain keys, so anything + * that needs to be signed is sent to the keymgr process instead. + */ + +#include +#include + +#include + +#include +#include + +#include "kore.h" +#include "acme.h" +#include "curl.h" + +#define ACME_CREATE_ACCOUNT 0 +#define ACME_RESOLVE_ACCOUNT 1 + +#define ACME_STATUS_PENDING 1 +#define ACME_STATUS_PROCESSING 2 +#define ACME_STATUS_VALID 3 +#define ACME_STATUS_INVALID 4 +#define ACME_STATUS_READY 5 +#define ACME_STATUS_EXPIRED 6 +#define ACME_STATUS_REVOKED 7 + +/* + * The default provider is letsencrypt, can be overwritten via the config + * file its acme_provider setting. + */ +#define ACME_DEFAULT_PROVIDER "https://acme-v02.api.letsencrypt.org/directory" + +#if defined(__linux__) +#include "seccomp.h" + +/* + * The syscalls our acme worker is allowed to perform, only. + * + * Since we drop all previously loaded seccomp rules to apply our own + * we will have to reinclude the ones curl does. + */ +static struct sock_filter filter_acme[] = { +#if defined(SYS_poll) + KORE_SYSCALL_ALLOW(poll), +#endif + KORE_SYSCALL_ALLOW(ppoll), + KORE_SYSCALL_ALLOW(sendto), + KORE_SYSCALL_ALLOW(recvfrom), +#if defined(SYS_epoll_wait) + KORE_SYSCALL_ALLOW(epoll_wait), +#endif + KORE_SYSCALL_ALLOW(epoll_pwait), + KORE_SYSCALL_ALLOW(recvmsg), + KORE_SYSCALL_ALLOW(sendmmsg), + KORE_SYSCALL_ALLOW(getpeername), + + KORE_SYSCALL_ALLOW(exit), + + KORE_SYSCALL_ALLOW(brk), + KORE_SYSCALL_ALLOW(mmap), + KORE_SYSCALL_ALLOW(ioctl), + KORE_SYSCALL_ALLOW(uname), + KORE_SYSCALL_ALLOW(munmap), + KORE_SYSCALL_ALLOW(madvise), + KORE_SYSCALL_ALLOW(faccessat), + KORE_SYSCALL_ALLOW(newfstatat), + KORE_SYSCALL_ALLOW(clock_gettime), + + KORE_SYSCALL_ALLOW(bind), + KORE_SYSCALL_ALLOW(ioctl), + KORE_SYSCALL_ALLOW(connect), + KORE_SYSCALL_ALLOW(getsockopt), + KORE_SYSCALL_ALLOW(socketpair), + KORE_SYSCALL_ALLOW(getsockname), + KORE_SYSCALL_ALLOW_ARG(socket, 0, AF_INET), + KORE_SYSCALL_ALLOW_ARG(socket, 0, AF_INET6), + KORE_SYSCALL_ALLOW_ARG(socket, 0, AF_UNIX), + KORE_SYSCALL_ALLOW_ARG(socket, 0, AF_NETLINK), + + KORE_SYSCALL_ALLOW(clone), + KORE_SYSCALL_ALLOW(membarrier), + KORE_SYSCALL_ALLOW(set_robust_list), +}; +#endif + +struct acme_request { + struct kore_curl curl; +}; + +struct acme_sign_op { + u_int32_t id; + struct kore_timer *t; + void *udata; + char *nonce; + char *payload; + char *protected; + void (*cb)(struct acme_sign_op *, + struct kore_buf *); + LIST_ENTRY(acme_sign_op) list; +}; + +#define ACME_AUTH_STATE_DOWNLOAD 1 +#define ACME_AUTH_STATE_CHALLENGE 2 + +struct acme_auth { + char *url; + int status; + struct acme_challenge *challenge; + LIST_ENTRY(acme_auth) list; +}; + +#define ACME_ORDER_STATE_RUNNING 1 +#define ACME_ORDER_STATE_ERROR 2 +#define ACME_ORDER_TICK 1000 +#define ACME_ORDER_TIMEOUT 60000 + +#define ACME_ORDER_CSR_REQUESTED 0x1000 + +struct acme_order { + int state; + int status; + int flags; + u_int64_t start; + char *id; + char *final; + char *domain; + char *certloc; + struct acme_auth *curauth; + LIST_HEAD(, acme_auth) auth; + LIST_ENTRY(acme_order) list; +}; + +static LIST_HEAD(, acme_order) orders; + +#define ACME_FLAG_CHALLENGE_CREATED 0x0001 +#define ACME_CHALLENGE_TOKEN_MAXLEN 64 + +struct acme_challenge { + int status; + int flags; + char *url; + char *type; + char *token; + char *error_type; + char *error_detail; + int (*process)(struct acme_order *, + struct acme_challenge *); +}; + +static LIST_HEAD(, acme_sign_op) signops; + +static int acme_status_type(const char *); +static int acme_request_run(struct acme_request *); +static void acme_request_cleanup(struct acme_request *); +static void acme_request_prepare(struct acme_request *, + int, const char *, const void *, size_t); +static void acme_request_json(struct kore_buf *, const char *, + const char *, const char *); + +static char *acme_nonce_fetch(void); +static char *acme_thumbprint_component(void); +static char *acme_base64url(const void *, size_t); +static char *acme_protected_component(const char *, const char *); +static void acme_tls_challenge_use_cert(SSL *, struct kore_domain *); +static void acme_keymgr_key_req(const char *, const void *, size_t, int); + +static void acme_parse_directory(void); +static void acme_directory_set(struct kore_json *, const char *, char **); + +static void acme_sign_expire(void *, u_int64_t); +static void acme_sign_result(struct kore_msg *, const void *); +static void acme_sign_submit(struct kore_json_item *, const char *, void *, + void (*cb)(struct acme_sign_op *, struct kore_buf *)); + +static void acme_rsakey_exp(struct kore_msg *, const void *); +static void acme_rsakey_mod(struct kore_msg *, const void *); + +static void acme_account_reg(int); +static void acme_account_create(struct kore_msg *, const void *); +static void acme_account_resolve(struct kore_msg *, const void *); +static void acme_generic_submit(struct acme_sign_op *, struct kore_buf *); +static void acme_account_reg_submit(struct acme_sign_op *, + struct kore_buf *); + +static void acme_order_retry(const char *); +static void acme_order_process(void *, u_int64_t); +static void acme_order_update(struct acme_order *); +static void acme_order_request_csr(struct acme_order *); +static int acme_order_fetch_certificate(struct acme_order *); +static void acme_order_create(struct kore_msg *, const void *); +static void acme_order_remove(struct acme_order *, const char *); +static void acme_order_csr_response(struct kore_msg *, const void *); +static void acme_order_create_submit(struct acme_sign_op *, + struct kore_buf *); + +static void acme_order_auth_log_error(struct acme_order *); +static void acme_order_auth_deactivate(struct acme_order *); +static int acme_order_auth_process(struct acme_order *, + struct acme_auth *); +static int acme_order_auth_update(struct acme_order *, + struct acme_auth *); + +static int acme_challenge_tls_alpn_01(struct acme_order *, + struct acme_challenge *); +static void acme_challenge_tls_alpn_01_create(struct acme_order *, + struct acme_challenge *); + +static void acme_challenge_respond(struct acme_order *, + const char *, const char *); + +static int signop_id = 0; +static char *rsakey_n = NULL; +static char *rsakey_e = NULL; +static char *nonce_url = NULL; +static char *order_url = NULL; +static char *revoke_url = NULL; +static char *account_id = NULL; +static char *account_url = NULL; + +static u_int8_t acme_alpn_name[] = + { 0xa, 'a', 'c', 'm', 'e', '-', 't', 'l', 's', '/', '1' }; + +char *acme_provider = NULL; +char *acme_root_path = NULL; +char *acme_runas_user = NULL; +u_int32_t acme_request_timeout = 8; + +void +kore_acme_init(void) +{ + acme_provider = kore_strdup(ACME_DEFAULT_PROVIDER); +} + +void +kore_acme_run(void) +{ + int quit; + u_int64_t now, netwait; + + quit = 0; + + kore_server_closeall(); + kore_module_cleanup(); + + net_init(); + kore_timer_init(); + kore_connection_init(); + kore_platform_event_init(); + kore_msg_worker_init(); + + kore_msg_register(KORE_ACME_RSAKEY_E, acme_rsakey_exp); + kore_msg_register(KORE_ACME_RSAKEY_N, acme_rsakey_mod); + kore_msg_register(KORE_ACME_SIGN_RESULT, acme_sign_result); + kore_msg_register(KORE_ACME_ORDER_CREATE, acme_order_create); + kore_msg_register(KORE_ACME_ACCOUNT_CREATE, acme_account_create); + kore_msg_register(KORE_ACME_ACCOUNT_RESOLVE, acme_account_resolve); + kore_msg_register(KORE_ACME_CSR_RESPONSE, acme_order_csr_response); + +#if defined(__linux__) + /* Drop all enabled seccomp filters, and add only ours. */ + kore_seccomp_drop(); + kore_seccomp_filter("acme", filter_acme, KORE_FILTER_LEN(filter_acme)); +#endif +#if defined(KORE_USE_PYTHON) + kore_msg_unregister(KORE_PYTHON_SEND_OBJ); +#endif + kore_worker_privdrop(acme_runas_user, acme_root_path); + +#if defined(__OpenBSD__) + if (unveil("/etc/ssl/", "r") == -1) + fatal("unveil: %s", errno_s); + if (pledge("stdio inet dns rpath", NULL) == -1) + fatal("pledge acme process: %s", errno_s); +#endif + + http_init(); + + if (!kore_quiet) { + kore_log(LOG_NOTICE, + "acme worker started (pid#%d)", worker->pid); + } + + LIST_INIT(&orders); + LIST_INIT(&signops); + + acme_parse_directory(); + + while (quit != 1) { + now = kore_time_ms(); + netwait = kore_timer_next_run(now); + kore_platform_event_wait(netwait); + + if (sig_recv != 0) { + switch (sig_recv) { + case SIGQUIT: + case SIGINT: + case SIGTERM: + quit = 1; + break; + default: + break; + } + sig_recv = 0; + } + + if (quit) + break; + + now = kore_time_ms(); + kore_timer_run(now); + kore_connection_prune(KORE_CONNECTION_PRUNE_DISCONNECT); + } + + kore_platform_event_cleanup(); + kore_connection_cleanup(); + net_cleanup(); +} + +int +kore_acme_tls_alpn(SSL *ssl, const unsigned char **out, unsigned char *outlen, + const unsigned char *in, unsigned int inlen, void *udata) +{ + struct kore_domain *dom = udata; + + if (dom->acme == 0) + return (SSL_TLSEXT_ERR_NOACK); + + if (dom->acme_challenge == 0) + return (SSL_TLSEXT_ERR_NOACK); + + if (inlen != sizeof(acme_alpn_name)) + return (SSL_TLSEXT_ERR_NOACK); + + if (memcmp(acme_alpn_name, in, sizeof(acme_alpn_name))) + return (SSL_TLSEXT_ERR_NOACK); + + kore_log(LOG_NOTICE, "[%s] acme-tls/1 challenge requested", + dom->domain); + + *out = in + 1; + *outlen = inlen - 1; + + acme_tls_challenge_use_cert(ssl, dom); + + return (SSL_TLSEXT_ERR_OK); +} + +static void +acme_tls_challenge_use_cert(SSL *ssl, struct kore_domain *dom) +{ + struct connection *c; + const unsigned char *ptr; + X509 *x509; + + if ((c = SSL_get_ex_data(ssl, 0)) == NULL) + fatal("%s: no connection data present", __func__); + + ptr = dom->acme_cert; + if ((x509 = d2i_X509(NULL, &ptr, dom->acme_cert_len)) == NULL) + fatal("d2i_X509: %s", ssl_errno_s); + + if (SSL_use_certificate(ssl, x509) == 0) + fatal("SSL_use_certificate: %s", ssl_errno_s); + + SSL_clear_chain_certs(ssl); + SSL_set_verify(ssl, SSL_VERIFY_NONE, NULL); + + c->flags |= CONN_ACME_CHALLENGE; +} + +static void +acme_parse_directory(void) +{ + struct acme_request req; + size_t len; + struct kore_json json; + const u_int8_t *body; + + acme_request_prepare(&req, HTTP_METHOD_GET, acme_provider, NULL, 0); + + if (!acme_request_run(&req)) { + acme_request_cleanup(&req); + return; + } + + if (req.curl.http.status != HTTP_STATUS_OK) { + kore_log(LOG_NOTICE, + "request to '%s' failed: got %ld - expected 200", + req.curl.url, req.curl.http.status); + acme_request_cleanup(&req); + return; + } + + kore_curl_response_as_bytes(&req.curl, &body, &len); + + kore_json_init(&json, body, len); + + if (!kore_json_parse(&json)) { + kore_log(LOG_NOTICE, + "failed to parse directory payload from ACME server (%s)", + kore_json_strerror(&json)); + goto cleanup; + } + + acme_directory_set(&json, "newNonce", &nonce_url); + acme_directory_set(&json, "newOrder", &order_url); + acme_directory_set(&json, "newAccount", &account_url); + acme_directory_set(&json, "revokeCert", &revoke_url); + +cleanup: + kore_json_cleanup(&json); + acme_request_cleanup(&req); +} + +static char * +acme_nonce_fetch(void) +{ + struct acme_request req; + char *ret; + const char *nonce; + + ret = NULL; + acme_request_prepare(&req, HTTP_METHOD_HEAD, nonce_url, NULL, 0); + + if (!acme_request_run(&req)) + goto cleanup; + + if (req.curl.http.status != HTTP_STATUS_OK) { + kore_log(LOG_NOTICE, + "request to '%s' failed: got %ld - expected 200", + req.curl.url, req.curl.http.status); + goto cleanup; + } + + if (!kore_curl_http_get_header(&req.curl, "replay-nonce", &nonce)) { + kore_log(LOG_NOTICE, "new-nonce: no replay-nonce header found"); + goto cleanup; + } + + ret = kore_strdup(nonce); + +cleanup: + acme_request_cleanup(&req); + + return (ret); +} + +static void +acme_account_create(struct kore_msg *msg, const void *data) +{ + acme_account_reg(ACME_CREATE_ACCOUNT); +} + +static void +acme_account_resolve(struct kore_msg *msg, const void *data) +{ + acme_account_reg(ACME_RESOLVE_ACCOUNT); +} + +static void +acme_account_reg(int resolve_only) +{ + struct kore_json_item *json; + + if (account_url == NULL) + return; + + kore_free(account_id); + account_id = NULL; + + kore_log(LOG_INFO, "%s account with ACME provider", + resolve_only ? "resolving" : "creating"); + + json = kore_json_create_object(NULL, NULL); + kore_json_create_literal(json, "termsOfServiceAgreed", KORE_JSON_TRUE); + + if (resolve_only) { + kore_json_create_literal(json, + "onlyReturnExisting", KORE_JSON_TRUE); + } + + acme_sign_submit(json, account_url, NULL, acme_account_reg_submit); + kore_json_item_free(json); +} + +static void +acme_account_reg_submit(struct acme_sign_op *op, struct kore_buf *payload) +{ + struct acme_request req; + const char *header; + + acme_request_prepare(&req, HTTP_METHOD_POST, account_url, + payload->data, payload->offset); + + if (!acme_request_run(&req)) + goto cleanup; + + switch (req.curl.http.status) { + case HTTP_STATUS_OK: + case HTTP_STATUS_CREATED: + break; + default: + kore_log(LOG_NOTICE, + "request to '%s' failed: status %ld - body '%s'", + req.curl.url, req.curl.http.status, + kore_curl_response_as_string(&req.curl)); + goto cleanup; + } + + if (!kore_curl_http_get_header(&req.curl, "location", &header)) { + kore_log(LOG_NOTICE, "new-acct: no location header found"); + goto cleanup; + } + + account_id = kore_strdup(header); + kore_log(LOG_INFO, "account_id = %s", account_id); + kore_msg_send(KORE_WORKER_KEYMGR, KORE_ACME_PROC_READY, NULL, 0); + +cleanup: + acme_request_cleanup(&req); +} + +static void +acme_order_create(struct kore_msg *msg, const void *data) +{ + char *domain; + struct kore_json_item *json, *identifiers, *identifier; + + domain = kore_calloc(1, msg->length + 1); + memcpy(domain, data, msg->length); + domain[msg->length] = '\0'; + + kore_log(LOG_INFO, "[%s] creating order", domain); + + json = kore_json_create_object(NULL, NULL); + identifiers = kore_json_create_array(json, "identifiers"); + + identifier = kore_json_create_object(identifiers, NULL); + kore_json_create_string(identifier, "type", "dns"); + kore_json_create_string(identifier, "value", domain); + + acme_sign_submit(json, order_url, domain, acme_order_create_submit); + kore_json_item_free(json); +} + +static void +acme_order_create_submit(struct acme_sign_op *op, struct kore_buf *payload) +{ + struct acme_request req; + size_t len; + struct kore_json json; + int stval; + const u_int8_t *body; + struct acme_order *order; + struct acme_auth *auth; + const char *header; + const char *domain; + struct kore_json_item *item, *array, *final, *status; + + order = NULL; + domain = op->udata; + acme_request_prepare(&req, HTTP_METHOD_POST, order_url, + payload->data, payload->offset); + + if (!acme_request_run(&req)) { + acme_request_cleanup(&req); + acme_order_retry(domain); + return; + } + + if (req.curl.http.status != HTTP_STATUS_CREATED) { + kore_log(LOG_NOTICE, + "[%s] - request to '%s' failed: status %ld - body '%s'", + domain, req.curl.url, req.curl.http.status, + kore_curl_response_as_string(&req.curl)); + acme_request_cleanup(&req); + acme_order_retry(domain); + return; + } + + if (!kore_curl_http_get_header(&req.curl, "location", &header)) { + kore_log(LOG_NOTICE, + "[%s] new-order: no order id found", domain); + acme_request_cleanup(&req); + acme_order_retry(domain); + return; + } + + kore_curl_response_as_bytes(&req.curl, &body, &len); + kore_json_init(&json, body, len); + + if (!kore_json_parse(&json)) { + kore_log(LOG_NOTICE, + "[%s] failed to parse order payload from ACME server (%s)", + domain, kore_json_strerror(&json)); + goto cleanup; + } + + array = kore_json_find_array(json.root, "authorizations"); + if (array == NULL) { + kore_log(LOG_NOTICE, "[%s] body has no 'authorizations' array", + domain); + goto cleanup; + } + + if (TAILQ_EMPTY(&array->data.items)) { + kore_log(LOG_NOTICE, "[%s] no authoritization URLs in payload", + domain); + goto cleanup; + } + + if ((status = kore_json_find_string(json.root, "status")) == NULL) { + kore_log(LOG_NOTICE, "[%s] order has no 'status' string", + domain); + goto cleanup; + } + + if ((final = kore_json_find_string(json.root, "finalize")) == NULL) { + kore_log(LOG_NOTICE, "[%s] order has no 'finalize' string", + domain); + goto cleanup; + } + + if ((stval = acme_status_type(status->data.string)) == -1) { + kore_log(LOG_NOTICE, "[%s] order has invalid status", + domain); + goto cleanup; + } + + order = kore_calloc(1, sizeof(*order)); + LIST_INSERT_HEAD(&orders, order, list); + + LIST_INIT(&order->auth); + + order->status = stval; + order->start = kore_time_ms(); + order->id = kore_strdup(header); + order->domain = kore_strdup(domain); + order->state = ACME_ORDER_STATE_RUNNING; + order->final = kore_strdup(final->data.string); + + kore_timer_add(acme_order_process, ACME_ORDER_TICK, + order, KORE_TIMER_ONESHOT); + + TAILQ_FOREACH(item, &array->data.items, list) { + if (item->type != KORE_JSON_TYPE_STRING) + continue; + + auth = kore_calloc(1, sizeof(*auth)); + auth->url = kore_strdup(item->data.string); + LIST_INSERT_HEAD(&order->auth, auth, list); + } + + order->curauth = LIST_FIRST(&order->auth); + kore_log(LOG_INFO, "[%s] order_id = %s", order->domain, order->id); + +cleanup: + if (order == NULL) + acme_order_retry(domain); + + kore_json_cleanup(&json); + acme_request_cleanup(&req); +} + +static void +acme_order_update(struct acme_order *order) +{ + struct acme_request req; + size_t len; + struct kore_json json; + const u_int8_t *body; + int stval, ret; + struct kore_json_item *status, *cert; + + acme_request_prepare(&req, HTTP_METHOD_GET, order->id, NULL, 0); + + if (!acme_request_run(&req)) { + acme_request_cleanup(&req); + order->state = ACME_ORDER_STATE_ERROR; + return; + } + + if (req.curl.http.status != HTTP_STATUS_OK) { + kore_log(LOG_NOTICE, + "[%s] - request to '%s' failed: status %ld - body '%s'", + order->domain, req.curl.url, req.curl.http.status, + kore_curl_response_as_string(&req.curl)); + acme_request_cleanup(&req); + order->state = ACME_ORDER_STATE_ERROR; + return; + } + + ret = KORE_RESULT_ERROR; + kore_curl_response_as_bytes(&req.curl, &body, &len); + + kore_json_init(&json, body, len); + + if (!kore_json_parse(&json)) { + kore_log(LOG_NOTICE, + "[%s] failed to parse order payload from ACME server (%s)", + order->domain, kore_json_strerror(&json)); + goto cleanup; + } + + if ((status = kore_json_find_string(json.root, "status")) == NULL) { + kore_log(LOG_NOTICE, "[%s] order has no 'status' string", + order->domain); + goto cleanup; + } + + if ((stval = acme_status_type(status->data.string)) == -1) { + kore_log(LOG_NOTICE, "[%s] order has invalid status", + order->domain); + goto cleanup; + } + + order->status = stval; + + if (order->status == ACME_STATUS_VALID) { + cert = kore_json_find_string(json.root, "certificate"); + if (cert == NULL) { + kore_log(LOG_NOTICE, + "[%s] order has 'certificate' member", + order->domain); + goto cleanup; + } + + order->certloc = kore_strdup(cert->data.string); + } + + ret = KORE_RESULT_OK; + +cleanup: + if (ret == KORE_RESULT_ERROR) + order->state = ACME_ORDER_STATE_ERROR; + + kore_json_cleanup(&json); + acme_request_cleanup(&req); +} + +/* + * We currently don't care why an order may have failed, (rate-limited, + * auth failed, etc). + * + * It would be neat if we could obey that a bit better. + */ +static void +acme_order_retry(const char *domain) +{ + u_int32_t retry_after; + + /* arbitrary number */ + retry_after = 60000; + + acme_keymgr_key_req(domain, &retry_after, sizeof(retry_after), + KORE_ACME_ORDER_FAILED); +} + +static void +acme_order_process(void *udata, u_int64_t now) +{ + struct acme_auth *auth; + struct acme_order *order = udata; + + acme_order_update(order); + + LIST_FOREACH(auth, &order->auth, list) { + if (!acme_order_auth_update(order, auth)) { + acme_order_remove(order, "cancelled"); + return; + } + } + + if ((now - order->start) >= ACME_ORDER_TIMEOUT) { + acme_order_auth_deactivate(order); + acme_order_remove(order, "order ran too long"); + return; + } + + switch (order->state) { + case ACME_ORDER_STATE_RUNNING: + switch (order->status) { + case ACME_STATUS_PENDING: + if (!acme_order_auth_process(order, order->curauth)) { + acme_order_auth_log_error(order); + acme_order_remove(order, "cancelled"); + order = NULL; + } + break; + case ACME_STATUS_READY: + acme_order_request_csr(order); + break; + case ACME_STATUS_PROCESSING: + kore_log(LOG_INFO, "[%s] waiting for certificate", + order->domain); + break; + case ACME_STATUS_VALID: + kore_log(LOG_INFO, "[%s] certificate available", + order->domain); + if (!acme_order_fetch_certificate(order)) { + acme_order_remove(order, + "failed to fetch certificate"); + } else { + acme_order_remove(order, "completed"); + } + order = NULL; + break; + case ACME_STATUS_INVALID: + kore_log(LOG_INFO, "[%s] order authorization failed", + order->domain); + acme_order_auth_log_error(order); + acme_order_remove(order, "authorization failure"); + order = NULL; + break; + default: + acme_order_auth_deactivate(order); + acme_order_remove(order, "unknown status"); + order = NULL; + break; + } + break; + case ACME_ORDER_STATE_ERROR: + acme_order_auth_deactivate(order); + acme_order_remove(order, "error"); + order = NULL; + break; + default: + fatal("%s: invalid order state %d", __func__, order->state); + } + + if (order != NULL) { + kore_timer_add(acme_order_process, ACME_ORDER_TICK, + order, KORE_TIMER_ONESHOT); + } +} + +static void +acme_order_remove(struct acme_order *order, const char *reason) +{ + struct acme_auth *auth; + + LIST_REMOVE(order, list); + + while ((auth = LIST_FIRST(&order->auth)) != NULL) { + LIST_REMOVE(auth, list); + + if (auth->challenge != NULL) { + kore_free(auth->challenge->error_detail); + kore_free(auth->challenge->error_type); + kore_free(auth->challenge->token); + kore_free(auth->challenge->type); + kore_free(auth->challenge->url); + kore_free(auth->challenge); + } + + kore_free(auth->url); + kore_free(auth); + } + + kore_log(LOG_NOTICE, "[%s] order removed (%s)", order->domain, reason); + + if (strcmp(reason, "completed")) + acme_order_retry(order->domain); + + kore_free(order->domain); + kore_free(order->final); + kore_free(order->id); + kore_free(order); +} + +static int +acme_order_fetch_certificate(struct acme_order *order) +{ + struct acme_request req; + size_t len; + const u_int8_t *body; + + if (order->certloc == NULL) + return (KORE_RESULT_ERROR); + + acme_request_prepare(&req, HTTP_METHOD_GET, order->certloc, NULL, 0); + + if (!acme_request_run(&req)) { + acme_request_cleanup(&req); + return (KORE_RESULT_ERROR); + } + + if (req.curl.http.status != HTTP_STATUS_OK) { + kore_log(LOG_NOTICE, + "[%s] request to '%s' failed: got %ld - expected 200", + order->domain, order->certloc, req.curl.http.status); + acme_request_cleanup(&req); + return (KORE_RESULT_ERROR); + } + + kore_curl_response_as_bytes(&req.curl, &body, &len); + + kore_log(LOG_INFO, "got %zu bytes of cert data", len); + acme_keymgr_key_req(order->domain, body, len, KORE_ACME_INSTALL_CERT); + + acme_request_cleanup(&req); + + return (KORE_RESULT_OK); +} + +static void +acme_order_request_csr(struct acme_order *order) +{ + if (order->flags & ACME_ORDER_CSR_REQUESTED) + return; + + kore_log(LOG_INFO, "[%s] requesting CSR", order->domain); + + order->flags |= ACME_ORDER_CSR_REQUESTED; + acme_keymgr_key_req(order->domain, NULL, 0, KORE_ACME_CSR_REQUEST); +} + +static void +acme_order_csr_response(struct kore_msg *msg, const void *data) +{ + const struct kore_x509_msg *req; + struct kore_json_item *json; + struct acme_order *order; + char *b64, *url; + + if (!kore_worker_keymgr_response_verify(msg, data, NULL)) + return; + + req = (const struct kore_x509_msg *)data; + + LIST_FOREACH(order, &orders, list) { + if (!strcmp(order->domain, req->domain)) + break; + } + + if (order == NULL) { + kore_log(LOG_NOTICE, "[%s] csr received but no order active", + req->domain); + return; + } + + url = kore_strdup(order->final); + b64 = acme_base64url(req->data, req->data_len); + + json = kore_json_create_object(NULL, NULL); + kore_json_create_string(json, "csr", b64); + acme_sign_submit(json, url, url, acme_generic_submit); + + kore_json_item_free(json); + kore_free(b64); +} + +static void +acme_order_auth_deactivate(struct acme_order *order) +{ + struct acme_request req; + struct acme_auth *auth; + + LIST_FOREACH(auth, &order->auth, list) { + acme_request_prepare(&req, HTTP_METHOD_GET, auth->url, NULL, 0); + + if (!acme_request_run(&req)) { + kore_log(LOG_NOTICE, + "[%s:auth] failed to deactivate %s", order->domain, + auth->url); + } else { + kore_log(LOG_NOTICE, "[%s:auth] deactivated %s", + order->domain, auth->url); + } + + acme_request_cleanup(&req); + } +} + +static void +acme_order_auth_log_error(struct acme_order *order) +{ + struct acme_auth *auth; + + LIST_FOREACH(auth, &order->auth, list) { + if (auth->challenge->status == ACME_STATUS_PENDING || + auth->challenge->status == ACME_STATUS_VALID || + auth->challenge->status == ACME_STATUS_PROCESSING) + continue; + + kore_log(LOG_INFO, "[%s:auth:challenge] %s = %s (%s)", + order->domain, auth->challenge->type, + auth->challenge->error_type, auth->challenge->error_detail); + } +} + +static int +acme_order_auth_process(struct acme_order *order, struct acme_auth *auth) +{ + int ret; + + if (auth == NULL) + return (KORE_RESULT_OK); + + ret = KORE_RESULT_ERROR; + kore_log(LOG_INFO, "[%s] processing authentication", order->domain); + + switch (auth->status) { + case ACME_STATUS_PENDING: + ret = auth->challenge->process(order, auth->challenge); + break; + case ACME_STATUS_VALID: + case ACME_STATUS_PROCESSING: + ret = KORE_RESULT_OK; + break; + case ACME_STATUS_INVALID: + kore_log(LOG_NOTICE, "[%s:auth] authorization invalid", + order->domain); + break; + case ACME_STATUS_EXPIRED: + kore_log(LOG_NOTICE, "[%s:auth] authorization expired", + order->domain); + break; + case ACME_STATUS_REVOKED: + kore_log(LOG_NOTICE, "[%s:auth] authorization revoked", + order->domain); + break; + default: + kore_log(LOG_NOTICE, "[%s:auth] invalid auth status %d", + order->domain, auth->status); + break; + } + + if (ret == KORE_RESULT_OK) + order->curauth = LIST_NEXT(order->curauth, list); + + return (ret); +} + +static int +acme_order_auth_update(struct acme_order *order, struct acme_auth *auth) +{ + const char *p; + struct acme_request req; + size_t len; + struct kore_json json; + const u_int8_t *body; + int ret, stval; + struct acme_challenge *challenge; + struct kore_json_item *status, *type, *url, *token; + struct kore_json_item *array, *object, *err, *detail; + + ret = KORE_RESULT_ERROR; + acme_request_prepare(&req, HTTP_METHOD_GET, auth->url, NULL, 0); + + if (!acme_request_run(&req)) { + acme_request_cleanup(&req); + return (ret); + } + + if (req.curl.http.status != HTTP_STATUS_OK) { + kore_log(LOG_NOTICE, + "[%s:auth] request to '%s' failed: got %ld - expected 200", + order->domain, auth->url, req.curl.http.status); + acme_request_cleanup(&req); + return (ret); + } + + kore_curl_response_as_bytes(&req.curl, &body, &len); + + kore_json_init(&json, body, len); + + if (!kore_json_parse(&json)) { + kore_log(LOG_NOTICE, + "[%s:auth] failed to parse payload from ACME server (%s)", + order->domain, kore_json_strerror(&json)); + goto cleanup; + } + + kore_log(LOG_INFO, "[%s:auth] %s updated", order->domain, auth->url); + + if ((status = kore_json_find_string(json.root, "status")) == NULL) { + kore_log(LOG_NOTICE, "[%s:auth] payload has no 'status' string", + order->domain); + goto cleanup; + } + + if ((array = kore_json_find_array(json.root, "challenges")) == NULL) { + kore_log(LOG_NOTICE, + "[%s:auth] payload has no 'challenges' array", + order->domain); + goto cleanup; + } + + if (TAILQ_EMPTY(&array->data.items)) { + kore_log(LOG_NOTICE, + "[%s:auth] no challenges URLs in challenge array", + order->domain); + goto cleanup; + } + + if ((stval = acme_status_type(status->data.string)) == -1) { + kore_log(LOG_NOTICE, "[%s] auth has invalid status", + order->domain); + goto cleanup; + } + + auth->status = stval; + + TAILQ_FOREACH(object, &array->data.items, list) { + if (object->type != KORE_JSON_TYPE_OBJECT) + continue; + + if ((type = kore_json_find_string(object, "type")) == NULL) { + kore_log(LOG_NOTICE, + "[%s:auth:challenge] no type", order->domain); + continue; + } + + /* + * We only support tls-alpn-01 for now, we ignore the rest. + */ + if (strcmp(type->data.string, "tls-alpn-01")) + continue; + + url = kore_json_find_string(object, "url"); + token = kore_json_find_string(object, "token"); + status = kore_json_find_string(object, "status"); + + if (url == NULL || token == NULL || status == NULL) { + kore_log(LOG_NOTICE, + "[%s:auth:challenge] missing members", + order->domain); + continue; + } + + if (strlen(token->data.string) > ACME_CHALLENGE_TOKEN_MAXLEN) { + kore_log(LOG_NOTICE, + "[%s:auth:challenge] invalid token length", + order->domain); + continue; + } + + for (p = token->data.string; *p != '\0'; p++) { + if ((*p >= 'a' && *p <= 'z') || + (*p >= 'A' && *p <= 'Z') || + (*p >= '0' && *p <= '9') || *p == '_' || *p == '-') + continue; + break; + } + + if (*p != '\0') { + kore_log(LOG_NOTICE, + "[%s:auth:challenge] invalid token", + order->domain); + continue; + } + + if ((stval = acme_status_type(status->data.string)) == -1) { + kore_log(LOG_NOTICE, + "[%s:auth:challenge] invalid challenge status", + order->domain); + continue; + } + + if (auth->challenge == NULL) { + challenge = kore_calloc(1, sizeof(*challenge)); + + challenge->url = kore_strdup(url->data.string); + challenge->process = acme_challenge_tls_alpn_01; + challenge->token = kore_strdup(token->data.string); + challenge->type = kore_strdup(type->data.string); + + auth->challenge = challenge; + } else { + challenge = auth->challenge; + } + + challenge->status = stval; + + if (challenge->status == ACME_STATUS_INVALID && + (err = kore_json_find_object(object, "error")) != NULL) { + type = kore_json_find_string(err, "type"); + detail = kore_json_find_string(err, "detail"); + + if (type == NULL || detail == NULL) { + kore_log(LOG_NOTICE, + "[%s:auth:challenge] error missing fields", + order->domain); + } else { + kore_free(challenge->error_type); + kore_free(challenge->error_detail); + + challenge->error_type = + kore_strdup(type->data.string); + challenge->error_detail = + kore_strdup(detail->data.string); + } + } + + break; + } + + if (auth->challenge == NULL) { + kore_log(LOG_NOTICE, + "[%s:auth] no supported challenges found", order->domain); + goto cleanup; + } + + ret = KORE_RESULT_OK; + +cleanup: + kore_json_cleanup(&json); + acme_request_cleanup(&req); + + return (ret); +} + +static int +acme_challenge_tls_alpn_01(struct acme_order *order, + struct acme_challenge *challenge) +{ + int ret; + + ret = KORE_RESULT_RETRY; + + switch (challenge->status) { + case ACME_STATUS_PENDING: + acme_challenge_tls_alpn_01_create(order, challenge); + break; + case ACME_STATUS_PROCESSING: + kore_log(LOG_NOTICE, + "[%s:auth:challenge:tls-alpn-01] processing", + order->domain); + break; + case ACME_STATUS_VALID: + kore_log(LOG_NOTICE, + "[%s:auth:challenge:tls-alpn-01] valid", + order->domain); + ret = KORE_RESULT_OK; + break; + default: + kore_log(LOG_NOTICE, + "[%s:auth:challenge:tls-alpn-01] invalid (%d)", + order->domain, challenge->status); + ret = KORE_RESULT_ERROR; + break; + } + + return (ret); +} + +static void +acme_challenge_tls_alpn_01_create(struct acme_order *order, + struct acme_challenge *challenge) +{ + struct kore_buf auth; + char *thumb; + u_int8_t digest[SHA256_DIGEST_LENGTH]; + + if (challenge->flags & ACME_FLAG_CHALLENGE_CREATED) { + kore_log(LOG_NOTICE, + "[%s:auth:challenge:tls-alpn-01] pending keymgr", + order->domain); + return; + } + + challenge->flags |= ACME_FLAG_CHALLENGE_CREATED; + + kore_log(LOG_NOTICE, + "[%s:auth:challenge:tls-alpn-01] requested from keymgr", + order->domain); + + thumb = acme_thumbprint_component(); + + kore_buf_init(&auth, 128); + kore_buf_appendf(&auth, "%s.%s", challenge->token, thumb); + (void)SHA256(auth.data, auth.offset, digest); + + kore_buf_cleanup(&auth); + kore_free(thumb); + + acme_keymgr_key_req(order->domain, digest, sizeof(digest), + KORE_ACME_CHALLENGE_CERT); + + /* XXX - this maybe too fast, keymgr may not have had time. */ + acme_challenge_respond(order, challenge->url, "tls-alpn-01"); +} + +static void +acme_challenge_respond(struct acme_order *order, const char *url, + const char *name) +{ + struct kore_json_item *json; + char *copy; + + kore_log(LOG_INFO, "[%s:auth:challenge:%s] submitting challenge", + order->domain, name); + + copy = kore_strdup(url); + + json = kore_json_create_object(NULL, NULL); + acme_sign_submit(json, url, copy, acme_generic_submit); + kore_json_item_free(json); +} + +static void +acme_generic_submit(struct acme_sign_op *op, struct kore_buf *payload) +{ + struct acme_request req; + + acme_request_prepare(&req, HTTP_METHOD_POST, op->udata, + payload->data, payload->offset); + + if (!acme_request_run(&req)) + goto cleanup; + + if (req.curl.http.status != HTTP_STATUS_OK) { + kore_log(LOG_NOTICE, + "request to '%s' failed: status %ld - body '%s'", + req.curl.url, req.curl.http.status, + kore_curl_response_as_string(&req.curl)); + goto cleanup; + } + + kore_log(LOG_INFO, "submitted %zu bytes to %s", + payload->offset, req.curl.url); + +cleanup: + acme_request_cleanup(&req); +} + +static void +acme_request_prepare(struct acme_request *req, int method, + const char *url, const void *data, size_t len) +{ + memset(req, 0, sizeof(*req)); + + if (!kore_curl_init(&req->curl, url, KORE_CURL_SYNC)) + fatal("failed to initialize request to '%s'", url); + + /* Override default timeout. */ + curl_easy_setopt(req->curl.handle, + CURLOPT_TIMEOUT, acme_request_timeout); + + kore_curl_http_setup(&req->curl, method, data, len); + kore_curl_http_set_header(&req->curl, "content-type", + "application/jose+json"); +} + +static void +acme_request_json(struct kore_buf *buf, const char *payload, + const char *protected, const char *sig) +{ + struct kore_json_item *json; + + json = kore_json_create_object(NULL, NULL); + + kore_json_create_string(json, "signature", sig); + kore_json_create_string(json, "payload", payload); + kore_json_create_string(json, "protected", protected); + + kore_json_item_tobuf(json, buf); + kore_json_item_free(json); +} + +static int +acme_request_run(struct acme_request *req) +{ + size_t len; + struct kore_json json; + const u_int8_t *body; + struct kore_json_item *detail; + + kore_curl_run(&req->curl); + + if (!kore_curl_success(&req->curl)) { + kore_log(LOG_NOTICE, "request to '%s' failed: %s", + req->curl.url, kore_curl_strerror(&req->curl)); + return (KORE_RESULT_ERROR); + } + + if (req->curl.http.status == HTTP_STATUS_BAD_REQUEST) { + kore_curl_response_as_bytes(&req->curl, &body, &len); + kore_json_init(&json, body, len); + + if (!kore_json_parse(&json)) { + detail = NULL; + } else { + detail = kore_json_find_string(json.root, "detail"); + } + + if (detail != NULL) { + kore_log(LOG_NOTICE, + "request to '%s' failed with 400 - detail: %s", + req->curl.url, detail->data.string); + } else { + kore_log(LOG_NOTICE, + "request to '%s' failed with 400 - body: %.*s", + req->curl.url, (int)len, (const char *)body); + } + + kore_json_cleanup(&json); + return (KORE_RESULT_ERROR); + } + + return (KORE_RESULT_OK); +} + +static void +acme_request_cleanup(struct acme_request *req) +{ + kore_curl_cleanup(&req->curl); +} + +static void +acme_directory_set(struct kore_json *json, const char *name, char **out) +{ + struct kore_json_item *item; + + if ((item = kore_json_find_string(json->root, name)) == NULL) { + kore_log(LOG_NOTICE, "directory has missing '%s' URI", name); + return; + } + + *out = kore_strdup(item->data.string); +} + +static void +acme_sign_submit(struct kore_json_item *json, const char *url, void *udata, + void (*cb)(struct acme_sign_op *, struct kore_buf *)) +{ + struct acme_sign_op *op; + struct kore_buf buf; + char *nonce; + + if ((nonce = acme_nonce_fetch()) == NULL) { + kore_log(LOG_ERR, "failed to fetch nonce from servers"); + return; + } + + kore_buf_init(&buf, 1024); + kore_json_item_tobuf(json, &buf); + + op = kore_calloc(1, sizeof(*op)); + LIST_INSERT_HEAD(&signops, op, list); + + op->cb = cb; + op->udata = udata; + op->nonce = nonce; + op->id = signop_id++; + op->payload = acme_base64url(buf.data, buf.offset); + op->protected = acme_protected_component(op->nonce, url); + op->t = kore_timer_add(acme_sign_expire, 5000, op, KORE_TIMER_ONESHOT); + + kore_buf_reset(&buf); + kore_buf_append(&buf, &op->id, sizeof(op->id)); + kore_buf_appendf(&buf, "%s.%s", op->protected, op->payload); + + kore_msg_send(KORE_WORKER_KEYMGR, KORE_ACME_SIGN, buf.data, buf.offset); + kore_buf_cleanup(&buf); +} + +static void +acme_sign_expire(void *udata, u_int64_t now) +{ + struct acme_sign_op *op = udata; + + kore_log(LOG_NOTICE, "signop %u expired (no answer)", op->id); + + LIST_REMOVE(op, list); + + kore_free(op->protected); + kore_free(op->payload); + kore_free(op->udata); + kore_free(op->nonce); + kore_free(op); +} + +static void +acme_sign_result(struct kore_msg *msg, const void *data) +{ + u_int32_t id; + struct kore_buf buf; + struct acme_sign_op *op; + char *sig; + const u_int8_t *ptr; + + if (msg->length < sizeof(id)) + fatal("%s: invalid length (%zu)", __func__, msg->length); + + ptr = data; + memcpy(&id, ptr, sizeof(id)); + + ptr += sizeof(id); + msg->length -= sizeof(id); + + LIST_FOREACH(op, &signops, list) { + if (op->id == id) + break; + } + + if (op == NULL) { + kore_log(LOG_NOTICE, + "received KORE_ACME_SIGN_RESULT for unknown op: %u", id); + return; + } + + kore_timer_remove(op->t); + LIST_REMOVE(op, list); + + sig = kore_malloc(msg->length + 1); + memcpy(sig, ptr, msg->length); + sig[msg->length] = '\0'; + + kore_buf_init(&buf, 1024); + acme_request_json(&buf, op->payload, op->protected, sig); + + op->cb(op, &buf); + + kore_free(op->protected); + kore_free(op->payload); + kore_free(op->udata); + kore_free(op->nonce); + kore_free(op); + + kore_free(sig); + kore_buf_cleanup(&buf); +} + +static char * +acme_protected_component(const char *nonce, const char *url) +{ + char *b64; + struct kore_buf payload; + struct kore_json_item *root, *jwk; + + root = kore_json_create_object(NULL, NULL); + + kore_json_create_string(root, "url", url); + kore_json_create_string(root, "alg", "RS256"); + kore_json_create_string(root, "nonce", nonce); + + if (account_id == NULL) { + jwk = kore_json_create_object(root, "jwk"); + kore_json_create_string(jwk, "kty", "RSA"); + kore_json_create_string(jwk, "e", rsakey_e); + kore_json_create_string(jwk, "n", rsakey_n); + } else { + kore_json_create_string(root, "kid", account_id); + } + + kore_buf_init(&payload, 128); + kore_json_item_tobuf(root, &payload); + + b64 = acme_base64url(payload.data, payload.offset); + + kore_json_item_free(root); + kore_buf_cleanup(&payload); + + return (b64); +} + +static char * +acme_thumbprint_component(void) +{ + struct kore_json_item *json; + struct kore_buf payload; + u_int8_t digest[SHA256_DIGEST_LENGTH]; + + json = kore_json_create_object(NULL, NULL); + + /* Order matters here, see RFC7638. */ + kore_json_create_string(json, "e", rsakey_e); + kore_json_create_string(json, "kty", "RSA"); + kore_json_create_string(json, "n", rsakey_n); + + kore_buf_init(&payload, 128); + kore_json_item_tobuf(json, &payload); + + (void)SHA256(payload.data, payload.offset, digest); + + kore_json_item_free(json); + kore_buf_cleanup(&payload); + + return (acme_base64url(digest, sizeof(digest))); +} + +static char * +acme_base64url(const void *data, size_t len) +{ + char *b64; + + if (!kore_base64url_encode(data, len, &b64, KORE_BASE64_RAW)) + fatal("%s: failed to encode base64url data of %zu bytes", len); + + return (b64); +} + +static void +acme_keymgr_key_req(const char *domain, const void *data, size_t len, int msg) +{ + struct kore_keyreq req; + struct kore_buf buf; + + memset(&req, 0, sizeof(req)); + req.data_len = len; + + if (kore_strlcpy(req.domain, domain, sizeof(req.domain)) >= + sizeof(req.domain)) + fatal("%s: domain truncated", __func__); + + kore_buf_init(&buf, sizeof(req) + len); + kore_buf_append(&buf, &req, sizeof(req)); + + if (data != NULL) + kore_buf_append(&buf, data, len); + + kore_msg_send(KORE_WORKER_KEYMGR, msg, buf.data, buf.offset); + kore_buf_cleanup(&buf); +} + +static int +acme_status_type(const char *status) +{ + int type; + + if (!strcmp(status, "pending")) { + type = ACME_STATUS_PENDING; + } else if (!strcmp(status, "processing")) { + type = ACME_STATUS_PROCESSING; + } else if (!strcmp(status, "valid")) { + type = ACME_STATUS_VALID; + } else if (!strcmp(status, "invalid")) { + type = ACME_STATUS_INVALID; + } else if (!strcmp(status, "ready")) { + type = ACME_STATUS_READY; + } else if (!strcmp(status, "expired")) { + type = ACME_STATUS_EXPIRED; + } else if (!strcmp(status, "revoked")) { + type = ACME_STATUS_REVOKED; + } else { + type = -1; + } + + return (type); +} + +static void +acme_rsakey_exp(struct kore_msg *msg, const void *data) +{ + kore_free(rsakey_e); + rsakey_e = kore_calloc(1, msg->length + 1); + memcpy(rsakey_e, data, msg->length); + rsakey_e[msg->length] = '\0'; +} + +static void +acme_rsakey_mod(struct kore_msg *msg, const void *data) +{ + kore_free(rsakey_n); + rsakey_n = kore_calloc(1, msg->length + 1); + memcpy(rsakey_n, data, msg->length); + rsakey_n[msg->length] = '\0'; +} diff --git a/src/bsd.c b/src/bsd.c index 012b8c9..b897286 100644 --- a/src/bsd.c +++ b/src/bsd.c @@ -236,7 +236,7 @@ kore_platform_disable_write(int fd) } void -kore_platform_proctitle(char *title) +kore_platform_proctitle(const char *title) { #ifdef __MACH__ kore_proctitle(title); @@ -299,6 +299,9 @@ kore_platform_sandbox(void) void kore_platform_pledge(void) { + if (worker->id == KORE_WORKER_KEYMGR || worker->id == KORE_WORKER_ACME) + return; + if (pledge(pledges, NULL) == -1) fatal("failed to pledge process"); } diff --git a/src/config.c b/src/config.c index 00cfefa..e18c4a7 100644 --- a/src/config.c +++ b/src/config.c @@ -45,13 +45,18 @@ #include "curl.h" #endif +#if defined(KORE_USE_ACME) +#include "acme.h" +#endif + #if defined(__linux__) #include "seccomp.h" #endif /* XXX - This is becoming a clusterfuck. Fix it. */ -static int configure_load(char *); +static int configure_load(char *); +static void configure_check_var(char **, const char *, const char *); #if defined(KORE_SINGLE_BINARY) static FILE *config_file_write(void); @@ -61,6 +66,13 @@ extern u_int32_t asset_len_builtin_kore_conf; static int configure_file(char *); #endif +#if defined(KORE_USE_ACME) +static int configure_acme(char *); +static int configure_acme_root(char *); +static int configure_acme_runas(char *); +static int configure_acme_provider(char *); +#endif + static int configure_tls(char *); static int configure_server(char *); static int configure_include(char *); @@ -155,15 +167,18 @@ static struct { int (*configure)(char *); } config_directives[] = { { "tls", configure_tls }, - { "server", configure_server }, - { "include", configure_include }, +#if defined(KORE_USE_ACME) + { "acme", configure_acme }, +#endif { "bind", configure_bind }, - { "unix", configure_bind_unix }, { "load", configure_load }, { "domain", configure_domain }, + { "server", configure_server }, { "attach", configure_attach }, - { "certfile", configure_certfile }, { "certkey", configure_certkey }, + { "certfile", configure_certfile }, + { "include", configure_include }, + { "unix", configure_bind_unix }, { "client_verify", configure_client_verify }, { "client_verify_depth", configure_client_verify_depth }, #if defined(KORE_USE_PYTHON) @@ -209,6 +224,11 @@ static struct { { "rand_file", configure_rand_file }, { "keymgr_runas", configure_keymgr_runas }, { "keymgr_root", configure_keymgr_root }, +#if defined(KORE_USE_ACME) + { "acme_runas", configure_acme_runas }, + { "acme_root", configure_acme_root }, + { "acme_provider", configure_acme_provider }, +#endif #if defined(KORE_USE_PLATFORM_PLEDGE) { "pledge", configure_add_pledge }, #endif @@ -326,22 +346,21 @@ kore_parse_config(void) if (!kore_quiet) kore_log(LOG_WARNING, "privsep: will not change user"); } else { - if (keymgr_runas_user == NULL) { - if (!kore_quiet) { - kore_log(LOG_NOTICE, "privsep: no keymgr_runas " - "set, using 'runas` user"); - } - keymgr_runas_user = kore_strdup(kore_runas_user); - } + configure_check_var(&keymgr_runas_user, kore_runas_user, + "privsep: no keymgr_runas set, using 'runas` user"); +#if defined(KORE_USE_ACME) + configure_check_var(&acme_runas_user, kore_runas_user, + "privsep: no acme_runas set, using 'runas` user"); +#endif } - if (keymgr_root_path == NULL) { - if (!kore_quiet) { - kore_log(LOG_NOTICE, "privsep: no keymgr_root set, " - "using 'root` directory"); - } - keymgr_root_path = kore_strdup(kore_root_path); - } + configure_check_var(&keymgr_root_path, kore_root_path, + "privsep: no keymgr_root set, using 'root` directory"); + +#if defined(KORE_USE_ACME) + configure_check_var(&acme_root_path, kore_root_path, + "privsep: no acme_root set, using 'root` directory"); +#endif if (skip_chroot && !kore_quiet) kore_log(LOG_WARNING, "privsep: will not chroot"); @@ -396,11 +415,19 @@ kore_parse_config_file(FILE *fp) current_domain->domain); } - if (current_domain->server->tls == 1 && - (current_domain->certfile == NULL || - current_domain->certkey == NULL)) { - fatal("incomplete TLS setup for '%s'", - current_domain->domain); + if (current_domain->server->tls == 1) { +#if defined(KORE_USE_ACME) + if (current_domain->acme) { + lineno++; + current_domain = NULL; + continue; + } +#endif + if (current_domain->certfile == NULL || + current_domain->certkey == NULL) { + fatal("incomplete TLS setup for '%s'", + current_domain->domain); + } } current_domain = NULL; @@ -478,6 +505,16 @@ kore_configure_setting(const char *name, char *value) } #endif +static void +configure_check_var(char **var, const char *other, const char *logmsg) +{ + if (*var == NULL) { + if (!kore_quiet) + kore_log(LOG_NOTICE, "%s", logmsg); + *var = kore_strdup(other); + } +} + static int configure_include(char *path) { @@ -545,6 +582,82 @@ configure_tls(char *yesno) return (KORE_RESULT_OK); } +#if defined(KORE_USE_ACME) +static int +configure_acme(char *yesno) +{ + int len; + char path[MAXPATHLEN]; + + if (current_domain == NULL) { + printf("acme directive not inside a domain context\n"); + return (KORE_RESULT_ERROR); + } + + if (strchr(current_domain->domain, '*')) { + printf("wildcards not supported due to lack of dns-01\n"); + return (KORE_RESULT_ERROR); + } + + if (!strcmp(yesno, "no")) { + current_domain->acme = 0; + } else if (!strcmp(yesno, "yes")) { + current_domain->acme = 1; + + /* Override keyfile and certfile locations. */ + kore_free(current_domain->certkey); + kore_free(current_domain->certfile); + + len = snprintf(path, sizeof(path), "%s/%s/fullchain.pem", + KORE_ACME_CERTDIR, current_domain->domain); + if (len == -1 || (size_t)len >= sizeof(path)) + fatal("failed to create certfile path"); + + current_domain->certfile = kore_strdup(path); + + len = snprintf(path, sizeof(path), "%s/%s/key.pem", + KORE_ACME_CERTDIR, current_domain->domain); + if (len == -1 || (size_t)len >= sizeof(path)) + fatal("failed to create certkey path"); + + current_domain->certkey = kore_strdup(path); + } else { + printf("invalid '%s' for yes|no acme option\n", yesno); + return (KORE_RESULT_ERROR); + } + + return (KORE_RESULT_OK); +} + +static int +configure_acme_runas(char *user) +{ + kore_free(acme_runas_user); + acme_runas_user = kore_strdup(user); + + return (KORE_RESULT_OK); +} + +static int +configure_acme_root(char *root) +{ + kore_free(acme_root_path); + acme_root_path = kore_strdup(root); + + return (KORE_RESULT_OK); +} + +static int +configure_acme_provider(char *provider) +{ + kore_free(acme_provider); + acme_provider = kore_strdup(provider); + + return (KORE_RESULT_OK); +} + +#endif + static int configure_bind(char *options) { @@ -767,12 +880,7 @@ configure_certfile(char *path) return (KORE_RESULT_ERROR); } - if (current_domain->certfile != NULL) { - printf("certfile specified twice for %s\n", - current_domain->domain); - return (KORE_RESULT_ERROR); - } - + kore_free(current_domain->certfile); current_domain->certfile = kore_strdup(path); return (KORE_RESULT_OK); } @@ -785,12 +893,7 @@ configure_certkey(char *path) return (KORE_RESULT_ERROR); } - if (current_domain->certkey != NULL) { - printf("certkey specified twice for %s\n", - current_domain->domain); - return (KORE_RESULT_ERROR); - } - + kore_free(current_domain->certkey); current_domain->certkey = kore_strdup(path); return (KORE_RESULT_OK); } diff --git a/src/connection.c b/src/connection.c index d0cad2f..b0ed7f0 100644 --- a/src/connection.c +++ b/src/connection.c @@ -274,8 +274,12 @@ kore_connection_handle(struct connection *c) SSL_set_fd(c->ssl, c->fd); SSL_set_accept_state(c->ssl); - SSL_set_app_data(c->ssl, c); - SSL_set_ex_data(c->ssl, 0, c); + + if (!SSL_set_ex_data(c->ssl, 0, c)) { + kore_debug("SSL_set_ex_data(): %s", + ssl_errno_s); + return (KORE_RESULT_ERROR); + } } ERR_clear_error(); @@ -293,6 +297,14 @@ kore_connection_handle(struct connection *c) } } +#if defined(KORE_USE_ACME) + if (c->flags & CONN_ACME_CHALLENGE) { + kore_log(LOG_INFO, "disconnecting acme client"); + kore_connection_disconnect(c); + return (KORE_RESULT_OK); + } +#endif + if (SSL_get_verify_mode(c->ssl) & SSL_VERIFY_PEER) { c->cert = SSL_get_peer_certificate(c->ssl); if (c->cert == NULL) { diff --git a/src/curl.c b/src/curl.c index 9412cb8..3762bd1 100644 --- a/src/curl.c +++ b/src/curl.c @@ -42,10 +42,13 @@ static struct sock_filter filter_curl[] = { KORE_SYSCALL_ALLOW(set_robust_list), /* Other */ + KORE_SYSCALL_ALLOW(uname), KORE_SYSCALL_ALLOW(ioctl), KORE_SYSCALL_ALLOW(madvise), KORE_SYSCALL_ALLOW(recvmsg), KORE_SYSCALL_ALLOW(sendmmsg), + KORE_SYSCALL_ALLOW(faccessat), + KORE_SYSCALL_ALLOW(newfstatat), KORE_SYSCALL_ALLOW(getpeername), }; #endif diff --git a/src/domain.c b/src/domain.c index d228698..0f3944c 100644 --- a/src/domain.c +++ b/src/domain.c @@ -38,6 +38,10 @@ #include "http.h" #endif +#if defined(KORE_USE_ACME) +#include "acme.h" +#endif + #define KORE_DOMAIN_CACHE 16 #define SSL_SESSION_ID "kore_ssl_sessionid" @@ -256,8 +260,10 @@ kore_domain_free(struct kore_domain *dom) } void -kore_domain_tlsinit(struct kore_domain *dom, const void *pem, size_t pemlen) +kore_domain_tlsinit(struct kore_domain *dom, int type, + const void *data, size_t datalen) { + const u_int8_t *ptr; RSA *rsa; X509 *x509; EVP_PKEY *pkey; @@ -329,7 +335,30 @@ kore_domain_tlsinit(struct kore_domain *dom, const void *pem, size_t pemlen) } #endif - x509 = domain_load_certificate_chain(dom->ssl_ctx, pem, pemlen); + switch (type) { + case KORE_PEM_CERT_CHAIN: + x509 = domain_load_certificate_chain(dom->ssl_ctx, + data, datalen); + break; + case KORE_DER_CERT_DATA: + ptr = data; + if ((x509 = d2i_X509(NULL, &ptr, datalen)) == NULL) + fatalx("d2i_X509: %s", ssl_errno_s); + if (SSL_CTX_use_certificate(dom->ssl_ctx, x509) == 0) + fatalx("SSL_CTX_use_certificate: %s", ssl_errno_s); + break; + default: + fatalx("%s: unknown type %d", __func__, type); + } + + if (x509 == NULL) { + kore_log(LOG_NOTICE, "failed to load certificate for '%s': %s", + dom->domain, ssl_errno_s); + SSL_CTX_free(dom->ssl_ctx); + dom->ssl_ctx = NULL; + return; + } + if ((pkey = X509_get_pubkey(x509)) == NULL) fatalx("certificate has no public key"); @@ -362,8 +391,10 @@ kore_domain_tlsinit(struct kore_domain *dom, const void *pem, size_t pemlen) if (!SSL_CTX_use_PrivateKey(dom->ssl_ctx, pkey)) fatalx("SSL_CTX_use_PrivateKey(): %s", ssl_errno_s); - if (!SSL_CTX_check_private_key(dom->ssl_ctx)) - fatalx("Public/Private key for %s do not match", dom->domain); + if (!SSL_CTX_check_private_key(dom->ssl_ctx)) { + fatalx("Public/Private key for %s do not match (%s)", + dom->domain, ssl_errno_s); + } if (tls_dhparam == NULL) fatalx("No DH parameters given"); @@ -415,6 +446,10 @@ kore_domain_tlsinit(struct kore_domain *dom, const void *pem, size_t pemlen) SSL_CTX_set_info_callback(dom->ssl_ctx, kore_tls_info_callback); SSL_CTX_set_tlsext_servername_callback(dom->ssl_ctx, kore_tls_sni_cb); +#if defined(KORE_USE_ACME) + SSL_CTX_set_alpn_select_cb(dom->ssl_ctx, kore_acme_tls_alpn, dom); +#endif + X509_free(x509); } @@ -599,18 +634,18 @@ keymgr_rsa_privenc(int flen, const unsigned char *from, unsigned char *to, if ((dom = RSA_get_app_data(rsa)) == NULL) fatal("RSA key has no domain attached"); - if (strlen(dom->domain) >= KORE_DOMAINNAME_LEN - 1) - fatal("domain name too long"); memset(keymgr_buf, 0, sizeof(keymgr_buf)); req = (struct kore_keyreq *)keymgr_buf; + + if (kore_strlcpy(req->domain, dom->domain, sizeof(req->domain)) >= + sizeof(req->domain)) + fatal("%s: domain truncated", __func__); + req->data_len = flen; req->padding = padding; - req->domain_len = strlen(dom->domain); - memcpy(&req->data[0], from, req->data_len); - memcpy(req->domain, dom->domain, req->domain_len); kore_msg_send(KORE_WORKER_KEYMGR, KORE_MSG_KEYMGR_REQ, keymgr_buf, len); keymgr_await_data(); @@ -663,13 +698,14 @@ keymgr_ecdsa_sign(const unsigned char *dgst, int dgst_len, #endif memset(keymgr_buf, 0, sizeof(keymgr_buf)); - req = (struct kore_keyreq *)keymgr_buf; - req->data_len = dgst_len; - req->domain_len = strlen(dom->domain); + if (kore_strlcpy(req->domain, dom->domain, sizeof(req->domain)) >= + sizeof(req->domain)) + fatal("%s: domain truncated", __func__); + + req->data_len = dgst_len; memcpy(&req->data[0], dgst, req->data_len); - memcpy(req->domain, dom->domain, req->domain_len); kore_msg_send(KORE_WORKER_KEYMGR, KORE_MSG_KEYMGR_REQ, keymgr_buf, len); keymgr_await_data(); @@ -835,11 +871,11 @@ domain_load_certificate_chain(SSL_CTX *ctx, const void *data, size_t len) in = BIO_new_mem_buf(data, len); if ((x = PEM_read_bio_X509_AUX(in, NULL, NULL, NULL)) == NULL) - fatal("PEM_read_bio_X509_AUX: %s", ssl_errno_s); + return (NULL); /* refcount for x509 will go up one. */ if (SSL_CTX_use_certificate(ctx, x) == 0) - fatal("SSL_CTX_use_certificate: %s", ssl_errno_s); + return (NULL); #if defined(KORE_OPENSSL_NEWER_API) SSL_CTX_clear_chain_certs(ctx); @@ -853,10 +889,10 @@ domain_load_certificate_chain(SSL_CTX *ctx, const void *data, size_t len) /* ca its reference count won't be increased. */ #if defined(KORE_OPENSSL_NEWER_API) if (SSL_CTX_add0_chain_cert(ctx, ca) == 0) - fatal("SSL_CTX_add0_chain_cert: %s", ssl_errno_s); + return (NULL); #else if (SSL_CTX_add_extra_chain_cert(ctx, ca) == 0) - fatal("SSL_CTX_add_extra_chain_cert: %s", ssl_errno_s); + return (NULL); #endif } @@ -864,7 +900,7 @@ domain_load_certificate_chain(SSL_CTX *ctx, const void *data, size_t len) if (ERR_GET_LIB(err) != ERR_LIB_PEM || ERR_GET_REASON(err) != PEM_R_NO_START_LINE) - fatal("PEM_read_bio_X509: %s", ssl_errno_s); + return (NULL); BIO_free(in); diff --git a/src/keymgr.c b/src/keymgr.c index fa83bab..35b30b3 100644 --- a/src/keymgr.c +++ b/src/keymgr.c @@ -29,14 +29,25 @@ * for a configured domain when it receives a SIGUSR1. It it reloads them * it will send the newly loaded certificate chains to the worker processes * which will update their TLS contexts accordingly. + * + * If ACME is turned on the keymgr will also hold all account and domain + * keys and will initiate the process of acquiring new certificates against + * the ACME provider that is configured if those certificates do not exist + * or are expired (or are expiring soon). */ #include +#include #include #include +#include #include +#include +#include +#include +#include #include #include #include @@ -46,6 +57,10 @@ #include "kore.h" +#if defined(KORE_USE_ACME) +#include "acme.h" +#endif + #define RAND_TMP_FILE "rnd.tmp" #define RAND_POLL_INTERVAL (1800 * 1000) #define RAND_FILE_SIZE 1024 @@ -55,11 +70,15 @@ /* The syscalls our keymgr is allowed to perform, only. */ static struct sock_filter filter_keymgr[] = { + /* Deny these, but with EACCESS instead of dying. */ + KORE_SYSCALL_DENY(ioctl, EACCES), + /* Required to deal with private keys and certs. */ #if defined(SYS_open) KORE_SYSCALL_ALLOW(open), #endif KORE_SYSCALL_ALLOW(read), + KORE_SYSCALL_ALLOW(lseek), KORE_SYSCALL_ALLOW(write), KORE_SYSCALL_ALLOW(close), KORE_SYSCALL_ALLOW(fstat), @@ -100,6 +119,18 @@ static struct sock_filter filter_keymgr[] = { #if defined(__NR_getrandom) KORE_SYSCALL_ALLOW(getrandom), #endif + +#if defined(KORE_USE_ACME) +#if defined(SYS_mkdir) + KORE_SYSCALL_ALLOW(mkdir), +#endif + KORE_SYSCALL_ALLOW(mkdirat), + KORE_SYSCALL_ALLOW(umask), +#if defined(SYS_access) + KORE_SYSCALL_ALLOW(access), +#endif + KORE_SYSCALL_ALLOW(faccessat), +#endif }; #endif @@ -114,17 +145,77 @@ char *rand_file = NULL; static TAILQ_HEAD(, key) keys; static int initialized = 0; +#if defined(KORE_USE_ACME) + +#define ACME_ORDER_STATE_INIT 1 +#define ACME_ORDER_STATE_SUBMIT 2 + +#define ACME_X509_EXPIRATION 120 +#define ACME_TLS_ALPN_01_OID "1.3.6.1.5.5.7.1.31" + +#define ACME_RENEWAL_THRESHOLD 5 +#define ACME_RENEWAL_TIMER (3600 * 1000) + +/* UTCTIME in format of YYMMDDHHMMSSZ */ +#define ASN1_UTCTIME_LEN 13 + +/* GENERALIZEDTIME in format of YYYYMMDDHHMMSSZ */ +#define ASN1_GENERALIZEDTIME_LEN 15 + +/* Set to 1 when we receive KORE_ACME_PROC_READY. */ +static int acmeproc_ready = 0; + +/* Renewal timer for all domains under acme control. */ +static struct kore_timer *acme_renewal = NULL; + +struct acme_order { + int state; + struct kore_timer *timer; + char *domain; +}; + +static char *keymgr_bignum_base64(const BIGNUM *); + +static void keymgr_acme_init(void); +static void keymgr_acme_renewal(void *, u_int64_t); +static void keymgr_acme_check(struct kore_domain *); +static void keymgr_acme_sign(struct kore_msg *, const void *); +static void keymgr_acme_ready(struct kore_msg *, const void *); +static void keymgr_acme_domainkey(struct kore_domain *, struct key *); + +static void keymgr_acme_order_create(const char *); +static void keymgr_acme_order_redo(void *, u_int64_t); +static void keymgr_acme_order_start(void *, u_int64_t); + +static void keymgr_x509_ext(STACK_OF(X509_EXTENSION) *, + int, const char *, ...); + +static void keymgr_acme_csr(const struct kore_keyreq *, struct key *); +static void keymgr_acme_install_cert(const void *, size_t, struct key *); +static void keymgr_acme_order_failed(const void *, size_t, struct key *); +static void keymgr_acme_challenge_cert(const void *, size_t, struct key *); + +static int keymgr_x509_not_after(X509 *, time_t *); +static int keymgr_asn1_convert_utctime(const ASN1_TIME *, time_t *); +static int keymgr_asn1_convert_generalizedtime(const void *, + size_t, time_t *); + +#endif /* KORE_USE_ACME */ + static void keymgr_reload(void); static void keymgr_load_randfile(void); static void keymgr_save_randfile(void); -static void keymgr_load_privatekey(struct kore_domain *); +static struct key *keymgr_load_privatekey(const char *); +static void keymgr_load_domain_privatekey(struct kore_domain *); + static void keymgr_msg_recv(struct kore_msg *, const void *); static void keymgr_entropy_request(struct kore_msg *, const void *); static void keymgr_certificate_request(struct kore_msg *, const void *); static void keymgr_submit_certificates(struct kore_domain *, u_int16_t); static void keymgr_submit_file(u_int8_t, struct kore_domain *, const char *, u_int16_t, int); +static void keymgr_x509_msg(const char *, const void *, size_t, int, int); static void keymgr_rsa_encrypt(struct kore_msg *, const void *, struct key *); @@ -139,10 +230,10 @@ void kore_keymgr_run(void) { int quit; - u_int64_t now, last_seed; + u_int64_t now, netwait, last_seed; if (keymgr_active == 0) - fatal("%s: called with keymgr_active == 0", __func__); + fatalx("%s: called with keymgr_active == 0", __func__); quit = 0; @@ -150,8 +241,10 @@ kore_keymgr_run(void) kore_module_cleanup(); net_init(); + kore_timer_init(); kore_connection_init(); kore_platform_event_init(); + kore_msg_worker_init(); kore_msg_register(KORE_MSG_KEYMGR_REQ, keymgr_msg_recv); kore_msg_register(KORE_MSG_ENTROPY_REQ, keymgr_entropy_request); @@ -168,6 +261,9 @@ kore_keymgr_run(void) #endif kore_worker_privdrop(keymgr_runas_user, keymgr_root_path); + if (!kore_quiet) + kore_log(LOG_NOTICE, "key manager started (pid#%d)", getpid()); + if (rand_file != NULL) { keymgr_load_randfile(); keymgr_save_randfile(); @@ -175,20 +271,17 @@ kore_keymgr_run(void) kore_log(LOG_WARNING, "no rand_file location specified"); } - initialized = 1; - - keymgr_reload(); RAND_poll(); last_seed = 0; + initialized = 1; + keymgr_reload(); + #if defined(__OpenBSD__) if (pledge("stdio rpath", NULL) == -1) - fatal("failed to pledge keymgr process"); + fatalx("failed to pledge keymgr process"); #endif - if (!kore_quiet) - kore_log(LOG_NOTICE, "key manager started"); - while (quit != 1) { now = kore_time_ms(); if ((now - last_seed) > RAND_POLL_INTERVAL) { @@ -196,6 +289,9 @@ kore_keymgr_run(void) last_seed = now; } + netwait = kore_timer_next_run(now); + kore_platform_event_wait(netwait); + if (sig_recv != 0) { switch (sig_recv) { case SIGQUIT: @@ -212,7 +308,11 @@ kore_keymgr_run(void) sig_recv = 0; } - kore_platform_event_wait(1000); + if (quit) + break; + + now = kore_time_ms(); + kore_timer_run(now); kore_connection_prune(KORE_CONNECTION_PRUNE_DISCONNECT); } @@ -254,7 +354,11 @@ keymgr_reload(void) kore_keymgr_cleanup(0); TAILQ_INIT(&keys); - kore_domain_callback(keymgr_load_privatekey); +#if defined(KORE_USE_ACME) + keymgr_acme_init(); +#endif + + kore_domain_callback(keymgr_load_domain_privatekey); /* can't use kore_domain_callback() due to dst parameter. */ LIST_FOREACH(srv, &kore_servers, list) { @@ -268,6 +372,15 @@ keymgr_reload(void) static void keymgr_submit_certificates(struct kore_domain *dom, u_int16_t dst) { + if (access(dom->certfile, R_OK) == -1) { +#if defined(KORE_USE_ACME) + if (dom->acme && errno == ENOENT) + return; +#endif + fatalx("cannot read '%s' for %s: %s", + dom->certfile, dom->domain, errno_s); + } + keymgr_submit_file(KORE_MSG_CERTIFICATE, dom, dom->certfile, dst, 0); if (dom->crlfile != NULL) @@ -280,51 +393,27 @@ keymgr_submit_file(u_int8_t id, struct kore_domain *dom, { int fd; struct stat st; - ssize_t ret; - size_t len; - struct kore_x509_msg *msg; u_int8_t *payload; if ((fd = open(file, O_RDONLY)) == -1) { if (errno == ENOENT && can_fail) return; - fatal("open(%s): %s", file, errno_s); + fatalx("open(%s): %s", file, errno_s); } if (fstat(fd, &st) == -1) - fatal("stat(%s): %s", file, errno_s); + fatalx("stat(%s): %s", file, errno_s); if (!S_ISREG(st.st_mode)) - fatal("%s is not a file", file); + fatalx("%s is not a file", file); - if (st.st_size <= 0 || st.st_size > (1024 * 1024 * 10)) { - fatal("%s length is not valid (%jd)", file, - (intmax_t)st.st_size); - } + payload = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0); + if (payload == MAP_FAILED) + fatalx("mmap(): %s", errno_s); - len = sizeof(*msg) + st.st_size; - payload = kore_calloc(1, len); + keymgr_x509_msg(dom->domain, payload, st.st_size, dst, id); - msg = (struct kore_x509_msg *)payload; - msg->domain_len = strlen(dom->domain); - if (msg->domain_len > sizeof(msg->domain)) - fatal("domain name '%s' too long", dom->domain); - memcpy(msg->domain, dom->domain, msg->domain_len); - - msg->data_len = st.st_size; - ret = read(fd, &msg->data[0], msg->data_len); - if (ret == -1) - fatal("failed to read from %s: %s", file, errno_s); - if (ret == 0) - fatal("eof while reading %s", file); - - if ((size_t)ret != msg->data_len) { - fatal("bad read on %s: expected %zu, got %zd", - file, msg->data_len, ret); - } - - kore_msg_send(dst, id, payload, len); - kore_free(payload); + (void)munmap(payload, st.st_size); close(fd); } @@ -341,26 +430,26 @@ keymgr_load_randfile(void) return; if ((fd = open(rand_file, O_RDONLY)) == -1) - fatal("open(%s): %s", rand_file, errno_s); + fatalx("open(%s): %s", rand_file, errno_s); if (fstat(fd, &st) == -1) - fatal("stat(%s): %s", rand_file, errno_s); + fatalx("stat(%s): %s", rand_file, errno_s); if (!S_ISREG(st.st_mode)) - fatal("%s is not a file", rand_file); + fatalx("%s is not a file", rand_file); if (st.st_size != RAND_FILE_SIZE) - fatal("%s has an invalid size", rand_file); + fatalx("%s has an invalid size", rand_file); total = 0; while (total != RAND_FILE_SIZE) { ret = read(fd, buf, sizeof(buf)); if (ret == 0) - fatal("EOF on %s", rand_file); + fatalx("EOF on %s", rand_file); if (ret == -1) { if (errno == EINTR) continue; - fatal("read(%s): %s", rand_file, errno_s); + fatalx("read(%s): %s", rand_file, errno_s); } total += (size_t)ret; @@ -427,26 +516,44 @@ cleanup: } static void -keymgr_load_privatekey(struct kore_domain *dom) +keymgr_load_domain_privatekey(struct kore_domain *dom) { - FILE *fp; - struct key *key; + struct key *key; if (dom->server->tls == 0) return; - if ((fp = fopen(dom->certkey, "r")) == NULL) - fatal("failed to open private key: %s", dom->certkey); + key = keymgr_load_privatekey(dom->certkey); + + if (key->pkey == NULL) { +#if defined(KORE_USE_ACME) + if (dom->acme) + keymgr_acme_domainkey(dom, key); +#endif + if (key->pkey == NULL) { + fatalx("failed to load private key for '%s' (%s)", + dom->domain, errno_s); + } + } - key = kore_calloc(1, sizeof(*key)); key->dom = dom; - if ((key->pkey = PEM_read_PrivateKey(fp, NULL, NULL, NULL)) == NULL) - fatal("PEM_read_PrivateKey: %s", ssl_errno_s); + kore_log(LOG_INFO, "loaded private key for '%s'", dom->domain); +} - (void)fclose(fp); +static struct key * +keymgr_load_privatekey(const char *path) +{ + struct key *key; + key = kore_calloc(1, sizeof(*key)); TAILQ_INSERT_TAIL(&keys, key, list); + + /* Caller should check if pkey was loaded. */ + if (path) + key->pkey = kore_rsakey_load(path); + + return (key); } static void @@ -492,27 +599,48 @@ keymgr_msg_recv(struct kore_msg *msg, const void *data) if (msg->length != (sizeof(*req) + req->data_len)) return; - if (req->domain_len > KORE_DOMAINNAME_LEN) + + if (req->domain[KORE_DOMAINNAME_LEN] != '\0') return; key = NULL; TAILQ_FOREACH(key, &keys, list) { - if (!strncmp(key->dom->domain, req->domain, req->domain_len)) + if (key->dom == NULL) + continue; + if (!strcmp(key->dom->domain, req->domain)) break; } if (key == NULL) return; - switch (EVP_PKEY_id(key->pkey)) { - case EVP_PKEY_RSA: - keymgr_rsa_encrypt(msg, data, key); + switch (msg->id) { + case KORE_MSG_KEYMGR_REQ: + switch (EVP_PKEY_id(key->pkey)) { + case EVP_PKEY_RSA: + keymgr_rsa_encrypt(msg, data, key); + break; + case EVP_PKEY_EC: + keymgr_ecdsa_sign(msg, data, key); + break; + default: + break; + } break; - case EVP_PKEY_EC: - keymgr_ecdsa_sign(msg, data, key); +#if defined(KORE_USE_ACME) + case KORE_ACME_CSR_REQUEST: + keymgr_acme_csr(req, key); break; - default: + case KORE_ACME_ORDER_FAILED: + keymgr_acme_order_failed(req->data, req->data_len, key); break; + case KORE_ACME_CHALLENGE_CERT: + keymgr_acme_challenge_cert(req->data, req->data_len, key); + break; + case KORE_ACME_INSTALL_CERT: + keymgr_acme_install_cert(req->data, req->data_len, key); + break; +#endif } } @@ -572,3 +700,652 @@ keymgr_ecdsa_sign(struct kore_msg *msg, const void *data, struct key *key) kore_msg_send(msg->src, KORE_MSG_KEYMGR_RESP, sig, siglen); } + +static void +keymgr_x509_msg(const char *domain, const void *data, size_t len, + int target, int msg) +{ + struct kore_buf buf; + struct kore_x509_msg hdr; + + memset(&hdr, 0, sizeof(hdr)); + + hdr.data_len = len; + + if (kore_strlcpy(hdr.domain, domain, sizeof(hdr.domain)) >= + sizeof(hdr.domain)) + fatalx("%s: domain truncated", __func__); + + kore_buf_init(&buf, sizeof(hdr) + len); + kore_buf_append(&buf, &hdr, sizeof(hdr)); + kore_buf_append(&buf, data, len); + + kore_msg_send(target, msg, buf.data, buf.offset); + kore_buf_cleanup(&buf); +} + +#if defined(KORE_USE_ACME) +static void +keymgr_acme_init(void) +{ + RSA *rsa; + struct key *key; + char *e, *n; + int needsreg; + const BIGNUM *be, *bn; + + if (acme_provider == NULL) + return; + + if (mkdir(KORE_ACME_CERTDIR, 0700) == -1) { + if (errno != EEXIST) + fatalx("mkdir(%s): %s", KORE_ACME_CERTDIR, errno_s); + } + + umask(S_IWGRP | S_IWOTH | S_IRGRP | S_IROTH); + + needsreg = 0; + acmeproc_ready = 0; + key = keymgr_load_privatekey(KORE_ACME_ACCOUNT_KEY); + + if (acme_renewal != NULL) + kore_timer_remove(acme_renewal); + + acme_renewal = kore_timer_add(keymgr_acme_renewal, + ACME_RENEWAL_TIMER, NULL, 0); + + if (key->pkey == NULL) { + kore_log(LOG_NOTICE, "generating new ACME account key"); + key->pkey = kore_rsakey_generate(KORE_ACME_ACCOUNT_KEY); + needsreg = 1; + } else { + kore_log(LOG_INFO, "loaded existing ACME account key"); + } + +#if defined(KORE_OPENSSL_NEWER_API) + rsa = EVP_PKEY_get0_RSA(key->pkey); + RSA_get0_key(rsa, &bn, &be, NULL); +#else + rsa = key->pkey->pkey.rsa; + be = rsa->e; + bn = rsa->n; +#endif + + e = keymgr_bignum_base64(be); + n = keymgr_bignum_base64(bn); + + kore_msg_send(KORE_WORKER_ACME, KORE_ACME_RSAKEY_E, e, strlen(e)); + kore_msg_send(KORE_WORKER_ACME, KORE_ACME_RSAKEY_N, n, strlen(n)); + + kore_free(e); + kore_free(n); + + if (needsreg) { + kore_msg_send(KORE_WORKER_ACME, + KORE_ACME_ACCOUNT_CREATE, NULL, 0); + } else { + kore_msg_send(KORE_WORKER_ACME, + KORE_ACME_ACCOUNT_RESOLVE, NULL, 0); + } + + kore_msg_register(KORE_ACME_SIGN, keymgr_acme_sign); + kore_msg_register(KORE_ACME_CSR_REQUEST, keymgr_msg_recv); + kore_msg_register(KORE_ACME_PROC_READY, keymgr_acme_ready); + kore_msg_register(KORE_ACME_ORDER_FAILED, keymgr_msg_recv); + kore_msg_register(KORE_ACME_INSTALL_CERT, keymgr_msg_recv); + kore_msg_register(KORE_ACME_CHALLENGE_CERT, keymgr_msg_recv); +} + +static void +keymgr_acme_domainkey(struct kore_domain *dom, struct key *key) +{ + char *p; + + kore_log(LOG_NOTICE, "generated new domain key for %s", dom->domain); + + if ((p = strrchr(dom->certkey, '/')) == NULL) + fatalx("invalid certkey path '%s'", dom->certkey); + + *p = '\0'; + + if (mkdir(dom->certkey, 0700) == -1) { + if (errno != EEXIST) + fatalx("mkdir(%s): %s", dom->certkey, errno_s); + } + + *p = '/'; + key->pkey = kore_rsakey_generate(dom->certkey); +} + +static void +keymgr_acme_order_create(const char *domain) +{ + struct acme_order *order; + + order = kore_calloc(1, sizeof(*order)); + + order->state = ACME_ORDER_STATE_INIT; + order->domain = kore_strdup(domain); + order->timer = kore_timer_add(keymgr_acme_order_start, + 1000, order, KORE_TIMER_ONESHOT); +} + +static void +keymgr_acme_order_redo(void *udata, u_int64_t now) +{ + struct kore_domain *dom = udata; + + kore_log(LOG_INFO, "[%s] redoing order", dom->domain); + keymgr_acme_order_create(dom->domain); +} + +static void +keymgr_acme_order_start(void *udata, u_int64_t now) +{ + struct acme_order *order = udata; + + switch (order->state) { + case ACME_ORDER_STATE_INIT: + if (acmeproc_ready == 0) + break; + order->state = ACME_ORDER_STATE_SUBMIT; + /* fallthrough */ + case ACME_ORDER_STATE_SUBMIT: + kore_msg_send(KORE_WORKER_ACME, KORE_ACME_ORDER_CREATE, + order->domain, strlen(order->domain)); + kore_free(order->domain); + kore_free(order); + order = NULL; + break; + default: + fatalx("%s: unknown order state %d", __func__, order->state); + } + + if (order != NULL) { + order->timer = kore_timer_add(keymgr_acme_order_start, + 5000, order, KORE_TIMER_ONESHOT); + } +} + +static void +keymgr_acme_ready(struct kore_msg *msg, const void *data) +{ + acmeproc_ready = 1; + kore_log(LOG_INFO, "acme process ready to receive orders"); + + keymgr_acme_renewal(NULL, kore_time_ms()); +} + +static void +keymgr_acme_check(struct kore_domain *dom) +{ + FILE *fp; + int days; + X509 *x509; + time_t expires, now; + + if (dom->acme == 0) + return; + + if (access(dom->certfile, R_OK) == -1) { + if (errno == ENOENT) { + keymgr_acme_order_create(dom->domain); + return; + } + kore_log(LOG_ERR, "access(%s): %s", dom->certfile, errno_s); + return; + } + + if ((fp = fopen(dom->certfile, "r")) == NULL) { + kore_log(LOG_ERR, "fopen(%s): %s", dom->certfile, errno_s); + return; + } + + if ((x509 = PEM_read_X509(fp, NULL, NULL, NULL)) == NULL) { + fclose(fp); + kore_log(LOG_ERR, "PEM_read_X509: %s", ssl_errno_s); + return; + } + + fclose(fp); + + if (!keymgr_x509_not_after(x509, &expires)) { + X509_free(x509); + return; + } + + time(&now); + days = (expires - now) / 86400; + + kore_log(LOG_INFO, "%s certificate expires in %d days", + dom->domain, days); + + if (days <= ACME_RENEWAL_THRESHOLD) { + kore_log(LOG_INFO, "%s renewing certificate", dom->domain); + keymgr_acme_order_create(dom->domain); + } + + X509_free(x509); +} + +static void +keymgr_acme_renewal(void *udata, u_int64_t now) +{ + kore_domain_callback(keymgr_acme_check); +} + +static void +keymgr_acme_sign(struct kore_msg *msg, const void *data) +{ + u_int32_t id; + struct kore_buf buf; + const u_int8_t *ptr; + u_int8_t *sig; + EVP_MD_CTX *ctx; + struct key *key; + char *b64; + unsigned int siglen; + + TAILQ_FOREACH(key, &keys, list) { + if (key->dom == NULL) + break; + } + + if (key == NULL) + fatalx("%s: missing key", __func__); + + if (msg->length < sizeof(id)) + fatalx("%s: invalid length (%zu)", __func__, msg->length); + + ptr = data; + memcpy(&id, ptr, sizeof(id)); + + ptr += sizeof(id); + msg->length -= sizeof(id); + + sig = kore_calloc(1, EVP_PKEY_size(key->pkey)); + + if ((ctx = EVP_MD_CTX_create()) == NULL) + fatalx("EVP_MD_CTX_create: %s", ssl_errno_s); + + if (!EVP_SignInit_ex(ctx, EVP_sha256(), NULL)) + fatalx("EVP_SignInit_ex: %s", ssl_errno_s); + + if (!EVP_SignUpdate(ctx, ptr, msg->length)) + fatalx("EVP_SignUpdate: %s", ssl_errno_s); + + if (!EVP_SignFinal(ctx, sig, &siglen, key->pkey)) + fatalx("EVP_SignFinal: %s", ssl_errno_s); + + if (!kore_base64url_encode(sig, siglen, &b64, KORE_BASE64_RAW)) + fatalx("%s: failed to b64url encode signed data", __func__); + + kore_buf_init(&buf, siglen + sizeof(id)); + kore_buf_append(&buf, &id, sizeof(id)); + kore_buf_append(&buf, b64, strlen(b64)); + + kore_msg_send(KORE_WORKER_ACME, + KORE_ACME_SIGN_RESULT, buf.data, buf.offset); + + EVP_MD_CTX_destroy(ctx); + + kore_free(sig); + kore_free(b64); + kore_buf_cleanup(&buf); +} + +static void +keymgr_acme_install_cert(const void *data, size_t len, struct key *key) +{ + int fd; + ssize_t ret; + + fd = open(key->dom->certfile, O_CREAT | O_TRUNC | O_WRONLY, 0700); + if (fd == -1) + fatal("open(%s): %s", key->dom->certfile, errno_s); + + kore_log(LOG_INFO, "writing %zu bytes of data", len); + + for (;;) { + ret = write(fd, data, len); + if (ret == -1) { + if (errno == EINTR) + continue; + fatal("write(%s): %s", key->dom->certfile, errno_s); + } + + break; + } + + if ((size_t)ret != len) { + fatal("incorrect write on %s (%zd/%zu)", + key->dom->certfile, ret, len); + } + + if (close(fd) == -1) { + kore_log(LOG_NOTICE, + "close error on '%s' (%s)", key->dom->certfile, errno_s); + } + + keymgr_submit_certificates(key->dom, KORE_MSG_WORKER_ALL); + + keymgr_x509_msg(key->dom->domain, NULL, 0, + KORE_MSG_WORKER_ALL, KORE_ACME_CHALLENGE_CLEAR_CERT); +} + +static void +keymgr_acme_order_failed(const void *data, size_t len, struct key *key) +{ + u_int32_t retry; + + if (len != sizeof(retry)) { + kore_log(LOG_ERR, "%s: invalid payload (%zu)", __func__, len); + return; + } + + memcpy(&retry, data, len); + + kore_timer_add(keymgr_acme_order_redo, retry, key->dom, + KORE_TIMER_ONESHOT); +} + +static void +keymgr_acme_challenge_cert(const void *data, size_t len, struct key *key) +{ + STACK_OF(X509_EXTENSION) *sk; + size_t idx; + time_t now; + X509_EXTENSION *ext; + X509_NAME *name; + X509 *x509; + const u_int8_t *digest; + u_int8_t *cert, *uptr; + int slen, acme, i; + char hex[(SHA256_DIGEST_LENGTH * 2) + 1]; + + kore_log(LOG_INFO, "[%s] generating tls-alpn-01 challenge cert", + key->dom->domain); + + if (len != SHA256_DIGEST_LENGTH) + fatalx("invalid digest length of %zu bytes", len); + + digest = data; + + for (idx = 0; idx < SHA256_DIGEST_LENGTH; idx++) { + slen = snprintf(hex + (idx * 2), sizeof(hex) - (idx * 2), + "%02x", digest[idx]); + if (slen == -1 || (size_t)slen >= sizeof(hex)) + fatal("failed to convert digest to hex"); + } + + if ((x509 = X509_new()) == NULL) + fatalx("X509_new(): %s", ssl_errno_s); + + if (!X509_set_version(x509, 2)) + fatalx("X509_set_version(): %s", ssl_errno_s); + + time(&now); + if (!ASN1_INTEGER_set(X509_get_serialNumber(x509), now)) + fatalx("ASN1_INTEGER_set(): %s", ssl_errno_s); + + if (!X509_gmtime_adj(X509_get_notBefore(x509), 0)) + fatalx("X509_gmtime_adj(): %s", ssl_errno_s); + + if (!X509_gmtime_adj(X509_get_notAfter(x509), ACME_X509_EXPIRATION)) + fatalx("X509_gmtime_adj(): %s", ssl_errno_s); + + if (!X509_set_pubkey(x509, key->pkey)) + fatalx("X509_set_pubkey(): %s", ssl_errno_s); + + if ((name = X509_get_subject_name(x509)) == NULL) + fatalx("X509_get_subject_name(): %s", ssl_errno_s); + + if (!X509_NAME_add_entry_by_txt(name, "CN", + MBSTRING_ASC, (const unsigned char *)key->dom->domain, -1, -1, 0)) + fatalx("X509_NAME_add_entry_by_txt(): CN %s", ssl_errno_s); + + if (!X509_set_issuer_name(x509, name)) + fatalx("X509_set_issuer_name(): %s", ssl_errno_s); + + acme = OBJ_create(ACME_TLS_ALPN_01_OID, "acme", "acmeIdentifier"); + X509V3_EXT_add_alias(acme, NID_subject_key_identifier); + + sk = sk_X509_EXTENSION_new_null(); + keymgr_x509_ext(sk, acme, "critical,%s", hex); + keymgr_x509_ext(sk, NID_subject_alt_name, "DNS:%s", key->dom->domain); + + for (i = 0; i < sk_X509_EXTENSION_num(sk); i++) { + ext = sk_X509_EXTENSION_value(sk, i); + if (!X509_add_ext(x509, ext, 0)) + fatalx("X509_add_ext(): %s", ssl_errno_s); + } + + if (!X509_sign(x509, key->pkey, EVP_sha256())) + fatalx("X509_sign(): %s", ssl_errno_s); + + if ((slen = i2d_X509(x509, NULL)) <= 0) + fatalx("i2d_X509: %s", ssl_errno_s); + + cert = kore_calloc(1, slen); + uptr = cert; + + if (i2d_X509(x509, &uptr) <= 0) + fatalx("i2d_X509: %s", ssl_errno_s); + + keymgr_x509_msg(key->dom->domain, cert, slen, + KORE_MSG_WORKER_ALL, KORE_ACME_CHALLENGE_SET_CERT); + + kore_free(cert); + X509_free(x509); + sk_X509_EXTENSION_pop_free(sk, X509_EXTENSION_free); +} + +static void +keymgr_acme_csr(const struct kore_keyreq *req, struct key *key) +{ + int len; + STACK_OF(X509_EXTENSION) *sk; + X509_REQ *csr; + X509_NAME *name; + u_int8_t *data, *uptr; + + kore_log(LOG_INFO, "[%s] creating CSR", req->domain); + + if ((csr = X509_REQ_new()) == NULL) + fatal("X509_REQ_new: %s", ssl_errno_s); + + if (!X509_REQ_set_version(csr, 3)) + fatalx("X509_REQ_set_version(): %s", ssl_errno_s); + + if (!X509_REQ_set_pubkey(csr, key->pkey)) + fatalx("X509_REQ_set_pubkey(): %s", ssl_errno_s); + + if ((name = X509_REQ_get_subject_name(csr)) == NULL) + fatalx("X509_REQ_get_subject_name(): %s", ssl_errno_s); + + if (!X509_NAME_add_entry_by_txt(name, "CN", + MBSTRING_ASC, (const unsigned char *)key->dom->domain, -1, -1, 0)) + fatalx("X509_NAME_add_entry_by_txt(): %s", ssl_errno_s); + + sk = sk_X509_EXTENSION_new_null(); + keymgr_x509_ext(sk, NID_subject_alt_name, "DNS:%s", key->dom->domain); + + if (!X509_REQ_add_extensions(csr, sk)) + fatalx("X509_REQ_add_extensions(): %s", ssl_errno_s); + + if (!X509_REQ_sign(csr, key->pkey, EVP_sha256())) + fatalx("X509_REQ_sign(): %s", ssl_errno_s); + + if ((len = i2d_X509_REQ(csr, NULL)) <= 0) + fatalx("i2d_X509_REQ: %s", ssl_errno_s); + + data = kore_calloc(1, len); + uptr = data; + + if (i2d_X509_REQ(csr, &uptr) <= 0) + fatalx("i2d_X509_REQ: %s", ssl_errno_s); + + keymgr_x509_msg(key->dom->domain, data, len, + KORE_WORKER_ACME, KORE_ACME_CSR_RESPONSE); + + kore_free(data); + X509_REQ_free(csr); + + sk_X509_EXTENSION_pop_free(sk, X509_EXTENSION_free); +} + +static void +keymgr_x509_ext(STACK_OF(X509_EXTENSION) *sk, int extnid, const char *fmt, ...) +{ + int len; + va_list args; + X509_EXTENSION *ext; + char buf[1024]; + + va_start(args, fmt); + len = vsnprintf(buf, sizeof(buf), fmt, args); + va_end(args); + + if (len == -1 || (size_t)len >= sizeof(buf)) + fatalx("failed to create buffer for extension %d", extnid); + + if ((ext = X509V3_EXT_conf_nid(NULL, NULL, extnid, buf)) == NULL) { + fatalx("X509V3_EXT_conf_nid(%d, %s): %s", + extnid, buf, ssl_errno_s); + } + + sk_X509_EXTENSION_push(sk, ext); +} + +static char * +keymgr_bignum_base64(const BIGNUM *bn) +{ + int len; + void *buf; + char *encoded; + + len = BN_num_bytes(bn); + buf = kore_calloc(1, len); + + if (BN_bn2bin(bn, buf) != len) + fatalx("BN_bn2bin: %s", ssl_errno_s); + + if (!kore_base64url_encode(buf, len, &encoded, KORE_BASE64_RAW)) + fatalx("failed to base64 encode BIGNUM"); + + return (encoded); +} + +static int +keymgr_x509_not_after(X509 *x509, time_t *out) +{ + const ASN1_TIME *na; + int ret; + + ret = KORE_RESULT_ERROR; + + if ((na = X509_get_notAfter(x509)) == NULL) { + kore_log(LOG_ERR, "no notAfter date in x509"); + return (KORE_RESULT_ERROR); + } + + switch (na->type) { + case V_ASN1_UTCTIME: + ret = keymgr_asn1_convert_utctime(na, out); + break; + case V_ASN1_GENERALIZEDTIME: + ret = keymgr_asn1_convert_generalizedtime(na->data, + na->length, out); + break; + default: + kore_log(LOG_ERR, "invalid notAfter type (%d)", na->type); + break; + } + + return (ret); +} + +static int +keymgr_asn1_convert_utctime(const ASN1_TIME *na, time_t *out) +{ + int len, year; + char buf[ASN1_GENERALIZEDTIME_LEN + 1]; + + if (na->length != ASN1_UTCTIME_LEN) { + kore_log(LOG_ERR, "invalid UTCTIME: too short (%d)", + na->length); + return (KORE_RESULT_ERROR); + } + + if (!isdigit(na->data[0]) || !isdigit(na->data[1])) { + kore_log(LOG_ERR, "invalid UTCTIME: YY are not digits"); + return (KORE_RESULT_ERROR); + } + + year = (na->data[0] - '0') * 10 + (na->data[1] - '0'); + + /* RFC 5280 says years >= 50 are intepreted as 19YY */ + if (year >= 50) + year = 1900 + year; + else + year = 2000 + year; + + /* Convert it to GENERALIZEDTIME format and call that parser. */ + len = snprintf(buf, sizeof(buf), "%04d%.*s", year, + na->length - 2, (const char *)na->data+ 2); + if (len == -1 || (size_t)len >= sizeof(buf)) { + kore_log(LOG_ERR, "invalid UTCTIME: failed to convert"); + return (KORE_RESULT_ERROR); + } + + return (keymgr_asn1_convert_generalizedtime(buf, len, out)); +} + +static int +keymgr_asn1_convert_generalizedtime(const void *ptr, size_t len, time_t *out) +{ + size_t i; + struct tm tm; + const u_int8_t *buf; + + if (len != ASN1_GENERALIZEDTIME_LEN) { + kore_log(LOG_ERR, "invalid GENERALIZEDTIME: too short (%zu)", + len); + return (KORE_RESULT_ERROR); + } + + buf = ptr; + + for (i = 0; i < len - 1; i++) { + if (!isdigit(buf[i])) { + kore_log(LOG_ERR, + "invalid GENERALIZEDTIME: invalid bytes"); + return (KORE_RESULT_ERROR); + } + } + + /* RFC 5280 states that Zulu time must be used (Z). */ + if (buf[i] != 'Z') { + kore_log(LOG_ERR, "invalid GENERALIZEDTIME: not Zulu time"); + return (KORE_RESULT_ERROR); + } + + memset(&tm, 0, sizeof(tm)); + + tm.tm_year = (buf[0] - '0') * 1000 + (buf[1] - '0') * 100 + + (buf[2] - '0') * 10 + (buf[3] - '0'); + + tm.tm_mon = (buf[4] - '0') * 10 + (buf[5] - '0'); + tm.tm_mday = (buf[6] - '0') * 10 + (buf[7] - '0'); + tm.tm_hour = (buf[8] - '0') * 10 + (buf[9] - '0'); + tm.tm_min = (buf[10] - '0') * 10 + (buf[11] - '0'); + tm.tm_sec = (buf[12] - '0') * 10 + (buf[13] - '0'); + + tm.tm_mon = tm.tm_mon - 1; + tm.tm_year = tm.tm_year - 1900; + + *out = mktime(&tm); + + return (KORE_RESULT_OK); +} +#endif diff --git a/src/kore.c b/src/kore.c index 0df9eb8..f56e21f 100644 --- a/src/kore.c +++ b/src/kore.c @@ -45,6 +45,10 @@ #include "python_api.h" #endif +#if defined(KORE_USE_ACME) +#include "acme.h" +#endif + volatile sig_atomic_t sig_recv; struct kore_server_list kore_servers; u_int8_t nlisteners; @@ -255,6 +259,9 @@ main(int argc, char *argv[]) kore_auth_init(); kore_validator_init(); kore_filemap_init(); +#endif +#if defined(KORE_USE_ACME) + kore_acme_init(); #endif kore_domain_init(); kore_module_init(); diff --git a/src/linux.c b/src/linux.c index af5da22..505e19d 100644 --- a/src/linux.c +++ b/src/linux.c @@ -220,7 +220,7 @@ kore_platform_disable_accept(void) } void -kore_platform_proctitle(char *title) +kore_platform_proctitle(const char *title) { kore_proctitle(title); } diff --git a/src/msg.c b/src/msg.c index 953c2f3..7f7dd9e 100644 --- a/src/msg.c +++ b/src/msg.c @@ -40,30 +40,29 @@ static void msg_type_websocket(struct kore_msg *, const void *); #endif static TAILQ_HEAD(, msg_type) msg_types; -static int cacheidx = 0; -static struct connection *conncache[KORE_WORKER_MAX]; +static size_t cacheidx = 0; +static struct connection **conncache = NULL; void kore_msg_init(void) { - int i; - - for (i = 0; i < KORE_WORKER_MAX; i++) - conncache[i] = NULL; - TAILQ_INIT(&msg_types); } void kore_msg_parent_init(void) { - u_int8_t i; + u_int8_t idx; struct kore_worker *kw; - for (i = 0; i < worker_count; i++) { - if (keymgr_active == 0 && i == KORE_WORKER_KEYMGR) - continue; - kw = kore_worker_data(i); + for (idx = 0; idx < worker_count; idx++) { + if (keymgr_active == 0) { + if (idx == KORE_WORKER_KEYMGR_IDX || + idx == KORE_WORKER_ACME_IDX) + continue; + } + + kw = kore_worker_data(idx); kore_msg_parent_add(kw); } @@ -83,8 +82,8 @@ kore_msg_parent_add(struct kore_worker *kw) kw->msg[0]->disconnect = msg_disconnected_worker; kw->msg[0]->handle = kore_connection_handle; - if (cacheidx >= KORE_WORKER_MAX) - fatal("%s: too many workers", __func__); + conncache = kore_realloc(conncache, + (cacheidx + 1) * sizeof(struct connection *)); conncache[cacheidx++] = kw->msg[0]; @@ -187,11 +186,11 @@ msg_recv_packet(struct netbuf *nb) static int msg_recv_data(struct netbuf *nb) { + size_t i; struct connection *c; - u_int8_t dst; struct msg_type *type; - int deliver, i; - u_int16_t destination; + int deliver; + u_int16_t dst, destination; struct kore_msg *msg = (struct kore_msg *)nb->buf; if ((type = msg_type_lookup(msg->id)) != NULL) { @@ -209,13 +208,20 @@ msg_recv_data(struct netbuf *nb) if (worker == NULL && type == NULL) { destination = msg->dst; - for (i = 0; conncache[i] != NULL; i++) { + for (i = 0; i < cacheidx; i++) { c = conncache[i]; - if (c->proto != CONN_PROTO_MSG || c->hdlr_extra == NULL) + if (c->proto != CONN_PROTO_MSG) + fatal("connection not a msg connection"); + + /* + * If hdlr_extra is NULL it just means the worker + * never started, ignore it. + */ + if (c->hdlr_extra == NULL) continue; deliver = 1; - dst = *(u_int8_t *)c->hdlr_extra; + dst = *(u_int16_t *)c->hdlr_extra; if (destination == KORE_MSG_WORKER_ALL) { if (keymgr_active && dst == 0) @@ -229,7 +235,7 @@ msg_recv_data(struct netbuf *nb) continue; /* This allows the worker to receive the correct id. */ - msg->dst = *(u_int8_t *)c->hdlr_extra; + msg->dst = *(u_int16_t *)c->hdlr_extra; net_send_queue(c, nb->buf, nb->s_off); net_send_flush(c); diff --git a/src/seccomp.c b/src/seccomp.c index 60dfe76..2ca5a13 100644 --- a/src/seccomp.c +++ b/src/seccomp.c @@ -103,6 +103,7 @@ static struct sock_filter filter_kore[] = { #if defined(SYS_poll) KORE_SYSCALL_ALLOW(poll), #endif + KORE_SYSCALL_ALLOW(ppoll), KORE_SYSCALL_ALLOW(sendto), KORE_SYSCALL_ALLOW(accept), KORE_SYSCALL_ALLOW(sendfile), @@ -154,7 +155,7 @@ static struct sock_filter *seccomp_filter_update(struct sock_filter *, #define filter_prologue_len KORE_FILTER_LEN(filter_prologue) #define filter_epilogue_len KORE_FILTER_LEN(filter_epilogue) -static void seccomp_register_violation(struct kore_worker *); +static void seccomp_register_violation(pid_t); struct filter { char *name; @@ -313,35 +314,34 @@ kore_seccomp_traceme(void) } int -kore_seccomp_trace(struct kore_worker *kw, int status) +kore_seccomp_trace(pid_t pid, int status) { + int evt; + if (kore_seccomp_tracing == 0) return (KORE_RESULT_ERROR); if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGSTOP) { - if (kw->tracing == 0) { - kw->tracing = 1; - if (ptrace(PTRACE_SETOPTIONS, kw->pid, NULL, - PTRACE_O_TRACESECCOMP) == -1) - fatal("ptrace: %s", errno_s); - if (ptrace(PTRACE_CONT, kw->pid, NULL, NULL) == -1) - fatal("ptrace: %s", errno_s); - } - - return (KORE_RESULT_OK); - } - - if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP) { - if ((status >> 8) == - (SIGTRAP | (PTRACE_EVENT_SECCOMP << 8))) - seccomp_register_violation(kw); - if (ptrace(PTRACE_CONT, kw->pid, NULL, NULL) == -1) + if (ptrace(PTRACE_SETOPTIONS, pid, NULL, + PTRACE_O_TRACESECCOMP | PTRACE_O_TRACECLONE | + PTRACE_O_TRACEFORK) == -1) + fatal("ptrace: %s", errno_s); + if (ptrace(PTRACE_CONT, pid, NULL, NULL) == -1) fatal("ptrace: %s", errno_s); return (KORE_RESULT_OK); } - if (WIFSTOPPED(status) && kw->tracing) { - if (ptrace(PTRACE_CONT, kw->pid, NULL, WSTOPSIG(status)) == -1) + if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP) { + evt = status >> 8; + if (evt == (SIGTRAP | (PTRACE_EVENT_SECCOMP << 8))) + seccomp_register_violation(pid); + if (ptrace(PTRACE_CONT, pid, NULL, NULL) == -1) + fatal("ptrace: %s", errno_s); + return (KORE_RESULT_OK); + } + + if (WIFSTOPPED(status)) { + if (ptrace(PTRACE_CONT, pid, NULL, WSTOPSIG(status)) == -1) fatal("ptrace: %s", errno_s); return (KORE_RESULT_OK); } @@ -420,16 +420,19 @@ kore_seccomp_syscall_flag(const char *name, int action, int arg, int value) } static void -seccomp_register_violation(struct kore_worker *kw) +seccomp_register_violation(pid_t pid) { + int idx; + struct kore_worker *kw; struct iovec iov; struct user_regs_struct regs; long sysnr; + const char *name; iov.iov_base = ®s; iov.iov_len = sizeof(regs); - if (ptrace(PTRACE_GETREGSET, kw->pid, 1, &iov) == -1) + if (ptrace(PTRACE_GETREGSET, pid, 1, &iov) == -1) fatal("ptrace: %s", errno_s); #if SECCOMP_AUDIT_ARCH == AUDIT_ARCH_X86_64 @@ -442,8 +445,20 @@ seccomp_register_violation(struct kore_worker *kw) #error "platform not yet supported" #endif - kore_log(LOG_INFO, "seccomp violation, worker=%d, syscall=%s", - kw->id, kore_seccomp_syscall_name(sysnr)); + name = NULL; + for (idx = 0; idx < worker_count; idx++) { + kw = kore_worker_data(idx); + if (kw->pid == pid) { + name = kore_worker_name(kw->id); + break; + } + } + + if (name == NULL) + name = ""; + + kore_log(LOG_INFO, "seccomp violation, %s pid=%d, syscall=%ld:%s", + name, pid, sysnr, kore_seccomp_syscall_name(sysnr)); } static struct sock_filter * diff --git a/src/utils.c b/src/utils.c index 932c087..2c10031 100644 --- a/src/utils.c +++ b/src/utils.c @@ -17,6 +17,9 @@ #include #include +#include +#include + #include #include #include @@ -47,8 +50,19 @@ static struct { }; static void fatal_log(const char *, va_list); +static int utils_base64_encode(const void *, size_t, char **, + const char *, int); +static int utils_base64_decode(const char *, u_int8_t **, + size_t *, const char *, int); -static char b64table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +static char b64_table[] = \ + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +static char b64url_table[] = \ + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + +/* b64_table and b64url_table are the same size. */ +#define B64_TABLE_LEN (sizeof(b64_table)) #if defined(KORE_DEBUG) void @@ -83,20 +97,20 @@ void kore_log(int prio, const char *fmt, ...) { va_list args; - char buf[2048], tmp[32]; + const char *name; + char buf[2048]; va_start(args, fmt); (void)vsnprintf(buf, sizeof(buf), fmt, args); va_end(args); if (worker != NULL) { - (void)snprintf(tmp, sizeof(tmp), "wrk %d", worker->id); - if (worker->id == KORE_WORKER_KEYMGR) - (void)kore_strlcpy(tmp, "keymgr", sizeof(tmp)); + name = kore_worker_name(worker->id); + if (foreground) - printf("[%s]: %s\n", tmp, buf); + printf("%s: %s\n", name, buf); else - syslog(prio, "[%s]: %s", tmp, buf); + syslog(prio, "%s: %s", name, buf); } else { if (foreground) printf("[parent]: %s\n", buf); @@ -410,135 +424,28 @@ kore_time_ms(void) return ((u_int64_t)(ts.tv_sec * 1000 + (ts.tv_nsec / 1000000))); } +int +kore_base64url_encode(const void *data, size_t len, char **out, int flags) +{ + return (utils_base64_encode(data, len, out, b64url_table, flags)); +} + int kore_base64_encode(const void *data, size_t len, char **out) { - u_int8_t n; - size_t nb; - const u_int8_t *ptr; - u_int32_t bytes; - struct kore_buf result; + return (utils_base64_encode(data, len, out, b64_table, 0)); +} - nb = 0; - ptr = data; - kore_buf_init(&result, (len / 3) * 4); - - while (len > 0) { - if (len > 2) { - nb = 3; - bytes = *ptr++ << 16; - bytes |= *ptr++ << 8; - bytes |= *ptr++; - } else if (len > 1) { - nb = 2; - bytes = *ptr++ << 16; - bytes |= *ptr++ << 8; - } else if (len == 1) { - nb = 1; - bytes = *ptr++ << 16; - } else { - kore_buf_cleanup(&result); - return (KORE_RESULT_ERROR); - } - - n = (bytes >> 18) & 0x3f; - kore_buf_append(&result, &(b64table[n]), 1); - n = (bytes >> 12) & 0x3f; - kore_buf_append(&result, &(b64table[n]), 1); - if (nb > 1) { - n = (bytes >> 6) & 0x3f; - kore_buf_append(&result, &(b64table[n]), 1); - if (nb > 2) { - n = bytes & 0x3f; - kore_buf_append(&result, &(b64table[n]), 1); - } - } - - len -= nb; - } - - switch (nb) { - case 1: - kore_buf_appendf(&result, "=="); - break; - case 2: - kore_buf_appendf(&result, "="); - break; - case 3: - break; - default: - kore_buf_cleanup(&result); - return (KORE_RESULT_ERROR); - } - - /* result.data gets taken over so no need to cleanup result. */ - *out = kore_buf_stringify(&result, NULL); - - return (KORE_RESULT_OK); +int +kore_base64url_decode(const char *in, u_int8_t **out, size_t *olen, int flags) +{ + return (utils_base64_decode(in, out, olen, b64url_table, flags)); } int kore_base64_decode(const char *in, u_int8_t **out, size_t *olen) { - int i, c; - struct kore_buf *res; - u_int8_t d, n, o; - u_int32_t b, len, idx; - - i = 4; - b = 0; - d = 0; - c = 0; - len = strlen(in); - res = kore_buf_alloc(len); - - for (idx = 0; idx < len; idx++) { - c = in[idx]; - if (c == '=') - break; - - for (o = 0; o < sizeof(b64table); o++) { - if (b64table[o] == c) { - d = o; - break; - } - } - - if (o == sizeof(b64table)) { - *out = NULL; - kore_buf_free(res); - return (KORE_RESULT_ERROR); - } - - b |= (d & 0x3f) << ((i - 1) * 6); - i--; - if (i == 0) { - for (i = 2; i >= 0; i--) { - n = (b >> (8 * i)); - kore_buf_append(res, &n, 1); - } - - b = 0; - i = 4; - } - } - - if (c == '=') { - if (i > 2) { - *out = NULL; - kore_buf_free(res); - return (KORE_RESULT_ERROR); - } - - o = i; - for (i = 2; i >= o; i--) { - n = (b >> (8 * i)); - kore_buf_append(res, &n, 1); - } - } - - *out = kore_buf_release(res, olen); - return (KORE_RESULT_OK); + return (utils_base64_decode(in, out, olen, b64_table, 0)); } void * @@ -605,6 +512,79 @@ kore_read_line(FILE *fp, char *in, size_t len) return (p); } +EVP_PKEY * +kore_rsakey_load(const char *path) +{ + FILE *fp; + EVP_PKEY *pkey; + + if (access(path, R_OK) == -1) + return (NULL); + + if ((fp = fopen(path, "r")) == NULL) + fatalx("%s(%s): %s", __func__, path, errno_s); + + if ((pkey = PEM_read_PrivateKey(fp, NULL, NULL, NULL)) == NULL) + fatalx("PEM_read_PrivateKey: %s", ssl_errno_s); + + fclose(fp); + + return (pkey); +} + +EVP_PKEY * +kore_rsakey_generate(const char *path) +{ + FILE *fp; + EVP_PKEY_CTX *ctx; + EVP_PKEY *pkey; + + if ((ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL)) == NULL) + fatalx("EVP_PKEY_CTX_new_id: %s", ssl_errno_s); + + if (EVP_PKEY_keygen_init(ctx) <= 0) + fatalx("EVP_PKEY_keygen_init: %s", ssl_errno_s); + + if (EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, KORE_RSAKEY_BITS) <= 0) + fatalx("EVP_PKEY_CTX_set_rsa_keygen_bits: %s", ssl_errno_s); + + pkey = NULL; + if (EVP_PKEY_keygen(ctx, &pkey) <= 0) + fatalx("EVP_PKEY_keygen: %s", ssl_errno_s); + + if (path != NULL) { + if ((fp = fopen(path, "w")) == NULL) + fatalx("fopen(%s): %s", path, errno_s); + + if (!PEM_write_PrivateKey(fp, pkey, NULL, NULL, 0, NULL, NULL)) + fatalx("PEM_write_PrivateKey: %s", ssl_errno_s); + + fclose(fp); + } + + return (pkey); +} + +const char * +kore_worker_name(int id) +{ + static char buf[64]; + + switch (id) { + case KORE_WORKER_KEYMGR: + (void)snprintf(buf, sizeof(buf), "[keymgr]"); + break; + case KORE_WORKER_ACME: + (void)snprintf(buf, sizeof(buf), "[acme]"); + break; + default: + (void)snprintf(buf, sizeof(buf), "[wrk %d]", id); + break; + } + + return (buf); +} + void fatal(const char *fmt, ...) { @@ -649,3 +629,169 @@ fatal_log(const char *fmt, va_list args) printf("%s: %s\n", kore_progname, buf); } + +static int +utils_base64_encode(const void *data, size_t len, char **out, + const char *table, int flags) +{ + u_int8_t n; + size_t nb; + const u_int8_t *ptr; + u_int32_t bytes; + struct kore_buf result; + + nb = 0; + ptr = data; + kore_buf_init(&result, (len / 3) * 4); + + while (len > 0) { + if (len > 2) { + nb = 3; + bytes = *ptr++ << 16; + bytes |= *ptr++ << 8; + bytes |= *ptr++; + } else if (len > 1) { + nb = 2; + bytes = *ptr++ << 16; + bytes |= *ptr++ << 8; + } else if (len == 1) { + nb = 1; + bytes = *ptr++ << 16; + } else { + kore_buf_cleanup(&result); + return (KORE_RESULT_ERROR); + } + + n = (bytes >> 18) & 0x3f; + kore_buf_append(&result, &(table[n]), 1); + n = (bytes >> 12) & 0x3f; + kore_buf_append(&result, &(table[n]), 1); + if (nb > 1) { + n = (bytes >> 6) & 0x3f; + kore_buf_append(&result, &(table[n]), 1); + if (nb > 2) { + n = bytes & 0x3f; + kore_buf_append(&result, &(table[n]), 1); + } + } + + len -= nb; + } + + if (!(flags & KORE_BASE64_RAW)) { + switch (nb) { + case 1: + kore_buf_appendf(&result, "=="); + break; + case 2: + kore_buf_appendf(&result, "="); + break; + case 3: + break; + default: + kore_buf_cleanup(&result); + return (KORE_RESULT_ERROR); + } + } + + /* result.data gets taken over so no need to cleanup result. */ + *out = kore_buf_stringify(&result, NULL); + + return (KORE_RESULT_OK); +} + +static int +utils_base64_decode(const char *in, u_int8_t **out, size_t *olen, + const char *table, int flags) +{ + int i, c; + u_int8_t d, n, o; + struct kore_buf *res, buf; + const char *ptr, *pad; + u_int32_t b, len, plen, idx; + + i = 4; + b = 0; + d = 0; + c = 0; + len = strlen(in); + memset(&buf, 0, sizeof(buf)); + + if (flags & KORE_BASE64_RAW) { + switch (len % 4) { + case 2: + plen = 2; + pad = "=="; + break; + case 3: + plen = 1; + pad = "="; + break; + default: + return (KORE_RESULT_ERROR); + } + + kore_buf_init(&buf, len + plen); + kore_buf_appendf(&buf, in, len); + kore_buf_appendf(&buf, pad, plen); + + len = len + plen; + ptr = (const char *)buf.data; + } else { + ptr = in; + } + + res = kore_buf_alloc(len); + + for (idx = 0; idx < len; idx++) { + c = ptr[idx]; + if (c == '=') + break; + + for (o = 0; o < B64_TABLE_LEN; o++) { + if (table[o] == c) { + d = o; + break; + } + } + + if (o == B64_TABLE_LEN) { + *out = NULL; + kore_buf_free(res); + kore_buf_cleanup(&buf); + return (KORE_RESULT_ERROR); + } + + b |= (d & 0x3f) << ((i - 1) * 6); + i--; + if (i == 0) { + for (i = 2; i >= 0; i--) { + n = (b >> (8 * i)); + kore_buf_append(res, &n, 1); + } + + b = 0; + i = 4; + } + } + + if (c == '=') { + if (i > 2) { + *out = NULL; + kore_buf_free(res); + kore_buf_cleanup(&buf); + return (KORE_RESULT_ERROR); + } + + o = i; + for (i = 2; i >= o; i--) { + n = (b >> (8 * i)); + kore_buf_append(res, &n, 1); + } + } + + kore_buf_cleanup(&buf); + *out = kore_buf_release(res, olen); + + return (KORE_RESULT_OK); +} diff --git a/src/worker.c b/src/worker.c index 07a9904..9914f42 100644 --- a/src/worker.c +++ b/src/worker.c @@ -31,6 +31,10 @@ #include "kore.h" +#if defined(KORE_USE_ACME) +#include "acme.h" +#endif + #if !defined(KORE_NO_HTTP) #include "http.h" #endif @@ -59,7 +63,7 @@ #define WAIT_ANY (-1) #endif -#define WORKER_SOLO_COUNT 2 +#define WORKER_SOLO_COUNT 3 #define WORKER(id) \ (struct kore_worker *)((u_int8_t *)kore_workers + \ @@ -80,8 +84,6 @@ static void worker_accept_avail(struct kore_msg *, const void *); static void worker_entropy_recv(struct kore_msg *, const void *); static void worker_keymgr_response(struct kore_msg *, const void *); -static int worker_keymgr_response_verify(struct kore_msg *, const void *, - struct kore_domain **); static int accept_avail; static struct kore_worker *kore_workers; @@ -102,15 +104,15 @@ kore_worker_init(void) { size_t len; struct kore_worker *kw; - u_int16_t i, cpu; + u_int16_t idx, id, cpu; worker_no_lock = 0; if (worker_count == 0) worker_count = cpu_count; - /* Account for the keymgr even if we don't end up starting it. */ - worker_count += 1; + /* Account for the keymgr/acme even if we don't end up starting it. */ + worker_count += 2; len = sizeof(*accept_lock) + (sizeof(struct kore_worker) * worker_count); @@ -136,30 +138,41 @@ kore_worker_init(void) } /* Setup log buffers. */ - for (i = 0; i < worker_count; i++) { - kw = WORKER(i); + for (idx = KORE_WORKER_BASE; idx < worker_count; idx++) { + kw = WORKER(idx); kw->lb.offset = 0; } - /* Start keymgr if required. */ - if (keymgr_active) - kore_worker_spawn(0, 0); - /* Now start all the workers. */ + id = 1; cpu = 1; - for (i = 1; i < worker_count; i++) { + for (idx = KORE_WORKER_BASE; idx < worker_count; idx++) { if (cpu >= cpu_count) cpu = 0; - kore_worker_spawn(i, cpu++); + kore_worker_spawn(idx, id++, cpu++); + } + + if (keymgr_active) { +#if defined(KORE_USE_ACME) + /* The ACME process is only started if we need it. */ + if (acme_provider) { + kore_worker_spawn(KORE_WORKER_ACME_IDX, + KORE_WORKER_ACME, 0); + } +#endif + + /* Now we can start the keymgr. */ + kore_worker_spawn(KORE_WORKER_KEYMGR_IDX, + KORE_WORKER_KEYMGR, 0); } } void -kore_worker_spawn(u_int16_t id, u_int16_t cpu) +kore_worker_spawn(u_int16_t idx, u_int16_t id, u_int16_t cpu) { struct kore_worker *kw; - kw = WORKER(id); + kw = WORKER(idx); kw->id = id; kw->cpu = cpu; kw->has_lock = 0; @@ -198,7 +211,7 @@ kore_worker_shutdown(void) struct kore_worker *kw; pid_t pid; int status; - u_int16_t id, done; + u_int16_t idx, done; if (!kore_quiet) { kore_log(LOG_NOTICE, @@ -206,15 +219,15 @@ kore_worker_shutdown(void) } for (;;) { - for (id = 0; id < worker_count; id++) { - kw = WORKER(id); + for (idx = 0; idx < worker_count; idx++) { + kw = WORKER(idx); if (kw->pid != 0) { pid = waitpid(kw->pid, &status, 0); if (pid == -1) continue; #if defined(__linux__) - kore_seccomp_trace(kw, status); + kore_seccomp_trace(kw->pid, status); #endif if (WIFEXITED(status)) { @@ -229,8 +242,8 @@ kore_worker_shutdown(void) } done = 0; - for (id = 0; id < worker_count; id++) { - kw = WORKER(id); + for (idx = 0; idx < worker_count; idx++) { + kw = WORKER(idx); if (kw->pid == 0) done++; } @@ -248,11 +261,11 @@ kore_worker_shutdown(void) void kore_worker_dispatch_signal(int sig) { - u_int16_t id; + u_int16_t idx; struct kore_worker *kw; - for (id = 0; id < worker_count; id++) { - kw = WORKER(id); + for (idx = 0; idx < worker_count; idx++) { + kw = WORKER(idx); if (kill(kw->pid, sig) == -1) { kore_debug("kill(%d, %d): %s", kw->pid, sig, errno_s); } @@ -329,7 +342,6 @@ void kore_worker_entry(struct kore_worker *kw) { struct kore_runtime_call *rcall; - char buf[16]; u_int64_t last_seed; int quit, had_lock; u_int64_t netwait, now, next_prune; @@ -340,10 +352,7 @@ kore_worker_entry(struct kore_worker *kw) kore_seccomp_traceme(); #endif - (void)snprintf(buf, sizeof(buf), "[wrk %d]", kw->id); - if (kw->id == KORE_WORKER_KEYMGR) - (void)snprintf(buf, sizeof(buf), "[keymgr]"); - kore_platform_proctitle(buf); + kore_platform_proctitle(kore_worker_name(kw->id)); if (worker_set_affinity == 1) kore_platform_worker_setcpu(kw); @@ -357,6 +366,13 @@ kore_worker_entry(struct kore_worker *kw) exit(0); } +#if defined(KORE_USE_ACME) + if (kw->id == KORE_WORKER_ACME) { + kore_acme_run(); + exit(0); + } +#endif + net_init(); kore_connection_init(); kore_platform_event_init(); @@ -394,6 +410,12 @@ kore_worker_entry(struct kore_worker *kw) kore_msg_send(KORE_WORKER_KEYMGR, KORE_MSG_CERTIFICATE_REQ, NULL, 0); } +#if defined(KORE_USE_ACME) + kore_msg_register(KORE_ACME_CHALLENGE_SET_CERT, + worker_keymgr_response); + kore_msg_register(KORE_ACME_CHALLENGE_CLEAR_CERT, + worker_keymgr_response); +#endif } kore_msg_register(KORE_MSG_ACCEPT_AVAILABLE, worker_accept_avail); @@ -575,23 +597,79 @@ kore_worker_make_busy(void) } } +int +kore_worker_keymgr_response_verify(struct kore_msg *msg, const void *data, + struct kore_domain **out) +{ + struct kore_server *srv; + struct kore_domain *dom; + const struct kore_x509_msg *req; + + if (msg->length < sizeof(*req)) { + kore_log(LOG_WARNING, + "short keymgr message (%zu)", msg->length); + return (KORE_RESULT_ERROR); + } + + req = (const struct kore_x509_msg *)data; + if (msg->length != (sizeof(*req) + req->data_len)) { + kore_log(LOG_WARNING, + "invalid keymgr payload (%zu)", msg->length); + return (KORE_RESULT_ERROR); + } + + if (req->domain[KORE_DOMAINNAME_LEN] != '\0') { + kore_log(LOG_WARNING, "domain not NUL-terminated"); + return (KORE_RESULT_ERROR); + + } + + if (out == NULL) + return (KORE_RESULT_OK); + + LIST_FOREACH(srv, &kore_servers, list) { + dom = NULL; + + if (srv->tls == 0) + continue; + + TAILQ_FOREACH(dom, &srv->domains, list) { + if (!strcmp(dom->domain, req->domain)) + break; + } + + if (dom != NULL) + break; + } + + if (dom == NULL) { + kore_log(LOG_WARNING, + "got keymgr response for domain that does not exist"); + return (KORE_RESULT_ERROR); + } + + *out = dom; + + return (KORE_RESULT_OK); +} + static void worker_reaper(pid_t pid, int status) { - u_int16_t id; + u_int16_t idx; struct kore_worker *kw; const char *func; - for (id = 0; id < worker_count; id++) { - kw = WORKER(id); +#if defined(__linux__) + if (kore_seccomp_trace(pid, status)) + return; +#endif + + for (idx = 0; idx < worker_count; idx++) { + kw = WORKER(idx); if (kw->pid != pid) continue; -#if defined(__linux__) - if (kore_seccomp_trace(kw, status)) - break; -#endif - if (!kore_quiet) { kore_log(LOG_NOTICE, "worker %d (%d) exited with status %d", @@ -619,8 +697,10 @@ worker_reaper(pid_t pid, int status) } #endif - if (id == KORE_WORKER_KEYMGR) { - kore_log(LOG_CRIT, "keymgr gone, stopping"); + if (kw->id == KORE_WORKER_KEYMGR || + kw->id == KORE_WORKER_ACME) { + kore_log(LOG_CRIT, + "keymgr or acme process gone, stopping"); kw->pid = 0; if (raise(SIGTERM) != 0) { kore_log(LOG_WARNING, @@ -657,7 +737,7 @@ worker_reaper(pid_t pid, int status) kore_log(LOG_NOTICE, "restarting worker %d", kw->id); kw->restarted = 1; kore_msg_parent_remove(kw); - kore_worker_spawn(kw->id, kw->cpu); + kore_worker_spawn(idx, kw->id, kw->cpu); kore_msg_parent_add(kw); break; @@ -769,74 +849,45 @@ worker_keymgr_response(struct kore_msg *msg, const void *data) struct kore_domain *dom; const struct kore_x509_msg *req; - if (!worker_keymgr_response_verify(msg, data, &dom)) + if (!kore_worker_keymgr_response_verify(msg, data, &dom)) return; req = (const struct kore_x509_msg *)data; switch (msg->id) { case KORE_MSG_CERTIFICATE: - kore_domain_tlsinit(dom, req->data, req->data_len); + kore_domain_tlsinit(dom, KORE_PEM_CERT_CHAIN, + req->data, req->data_len); break; case KORE_MSG_CRL: kore_domain_crl_add(dom, req->data, req->data_len); break; +#if defined(KORE_USE_ACME) + case KORE_ACME_CHALLENGE_SET_CERT: + if (dom->ssl_ctx == NULL) { + kore_domain_tlsinit(dom, KORE_DER_CERT_DATA, + req->data, req->data_len); + } + + kore_free(dom->acme_cert); + dom->acme_cert_len = req->data_len; + dom->acme_cert = kore_calloc(1, req->data_len); + memcpy(dom->acme_cert, req->data, req->data_len); + + kore_log(LOG_NOTICE, "[%s] tls-alpn-01 challenge active", + dom->domain); + dom->acme_challenge = 1; + break; + case KORE_ACME_CHALLENGE_CLEAR_CERT: + dom->acme_cert_len = 0; + dom->acme_challenge = 0; + kore_free(dom->acme_cert); + kore_log(LOG_NOTICE, "[%s] tls-alpn-01 challenge disabled", + dom->domain); + break; +#endif default: kore_log(LOG_WARNING, "unknown keymgr request %u", msg->id); break; } } - -static int -worker_keymgr_response_verify(struct kore_msg *msg, const void *data, - struct kore_domain **out) -{ - struct kore_server *srv; - struct kore_domain *dom; - const struct kore_x509_msg *req; - - if (msg->length < sizeof(*req)) { - kore_log(LOG_WARNING, - "short keymgr message (%zu)", msg->length); - return (KORE_RESULT_ERROR); - } - - req = (const struct kore_x509_msg *)data; - if (msg->length != (sizeof(*req) + req->data_len)) { - kore_log(LOG_WARNING, - "invalid keymgr payload (%zu)", msg->length); - return (KORE_RESULT_ERROR); - } - - if (req->domain_len > KORE_DOMAINNAME_LEN) { - kore_log(LOG_WARNING, - "invalid keymgr domain (%u)", - req->domain_len); - return (KORE_RESULT_ERROR); - } - - LIST_FOREACH(srv, &kore_servers, list) { - dom = NULL; - - if (srv->tls == 0) - continue; - - TAILQ_FOREACH(dom, &srv->domains, list) { - if (!strncmp(dom->domain, req->domain, req->domain_len)) - break; - } - - if (dom != NULL) - break; - } - - if (dom == NULL) { - kore_log(LOG_WARNING, - "got keymgr response for domain that does not exist"); - return (KORE_RESULT_ERROR); - } - - *out = dom; - - return (KORE_RESULT_OK); -}