acme_http_01_token.c (23537B)
1 /* SPDX-License-Identifier: 0BSD */ 2 /* 3 This file is part of GNU libmicrohttpd. 4 Copyright (C) 2026 Evgeny Grin (Karlson2k) 5 6 Permission to use, copy, modify, and/or distribute this software for 7 any purpose with or without fee is hereby granted. 8 9 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL 10 WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES 11 OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE 12 FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY 13 DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 14 AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT 15 OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 */ 17 /** 18 * @file src/examples2/acme/acme_http_01_token.c 19 * @brief Answering the ACME HTTP-01 challenge given on the command line 20 * @author Karlson2k (Evgeny Grin) 21 */ 22 23 /* A minimal HTTP server that answers a single ACME HTTP-01 challenge given 24 on the command line. Nothing else is served. 25 26 Unlike the file-based example, nothing is read from the disk: the key 27 authorization is given directly, so the server is fully described by its 28 arguments. The ACME servers always use port 80. Another port may be 29 given if the traffic from port 80 is forwarded to it. 30 31 The example can be used as a ready-to-run program, and its code can be 32 taken into an application as well: the daemon request processing callback 33 (req_cb()) and the data shared by all the requests 34 (struct RequestHandlingContextData) do not depend on how the daemon is 35 created. Everything else is just the stand-alone program around them. 36 37 Any path other than a challenge path is answered with "404 Not Found", 38 a challenge path requested with a method other than GET or HEAD with 39 "405 Method Not Allowed". 40 41 Usage: acme_http_01_token [--naive] PORT KEY_AUTHORIZATION 42 The arguments are told apart by their length, so their order is not 43 significant. With "--naive" the account thumbprint is given instead of 44 the key authorization; see the note at print_naive_warning(). */ 45 46 #include <stdio.h> 47 #include <stdlib.h> 48 #include <string.h> 49 #include <microhttpd2.h> 50 51 /** 52 * The minimal length of the challenge token. 53 * The token carries at least 128 bits of entropy, which needs at least 54 * twenty-two characters of the base64url alphabet. 55 * See RFC 8555, section 8.3. 56 */ 57 #define MIN_TOKEN_LEN 22 58 59 /** 60 * The maximal length of the port number given on the command line. 61 * An argument of at least #MIN_TOKEN_LEN characters is the key 62 * authorization or the thumbprint; the lengths in between are rejected. 63 */ 64 #define MAX_PORT_LEN 5 65 66 /** 67 * The maximal length of the key authorization built in the "naive" mode, 68 * without the terminating null character. The other mode sends the string 69 * given on the command line and needs no buffer at all. 70 */ 71 #define MAX_KEY_AUTH_LEN 511 72 73 /** 74 * The fixed prefix of the path of the ACME HTTP-01 challenge resources. 75 */ 76 static const char chlng_prfx[] = MHD_ACME_HTTP_01_CHALLENGE_PATH_PREFIX; 77 78 /** 79 * The length of #chlng_prfx, without the terminating null character. 80 */ 81 static const size_t chlng_prfx_len = sizeof(chlng_prfx) / sizeof(char) - 1; 82 83 /** 84 * The data shared by all requests handled by this example. 85 */ 86 struct RequestHandlingContextData 87 { 88 /** 89 * The key authorization to send, NULL in the "naive" mode. 90 */ 91 const char *key_auth; 92 93 /** 94 * The length of #key_auth, without the terminating null character. 95 */ 96 size_t key_auth_len; 97 98 /** 99 * The length of the token part of #key_auth, zero in the "naive" mode. 100 * The token is the part of the key authorization before the first dot, 101 * so it is also the prefix of #key_auth. See RFC 8555, section 8.1. 102 */ 103 size_t token_len; 104 105 /** 106 * The account thumbprint, NULL unless the "naive" mode is used. 107 */ 108 const char *thumbprint; 109 110 /** 111 * The length of #thumbprint, without the terminating null character. 112 */ 113 size_t thumbprint_len; 114 115 /** 116 * The ready response with the key authorization, used for every matching 117 * request. NULL in the "naive" mode. 118 */ 119 struct MHD_Response *resp_challenge; 120 121 /** 122 * The response for every request that is not answered with the key 123 * authorization: another resource, another token or a too long token. 124 */ 125 struct MHD_Response *resp_not_found; 126 127 /** 128 * The response for the ACME challenge resources requested with 129 * an unsupported method. 130 */ 131 struct MHD_Response *resp_not_allowed; 132 133 /** 134 * If not zero, every served challenge is reported to standard error. 135 */ 136 int log_challenges; 137 }; 138 139 140 /** 141 * Parse the TCP port number given on the command line. 142 * 143 * @param str the string to parse 144 * @param[out] port_out the resulting port number, set only on success 145 * @return non-zero if succeed, 146 * zero if @p str is not a valid port number 147 */ 148 static int 149 parse_port (const char *str, 150 uint_least16_t *port_out) 151 { 152 unsigned long value; 153 char *endptr; 154 155 if ('\0' == str[0]) 156 return 0; 157 158 value = strtoul (str, 159 &endptr, 160 10); 161 if ('\0' != endptr[0]) 162 return 0; 163 /* A negative number is wrapped by strtoul() to a large value and 164 is rejected by the range check below. */ 165 if ((1UL > value) || (65535UL < value)) 166 return 0; 167 168 *port_out = (uint_least16_t)value; 169 170 return 1; 171 } 172 173 174 /** 175 * Check the challenge token taken from the request. 176 * 177 * In the "naive" mode the token is copied from the request to the reply, 178 * which makes this check mandatory: RFC 8555, section 8.3, requires such 179 * a client to validate the token syntax. 180 * 181 * @param token the token to check 182 * @param token_len the length of @p token, without the terminating null 183 * character 184 * @return non-zero if the token is syntactically valid, 185 * zero otherwise 186 */ 187 static int 188 check_token (const char *token, 189 size_t token_len) 190 { 191 size_t i; 192 193 if (MIN_TOKEN_LEN > token_len) 194 return 0; 195 196 for (i = 0; token_len > i; ++i) 197 { 198 const unsigned char chr = (unsigned char)token[i]; 199 200 if ((('A' > chr) || ('Z' < chr)) 201 && (('a' > chr) || ('z' < chr)) 202 && (('0' > chr) || ('9' < chr)) 203 && ('-' != chr) && ('_' != chr)) 204 return 0; 205 } 206 207 return 1; 208 } 209 210 211 /** 212 * Report the "naive" mode to the user. 213 * 214 * In this mode the reply is built from the token of the request instead of 215 * being compared with the configured one, so any syntactically valid token 216 * is answered. RFC 8555, section 11.3, requires the entropy of the token 217 * partly to make such servers harder to write. 218 */ 219 static void 220 print_naive_warning (void) 221 { 222 fprintf (stderr, 223 "WARNING: the \"naive\" mode (also known as \"stateless\")" 224 " answers every syntactically valid token with the account" 225 " thumbprint, thus confirming any challenge that is asked for.\n" 226 "It is not recommended by RFC 8555, section 8.3, " 227 "and is less secure.\n"); 228 } 229 230 231 /** 232 * Create a re-usable response with the given body. 233 * 234 * @param sc the HTTP status code of the response 235 * @param body_len the length of @p body, without the terminating null 236 * character 237 * @param body the body of the response, must be valid for the whole 238 * lifetime of the response 239 * @param content_type the value of the "Content-Type:" header 240 * @param allow the value of the "Allow:" header, 241 * NULL if the header is not needed 242 * @return the new response, 243 * NULL if failed 244 */ 245 static struct MHD_Response * 246 make_static_response (enum MHD_HTTP_StatusCode sc, 247 size_t body_len, 248 const char *body, 249 const char *content_type, 250 const char *allow) 251 { 252 struct MHD_ResponseOptionAndValue reusable; 253 struct MHD_Response *r; 254 255 /* The option is kept in a variable: the address of the value returned by 256 the option helper cannot be taken in every supported language mode. */ 257 reusable = MHD_R_OPTION_REUSABLE (MHD_YES); 258 259 r = MHD_response_from_buffer_static (sc, 260 body_len, 261 body); 262 if (NULL != r) 263 { 264 if (MHD_SC_OK == 265 MHD_response_add_header (r, 266 MHD_HTTP_HEADER_CONTENT_TYPE, 267 content_type)) 268 { 269 if ((NULL == allow) 270 || (MHD_SC_OK == 271 MHD_response_add_header (r, 272 MHD_HTTP_HEADER_ALLOW, 273 allow))) 274 { 275 if (MHD_SC_OK == 276 MHD_response_set_option (r, 277 &reusable)) 278 return r; /* Success exit point */ 279 } 280 } 281 282 /* Below is a clean-up path */ 283 MHD_response_destroy (r); 284 } 285 286 return NULL; /* Failure exit point */ 287 } 288 289 290 /** 291 * Report the challenge that is being served. 292 * 293 * @param method the HTTP method used for the request, GET or HEAD 294 * @param token the token of the challenge resource 295 * @param token_len the length of @p token, without the terminating null 296 * character 297 */ 298 static void 299 log_challenge (enum MHD_HTTP_Method method, 300 const char *token, 301 size_t token_len) 302 { 303 /* No other method reaches this point. */ 304 fprintf (stderr, 305 "Replying to the %s request for challenge %.*s\n", 306 (MHD_HTTP_METHOD_HEAD == method) ? "HEAD" : "GET", 307 (int)token_len, 308 token); 309 } 310 311 312 /** 313 * Check whether the request is for an ACME HTTP-01 challenge resource. 314 * The token is checked as well, so the token of a matching request is 315 * syntactically valid. 316 * 317 * @param path the requested path 318 * @return non-zero if the request is for a challenge resource, 319 * zero otherwise 320 */ 321 static int 322 is_req_acme_http_01_challenge (const struct MHD_String *MHD_RESTRICT path) 323 { 324 return ((chlng_prfx_len < path->len) 325 && (0 == memcmp (path->cstr, 326 chlng_prfx, 327 chlng_prfx_len)) 328 && (check_token (path->cstr + chlng_prfx_len, 329 path->len - chlng_prfx_len))); 330 } 331 332 333 /** 334 * Handle the request for an ACME HTTP-01 challenge resource. 335 * 336 * @param request the request to handle 337 * @param path the requested path, must be a challenge resource path 338 * @param method the HTTP method used for the request 339 * @param context_data the data shared by all the requests 340 * @return the action to perform for the @p request 341 */ 342 static const struct MHD_Action * 343 handle_acme_http_01_challenge ( 344 struct MHD_Request *MHD_RESTRICT request, 345 const struct MHD_String *MHD_RESTRICT path, 346 enum MHD_HTTP_Method method, 347 const struct RequestHandlingContextData *const context_data) 348 { 349 const char *const token = path->cstr + chlng_prfx_len; 350 const size_t token_len = path->len - chlng_prfx_len; 351 char key_auth[MAX_KEY_AUTH_LEN + 1]; 352 struct MHD_Response *r; 353 354 /* Only the GET and the HEAD methods are allowed for the challenge 355 resources. */ 356 if ((MHD_HTTP_METHOD_GET != method) 357 && (MHD_HTTP_METHOD_HEAD != method)) 358 return MHD_action_from_response (request, 359 context_data->resp_not_allowed); 360 361 if (NULL != context_data->key_auth) 362 { 363 /* The key authorization is known, so only the configured token is 364 answered. The token is the prefix of the key authorization. */ 365 if ((token_len != context_data->token_len) 366 || (0 != memcmp (token, 367 context_data->key_auth, 368 token_len))) 369 return MHD_action_from_response (request, 370 context_data->resp_not_found); 371 372 if (context_data->log_challenges) 373 log_challenge (method, 374 token, 375 token_len); 376 377 /* There is only one key authorization, so the reply never changes: 378 the ready response is used and nothing has to be built here. */ 379 return MHD_action_from_response (request, 380 context_data->resp_challenge); 381 } 382 383 /* The "naive" mode: the key authorization is not known in advance, so it 384 is built for every request from the token of the request and the 385 configured account thumbprint, joined with a dot. 386 See RFC 8555, section 8.1. */ 387 if (MAX_KEY_AUTH_LEN - context_data->thumbprint_len - 1 < token_len) 388 return MHD_action_from_response (request, 389 context_data->resp_not_found); 390 391 memcpy (key_auth, 392 token, 393 token_len); 394 key_auth[token_len] = '.'; 395 memcpy (key_auth + token_len + 1, 396 context_data->thumbprint, 397 context_data->thumbprint_len); 398 399 r = MHD_response_from_buffer_copy (MHD_HTTP_STATUS_OK, 400 token_len + 1 401 + context_data->thumbprint_len, 402 key_auth); 403 if (NULL != r) 404 { 405 if (MHD_SC_OK == 406 MHD_response_add_header (r, 407 MHD_HTTP_HEADER_CONTENT_TYPE, 408 "application/octet-stream")) 409 { 410 if (context_data->log_challenges) 411 log_challenge (method, 412 token, 413 token_len); 414 415 return MHD_action_from_response (request, 416 r); /* Success exit point */ 417 } 418 419 /* Below is a clean-up path */ 420 MHD_response_destroy (r); 421 } 422 423 return MHD_action_abort_request (request); /* Failure exit point */ 424 } 425 426 427 /** 428 * The handler of the incoming requests. 429 * 430 * @param cls the pointer to the shared #RequestHandlingContextData 431 * @param request the request to handle 432 * @param path the requested path 433 * @param method the HTTP method used for the request 434 * @param upload_size the size of the request content, unused 435 * @return the action to perform for the @p request 436 */ 437 static MHD_FN_PAR_NONNULL_ (2) MHD_FN_PAR_NONNULL_ (3) 438 const struct MHD_Action * 439 req_cb (void *cls, 440 struct MHD_Request *MHD_RESTRICT request, 441 const struct MHD_String *MHD_RESTRICT path, 442 enum MHD_HTTP_Method method, 443 uint_fast64_t upload_size) 444 { 445 const struct RequestHandlingContextData *const context_data = 446 (const struct RequestHandlingContextData *)cls; 447 448 /* If the request is for an ACME HTTP-01 challenge resource, handle it 449 accordingly. */ 450 if (is_req_acme_http_01_challenge (path)) 451 return handle_acme_http_01_challenge (request, 452 path, 453 method, 454 context_data); 455 456 /* Here other requests can be handled for different needs. 457 This example just sends 404 (Not Found) replies. */ 458 (void)upload_size; /* Unused */ 459 460 return MHD_action_from_response (request, 461 context_data->resp_not_found); 462 } 463 464 465 /** 466 * Create the responses that do not change from request to request. 467 * 468 * @param[in,out] context_data the data to set the responses in 469 * @return non-zero if succeed, 470 * zero if failed 471 */ 472 static int 473 init_responses (struct RequestHandlingContextData *context_data) 474 { 475 static const char page_not_found[] = 476 "<html><body>Not found</body></html>"; 477 static const char page_not_allowed[] = 478 "<html><body>Method not allowed</body></html>"; 479 480 context_data->resp_challenge = NULL; 481 if (NULL != context_data->key_auth) 482 { 483 /* The key authorization never changes, so the response for it is built 484 once here instead of being built for every request. */ 485 context_data->resp_challenge = 486 make_static_response (MHD_HTTP_STATUS_OK, 487 context_data->key_auth_len, 488 context_data->key_auth, 489 "application/octet-stream", 490 NULL); 491 if (NULL == context_data->resp_challenge) 492 return 0; /* Failure exit point */ 493 } 494 495 context_data->resp_not_found = 496 make_static_response (MHD_HTTP_STATUS_NOT_FOUND, 497 sizeof(page_not_found) / sizeof(char) - 1, 498 page_not_found, 499 "text/html", 500 NULL); 501 if (NULL != context_data->resp_not_found) 502 { 503 /* The challenge resource is read-only. The "Allow:" header is required 504 for this status code by RFC 9110, section 15.5.6. */ 505 context_data->resp_not_allowed = 506 make_static_response (MHD_HTTP_STATUS_METHOD_NOT_ALLOWED, 507 sizeof(page_not_allowed) / sizeof(char) - 1, 508 page_not_allowed, 509 "text/html", 510 "GET, HEAD"); 511 if (NULL != context_data->resp_not_allowed) 512 return 1; /* Success exit point */ 513 514 /* Below is a clean-up path */ 515 MHD_response_destroy (context_data->resp_not_found); 516 } 517 if (NULL != context_data->resp_challenge) 518 MHD_response_destroy (context_data->resp_challenge); 519 520 return 0; /* Failure exit point */ 521 } 522 523 524 /** 525 * Destroy the responses created by init_responses(). 526 * 527 * @param[in] context_data the data with the responses to destroy 528 */ 529 static void 530 deinit_responses (struct RequestHandlingContextData *context_data) 531 { 532 MHD_response_destroy (context_data->resp_not_allowed); 533 MHD_response_destroy (context_data->resp_not_found); 534 if (NULL != context_data->resp_challenge) 535 MHD_response_destroy (context_data->resp_challenge); 536 } 537 538 539 /** 540 * Create, configure, start and run the daemon until the user presses ENTER. 541 * 542 * @param data the data shared by all requests, must be valid until this 543 * function returns 544 * @param port the TCP port to listen on 545 * @return zero if the daemon has been started and stopped normally, 546 * the exit code of the program otherwise 547 */ 548 static int 549 run_daemon (struct RequestHandlingContextData *data, 550 uint_least16_t port) 551 { 552 struct MHD_Daemon *d; 553 int ret; 554 555 if (!init_responses (data)) 556 { 557 fprintf (stderr, 558 "Failed to create a response object.\n"); 559 return 4; 560 } 561 562 d = MHD_daemon_create (&req_cb, 563 data); 564 if (NULL == d) 565 { 566 fprintf (stderr, 567 "Failed to create MHD daemon.\n"); 568 deinit_responses (data); 569 return 3; 570 } 571 572 ret = 0; 573 if (MHD_SC_OK != 574 MHD_DAEMON_SET_OPTIONS ( 575 d, 576 MHD_D_OPTION_WM_WORKER_THREADS (1), 577 MHD_D_OPTION_BIND_PORT (MHD_AF_AUTO, 578 port), 579 /* This daemon is exposed to the open Internet and talks to 580 well-behaving clients only, so the protocol is enforced strictly. 581 The nearest stricter level is taken if this exact level is not 582 available in the library build. */ 583 MHD_D_OPTION_PROTOCOL_STRICT_LEVEL (MHD_PSL_STRICT, 584 MHD_USL_THIS_OR_STRICTER), 585 /* The following three options are an optional optimisation only: 586 this daemon uses none of these features, so MHD is told not to 587 spend any resources on them. Everything works without them. */ 588 MHD_D_OPTION_DISABLE_COOKIES (MHD_YES), 589 MHD_D_OPTION_DISALLOW_UPGRADE (MHD_YES), 590 MHD_D_OPTION_DISALLOW_SUSPEND_RESUME (MHD_YES))) 591 { 592 fprintf (stderr, 593 "Failed to set MHD daemon run parameters.\n"); 594 ret = 3; 595 } 596 else if (MHD_SC_OK != 597 MHD_daemon_start (d)) 598 { 599 fprintf (stderr, 600 "Failed to start MHD daemon.\n"); 601 ret = 3; 602 } 603 else 604 { 605 printf ("The MHD daemon is listening on port %u\n" 606 "Press ENTER to stop.\n", 607 (unsigned int)port); 608 (void)fgetc (stdin); 609 } 610 printf ("Stopping... "); 611 fflush (stdout); 612 MHD_daemon_destroy (d); 613 printf ("OK\n"); 614 deinit_responses (data); 615 616 return ret; 617 } 618 619 620 /** 621 * Parse the command line arguments. 622 * 623 * The port number and the key authorization are told apart by their length, 624 * so the arguments may be given in any order. All the errors are reported 625 * by this function. 626 * 627 * @param argc the number of the command line arguments 628 * @param argv the command line arguments 629 * @param[out] port_out the TCP port to listen on, set only on success 630 * @param[out] auth_out the key authorization, or the account thumbprint if 631 * the "naive" mode is requested, set only on success 632 * @param[out] naive_out set to non-zero if the "naive" mode is requested 633 * @return non-zero if succeed, 634 * zero if the arguments are not valid 635 */ 636 static int 637 parse_cmd_line (int argc, 638 char *const *argv, 639 uint_least16_t *port_out, 640 const char **auth_out, 641 int *naive_out) 642 { 643 const char *port_str = NULL; 644 const char *auth = NULL; 645 int i; 646 647 *naive_out = 0; 648 for (i = 1; argc > i; ++i) 649 { 650 const size_t arg_len = strlen (argv[i]); 651 652 if ((0 == strcmp (argv[i], 653 "--naive")) 654 || (0 == strcmp (argv[i], 655 "--stateless"))) 656 *naive_out = !0; 657 else if ((MAX_PORT_LEN >= arg_len) && (NULL == port_str)) 658 port_str = argv[i]; 659 else if ((MIN_TOKEN_LEN <= arg_len) && (NULL == auth)) 660 auth = argv[i]; 661 else 662 { 663 fprintf (stderr, 664 "Unexpected argument \"%s\": wrong length or given twice.\n", 665 argv[i]); 666 return 0; 667 } 668 } 669 if ((NULL == port_str) || (NULL == auth)) 670 { 671 fprintf (stderr, 672 "Usage:\n%s [--naive] PORT KEY_AUTHORIZATION\n", 673 argv[0]); 674 return 0; 675 } 676 if (!parse_port (port_str, 677 port_out)) 678 { 679 fprintf (stderr, 680 "The PORT must be a numeric value between 1 and 65535.\n"); 681 return 0; 682 } 683 684 *auth_out = auth; 685 686 return 1; 687 } 688 689 690 /** 691 * Run the example. 692 * 693 * @param argc the number of the command line arguments 694 * @param argv the command line arguments: the TCP port to listen on, the key 695 * authorization and the optional "--naive" option 696 * @return zero if succeed, 697 * non-zero otherwise 698 */ 699 int 700 main (int argc, 701 char *const *argv) 702 { 703 struct RequestHandlingContextData data; 704 const char *auth; 705 const char *dot; 706 uint_least16_t port; 707 int naive; 708 709 if (!parse_cmd_line (argc, 710 argv, 711 &port, 712 &auth, 713 &naive)) 714 return 1; 715 716 data.key_auth = NULL; 717 data.key_auth_len = 0; 718 data.token_len = 0; 719 data.thumbprint = NULL; 720 data.thumbprint_len = 0; 721 if (naive) 722 { 723 print_naive_warning (); 724 data.thumbprint = auth; 725 data.thumbprint_len = strlen (auth); 726 if (MAX_KEY_AUTH_LEN - MIN_TOKEN_LEN - 1 < data.thumbprint_len) 727 { 728 fprintf (stderr, 729 "The thumbprint is too long.\n"); 730 return 2; 731 } 732 } 733 else 734 { 735 /* The token is the part of the key authorization before the first dot. 736 See RFC 8555, section 8.1. */ 737 dot = strchr (auth, '.'); 738 if ((NULL == dot) 739 || (!check_token (auth, 740 (size_t)(dot - auth)))) 741 { 742 fprintf (stderr, 743 "The KEY_AUTHORIZATION must be the token, a dot and" 744 " the account thumbprint.\n"); 745 return 2; 746 } 747 data.key_auth = auth; 748 data.key_auth_len = strlen (auth); 749 data.token_len = (size_t)(dot - auth); 750 } 751 /* A real application would take this from its configuration. */ 752 data.log_challenges = !0; 753 754 return run_daemon (&data, 755 port); 756 }