1
0
Fork 0
mirror of https://github.com/warmcat/libwebsockets.git synced 2025-03-16 00:00:07 +01:00
libwebsockets/plugins/acme-client/protocol_lws_acme_client.c
Andy Green 21889b53f7 ecdh-es
Mainly JWE support for ecdh-es and initial refactor to support multiple
recipients / signatures.
2018-12-27 06:45:32 +08:00

1625 lines
41 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* libwebsockets ACME client protocol plugin
*
* Copyright (C) 2017 Andy Green <andy@warmcat.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation:
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*
*
* Acme is in a big messy transition at the moment from a homebrewed api
* to an IETF one. The old repo for the homebrew api (they currently
* implement) is marked up as deprecated and "not accurate[ly] reflect[ing]"
* what they implement, but the IETF standard, currently at v7 is not yet
* implemented at let's encrypt (ETA Jan 2018).
*
* This implementation follows draft 7 of the IETF standard, and falls back
* to whatever differences exist for Boulder's tls-sni-01 challenge. The
* tls-sni-02 support is there but nothing to test it against at the time of
* writing (Nov 1 2017).
*/
#if !defined (LWS_PLUGIN_STATIC)
#define LWS_DLL
#define LWS_INTERNAL
#include <libwebsockets.h>
#endif
#include <string.h>
#include <stdlib.h>
typedef enum {
ACME_STATE_DIRECTORY, /* get the directory JSON using GET + parse */
ACME_STATE_NEW_REG, /* register a new RSA key + email combo */
ACME_STATE_NEW_AUTH, /* start the process to request a cert */
ACME_STATE_ACCEPT_CHALL, /* notify server ready for one challenge */
ACME_STATE_POLLING, /* he should be trying our challenge */
ACME_STATE_POLLING_CSR, /* sent CSR, checking result */
ACME_STATE_FINISHED
} lws_acme_state;
struct acme_connection {
char buf[4096];
char replay_nonce[64];
char chall_token[64];
char challenge_uri[256];
char detail[64];
char status[16];
char san_a[100];
char san_b[100];
char urls[6][100]; /* directory contents */
lws_acme_state state;
struct lws_client_connect_info i;
struct lejp_ctx jctx;
struct lws_context_creation_info ci;
struct lws_vhost *vhost;
struct lws *cwsi;
const char *real_vh_name;
const char *real_vh_iface;
char *alloc_privkey_pem;
char *dest;
int pos;
int len;
int resp;
int cpos;
int real_vh_port;
int goes_around;
size_t len_privkey_pem;
unsigned int yes:2;
unsigned int use:1;
unsigned int is_sni_02:1;
};
struct per_vhost_data__lws_acme_client {
struct lws_context *context;
struct lws_vhost *vhost;
const struct lws_protocols *protocol;
/*
* the vhd is allocated for every vhost using the plugin.
* But ac is only allocated when we are doing the server auth.
*/
struct acme_connection *ac;
struct lws_jwk jwk;
struct lws_genrsa_ctx rsactx;
char *pvo_data;
char *pvop[LWS_TLS_TOTAL_COUNT];
const char *pvop_active[LWS_TLS_TOTAL_COUNT];
int count_live_pss;
char *dest;
int pos;
int len;
int fd_updated_cert; /* these are opened while we have root... */
int fd_updated_key; /* ...if nonempty next startup will replace old */
};
static int
callback_acme_client(struct lws *wsi, enum lws_callback_reasons reason,
void *user, void *in, size_t len);
#define LWS_PLUGIN_PROTOCOL_LWS_ACME_CLIENT \
{ \
"lws-acme-client", \
callback_acme_client, \
0, \
512, \
0, NULL, 0 \
}
static const struct lws_protocols acme_protocols[] = {
LWS_PLUGIN_PROTOCOL_LWS_ACME_CLIENT,
{ NULL, NULL, 0, 0, 0, NULL, 0 }
};
/* directory JSON parsing */
static const char * const jdir_tok[] = {
"key-change",
"meta.terms-of-service",
"new-authz",
"new-cert",
"new-reg",
"revoke-cert",
};
enum enum_jhdr_tok {
JAD_KEY_CHANGE_URL,
JAD_TOS_URL,
JAD_NEW_AUTHZ_URL,
JAD_NEW_CERT_URL,
JAD_NEW_REG_URL,
JAD_REVOKE_CERT_URL,
};
static signed char
cb_dir(struct lejp_ctx *ctx, char reason)
{
struct per_vhost_data__lws_acme_client *s =
(struct per_vhost_data__lws_acme_client *)ctx->user;
if (reason == LEJPCB_VAL_STR_START && ctx->path_match) {
s->pos = 0;
s->len = sizeof(s->ac->urls[0]) - 1;
s->dest = s->ac->urls[ctx->path_match - 1];
return 0;
}
if (!(reason & LEJP_FLAG_CB_IS_VALUE) || !ctx->path_match)
return 0;
if (s->pos + ctx->npos > s->len) {
lwsl_notice("url too long\n");
return -1;
}
memcpy(s->dest + s->pos, ctx->buf, ctx->npos);
s->pos += ctx->npos;
s->dest[s->pos] = '\0';
return 0;
}
/* authz JSON parsing */
static const char * const jauthz_tok[] = {
"identifier.type",
"identifier.value",
"status",
"expires",
"challenges[].type",
"challenges[].status",
"challenges[].uri",
"challenges[].token",
"detail"
};
enum enum_jauthz_tok {
JAAZ_ID_TYPE,
JAAZ_ID_VALUE,
JAAZ_STATUS,
JAAZ_EXPIRES,
JAAZ_CHALLENGES_TYPE,
JAAZ_CHALLENGES_STATUS,
JAAZ_CHALLENGES_URI,
JAAZ_CHALLENGES_TOKEN,
JAAZ_DETAIL,
};
static signed char
cb_authz(struct lejp_ctx *ctx, char reason)
{
struct acme_connection *s = (struct acme_connection *)ctx->user;
if (reason == LEJPCB_CONSTRUCTED) {
s->yes = 0;
s->use = 0;
s->chall_token[0] = '\0';
s->is_sni_02 = 0;
}
if (!(reason & LEJP_FLAG_CB_IS_VALUE) || !ctx->path_match)
return 0;
switch (ctx->path_match - 1) {
case JAAZ_ID_TYPE:
break;
case JAAZ_ID_VALUE:
break;
case JAAZ_STATUS:
break;
case JAAZ_EXPIRES:
break;
case JAAZ_DETAIL:
lws_snprintf(s->detail, sizeof(s->detail), "%s", ctx->buf);
break;
case JAAZ_CHALLENGES_TYPE:
if (s->is_sni_02)
break;
s->use = !strcmp(ctx->buf, "tls-sni-01") ||
!strcmp(ctx->buf, "tls-sni-02");
s->is_sni_02 = !strcmp(ctx->buf, "tls-sni-02");
break;
case JAAZ_CHALLENGES_STATUS:
lws_strncpy(s->status, ctx->buf, sizeof(s->status));
break;
case JAAZ_CHALLENGES_URI:
if (s->use) {
lws_strncpy(s->challenge_uri, ctx->buf,
sizeof(s->challenge_uri));
s->yes |= 2;
}
break;
case JAAZ_CHALLENGES_TOKEN:
lwsl_notice("JAAZ_CHALLENGES_TOKEN: %s %d\n", ctx->buf, s->use);
if (s->use) {
lws_strncpy(s->chall_token, ctx->buf,
sizeof(s->chall_token));
s->yes |= 1;
}
break;
}
return 0;
}
/* challenge accepted JSON parsing */
static const char * const jchac_tok[] = {
"type",
"status",
"uri",
"token",
"error.detail"
};
enum enum_jchac_tok {
JCAC_TYPE,
JCAC_STATUS,
JCAC_URI,
JCAC_TOKEN,
JCAC_DETAIL,
};
static signed char
cb_chac(struct lejp_ctx *ctx, char reason)
{
struct acme_connection *s = (struct acme_connection *)ctx->user;
if (reason == LEJPCB_CONSTRUCTED) {
s->yes = 0;
s->use = 0;
}
if (!(reason & LEJP_FLAG_CB_IS_VALUE) || !ctx->path_match)
return 0;
switch (ctx->path_match - 1) {
case JCAC_TYPE:
if (strcmp(ctx->buf, "tls-sni-01") &&
strcmp(ctx->buf, "tls-sni-02"))
return 1;
break;
case JCAC_STATUS:
lws_strncpy(s->status, ctx->buf, sizeof(s->status));
break;
case JCAC_URI:
s->yes |= 2;
break;
case JCAC_TOKEN:
lws_strncpy(s->chall_token, ctx->buf,
sizeof(s->chall_token));
s->yes |= 1;
break;
case JCAC_DETAIL:
lws_snprintf(s->detail, sizeof(s->detail), "%s", ctx->buf);
break;
}
return 0;
}
/* https://github.com/letsencrypt/boulder/blob/release/docs/acme-divergences.md
*
* 7.1:
*
* Boulder does not implement the new-order resource.
* Instead of new-order Boulder implements the new-cert resource that is
* defined in draft-ietf-acme-02 Section 6.5.
*
* Boulder also doesn't implement the new-nonce endpoint.
*
* Boulder implements the new-account resource only under the new-reg key.
*
* Boulder implements Link: rel="next" headers from new-reg to new-authz, and
* new-authz to new-cert, as specified in draft-02, but these links are not
* provided in the latest draft, and clients should use URLs from the directory
* instead.
*
* Boulder does not provide the "index" link relation pointing at the
* directory URL.
*
* (ie, just use new-cert instead of new-order, use the directory for links)
*/
static int
lws_acme_report_status(struct lws_vhost *v, int state, const char *json)
{
lws_callback_vhost_protocols_vhost(v, LWS_CALLBACK_VHOST_CERT_UPDATE,
(void *)json, state);
return 0;
}
/*
* Notice: trashes i and url
*/
static struct lws *
lws_acme_client_connect(struct lws_context *context, struct lws_vhost *vh,
struct lws **pwsi, struct lws_client_connect_info *i,
char *url, const char *method)
{
const char *prot, *p;
char path[200], _url[256];
struct lws *wsi;
memset(i, 0, sizeof(*i));
i->port = 443;
lws_strncpy(_url, url, sizeof(_url));
if (lws_parse_uri(_url, &prot, &i->address, &i->port, &p)) {
lwsl_err("unable to parse uri %s\n", url);
return NULL;
}
/* add back the leading / on path */
path[0] = '/';
lws_strncpy(path + 1, p, sizeof(path) - 1);
i->path = path;
i->context = context;
i->vhost = vh;
i->ssl_connection = 1;
i->host = i->address;
i->origin = i->address;
i->method = method;
i->pwsi = pwsi;
i->protocol = "lws-acme-client";
wsi = lws_client_connect_via_info(i);
if (!wsi) {
lws_snprintf(path, sizeof(path) - 1,
"Unable to connect to %s", url);
lwsl_notice("%s: %s\n", __func__, path);
lws_acme_report_status(vh, LWS_CUS_FAILED, path);
}
return wsi;
}
static void
lws_acme_finished(struct per_vhost_data__lws_acme_client *vhd)
{
lwsl_debug("%s\n", __func__);
if (vhd->ac) {
if (vhd->ac->vhost)
lws_vhost_destroy(vhd->ac->vhost);
if (vhd->ac->alloc_privkey_pem)
free(vhd->ac->alloc_privkey_pem);
free(vhd->ac);
}
lws_genrsa_destroy(&vhd->rsactx);
lws_jwk_destroy(&vhd->jwk);
vhd->ac = NULL;
#if defined(LWS_WITH_ESP32)
lws_esp32.acme = 0; /* enable scanning */
#endif
}
static const char * const pvo_names[] = {
"country",
"state",
"locality",
"organization",
"common-name",
"email",
"directory-url",
"auth-path",
"cert-path",
"key-path",
};
static int
lws_acme_load_create_auth_keys(struct per_vhost_data__lws_acme_client *vhd,
int bits)
{
int n;
if (!lws_jwk_load(&vhd->jwk, vhd->pvop[LWS_TLS_SET_AUTH_PATH],
NULL, NULL))
return 0;
vhd->jwk.kty = LWS_GENCRYPTO_KTY_RSA;
lwsl_notice("Generating ACME %d-bit keypair... "
"will take a little while\n", bits);
n = lws_genrsa_new_keypair(vhd->context, &vhd->rsactx, LGRSAM_PKCS1_1_5,
vhd->jwk.e, bits);
if (n) {
lwsl_notice("failed to create keypair\n");
return 1;
}
lwsl_notice("...keypair generated\n");
if (lws_jwk_save(&vhd->jwk,
vhd->pvop[LWS_TLS_SET_AUTH_PATH])) {
lwsl_notice("unable to save %s\n",
vhd->pvop[LWS_TLS_SET_AUTH_PATH]);
return 1;
}
return 0;
}
static int
lws_acme_start_acquisition(struct per_vhost_data__lws_acme_client *vhd,
struct lws_vhost *v)
{
char buf[128];
/* ...and we were given enough info to do the update? */
if (!vhd->pvop[LWS_TLS_REQ_ELEMENT_COMMON_NAME])
return -1;
/*
* ...well... we should try to do something about it then...
*/
lwsl_notice("%s: ACME cert needs creating / updating: "
"vhost %s\n", __func__, lws_get_vhost_name(vhd->vhost));
vhd->ac = malloc(sizeof(*vhd->ac));
memset(vhd->ac, 0, sizeof(*vhd->ac));
/*
* So if we don't have it, the first job is get the directory.
*
* If we already have the directory, jump straight into trying
* to register our key.
*
* We always try to register the keys... if it's not the first
* time, we will get a JSON body in the (legal, nonfatal)
* response like this
*
* {
* "type": "urn:acme:error:malformed",
* "detail": "Registration key is already in use",
* "status": 409
* }
*/
if (!vhd->ac->urls[0][0]) {
vhd->ac->state = ACME_STATE_DIRECTORY;
lws_snprintf(buf, sizeof(buf) - 1, "%s",
vhd->pvop_active[LWS_TLS_SET_DIR_URL]);
} else {
vhd->ac->state = ACME_STATE_NEW_REG;
lws_snprintf(buf, sizeof(buf) - 1, "%s",
vhd->ac->urls[JAD_NEW_REG_URL]);
}
vhd->ac->real_vh_port = lws_get_vhost_port(vhd->vhost);
vhd->ac->real_vh_name = lws_get_vhost_name(vhd->vhost);
vhd->ac->real_vh_iface = lws_get_vhost_iface(vhd->vhost);
lws_acme_report_status(vhd->vhost, LWS_CUS_STARTING, NULL);
#if defined(LWS_WITH_ESP32)
lws_acme_report_status(vhd->vhost, LWS_CUS_CREATE_KEYS,
"Generating keys, please wait");
if (lws_acme_load_create_auth_keys(vhd, 2048))
goto bail;
lws_acme_report_status(vhd->vhost, LWS_CUS_CREATE_KEYS,
"Auth keys created");
#endif
if (lws_acme_client_connect(vhd->context, vhd->vhost,
&vhd->ac->cwsi, &vhd->ac->i, buf, "GET"))
return 0;
#if defined(LWS_WITH_ESP32)
bail:
#endif
free(vhd->ac);
vhd->ac = NULL;
return 1;
}
static int
callback_acme_client(struct lws *wsi, enum lws_callback_reasons reason,
void *user, void *in, size_t len)
{
struct per_vhost_data__lws_acme_client *vhd =
(struct per_vhost_data__lws_acme_client *)
lws_protocol_vh_priv_get(lws_get_vhost(wsi),
lws_get_protocol(wsi));
char buf[LWS_PRE + 2536], *start = buf + LWS_PRE, *p = start,
*end = buf + sizeof(buf) - 1, digest[32], *failreason = NULL;
const struct lws_protocol_vhost_options *pvo;
struct lws_acme_cert_aging_args *caa;
struct acme_connection *ac = NULL;
struct lws_genhash_ctx hctx;
unsigned char **pp, *pend;
const char *content_type;
struct lws_jwe jwe;
struct lws *cwsi;
int n, m;
if (vhd)
ac = vhd->ac;
lws_jwe_init(&jwe, lws_get_context(wsi));
switch ((int)reason) {
case LWS_CALLBACK_PROTOCOL_INIT:
vhd = lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi),
lws_get_protocol(wsi),
sizeof(struct per_vhost_data__lws_acme_client));
vhd->context = lws_get_context(wsi);
vhd->protocol = lws_get_protocol(wsi);
vhd->vhost = lws_get_vhost(wsi);
/* compute how much we need to hold all the pvo payloads */
m = 0;
pvo = (const struct lws_protocol_vhost_options *)in;
while (pvo) {
m += strlen(pvo->value) + 1;
pvo = pvo->next;
}
p = vhd->pvo_data = malloc(m);
if (!p)
return -1;
pvo = (const struct lws_protocol_vhost_options *)in;
while (pvo) {
start = p;
n = strlen(pvo->value) + 1;
memcpy(start, pvo->value, n);
p += n;
for (m = 0; m < (int)LWS_ARRAY_SIZE(pvo_names); m++)
if (!strcmp(pvo->name, pvo_names[m]))
vhd->pvop[m] = start;
pvo = pvo->next;
}
n = 0;
for (m = 0; m < (int)LWS_ARRAY_SIZE(pvo_names); m++)
if (!vhd->pvop[m] && m >= LWS_TLS_REQ_ELEMENT_COMMON_NAME) {
lwsl_notice("%s: require pvo '%s'\n", __func__,
pvo_names[m]);
n |= 1;
} else
if (vhd->pvop[m])
lwsl_info(" %s: %s\n", pvo_names[m],
vhd->pvop[m]);
if (n) {
free(vhd->pvo_data);
vhd->pvo_data = NULL;
return -1;
}
#if !defined(LWS_WITH_ESP32)
/*
* load (or create) the registration keypair while we
* still have root
*/
if (lws_acme_load_create_auth_keys(vhd, 4096))
return 1;
/*
* in case we do an update, open the update files while we
* still have root
*/
lws_snprintf(buf, sizeof(buf) - 1, "%s.upd",
vhd->pvop[LWS_TLS_SET_CERT_PATH]);
vhd->fd_updated_cert = lws_open(buf, LWS_O_WRONLY | LWS_O_CREAT |
LWS_O_TRUNC, 0600);
if (vhd->fd_updated_cert < 0) {
lwsl_err("unable to create update cert file %s\n", buf);
return -1;
}
lws_snprintf(buf, sizeof(buf) - 1, "%s.upd",
vhd->pvop[LWS_TLS_SET_KEY_PATH]);
vhd->fd_updated_key = lws_open(buf, LWS_O_WRONLY | LWS_O_CREAT |
LWS_O_TRUNC, 0600);
if (vhd->fd_updated_key < 0) {
lwsl_err("unable to create update key file %s\n", buf);
return -1;
}
#endif
break;
case LWS_CALLBACK_PROTOCOL_DESTROY:
if (vhd && vhd->pvo_data) {
free(vhd->pvo_data);
vhd->pvo_data = NULL;
}
if (vhd)
lws_acme_finished(vhd);
break;
case LWS_CALLBACK_VHOST_CERT_AGING:
if (!vhd)
break;
caa = (struct lws_acme_cert_aging_args *)in;
/*
* Somebody is telling us about a cert some vhost is using.
*
* First see if the cert is getting close enough to expiry that
* we *want* to do something about it.
*/
if ((int)(ssize_t)len > 14)
break;
/*
* ...is this a vhost we were configured on?
*/
if (vhd->vhost != caa->vh)
return 1;
for (n = 0; n < (int)LWS_ARRAY_SIZE(vhd->pvop);n++)
if (caa->element_overrides[n])
vhd->pvop_active[n] = caa->element_overrides[n];
else
vhd->pvop_active[n] = vhd->pvop[n];
lwsl_notice("starting acme acquisition on %s: %s\n",
lws_get_vhost_name(caa->vh), vhd->pvop_active[LWS_TLS_SET_DIR_URL]);
lws_acme_start_acquisition(vhd, caa->vh);
break;
/*
* Client
*/
case LWS_CALLBACK_CLIENT_ESTABLISHED:
lwsl_notice("%s: CLIENT_ESTABLISHED\n", __func__);
break;
case LWS_CALLBACK_CLIENT_CONNECTION_ERROR:
lwsl_notice("%s: CLIENT_CONNECTION_ERROR: %p\n", __func__, wsi);
break;
case LWS_CALLBACK_CLOSED_CLIENT_HTTP:
lwsl_notice("%s: CLOSED_CLIENT_HTTP: %p\n", __func__, wsi);
break;
case LWS_CALLBACK_CLOSED:
lwsl_notice("%s: CLOSED: %p\n", __func__, wsi);
break;
case LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP:
lwsl_notice("lws_http_client_http_response %d\n",
lws_http_client_http_response(wsi));
if (!ac)
break;
ac->resp = lws_http_client_http_response(wsi);
/* we get a new nonce each time */
if (lws_hdr_total_length(wsi, WSI_TOKEN_REPLAY_NONCE) &&
lws_hdr_copy(wsi, ac->replay_nonce,
sizeof(ac->replay_nonce),
WSI_TOKEN_REPLAY_NONCE) < 0) {
lwsl_notice("%s: nonce too large\n", __func__);
goto failed;
}
switch (ac->state) {
case ACME_STATE_DIRECTORY:
lejp_construct(&ac->jctx, cb_dir, vhd, jdir_tok,
LWS_ARRAY_SIZE(jdir_tok));
break;
case ACME_STATE_NEW_REG:
break;
case ACME_STATE_NEW_AUTH:
lejp_construct(&ac->jctx, cb_authz, ac, jauthz_tok,
LWS_ARRAY_SIZE(jauthz_tok));
break;
case ACME_STATE_POLLING:
case ACME_STATE_ACCEPT_CHALL:
lejp_construct(&ac->jctx, cb_chac, ac, jchac_tok,
LWS_ARRAY_SIZE(jchac_tok));
break;
case ACME_STATE_POLLING_CSR:
ac->cpos = 0;
if (ac->resp != 201)
break;
/*
* He acknowledges he will create the cert...
* get the URL to GET it from in the Location
* header.
*/
if (lws_hdr_copy(wsi, ac->challenge_uri,
sizeof(ac->challenge_uri),
WSI_TOKEN_HTTP_LOCATION) < 0) {
lwsl_notice("%s: missing cert location:\n",
__func__);
goto failed;
}
lwsl_notice("told to fetch cert from %s\n",
ac->challenge_uri);
break;
default:
break;
}
break;
case LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER:
if (!ac)
break;
switch (ac->state) {
case ACME_STATE_DIRECTORY:
break;
case ACME_STATE_NEW_REG:
p += lws_snprintf(p, end - p, "{"
"\"resource\":\"new-reg\","
"\"contact\":["
"\"mailto:%s\""
"],\"agreement\":\"%s\""
"}",
vhd->pvop_active[LWS_TLS_REQ_ELEMENT_EMAIL],
ac->urls[JAD_TOS_URL]);
puts(start);
pkt_add_hdrs:
if (lws_gencrypto_jwe_alg_to_definition("RSA1_5", &jwe.jose.alg)) {
ac->len = 0;
lwsl_notice("%s: no RSA1_5\n", __func__);
goto failed;
}
jwe.jws.jwk = &vhd->jwk;
ac->len = lws_jwe_create_packet(&jwe,
start, p - start,
ac->replay_nonce,
&ac->buf[LWS_PRE],
sizeof(ac->buf) -
LWS_PRE,
lws_get_context(wsi));
if (ac->len < 0) {
ac->len = 0;
lwsl_notice("lws_jwe_create_packet failed\n");
goto failed;
}
pp = (unsigned char **)in;
pend = (*pp) + len;
ac->pos = 0;
content_type = "application/jose+json";
if (ac->state == ACME_STATE_POLLING_CSR)
content_type = "application/pkix-cert";
if (lws_add_http_header_by_token(wsi,
WSI_TOKEN_HTTP_CONTENT_TYPE,
(uint8_t *)content_type, 21, pp, pend)) {
lwsl_notice("could not add content type\n");
goto failed;
}
n = sprintf(buf, "%d", ac->len);
if (lws_add_http_header_by_token(wsi,
WSI_TOKEN_HTTP_CONTENT_LENGTH,
(uint8_t *)buf, n, pp, pend)) {
lwsl_notice("could not add content length\n");
goto failed;
}
lws_client_http_body_pending(wsi, 1);
lws_callback_on_writable(wsi);
lwsl_notice("prepare to send ACME_STATE_NEW_REG\n");
break;
case ACME_STATE_NEW_AUTH:
p += lws_snprintf(p, end - p,
"{"
"\"resource\":\"new-authz\","
"\"identifier\":{"
"\"type\":\"http-01\","
"\"value\":\"%s\""
"}"
"}", vhd->pvop_active[LWS_TLS_REQ_ELEMENT_COMMON_NAME]);
goto pkt_add_hdrs;
case ACME_STATE_ACCEPT_CHALL:
/*
* Several of the challenges in this document makes use
* of a key authorization string. A key authorization
* expresses a domain holder's authorization for a
* specified key to satisfy a specified challenge, by
* concatenating the token for the challenge with a key
* fingerprint, separated by a "." character:
*
* key-authz = token || '.' ||
* base64(JWK_Thumbprint(accountKey))
*
* The "JWK_Thumbprint" step indicates the computation
* specified in [RFC7638], using the SHA-256 digest. As
* specified in the individual challenges below, the
* token for a challenge is a JSON string comprised
* entirely of characters in the base64 alphabet.
* The "||" operator indicates concatenation of strings.
*
* keyAuthorization (required, string): The key
* authorization for this challenge. This value MUST
* match the token from the challenge and the client's
* account key.
*
* draft acme-01 tls-sni-01:
*
* {
* "keyAuthorization": "evaGxfADs...62jcerQ",
* } (Signed as JWS)
*
* draft acme-07 tls-sni-02:
*
* POST /acme/authz/1234/1
* Host: example.com
* Content-Type: application/jose+json
*
* {
* "protected": base64url({
* "alg": "ES256",
* "kid": "https://example.com/acme/acct/1",
* "nonce": "JHb54aT_KTXBWQOzGYkt9A",
* "url": "https://example.com/acme/authz/1234/1"
* }),
* "payload": base64url({
* "keyAuthorization": "evaGxfADs...62jcerQ"
* }),
* "signature": "Q1bURgJoEslbD1c5...3pYdSMLio57mQNN4"
* }
*
* On receiving a response, the server MUST verify that
* the key authorization in the response matches the
* "token" value in the challenge and the client's
* account key. If they do not match, then the server
* MUST return an HTTP error in response to the POST
* request in which the client sent the challenge.
*/
lws_jwk_rfc7638_fingerprint(&vhd->jwk, digest);
p = start;
end = &buf[sizeof(buf) - 1];
p += lws_snprintf(p, end - p,
"{\"resource\":\"challenge\","
"\"type\":\"tls-sni-0%d\","
"\"keyAuthorization\":\"%s.",
1 + ac->is_sni_02,
ac->chall_token);
n = lws_jws_base64_enc(digest, 32, p, end - p);
if (n < 0)
goto failed;
p += n;
p += lws_snprintf(p, end - p, "\"}");
puts(start);
goto pkt_add_hdrs;
case ACME_STATE_POLLING:
break;
case ACME_STATE_POLLING_CSR:
/*
* "To obtain a certificate for the domain, the agent
* constructs a PKCS#10 Certificate Signing Request that
* asks the Lets Encrypt CA to issue a certificate for
* example.com with a specified public key. As usual,
* the CSR includes a signature by the private key
* corresponding to the public key in the CSR. The agent
* also signs the whole CSR with the authorized
* key for example.com so that the Lets Encrypt CA
* knows its authorized."
*
* IOW we must create a new RSA keypair which will be
* the cert public + private key, and put the public
* key in the CSR. The CSR, just for transport, is also
* signed with our JWK, showing that as the owner of the
* authorized JWK, the request should be allowed.
*
* The cert comes back with our public key in it showing
* that the owner of the matching private key (we
* created that keypair) is the owner of the cert.
*
* We feed the CSR the elements we want in the cert,
* like the CN etc, and it gives us the b64URL-encoded
* CSR and the PEM-encoded (public +)private key in
* memory buffers.
*/
if (ac->goes_around)
break;
p += lws_snprintf(p, end - p,
"{\"resource\":\"new-cert\","
"\"csr\":\"");
n = lws_tls_acme_sni_csr_create(vhd->context,
&vhd->pvop_active[0],
(uint8_t *)p, end - p,
&ac->alloc_privkey_pem,
&ac->len_privkey_pem);
if (n < 0) {
lwsl_notice("CSR generation failed\n");
goto failed;
}
p += n;
p += lws_snprintf(p, end - p, "\"}");
puts(start);
goto pkt_add_hdrs;
default:
break;
}
break;
case LWS_CALLBACK_CLIENT_HTTP_WRITEABLE:
lwsl_notice("LWS_CALLBACK_CLIENT_HTTP_WRITEABLE\n");
if (!ac)
break;
if (ac->pos == ac->len)
break;
ac->buf[LWS_PRE + ac->len] = '\0';
if (lws_write(wsi, (uint8_t *)ac->buf + LWS_PRE,
ac->len, LWS_WRITE_HTTP_FINAL) < 0)
return -1;
lwsl_notice("wrote %d\n", ac->len);
ac->pos = ac->len;
lws_client_http_body_pending(wsi, 0);
break;
/* chunked content */
case LWS_CALLBACK_RECEIVE_CLIENT_HTTP_READ:
if (!ac)
return -1;
switch (ac->state) {
case ACME_STATE_POLLING:
case ACME_STATE_ACCEPT_CHALL:
case ACME_STATE_NEW_AUTH:
case ACME_STATE_DIRECTORY:
((char *)in)[len] = '\0';
puts(in);
m = (int)(signed char)lejp_parse(&ac->jctx,
(uint8_t *)in, len);
if (m < 0 && m != LEJP_CONTINUE) {
lwsl_notice("lejp parse failed %d\n", m);
goto failed;
}
break;
case ACME_STATE_NEW_REG:
((char *)in)[len] = '\0';
puts(in);
break;
case ACME_STATE_POLLING_CSR:
/* it should be the DER cert! */
if (ac->cpos + len > sizeof(ac->buf)) {
lwsl_notice("Incoming cert is too large!\n");
goto failed;
}
memcpy(&ac->buf[ac->cpos], in, len);
ac->cpos += len;
break;
default:
break;
}
break;
/* unchunked content */
case LWS_CALLBACK_RECEIVE_CLIENT_HTTP:
lwsl_notice("%s: LWS_CALLBACK_RECEIVE_CLIENT_HTTP\n", __func__);
{
char buffer[2048 + LWS_PRE];
char *px = buffer + LWS_PRE;
int lenx = sizeof(buffer) - LWS_PRE;
if (lws_http_client_read(wsi, &px, &lenx) < 0)
return -1;
}
break;
case LWS_CALLBACK_COMPLETED_CLIENT_HTTP:
lwsl_notice("%s: COMPLETED_CLIENT_HTTP\n", __func__);
if (!ac)
return -1;
switch (ac->state) {
case ACME_STATE_DIRECTORY:
lejp_destruct(&ac->jctx);
/* check dir validity */
for (n = 0; n < 6; n++)
lwsl_notice(" %d: %s\n", n, ac->urls[n]);
/*
* So... having the directory now... we try to
* register our keys next. It's OK if it ends up
* they're already registered... this eliminates any
* gaps where we stored the key but registration did
* not complete for some reason...
*/
ac->state = ACME_STATE_NEW_REG;
lws_acme_report_status(vhd->vhost, LWS_CUS_REG, NULL);
strcpy(buf, ac->urls[JAD_NEW_REG_URL]);
cwsi = lws_acme_client_connect(vhd->context, vhd->vhost,
&ac->cwsi, &ac->i, buf,
"POST");
if (!cwsi) {
lwsl_notice("%s: failed to connect to acme\n",
__func__);
goto failed;
}
return -1; /* close the completed client connection */
case ACME_STATE_NEW_REG:
if ((ac->resp >= 200 && ac->resp < 299) ||
ac->resp == 409) {
/*
* Our account already existed, or exists now.
*
* Move on to requesting a cert auth.
*/
ac->state = ACME_STATE_NEW_AUTH;
lws_acme_report_status(vhd->vhost, LWS_CUS_AUTH,
NULL);
strcpy(buf, ac->urls[JAD_NEW_AUTHZ_URL]);
cwsi = lws_acme_client_connect(vhd->context,
vhd->vhost, &ac->cwsi,
&ac->i, buf, "POST");
if (!cwsi)
lwsl_notice("%s: failed to connect\n",
__func__);
return -1; /* close the completed client connection */
} else {
lwsl_notice("new-reg replied %d\n", ac->resp);
goto failed;
}
return -1; /* close the completed client connection */
case ACME_STATE_NEW_AUTH:
lejp_destruct(&ac->jctx);
if (ac->resp / 100 == 4) {
lws_snprintf(buf, sizeof(buf),
"Auth failed: %s", ac->detail);
failreason = buf;
lwsl_notice("auth failed\n");
goto failed;
}
lwsl_notice("chall: %s (%d)\n", ac->chall_token, ac->resp);
if (!ac->chall_token[0]) {
lwsl_notice("no challenge\n");
goto failed;
}
ac->state = ACME_STATE_ACCEPT_CHALL;
lws_acme_report_status(vhd->vhost, LWS_CUS_CHALLENGE,
NULL);
/* tls-sni-01 ... what a mess.
* The stuff in
* https://tools.ietf.org/html/
* draft-ietf-acme-acme-01#section-7.3
* "requires" n but it's missing from let's encrypt
* tls-sni-01 challenge. The go docs say that they just
* implement one hashing round regardless
* https://godoc.org/golang.org/x/crypto/acme
*
* The go way is what is actually implemented today by
* letsencrypt
*
* "A client responds to this challenge by constructing
* a key authorization from the "token" value provided
* in the challenge and the client's account key. The
* client first computes the SHA-256 digest Z0 of the
* UTF8-encoded key authorization, and encodes Z0 in
* UTF-8 lower-case hexadecimal form."
*/
/* tls-sni-02
*
* SAN A MUST be constructed as follows: compute the
* SHA-256 digest of the UTF-8-encoded challenge token
* and encode it in lowercase hexadecimal form. The
* dNSName is "x.y.token.acme.invalid", where x
* is the first half of the hexadecimal representation
* and y is the second half.
*/
memset(&ac->ci, 0, sizeof(ac->ci));
/* first compute the key authorization */
lws_jwk_rfc7638_fingerprint(&vhd->jwk, digest);
p = start;
end = &buf[sizeof(buf) - 1];
p += lws_snprintf(p, end - p, "%s.", ac->chall_token);
n = lws_jws_base64_enc(digest, 32, p, end - p);
if (n < 0)
goto failed;
p += n;
if (lws_genhash_init(&hctx, LWS_GENHASH_TYPE_SHA256))
return -1;
if (lws_genhash_update(&hctx, (uint8_t *)start,
lws_ptr_diff(p, start))) {
lws_genhash_destroy(&hctx, NULL);
return -1;
}
if (lws_genhash_destroy(&hctx, digest))
return -1;
p = buf;
for (n = 0; n < 32; n++) {
p += lws_snprintf(p, end - p, "%02x",
digest[n] & 0xff);
if (n == (32 / 2) - 1)
p = buf + 64;
}
p = ac->san_a;
if (ac->is_sni_02) {
lws_snprintf(p, sizeof(ac->san_a),
"%s.%s.token.acme.invalid",
buf, buf + 64);
/*
* SAN B MUST be constructed as follows: compute
* the SHA-256 digest of the UTF-8 encoded key
* authorization and encode it in lowercase
* hexadecimal form. The dNSName is
* "x.y.ka.acme.invalid" where x is the first
* half of the hexadecimal representation and y
* is the second half.
*/
lws_jwk_rfc7638_fingerprint(&vhd->jwk,
(char *)digest);
p = buf;
for (n = 0; n < 32; n++) {
p += lws_snprintf(p, end - p, "%02x",
digest[n] & 0xff);
if (n == (32 / 2) - 1)
p = buf + 64;
}
p = ac->san_b;
lws_snprintf(p, sizeof(ac->san_b),
"%s.%s.ka.acme.invalid",
buf, buf + 64);
} else {
lws_snprintf(p, sizeof(ac->san_a),
"%s.%s.acme.invalid", buf, buf + 64);
ac->san_b[0] = '\0';
}
lwsl_notice("san_a: '%s'\n", ac->san_a);
lwsl_notice("san_b: '%s'\n", ac->san_b);
/*
* tls-sni-01:
*
* The client then configures the TLS server at the
* domain such that when a handshake is initiated with
* the Server Name Indication extension set to
* "<Zi[0:32]>.<Zi[32:64]>.acme.invalid", the
* corresponding generated certificate is presented.
*
* tls-sni-02:
*
* The client MUST ensure that the certificate is
* served to TLS connections specifying a Server Name
* Indication (SNI) value of SAN A.
*/
ac->ci.vhost_name = ac->san_a;
/*
* we bind to exact iface of real vhost, so we can
* share the listen socket by SNI
*/
ac->ci.iface = ac->real_vh_iface;
/* listen on the same port as the vhost that triggered
* us */
ac->ci.port = ac->real_vh_port;
/* Skip filling in any x509 info into the ssl_ctx.
* It will be done at the callback
* LWS_CALLBACK_OPENSSL_LOAD_EXTRA_SERVER_VERIFY_CERTS
* in this callback handler (below)
*/
ac->ci.options = LWS_SERVER_OPTION_CREATE_VHOST_SSL_CTX |
LWS_SERVER_OPTION_SKIP_PROTOCOL_INIT |
LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
/* make ourselves protocols[0] for the new vhost */
ac->ci.protocols = acme_protocols;
/*
* vhost .user points to the ac associated with the
* temporary vhost
*/
ac->ci.user = ac;
ac->vhost = lws_create_vhost(lws_get_context(wsi),
&ac->ci);
if (!ac->vhost)
goto failed;
/*
* The challenge-specific vhost is up... let the ACME
* server know we are ready to roll...
*/
ac->goes_around = 0;
cwsi = lws_acme_client_connect(vhd->context, vhd->vhost,
&ac->cwsi, &ac->i,
ac->challenge_uri,
"POST");
if (!cwsi) {
lwsl_notice("%s: failed to connect\n",
__func__);
goto failed;
}
return -1; /* close the completed client connection */
case ACME_STATE_ACCEPT_CHALL:
/*
* he returned something like this (which we parsed)
*
* {
* "type": "tls-sni-01",
* "status": "pending",
* "uri": "https://acme-staging.api.letsencrypt.org/
* acme/challenge/xCt7bT3FaxoIQU3Qry87t5h
* uKDcC-L-0ERcD5DLAZts/71100507",
* "token": "j2Vs-vLI_dsza4A35SFHIU03aIe2PzFRijbqCY
* dIVeE",
* "keyAuthorization": "j2Vs-vLI_dsza4A35SFHIU03aIe2
* PzFRijbqCYdIVeE.nmOtdFd8Jikn6K8NnYYmT5
* vCM_PwSDT8nLdOYoFXhRU"
* }
*
*/
lwsl_notice("%s: COMPLETED accept chall: %s\n",
__func__, ac->challenge_uri);
poll_again:
ac->state = ACME_STATE_POLLING;
lws_acme_report_status(vhd->vhost, LWS_CUS_CHALLENGE, NULL);
if (ac->goes_around++ == 20) {
lwsl_notice("%s: too many chall retries\n",
__func__);
goto failed;
}
lws_timed_callback_vh_protocol(vhd->vhost, vhd->protocol,
LWS_CALLBACK_USER + 0xac33, ac->goes_around == 1 ? 10 : 2);
return -1; /* close the completed client connection */
case ACME_STATE_POLLING:
if (ac->resp == 202 &&
strcmp(ac->status, "invalid") &&
strcmp(ac->status, "valid")) {
lwsl_notice("status: %s\n", ac->status);
goto poll_again;
}
if (!strcmp(ac->status, "invalid")) {
lwsl_notice("%s: polling failed\n", __func__);
lws_snprintf(buf, sizeof(buf),
"Challenge Invalid: %s", ac->detail);
failreason = buf;
goto failed;
}
lwsl_notice("Challenge passed\n");
/*
* The challenge was validated... so delete the
* temp SNI vhost now its job is done
*/
if (ac->vhost)
lws_vhost_destroy(ac->vhost);
ac->vhost = NULL;
/*
* now our JWK is accepted as authorized to make
* requests for the domain, next move is create the
* CSR signed with the JWK, and send it to the ACME
* server to request the actual certs.
*/
ac->state = ACME_STATE_POLLING_CSR;
lws_acme_report_status(vhd->vhost, LWS_CUS_REQ, NULL);
ac->goes_around = 0;
strcpy(buf, ac->urls[JAD_NEW_CERT_URL]);
cwsi = lws_acme_client_connect(vhd->context, vhd->vhost,
&ac->cwsi, &ac->i, buf,
"POST");
if (!cwsi) {
lwsl_notice("%s: failed to connect to acme\n",
__func__);
goto failed;
}
return -1; /* close the completed client connection */
case ACME_STATE_POLLING_CSR:
/*
* (after POSTing the CSR)...
*
* If the CA decides to issue a certificate, then the
* server creates a new certificate resource and
* returns a URI for it in the Location header field
* of a 201 (Created) response.
*
* HTTP/1.1 201 Created
* Location: https://example.com/acme/cert/asdf
*
* If the certificate is available at the time of the
* response, it is provided in the body of the response.
* If the CA has not yet issued the certificate, the
* body of this response will be empty. The client
* should then send a GET request to the certificate URI
* to poll for the certificate. As long as the
* certificate is unavailable, the server MUST provide a
* 202 (Accepted) response and include a Retry-After
* header to indicate when the server believes the
* certificate will be issued.
*/
if (ac->resp < 200 || ac->resp > 202) {
lwsl_notice("CSR poll failed on resp %d\n",
ac->resp);
goto failed;
}
if (ac->resp == 200) {
char *pp;
int max;
lwsl_notice("The cert was sent..\n");
lws_acme_report_status(vhd->vhost,
LWS_CUS_ISSUE, NULL);
/*
* That means we have the issued cert DER in
* ac->buf, length in ac->cpos; and the key in
* ac->alloc_privkey_pem, length in
* ac->len_privkey_pem.
*
* We write out a PEM copy of the cert, and a
* PEM copy of the private key, using the
* write-only fds we opened while we still
* had root.
*
* Estimate the size of the PEM version of the
* cert and allocate a temp buffer for it.
*
* This is a bit complicated because first we
* drop the b64url version into the buffer at
* +384, then we add the header at 0 and move
* lines of it back + '\n' to make PEM.
*
* This avoids the need for two fullsize
* allocations.
*/
max = (ac->cpos * 4) / 3 + 16 + 384;
start = p = malloc(max);
if (!p)
goto failed;
n = lws_b64_encode_string(ac->buf, ac->cpos,
start + 384, max - 384);
if (n < 0) {
free(start);
goto failed;
}
pp = start + 384;
p += lws_snprintf(start, 64, "%s",
"-----BEGIN CERTIFICATE-----\n");
while (n) {
m = 65;
if (n < m)
m = n;
memcpy(p, pp, m);
n -= m;
p += m;
pp += m;
if (n)
*p++ = '\n';
}
p += lws_snprintf(p,
max - lws_ptr_diff(p, start),
"%s",
"\n-----END CERTIFICATE-----\n");
n = lws_plat_write_cert(vhd->vhost, 0,
vhd->fd_updated_cert, start,
lws_ptr_diff(p, start));
free(start);
if (n) {
lwsl_err("unable to write ACME cert! %d\n", n);
goto failed;
}
/*
* don't close it... we may update the certs
* again
*/
if (lws_plat_write_cert(vhd->vhost, 1,
vhd->fd_updated_key,
ac->alloc_privkey_pem,
ac->len_privkey_pem)) {
lwsl_err("unable to write ACME key!\n");
goto failed;
}
/*
* we have written the persistent copies
*/
lwsl_notice("%s: Updated certs written for %s "
"to %s.upd and %s.upd\n", __func__,
vhd->pvop_active[LWS_TLS_REQ_ELEMENT_COMMON_NAME],
vhd->pvop_active[LWS_TLS_SET_CERT_PATH],
vhd->pvop_active[LWS_TLS_SET_KEY_PATH]);
/* notify lws there was a cert update */
if (lws_tls_cert_updated(vhd->context,
vhd->pvop_active[LWS_TLS_SET_CERT_PATH],
vhd->pvop_active[LWS_TLS_SET_KEY_PATH],
ac->buf, ac->cpos,
ac->alloc_privkey_pem,
ac->len_privkey_pem)) {
lwsl_notice("problem setting certs\n");
}
lws_acme_finished(vhd);
lws_acme_report_status(vhd->vhost,
LWS_CUS_SUCCESS, NULL);
return 0;
}
lws_acme_report_status(vhd->vhost, LWS_CUS_CONFIRM, NULL);
/* he is preparing the cert, go again with a GET */
if (ac->goes_around++ == 30) {
lwsl_notice("%s: too many retries\n",
__func__);
goto failed;
}
strcpy(buf, ac->challenge_uri);
cwsi = lws_acme_client_connect(vhd->context, vhd->vhost,
&ac->cwsi, &ac->i, buf,
"GET");
if (!cwsi) {
lwsl_notice("%s: failed to connect to acme\n",
__func__);
goto failed;
}
return -1; /* close the completed client connection */
default:
break;
}
break;
case LWS_CALLBACK_USER + 0xac33:
if (!vhd)
break;
cwsi = lws_acme_client_connect(vhd->context, vhd->vhost,
&ac->cwsi, &ac->i,
ac->challenge_uri,
"GET");
if (!cwsi) {
lwsl_notice("%s: failed to connect\n", __func__);
goto failed;
}
break;
case LWS_CALLBACK_OPENSSL_LOAD_EXTRA_SERVER_VERIFY_CERTS:
/*
* This goes to vhost->protocols[0], but for our temp certs
* vhost we created, we have arranged that to be our protocol,
* so the callback will come here.
*
* When we created the temp vhost, we set its pvo to point
* to the ac associated with the temp vhost.
*/
lwsl_debug("LWS_CALLBACK_OPENSSL_LOAD_EXTRA_SERVER_VERIFY_CERTS\n");
ac = (struct acme_connection *)lws_get_vhost_user(
(struct lws_vhost *)in);
lws_acme_report_status((struct lws_vhost *)in,
LWS_CUS_CREATE_REQ,
"creating challenge cert");
if (lws_tls_acme_sni_cert_create((struct lws_vhost *)in,
ac->san_a, ac->san_b)) {
lwsl_err("%s: creating the sni test cert failed\n", __func__);
return -1;
}
break;
default:
break;
}
return 0;
failed:
lwsl_err("%s: failed out\n", __func__);
lws_acme_report_status(vhd->vhost, LWS_CUS_FAILED, failreason);
lws_acme_finished(vhd);
return -1;
}
#if !defined (LWS_PLUGIN_STATIC)
static const struct lws_protocols protocols[] = {
LWS_PLUGIN_PROTOCOL_LWS_ACME_CLIENT
};
LWS_EXTERN LWS_VISIBLE int
init_protocol_lws_acme_client(struct lws_context *context,
struct lws_plugin_capability *c)
{
if (c->api_magic != LWS_PLUGIN_API_MAGIC) {
lwsl_err("Plugin API %d, library API %d", LWS_PLUGIN_API_MAGIC,
c->api_magic);
return 1;
}
c->protocols = protocols;
c->count_protocols = LWS_ARRAY_SIZE(protocols);
c->extensions = NULL;
c->count_extensions = 0;
return 0;
}
LWS_EXTERN LWS_VISIBLE int
destroy_protocol_lws_acme_client(struct lws_context *context)
{
return 0;
}
#endif