kore

An easy to use, scalable and secure web application framework for writing web APIs in C.
Commits | Files | Refs | README | LICENSE | git clone https://git.kore.io/kore.git

commit c78535aa5dd2ff82fa8c8d86b132d14cfa25b081
parent eef1a058683f5646122d6206cc5ca83766540649
Author: Joris Vink <joris@coders.se>
Date:   Wed,  6 Nov 2019 19:33:53 +0100

Add acmev2 (RFC8555) support to Kore.

A new acme process is created that communicates with the acme servers.

This process does not hold any of your private keys (no account keys,
no domain keys etc).

Whenever the acme process requires a signed payload it will ask the keymgr
process to do the signing with the relevant keys.

This process is also sandboxed with pledge+unveil on OpenBSD and seccomp
syscall filtering on Linux.

The implementation only supports the tls-alpn-01 challenge. This means that
you do not need to open additional ports on your machine.

http-01 and dns-01 are currently not supported (no wildcard support).

A new configuration option "acme_provider" is available and can be set
to the acme server its directory. By default this will point to the
live letsencrypt environment:
    https://acme-v02.api.letsencrypt.org/directory

The acme process can be controlled via the following config options:
  - acme_root (where the acme process will chroot/chdir into).
  - acme_runas (the user the acme process will run as).

  If none are set, the values from 'root' and 'runas' are taken.

If you want to turn on acme for domains you do it as follows:

domain kore.io {
	acme yes
}

You do not need to specify certkey/certfile anymore, if they are present
still
they will be overwritten by the acme system.

The keymgr will store all certificates and keys under its root
(keymgr_root), the account key is stored as "/account-key.pem" and all
obtained certificates go under "certificates/<domain>/fullchain.pem" while
keys go under "certificates/<domain>/key.pem".

Kore will automatically renew certificates if they will expire in 7 days
or less.

Diffstat:
Makefile | 7+++++++
examples/messaging/src/messaging.c | 4++--
include/kore/acme.h | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
include/kore/kore.h | 62+++++++++++++++++++++++++++++++++++++++++++++++++-------------
include/kore/seccomp.h | 2+-
src/accesslog.c | 2+-
src/acme.c | 1684+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/bsd.c | 5++++-
src/config.c | 175++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
src/connection.c | 16++++++++++++++--
src/curl.c | 3+++
src/domain.c | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
src/keymgr.c | 911+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
src/kore.c | 7+++++++
src/linux.c | 2+-
src/msg.c | 48+++++++++++++++++++++++++++---------------------
src/seccomp.c | 57++++++++++++++++++++++++++++++++++++---------------------
src/utils.c | 400++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
src/worker.c | 245++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
19 files changed, 3354 insertions(+), 408 deletions(-)

diff --git 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 @@ -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 @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2019 Joris Vink <joris@coders.se> + * + * 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 @@ -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 @@ -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 @@ -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 @@ -0,0 +1,1684 @@ +/* + * Copyright (c) 2019 Joris Vink <joris@coders.se> + * + * 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 <sys/types.h> +#include <sys/stat.h> + +#include <openssl/sha.h> + +#include <stdio.h> +#include <stdlib.h> + +#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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 <sys/types.h> +#include <sys/mman.h> #include <sys/stat.h> #include <openssl/evp.h> +#include <openssl/rsa.h> #include <openssl/rand.h> +#include <openssl/sha.h> +#include <openssl/x509.h> +#include <openssl/x509v3.h> +#include <ctype.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> @@ -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); - } - - len = sizeof(*msg) + st.st_size; - payload = kore_calloc(1, len); - - 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); + payload = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0); + if (payload == MAP_FAILED) + fatalx("mmap(): %s", errno_s); - 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); - } + keymgr_x509_msg(dom->domain, payload, st.st_size, dst, id); - 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 @@ -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; @@ -256,6 +260,9 @@ main(int argc, char *argv[]) kore_validator_init(); kore_filemap_init(); #endif +#if defined(KORE_USE_ACME) + kore_acme_init(); +#endif kore_domain_init(); kore_module_init(); kore_server_sslstart(); diff --git 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 @@ -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 @@ -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); - } - + 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) && WSTOPSIG(status) == SIGTRAP) { - if ((status >> 8) == - (SIGTRAP | (PTRACE_EVENT_SECCOMP << 8))) - seccomp_register_violation(kw); - if (ptrace(PTRACE_CONT, kw->pid, NULL, NULL) == -1) + 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) && kw->tracing) { - if (ptrace(PTRACE_CONT, kw->pid, NULL, WSTOPSIG(status)) == -1) + 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 = &regs; 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 = "<child>"; + + 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 @@ -17,6 +17,9 @@ #include <sys/types.h> #include <sys/time.h> +#include <openssl/evp.h> +#include <openssl/rsa.h> + #include <ctype.h> #include <stdio.h> #include <stdarg.h> @@ -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 b64_table[] = \ + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +static char b64url_table[] = \ + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; -static char b64table[] = "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); @@ -411,134 +425,27 @@ kore_time_ms(void) } int -kore_base64_encode(const void *data, size_t len, char **out) +kore_base64url_encode(const void *data, size_t len, char **out, 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, &(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); - } + return (utils_base64_encode(data, len, out, b64url_table, flags)); +} - /* result.data gets taken over so no need to cleanup result. */ - *out = kore_buf_stringify(&result, NULL); +int +kore_base64_encode(const void *data, size_t len, char **out) +{ + return (utils_base64_encode(data, len, out, b64_table, 0)); +} - 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 @@ -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 (kw->pid != pid) - continue; - #if defined(__linux__) - if (kore_seccomp_trace(kw, status)) - break; + if (kore_seccomp_trace(pid, status)) + return; #endif + for (idx = 0; idx < worker_count; idx++) { + kw = WORKER(idx); + if (kw->pid != pid) + continue; + 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; - 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 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); } - if (dom != NULL) - break; - } + 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); - if (dom == NULL) { - kore_log(LOG_WARNING, - "got keymgr response for domain that does not exist"); - return (KORE_RESULT_ERROR); + 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; } - - *out = dom; - - return (KORE_RESULT_OK); }