/* * libwebsockets ACME client protocol plugin * * Copyright (C) 2010 - 2019 Andy Green * * 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) #if !defined(LWS_DLL) #define LWS_DLL #endif #if !defined(LWS_INTERNAL) #define LWS_INTERNAL #endif #include #endif #include #include #include #include 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; 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 = (int)strlen(ac->key_auth); if (lws_add_http_header_content_length(wsi, (lws_filepos_t)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, lws_ptr_diff_size_t(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_size_t(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((unsigned int)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, lws_ptr_diff_size_t(end, p), "{\"alg\":\"RS256\""); if (kid) p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), ",\"kid\":\"%s\"", kid); else { p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), ",\"jwk\":"); m = lws_ptr_diff(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, lws_ptr_diff_size_t(end, p), ",\"url\":\"%s\"", url); p += lws_snprintf(p, lws_ptr_diff_size_t(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, lws_ptr_diff_size_t(end1, p1), "{\"protected\":\""); jws.map_b64.buf[LJWS_JOSE] = p1; n = lws_jws_base64_enc(start, lws_ptr_diff_size_t(p, start), p1, lws_ptr_diff_size_t(end1, p1)); if (n < 0) { lwsl_notice("%s: failed to encode protected\n", __func__); goto bail; } jws.map_b64.len[LJWS_JOSE] = (uint32_t)n; p1 += n; p1 += lws_snprintf(p1, lws_ptr_diff_size_t(end1, p1), "\",\"payload\":\""); jws.map_b64.buf[LJWS_PYLD] = p1; n = lws_jws_base64_enc(payload, len, p1, lws_ptr_diff_size_t(end1, p1)); if (n < 0) { lwsl_notice("%s: failed to encode payload\n", __func__); goto bail; } jws.map_b64.len[LJWS_PYLD] = (uint32_t)n; p1 += n; p1 += lws_snprintf(p1, lws_ptr_diff_size_t(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, lws_ptr_diff_size_t(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] = (uint32_t)n; p1 += n; p1 += lws_snprintf(p1, lws_ptr_diff_size_t(end1, p1), "\"}"); free(buf); return lws_ptr_diff(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 = 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 = 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 = s->yes | 2; break; case JCAC_TOKEN: lws_strncpy(s->chall_token, ctx->buf, sizeof(s->chall_token)); s->yes = 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, (unsigned int)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)); if (vhd) return 0; 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 += (int)strlen(pvo->value) + 1; pvo = pvo->next; } p = vhd->pvo_data = malloc((unsigned int)m); if (!p) return -1; pvo = (const struct lws_protocol_vhost_options *)in; while (pvo) { start = p; n = (int)strlen(pvo->value) + 1; memcpy(start, pvo->value, (unsigned int)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 = (int)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, lws_ptr_diff_size_t(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, lws_ptr_diff_size_t(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, lws_ptr_diff_size_t(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, lws_ptr_diff_size_t(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, lws_ptr_diff_size_t(end, p), "{\"csr\":\""); n = lws_tls_acme_sni_csr_create(vhd->context, &vhd->pvop_active[0], (uint8_t *)p, lws_ptr_diff_size_t(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, lws_ptr_diff_size_t(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, (size_t)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, (int)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 ((unsigned int)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 += (int)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, lws_ptr_diff_size_t(end, p), "%s.", ac->chall_token); lws_jwk_rfc7638_fingerprint(&vhd->jwk, digest); n = lws_jws_base64_enc(digest, 32, p, lws_ptr_diff_size_t(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 = (unsigned char) 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, (size_t)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, (size_t)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) LWS_VISIBLE const struct lws_protocols lws_acme_client_protocols[] = { LWS_PLUGIN_PROTOCOL_LWS_ACME_CLIENT }; LWS_VISIBLE const lws_plugin_protocol_t protocol_lws_acme_client = { .hdr = { "acme client", "lws_protocol_plugin", LWS_BUILD_HASH, LWS_PLUGIN_API_MAGIC }, .protocols = lws_acme_client_protocols, .count_protocols = LWS_ARRAY_SIZE(lws_acme_client_protocols), .extensions = NULL, .count_extensions = 0, }; #endif