/* * libwebsockets ACME client protocol plugin * * Copyright (C) 2017 Andy Green * * 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 #endif #include #include 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 Let’s 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 Let’s Encrypt CA * knows it’s 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 * "..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