mirror of
https://github.com/warmcat/libwebsockets.git
synced 2025-03-16 00:00:07 +01:00

lejp_parse() return type is an int... but in the function, the temp for it is a char. This leads to badness that is currently worked around by casting the return through a signed char type. But that leads to more badness since if there's >127 bytes of buffer left after the end of the JSON object, we misreport it. Bite the bullet and fix the temp type, and fix up all the guys who were working around it at the caller return casting to use the resulting straight int. If you are using this api, remove any casting you may have cut- and-pasted like this n = (int)(signed char)lejp_parse(...); ... to just be like this... n = lejp_parse(...);
1657 lines
39 KiB
C
1657 lines
39 KiB
C
/*
|
|
* libwebsockets ACME client protocol plugin
|
|
*
|
|
* Copyright (C) 2010 - 2019 Andy Green <andy@warmcat.com>
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to
|
|
* deal in the Software without restriction, including without limitation the
|
|
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
* sell copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
* IN THE SOFTWARE.
|
|
*
|
|
* 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_NONCE, /* get the replay nonce */
|
|
ACME_STATE_NEW_ACCOUNT, /* register a new RSA key + email combo */
|
|
ACME_STATE_NEW_ORDER, /* start the process to request a cert */
|
|
ACME_STATE_AUTHZ, /* */
|
|
ACME_STATE_START_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_DOWNLOAD_CERT,
|
|
|
|
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 key_auth[256];
|
|
char http01_mountpoint[256];
|
|
struct lws_http_mount mount;
|
|
char urls[6][100]; /* directory contents */
|
|
char active_url[100];
|
|
char authz_url[100];
|
|
char order_url[100];
|
|
char finalize_url[100];
|
|
char cert_url[100];
|
|
char acct_id[100];
|
|
char *kid;
|
|
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_chall_http01(struct lws *wsi, enum lws_callback_reasons reason,
|
|
void *user, void *in, size_t len)
|
|
{
|
|
struct lws_vhost *vhost = lws_get_vhost(wsi);
|
|
struct acme_connection *ac = lws_vhost_user(vhost);
|
|
uint8_t buf[LWS_PRE + 2048], *start = &buf[LWS_PRE], *p = start,
|
|
*end = &buf[sizeof(buf) - LWS_PRE - 1];
|
|
int n;
|
|
|
|
switch (reason) {
|
|
case LWS_CALLBACK_HTTP:
|
|
lwsl_notice("%s: ca connection received, key_auth %s\n",
|
|
__func__, ac->key_auth);
|
|
|
|
if (lws_add_http_header_status(wsi, HTTP_STATUS_OK, &p, end)) {
|
|
lwsl_notice("%s: add status failed\n", __func__);
|
|
return -1;
|
|
}
|
|
|
|
if (lws_add_http_header_by_token(wsi,
|
|
WSI_TOKEN_HTTP_CONTENT_TYPE,
|
|
(unsigned char *)"text/plain", 10,
|
|
&p, end)) {
|
|
lwsl_notice("%s: add content_type failed\n", __func__);
|
|
return -1;
|
|
}
|
|
|
|
n = strlen(ac->key_auth);
|
|
if (lws_add_http_header_content_length(wsi, n, &p, end)) {
|
|
lwsl_notice("%s: add content_length failed\n",
|
|
__func__);
|
|
return -1;
|
|
}
|
|
|
|
if (lws_add_http_header_by_token(wsi,
|
|
WSI_TOKEN_HTTP_CONTENT_DISPOSITION,
|
|
(unsigned char *)"attachment", 10,
|
|
&p, end)) {
|
|
lwsl_notice("%s: add content_dispo failed\n", __func__);
|
|
return -1;
|
|
}
|
|
|
|
if (lws_finalize_write_http_header(wsi, start, &p, end)) {
|
|
lwsl_notice("%s: finalize http header failed\n",
|
|
__func__);
|
|
return -1;
|
|
}
|
|
|
|
lws_callback_on_writable(wsi);
|
|
return 0;
|
|
|
|
case LWS_CALLBACK_HTTP_WRITEABLE:
|
|
p += lws_snprintf((char *)p, end - p, "%s", ac->key_auth);
|
|
lwsl_notice("%s: len %d\n", __func__, lws_ptr_diff(p, start));
|
|
if (lws_write(wsi, (uint8_t *)start, lws_ptr_diff(p, start),
|
|
LWS_WRITE_HTTP_FINAL) != lws_ptr_diff(p, start)) {
|
|
lwsl_err("_write content failed\n");
|
|
return 1;
|
|
}
|
|
|
|
if (lws_http_transaction_completed(wsi))
|
|
return -1;
|
|
|
|
return 0;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return lws_callback_http_dummy(wsi, reason, user, in, len);
|
|
}
|
|
|
|
static const struct lws_protocols chall_http01_protocols[] = {
|
|
{ "http", callback_chall_http01, 0, 0, 0, NULL, 0 },
|
|
{ NULL, NULL, 0, 0, 0, NULL, 0 }
|
|
};
|
|
|
|
static int
|
|
jws_create_packet(struct lws_jwe *jwe, const char *payload, size_t len,
|
|
const char *nonce, const char *url, const char *kid,
|
|
char *out, size_t out_len, struct lws_context *context)
|
|
{
|
|
char *buf, *start, *p, *end, *p1, *end1;
|
|
struct lws_jws jws;
|
|
int n, m;
|
|
|
|
lws_jws_init(&jws, &jwe->jwk, context);
|
|
|
|
/*
|
|
* This buffer is local to the function, the actual output is prepared
|
|
* into out. Only the plaintext protected header
|
|
* (which contains the public key, 512 bytes for 4096b) goes in
|
|
* here temporarily.
|
|
*/
|
|
n = LWS_PRE + 2048;
|
|
buf = malloc(n);
|
|
if (!buf) {
|
|
lwsl_notice("%s: malloc %d failed\n", __func__, n);
|
|
return -1;
|
|
}
|
|
|
|
p = start = buf + LWS_PRE;
|
|
end = buf + n - LWS_PRE - 1;
|
|
|
|
/*
|
|
* temporary JWS protected header plaintext
|
|
*/
|
|
if (!jwe->jose.alg || !jwe->jose.alg->alg)
|
|
goto bail;
|
|
|
|
p += lws_snprintf(p, end - p, "{\"alg\":\"RS256\"");
|
|
if (kid)
|
|
p += lws_snprintf(p, end - p, ",\"kid\":\"%s\"", kid);
|
|
else {
|
|
p += lws_snprintf(p, end - p, ",\"jwk\":");
|
|
m = end - p;
|
|
n = lws_jwk_export(&jwe->jwk, 0, p, &m);
|
|
if (n < 0) {
|
|
lwsl_notice("failed to export jwk\n");
|
|
goto bail;
|
|
}
|
|
p += n;
|
|
}
|
|
p += lws_snprintf(p, end - p, ",\"url\":\"%s\"", url);
|
|
p += lws_snprintf(p, end - p, ",\"nonce\":\"%s\"}", nonce);
|
|
|
|
/*
|
|
* prepare the signed outer JSON with all the parts in
|
|
*/
|
|
p1 = out;
|
|
end1 = out + out_len - 1;
|
|
|
|
p1 += lws_snprintf(p1, end1 - p1, "{\"protected\":\"");
|
|
jws.map_b64.buf[LJWS_JOSE] = p1;
|
|
n = lws_jws_base64_enc(start, p - start, p1, end1 - p1);
|
|
if (n < 0) {
|
|
lwsl_notice("%s: failed to encode protected\n", __func__);
|
|
goto bail;
|
|
}
|
|
jws.map_b64.len[LJWS_JOSE] = n;
|
|
p1 += n;
|
|
|
|
p1 += lws_snprintf(p1, end1 - p1, "\",\"payload\":\"");
|
|
jws.map_b64.buf[LJWS_PYLD] = p1;
|
|
n = lws_jws_base64_enc(payload, len, p1, end1 - p1);
|
|
if (n < 0) {
|
|
lwsl_notice("%s: failed to encode payload\n", __func__);
|
|
goto bail;
|
|
}
|
|
jws.map_b64.len[LJWS_PYLD] = n;
|
|
p1 += n;
|
|
|
|
p1 += lws_snprintf(p1, end1 - p1, "\",\"signature\":\"");
|
|
|
|
/*
|
|
* taking the b64 protected header and the b64 payload, sign them
|
|
* and place the signature into the packet
|
|
*/
|
|
n = lws_jws_sign_from_b64(&jwe->jose, &jws, p1, end1 - p1);
|
|
if (n < 0) {
|
|
lwsl_notice("sig gen failed\n");
|
|
|
|
goto bail;
|
|
}
|
|
jws.map_b64.buf[LJWS_SIG] = p1;
|
|
jws.map_b64.len[LJWS_SIG] = n;
|
|
|
|
p1 += n;
|
|
p1 += lws_snprintf(p1, end1 - p1, "\"}");
|
|
|
|
free(buf);
|
|
|
|
return p1 - out;
|
|
|
|
bail:
|
|
lws_jws_destroy(&jws);
|
|
free(buf);
|
|
|
|
return -1;
|
|
}
|
|
|
|
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 \
|
|
}
|
|
|
|
/* directory JSON parsing */
|
|
|
|
static const char * const jdir_tok[] = {
|
|
"keyChange",
|
|
"meta.termsOfService",
|
|
"newAccount",
|
|
"newNonce",
|
|
"newOrder",
|
|
"revokeCert",
|
|
};
|
|
|
|
enum enum_jdir_tok {
|
|
JAD_KEY_CHANGE_URL,
|
|
JAD_TOS_URL,
|
|
JAD_NEW_ACCOUNT_URL,
|
|
JAD_NEW_NONCE_URL,
|
|
JAD_NEW_ORDER_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;
|
|
}
|
|
|
|
|
|
/* order JSON parsing */
|
|
|
|
static const char * const jorder_tok[] = {
|
|
"status",
|
|
"expires",
|
|
"identifiers[].type",
|
|
"identifiers[].value",
|
|
"authorizations",
|
|
"finalize",
|
|
"certificate"
|
|
};
|
|
|
|
enum enum_jorder_tok {
|
|
JAO_STATUS,
|
|
JAO_EXPIRES,
|
|
JAO_IDENTIFIERS_TYPE,
|
|
JAO_IDENTIFIERS_VALUE,
|
|
JAO_AUTHORIZATIONS,
|
|
JAO_FINALIZE,
|
|
JAO_CERT
|
|
};
|
|
|
|
static signed char
|
|
cb_order(struct lejp_ctx *ctx, char reason)
|
|
{
|
|
struct acme_connection *s = (struct acme_connection *)ctx->user;
|
|
|
|
if (reason == LEJPCB_CONSTRUCTED)
|
|
s->authz_url[0] = '\0';
|
|
|
|
if (!(reason & LEJP_FLAG_CB_IS_VALUE) || !ctx->path_match)
|
|
return 0;
|
|
|
|
switch (ctx->path_match - 1) {
|
|
case JAO_STATUS:
|
|
lws_strncpy(s->status, ctx->buf, sizeof(s->status));
|
|
break;
|
|
case JAO_EXPIRES:
|
|
break;
|
|
case JAO_IDENTIFIERS_TYPE:
|
|
break;
|
|
case JAO_IDENTIFIERS_VALUE:
|
|
break;
|
|
case JAO_AUTHORIZATIONS:
|
|
lws_snprintf(s->authz_url, sizeof(s->authz_url), "%s",
|
|
ctx->buf);
|
|
break;
|
|
case JAO_FINALIZE:
|
|
lws_snprintf(s->finalize_url, sizeof(s->finalize_url), "%s",
|
|
ctx->buf);
|
|
break;
|
|
case JAO_CERT:
|
|
lws_snprintf(s->cert_url, sizeof(s->cert_url), "%s", ctx->buf);
|
|
break;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* authz JSON parsing */
|
|
|
|
static const char * const jauthz_tok[] = {
|
|
"identifier.type",
|
|
"identifier.value",
|
|
"status",
|
|
"expires",
|
|
"challenges[].type",
|
|
"challenges[].status",
|
|
"challenges[].url",
|
|
"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_URL,
|
|
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';
|
|
}
|
|
|
|
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:
|
|
lwsl_notice("JAAZ_CHALLENGES_TYPE: %s\n", ctx->buf);
|
|
s->use = !strcmp(ctx->buf, "http-01");
|
|
break;
|
|
case JAAZ_CHALLENGES_STATUS:
|
|
lws_strncpy(s->status, ctx->buf, sizeof(s->status));
|
|
break;
|
|
case JAAZ_CHALLENGES_URL:
|
|
lwsl_notice("JAAZ_CHALLENGES_URL: %s %d\n", ctx->buf, s->use);
|
|
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, "http-01"))
|
|
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;
|
|
}
|
|
|
|
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 = LCCSCF_USE_SSL;
|
|
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_notice("%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",
|
|
"subject-alt-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_ACCOUNT;
|
|
lws_snprintf(buf, sizeof(buf) - 1, "%s",
|
|
vhd->ac->urls[JAD_NEW_ACCOUNT_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;
|
|
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 &&
|
|
m != LWS_TLS_REQ_ELEMENT_SUBJECT_ALT_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("%s: ESTABLISHED_CLIENT_HTTP:"
|
|
"%p, state:%d, status:%d\n", __func__, wsi,
|
|
ac->state, 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_NONCE:
|
|
/*
|
|
* 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_ACCOUNT;
|
|
lws_acme_report_status(vhd->vhost, LWS_CUS_REG, NULL);
|
|
|
|
strcpy(buf, ac->urls[JAD_NEW_ACCOUNT_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;
|
|
|
|
case ACME_STATE_NEW_ACCOUNT:
|
|
if (!lws_hdr_total_length(wsi,
|
|
WSI_TOKEN_HTTP_LOCATION)) {
|
|
lwsl_notice("%s: no Location\n", __func__);
|
|
goto failed;
|
|
}
|
|
|
|
if (lws_hdr_copy(wsi, ac->acct_id, sizeof(ac->acct_id),
|
|
WSI_TOKEN_HTTP_LOCATION) < 0) {
|
|
lwsl_notice("%s: Location too large\n",
|
|
__func__);
|
|
goto failed;
|
|
}
|
|
|
|
ac->kid = ac->acct_id;
|
|
|
|
lwsl_notice("Location: %s\n", ac->acct_id);
|
|
break;
|
|
|
|
case ACME_STATE_NEW_ORDER:
|
|
if (lws_hdr_copy(wsi, ac->order_url,
|
|
sizeof(ac->order_url),
|
|
WSI_TOKEN_HTTP_LOCATION) < 0) {
|
|
lwsl_notice("%s: missing cert location:\n",
|
|
__func__);
|
|
|
|
goto failed;
|
|
}
|
|
|
|
lejp_construct(&ac->jctx, cb_order, ac, jorder_tok,
|
|
LWS_ARRAY_SIZE(jorder_tok));
|
|
break;
|
|
|
|
case ACME_STATE_AUTHZ:
|
|
lejp_construct(&ac->jctx, cb_authz, ac, jauthz_tok,
|
|
LWS_ARRAY_SIZE(jauthz_tok));
|
|
break;
|
|
|
|
case ACME_STATE_START_CHALL:
|
|
lejp_construct(&ac->jctx, cb_chac, ac, jchac_tok,
|
|
LWS_ARRAY_SIZE(jchac_tok));
|
|
break;
|
|
|
|
case ACME_STATE_POLLING:
|
|
case ACME_STATE_POLLING_CSR:
|
|
lejp_construct(&ac->jctx, cb_order, ac, jorder_tok,
|
|
LWS_ARRAY_SIZE(jorder_tok));
|
|
break;
|
|
|
|
case ACME_STATE_DOWNLOAD_CERT:
|
|
ac->cpos = 0;
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER:
|
|
if (!ac)
|
|
break;
|
|
|
|
switch (ac->state) {
|
|
case ACME_STATE_DIRECTORY:
|
|
case ACME_STATE_NEW_NONCE:
|
|
break;
|
|
|
|
case ACME_STATE_NEW_ACCOUNT:
|
|
p += lws_snprintf(p, end - p, "{"
|
|
"\"termsOfServiceAgreed\":true"
|
|
",\"contact\": [\"mailto:%s\"]}",
|
|
vhd->pvop_active[LWS_TLS_REQ_ELEMENT_EMAIL]);
|
|
|
|
puts(start);
|
|
strcpy(ac->active_url, ac->urls[JAD_NEW_ACCOUNT_URL]);
|
|
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.jwk = vhd->jwk;
|
|
|
|
ac->len = jws_create_packet(&jwe,
|
|
start, p - start,
|
|
ac->replay_nonce,
|
|
ac->active_url,
|
|
ac->kid,
|
|
&ac->buf[LWS_PRE],
|
|
sizeof(ac->buf) - LWS_PRE,
|
|
lws_get_context(wsi));
|
|
if (ac->len < 0) {
|
|
ac->len = 0;
|
|
lwsl_notice("jws_create_packet failed\n");
|
|
goto failed;
|
|
}
|
|
|
|
pp = (unsigned char **)in;
|
|
pend = (*pp) + len;
|
|
|
|
ac->pos = 0;
|
|
content_type = "application/jose+json";
|
|
|
|
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);
|
|
break;
|
|
|
|
case ACME_STATE_NEW_ORDER:
|
|
p += lws_snprintf(p, end - p,
|
|
"{"
|
|
"\"identifiers\":[{"
|
|
"\"type\":\"dns\","
|
|
"\"value\":\"%s\""
|
|
"}]"
|
|
"}",
|
|
vhd->pvop_active[LWS_TLS_REQ_ELEMENT_COMMON_NAME]);
|
|
|
|
puts(start);
|
|
strcpy(ac->active_url, ac->urls[JAD_NEW_ORDER_URL]);
|
|
goto pkt_add_hdrs;
|
|
|
|
case ACME_STATE_AUTHZ:
|
|
puts(start);
|
|
strcpy(ac->active_url, ac->authz_url);
|
|
goto pkt_add_hdrs;
|
|
|
|
case ACME_STATE_START_CHALL:
|
|
p = start;
|
|
end = &buf[sizeof(buf) - 1];
|
|
|
|
p += lws_snprintf(p, end - p, "{}");
|
|
puts(start);
|
|
strcpy(ac->active_url, ac->challenge_uri);
|
|
goto pkt_add_hdrs;
|
|
|
|
case ACME_STATE_POLLING:
|
|
strcpy(ac->active_url, ac->order_url);
|
|
goto pkt_add_hdrs;
|
|
|
|
case ACME_STATE_POLLING_CSR:
|
|
if (ac->goes_around)
|
|
break;
|
|
|
|
p += lws_snprintf(p, end - p, "{\"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);
|
|
strcpy(ac->active_url, ac->finalize_url);
|
|
goto pkt_add_hdrs;
|
|
|
|
case ACME_STATE_DOWNLOAD_CERT:
|
|
strcpy(ac->active_url, ac->cert_url);
|
|
goto pkt_add_hdrs;
|
|
break;
|
|
|
|
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_CSR:
|
|
case ACME_STATE_POLLING:
|
|
case ACME_STATE_START_CHALL:
|
|
case ACME_STATE_AUTHZ:
|
|
case ACME_STATE_NEW_ORDER:
|
|
case ACME_STATE_DIRECTORY:
|
|
((char *)in)[len] = '\0';
|
|
puts(in);
|
|
m = 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_ACCOUNT:
|
|
((char *)in)[len] = '\0';
|
|
puts(in);
|
|
break;
|
|
case ACME_STATE_DOWNLOAD_CERT:
|
|
((char *)in)[len] = '\0';
|
|
puts(in);
|
|
/* 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__);
|
|
if (!ac)
|
|
return -1;
|
|
switch (ac->state) {
|
|
default:
|
|
{
|
|
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;
|
|
}
|
|
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]);
|
|
|
|
ac->state = ACME_STATE_NEW_NONCE;
|
|
|
|
strcpy(buf, ac->urls[JAD_NEW_NONCE_URL]);
|
|
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 */
|
|
|
|
case ACME_STATE_NEW_ACCOUNT:
|
|
if ((ac->resp >= 200 && ac->resp < 299) ||
|
|
ac->resp == 409) {
|
|
/*
|
|
* Our account already existed, or exists now.
|
|
*
|
|
*/
|
|
ac->state = ACME_STATE_NEW_ORDER;
|
|
|
|
strcpy(buf, ac->urls[JAD_NEW_ORDER_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__);
|
|
|
|
/* close the completed client connection */
|
|
return -1;
|
|
} else {
|
|
lwsl_notice("newAccount replied %d\n",
|
|
ac->resp);
|
|
goto failed;
|
|
}
|
|
return -1; /* close the completed client connection */
|
|
|
|
case ACME_STATE_NEW_ORDER:
|
|
lejp_destruct(&ac->jctx);
|
|
if (!ac->authz_url[0]) {
|
|
lwsl_notice("no authz\n");
|
|
goto failed;
|
|
}
|
|
|
|
/*
|
|
* Move on to requesting a cert auth.
|
|
*/
|
|
ac->state = ACME_STATE_AUTHZ;
|
|
lws_acme_report_status(vhd->vhost, LWS_CUS_AUTH,
|
|
NULL);
|
|
|
|
strcpy(buf, ac->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 */
|
|
|
|
case ACME_STATE_AUTHZ:
|
|
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_START_CHALL;
|
|
lws_acme_report_status(vhd->vhost, LWS_CUS_CHALLENGE,
|
|
NULL);
|
|
|
|
memset(&ac->ci, 0, sizeof(ac->ci));
|
|
|
|
/* compute the key authorization */
|
|
|
|
p = ac->key_auth;
|
|
end = p + sizeof(ac->key_auth) - 1;
|
|
|
|
p += lws_snprintf(p, end - p, "%s.", ac->chall_token);
|
|
lws_jwk_rfc7638_fingerprint(&vhd->jwk, digest);
|
|
n = lws_jws_base64_enc(digest, 32, p, end - p);
|
|
if (n < 0)
|
|
goto failed;
|
|
|
|
lwsl_notice("key_auth: '%s'\n", ac->key_auth);
|
|
|
|
lws_snprintf(ac->http01_mountpoint,
|
|
sizeof(ac->http01_mountpoint),
|
|
"/.well-known/acme-challenge/%s",
|
|
ac->chall_token);
|
|
|
|
memset(&ac->mount, 0, sizeof (struct lws_http_mount));
|
|
ac->mount.protocol = "http";
|
|
ac->mount.mountpoint = ac->http01_mountpoint;
|
|
ac->mount.mountpoint_len =
|
|
strlen(ac->http01_mountpoint);
|
|
ac->mount.origin_protocol = LWSMPRO_CALLBACK;
|
|
|
|
ac->ci.mounts = &ac->mount;
|
|
|
|
/* listen on the same port as the vhost that triggered
|
|
* us */
|
|
ac->ci.port = 80;
|
|
|
|
/* make ourselves protocols[0] for the new vhost */
|
|
ac->ci.protocols = chall_http01_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;
|
|
|
|
lwsl_notice("challenge_uri %s\n", ac->challenge_uri);
|
|
|
|
/*
|
|
* 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: connect failed\n", __func__);
|
|
goto failed;
|
|
}
|
|
return -1; /* close the completed client connection */
|
|
|
|
case ACME_STATE_START_CHALL:
|
|
lwsl_notice("%s: COMPLETED start 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;
|
|
}
|
|
|
|
strcpy(buf, ac->order_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:
|
|
|
|
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, "pending")) {
|
|
lwsl_notice("status: %s\n", ac->status);
|
|
goto poll_again;
|
|
}
|
|
|
|
if (!strcmp(ac->status, "invalid")) {
|
|
lwsl_notice("%s: Challenge 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 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->finalize_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:
|
|
if (ac->resp < 200 || ac->resp > 202) {
|
|
lwsl_notice("CSR poll failed on resp %d\n",
|
|
ac->resp);
|
|
goto failed;
|
|
}
|
|
|
|
if (ac->resp != 200) {
|
|
if (ac->goes_around++ == 30) {
|
|
lwsl_notice("%s: too many retries\n",
|
|
__func__);
|
|
|
|
goto failed;
|
|
}
|
|
strcpy(buf, ac->finalize_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;
|
|
}
|
|
/* close the completed client connection */
|
|
return -1;
|
|
}
|
|
|
|
ac->state = ACME_STATE_DOWNLOAD_CERT;
|
|
|
|
strcpy(buf, ac->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;
|
|
|
|
case ACME_STATE_DOWNLOAD_CERT:
|
|
|
|
if (ac->resp != 200) {
|
|
lwsl_notice("download cert failed on resp %d\n",
|
|
ac->resp);
|
|
goto failed;
|
|
}
|
|
lwsl_notice("The cert was sent..\n");
|
|
|
|
lws_acme_report_status(vhd->vhost, LWS_CUS_ISSUE, NULL);
|
|
|
|
/*
|
|
* That means we have the issued cert in
|
|
* ac->buf, length in ac->cpos; and the key in
|
|
* ac->alloc_privkey_pem, length in
|
|
* ac->len_privkey_pem.
|
|
*/
|
|
n = lws_plat_write_cert(vhd->vhost, 0,
|
|
vhd->fd_updated_cert,
|
|
ac->buf,
|
|
ac->cpos);
|
|
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 -1;
|
|
|
|
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;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return 0;
|
|
|
|
failed:
|
|
lwsl_notice("%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_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_VISIBLE int
|
|
destroy_protocol_lws_acme_client(struct lws_context *context)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
#endif
|