forked from mirrors/kore
1471 lines
35 KiB
C
1471 lines
35 KiB
C
/*
|
||
* 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.
|
||
*/
|
||
|
||
#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
|
||
|
||
#if defined(__linux__)
|
||
#include "seccomp.h"
|
||
|
||
/* The syscalls our acme worker is allowed to perform, only. */
|
||
static struct sock_filter filter_keymgr[] = {
|
||
/* Net related. */
|
||
KORE_SYSCALL_ALLOW(poll),
|
||
KORE_SYSCALL_ALLOW(sendto),
|
||
KORE_SYSCALL_ALLOW(recvfrom),
|
||
KORE_SYSCALL_ALLOW(epoll_wait),
|
||
KORE_SYSCALL_ALLOW(epoll_pwait),
|
||
|
||
/* Process things. */
|
||
KORE_SYSCALL_ALLOW(exit),
|
||
|
||
/* Other things. */
|
||
KORE_SYSCALL_ALLOW(brk),
|
||
KORE_SYSCALL_ALLOW(mmap),
|
||
KORE_SYSCALL_ALLOW(munmap),
|
||
KORE_SYSCALL_ALLOW(clock_gettime),
|
||
};
|
||
#endif
|
||
|
||
struct acme_request {
|
||
struct kore_curl curl;
|
||
};
|
||
|
||
struct acme_sign_op {
|
||
u_int32_t id;
|
||
struct kore_timer *timer;
|
||
void *udata;
|
||
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 10000
|
||
|
||
struct acme_order {
|
||
int state;
|
||
int status;
|
||
u_int64_t start;
|
||
char *id;
|
||
char *final;
|
||
char *domain;
|
||
struct acme_auth *curauth;
|
||
LIST_HEAD(, acme_auth) auth;
|
||
};
|
||
|
||
#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_protected_component(const char *, const char *);
|
||
|
||
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_account_reg_submit(struct acme_sign_op *,
|
||
struct kore_buf *);
|
||
|
||
static void acme_order_process(void *, u_int64_t);
|
||
static void acme_order_update(struct acme_order *);
|
||
static void acme_order_remove(struct acme_order *, const char *);
|
||
static void acme_order_create(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 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 void acme_challenge_respond_submit(struct acme_sign_op *,
|
||
struct kore_buf *);
|
||
|
||
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 const 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_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);
|
||
|
||
#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 (pledge("stdio inet", NULL) == -1)
|
||
fatal("failed to pledge acme process");
|
||
#endif
|
||
|
||
http_init();
|
||
|
||
if (!kore_quiet) {
|
||
kore_log(LOG_NOTICE,
|
||
"acme worker started (pid#%d)", worker->pid);
|
||
}
|
||
|
||
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);
|
||
|
||
*out = in;
|
||
*outlen = inlen;
|
||
|
||
kore_acme_tls_challenge_use_cert(ssl, dom);
|
||
|
||
return (SSL_TLSEXT_ERR_OK);
|
||
}
|
||
|
||
int
|
||
kore_acme_tls_challenge_selected(SSL *ssl, struct kore_domain *dom)
|
||
{
|
||
unsigned int len;
|
||
const u_int8_t *data;
|
||
|
||
if (dom->acme == 0)
|
||
return (KORE_RESULT_ERROR);
|
||
|
||
if (dom->acme_challenge == 0)
|
||
return (KORE_RESULT_ERROR);
|
||
|
||
SSL_get0_alpn_selected(ssl, &data, &len);
|
||
|
||
if (data == NULL || len != sizeof(acme_alpn_name))
|
||
return (KORE_RESULT_ERROR);
|
||
|
||
if (memcmp(acme_alpn_name, data, sizeof(acme_alpn_name)))
|
||
return (KORE_RESULT_ERROR);
|
||
|
||
return (KORE_RESULT_OK);
|
||
}
|
||
|
||
void
|
||
kore_acme_tls_challenge_use_cert(SSL *ssl, struct kore_domain *dom)
|
||
{
|
||
const unsigned char *ptr;
|
||
X509 *x509;
|
||
|
||
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);
|
||
}
|
||
|
||
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;
|
||
|
||
if (nonce_url == NULL)
|
||
return (NULL);
|
||
|
||
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_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;
|
||
|
||
if (order_url == NULL)
|
||
return;
|
||
|
||
domain = kore_calloc(1, msg->length + 1);
|
||
memcpy(domain, data, msg->length);
|
||
domain[msg->length] = '\0';
|
||
|
||
kore_log(LOG_INFO, "[%s] beginning certificate request", 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);
|
||
|
||
/* no need to free domain, acme_sign_submit takes it over. */
|
||
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;
|
||
struct kore_json_item *item, *array, *final, *status;
|
||
|
||
acme_request_prepare(&req, HTTP_METHOD_POST, order_url,
|
||
payload->data, payload->offset);
|
||
|
||
if (!acme_request_run(&req)) {
|
||
acme_request_cleanup(&req);
|
||
return;
|
||
}
|
||
|
||
if (req.curl.http.status != HTTP_STATUS_CREATED) {
|
||
kore_log(LOG_NOTICE,
|
||
"[%s] - request to '%s' failed: status %ld - body '%s'",
|
||
op->udata, req.curl.url, req.curl.http.status,
|
||
kore_curl_response_as_string(&req.curl));
|
||
acme_request_cleanup(&req);
|
||
return;
|
||
}
|
||
|
||
if (!kore_curl_http_get_header(&req.curl, "location", &header)) {
|
||
kore_log(LOG_NOTICE,
|
||
"[%s] new-order: no order id found", op->udata);
|
||
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,
|
||
"[%s] failed to parse order payload from ACME server (%s)",
|
||
op->udata, 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",
|
||
op->udata);
|
||
goto cleanup;
|
||
}
|
||
|
||
if (TAILQ_EMPTY(&array->data.items)) {
|
||
kore_log(LOG_NOTICE, "[%s] no authoritization URLs in payload",
|
||
op->udata);
|
||
goto cleanup;
|
||
}
|
||
|
||
if ((status = kore_json_find_string(json.root, "status")) == NULL) {
|
||
kore_log(LOG_NOTICE, "[%s] order has no 'status' string",
|
||
op->udata);
|
||
goto cleanup;
|
||
}
|
||
|
||
if ((final = kore_json_find_string(json.root, "finalize")) == NULL) {
|
||
kore_log(LOG_NOTICE, "[%s] order has no 'finalize' string",
|
||
op->udata);
|
||
goto cleanup;
|
||
}
|
||
|
||
if ((stval = acme_status_type(status->data.string)) == -1) {
|
||
kore_log(LOG_NOTICE, "[%s] order has invalid status",
|
||
op->udata);
|
||
goto cleanup;
|
||
}
|
||
|
||
order = kore_calloc(1, sizeof(*order));
|
||
|
||
LIST_INIT(&order->auth);
|
||
|
||
order->status = stval;
|
||
order->start = kore_time_ms();
|
||
order->id = kore_strdup(header);
|
||
order->domain = kore_strdup(op->udata);
|
||
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:
|
||
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;
|
||
struct kore_json_item *status;
|
||
int stval, ret;
|
||
|
||
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;
|
||
ret = KORE_RESULT_OK;
|
||
|
||
cleanup:
|
||
if (ret == KORE_RESULT_ERROR)
|
||
order->state = ACME_ORDER_STATE_ERROR;
|
||
|
||
kore_json_cleanup(&json);
|
||
acme_request_cleanup(&req);
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
switch (order->state) {
|
||
case ACME_ORDER_STATE_RUNNING:
|
||
switch (order->status) {
|
||
case ACME_STATUS_PENDING:
|
||
if ((now - order->start) >= ACME_ORDER_TIMEOUT) {
|
||
acme_order_remove(order, "order ran too long");
|
||
order = NULL;
|
||
break;
|
||
}
|
||
kore_log(LOG_INFO, "[%s] order pending", order->domain);
|
||
if (!acme_order_auth_process(order, order->curauth)) {
|
||
acme_order_remove(order, "cancelled");
|
||
order = NULL;
|
||
}
|
||
break;
|
||
case ACME_STATUS_READY:
|
||
kore_log(LOG_INFO, "[%s] finalizing", order->domain);
|
||
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);
|
||
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_remove(order, "unknown status");
|
||
order = NULL;
|
||
break;
|
||
}
|
||
break;
|
||
case ACME_ORDER_STATE_ERROR:
|
||
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;
|
||
|
||
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);
|
||
|
||
kore_free(order->domain);
|
||
kore_free(order->final);
|
||
kore_free(order->id);
|
||
kore_free(order);
|
||
}
|
||
|
||
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;
|
||
|
||
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 downloaded", 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;
|
||
}
|
||
|
||
if (strcmp(type->data.string, "tls-alpn-01")) {
|
||
kore_log(LOG_INFO,
|
||
"[%s:auth:challenge] unsupported '%s' challenge",
|
||
order->domain, type->data.string);
|
||
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_keyreq kreq;
|
||
char *thumb;
|
||
struct kore_buf buf, auth;
|
||
size_t len, token_len;
|
||
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);
|
||
|
||
len = strlen(order->domain);
|
||
if (len > KORE_DOMAINNAME_LEN)
|
||
fatal("order domain '%s' too long", order->domain);
|
||
|
||
token_len = strlen(challenge->token);
|
||
if (token_len > ACME_CHALLENGE_TOKEN_MAXLEN)
|
||
fatal("token exceeds length limit");
|
||
|
||
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_init(&buf, sizeof(kreq) + sizeof(digest));
|
||
|
||
memset(&kreq, 0, sizeof(kreq));
|
||
kreq.data_len = sizeof(digest);
|
||
|
||
if (kore_strlcpy(kreq.domain, order->domain, sizeof(kreq.domain)) >=
|
||
sizeof(kreq.domain))
|
||
fatal("%s: domain truncated", __func__);
|
||
|
||
kore_buf_append(&buf, &kreq, sizeof(kreq));
|
||
kore_buf_append(&buf, digest, sizeof(digest));
|
||
|
||
kore_msg_send(KORE_WORKER_KEYMGR, KORE_ACME_CHALLENGE_CERT,
|
||
buf.data, buf.offset);
|
||
acme_challenge_respond(order, challenge->url, "tls-alpn-01");
|
||
|
||
kore_buf_cleanup(&auth);
|
||
kore_buf_cleanup(&buf);
|
||
kore_free(thumb);
|
||
}
|
||
|
||
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_challenge_respond_submit);
|
||
kore_json_item_free(json);
|
||
}
|
||
|
||
static void
|
||
acme_challenge_respond_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;
|
||
}
|
||
|
||
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");
|
||
}
|
||
|
||
kore_log(LOG_NOTICE,
|
||
"request to '%s' failed with 400 - detail: %s",
|
||
req->curl.url, detail ? detail->data.string : "none");
|
||
|
||
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, *pay, *protected;
|
||
|
||
kore_buf_init(&buf, 1024);
|
||
kore_json_item_tobuf(json, &buf);
|
||
|
||
if (!kore_base64url_encode(buf.data, buf.offset, &pay, KORE_BASE64_RAW))
|
||
fatal("failed to generate base64url payload");
|
||
|
||
nonce = acme_nonce_fetch();
|
||
protected = acme_protected_component(nonce, url);
|
||
|
||
kore_free(nonce);
|
||
kore_buf_cleanup(&buf);
|
||
|
||
op = kore_calloc(1, sizeof(*op));
|
||
|
||
op->cb = cb;
|
||
op->udata = udata;
|
||
op->payload = pay;
|
||
op->id = signop_id++;
|
||
op->protected = protected;
|
||
LIST_INSERT_HEAD(&signops, op, list);
|
||
|
||
op->timer = kore_timer_add(acme_sign_expire, 5000, op,
|
||
KORE_TIMER_ONESHOT);
|
||
|
||
kore_buf_init(&buf, 1024);
|
||
kore_buf_append(&buf, &op->id, sizeof(op->id));
|
||
kore_buf_appendf(&buf, "%s.%s", protected, pay);
|
||
|
||
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);
|
||
}
|
||
|
||
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->timer);
|
||
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);
|
||
|
||
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);
|
||
|
||
if (!kore_base64url_encode(payload.data,
|
||
payload.offset, &b64, KORE_BASE64_RAW))
|
||
fatal("failed to convert protected payload to b64");
|
||
|
||
kore_json_item_free(root);
|
||
kore_buf_cleanup(&payload);
|
||
|
||
return (b64);
|
||
}
|
||
|
||
static char *
|
||
acme_thumbprint_component(void)
|
||
{
|
||
char *b64;
|
||
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);
|
||
|
||
if (!kore_base64url_encode(digest,
|
||
sizeof(digest), &b64, KORE_BASE64_RAW))
|
||
fatal("failed to convert thumbprint JSON to b64");
|
||
|
||
return (b64);
|
||
}
|
||
|
||
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)
|
||
{
|
||
if (rsakey_e != NULL)
|
||
fatal("RSA exponent already received");
|
||
|
||
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)
|
||
{
|
||
if (rsakey_n != NULL)
|
||
fatal("RSA modulus already received");
|
||
|
||
rsakey_n = kore_calloc(1, msg->length + 1);
|
||
memcpy(rsakey_n, data, msg->length);
|
||
rsakey_n[msg->length] = '\0';
|
||
}
|