CCF
Loading...
Searching...
No Matches
acme_client.h
Go to the documentation of this file.
1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the Apache 2.0 License.
3#pragma once
4
5#include "ccf/crypto/base64.h"
8#include "ccf/crypto/pem.h"
9#include "ccf/crypto/san.h"
10#include "ccf/crypto/sha256.h"
11#include "ccf/crypto/verifier.h"
12#include "ccf/ds/logger.h"
13#include "ccf/http_consts.h"
14#include "ccf/http_status.h"
15#include "ccf/pal/locking.h"
16#include "ds/messaging.h"
17#include "ds/thread_messaging.h"
18#include "http/http_parser.h"
19
20#include <cctype>
21#include <chrono>
22#include <cstddef>
23#include <list>
24#include <optional>
25#include <string>
26#include <unordered_set>
27#include <vector>
28
29namespace ACME
30{
32 {
33 // Root certificate(s) of the CA to connect to in PEM format (for TLS
34 // connections to the CA, e.g. Let's Encrypt's ISRG Root X1)
35 std::vector<std::string> ca_certs;
36
37 // URL of the ACME server's directory
38 std::string directory_url;
39
40 // DNS name of the service we represent
41 std::string service_dns_name;
42
43 // Alternative DNS names of the service we represent
44 std::vector<std::string> alternative_names;
45
46 // Contact addresses (see RFC8555 7.3, e.g. mailto:john@example.com)
47 std::vector<std::string> contact;
48
49 // Indication that the user/operator is aware of the latest terms and
50 // conditions for the CA
52
53 // Type of the ACME challenge
54 std::string challenge_type = "http-01";
55
56 // Validity range (Note: not supported by Let's Encrypt)
57 std::optional<std::string> not_before;
58 std::optional<std::string> not_after;
59
60 bool operator==(const ClientConfig& other) const = default;
61 };
62
63 class Client
64 {
65 public:
67 const ClientConfig& config,
68 std::shared_ptr<ccf::crypto::KeyPair> account_key_pair = nullptr) :
70 {
72 }
73
74 virtual ~Client() {}
75
77 std::shared_ptr<ccf::crypto::KeyPair> service_key_,
78 bool override_time = false)
79 {
80 using namespace std::chrono_literals;
81 using namespace std::chrono;
82
83 bool ok = true;
84 system_clock::duration delta(0);
85
86 if (last_request && !override_time)
87 {
88 // Let's encrypt recommends this retry strategy in their integration
89 // guide, see https://letsencrypt.org/docs/integration-guide/
90
91 delta = system_clock::now() - *last_request;
92 ok = false;
93 switch (num_failed_attempts)
94 {
95 case 0:
96 ok = true;
97 break;
98 case 1:
99 ok = delta >= 1min;
100 break;
101 case 2:
102 ok = delta >= 10min;
103 break;
104 case 3:
105 ok = delta >= 100min;
106 break;
107 default:
108 ok = delta >= 24h;
109 break;
110 }
111 }
112
113 if (ok)
114 {
115 service_key = service_key_;
116 last_request = system_clock::now();
119 }
120 else
121 {
123 "ACME: Ignoring certificate request due to {} recent failed "
124 "attempt(s) within {} seconds",
126 duration_cast<seconds>(delta).count());
127 }
128 }
129
130 void start_challenge(const std::string& token)
131 {
132 for (auto& order : active_orders)
133 {
134 auto cit = order.challenges.find(token);
135 if (cit != order.challenges.end())
136 {
138 order.account_url,
139 cit->second.challenge_url,
140 [this, order_url = order.order_url, &challenge = cit->second](
141 const ccf::http::HeaderMap& headers, const nlohmann::json& j) {
142 threading::ThreadMessaging::instance().add_task_after(
143 schedule_check_challenge(order_url, challenge),
144 std::chrono::milliseconds(0));
145 return true;
146 });
147 }
148 }
149 }
150
151 virtual void set_account_key(
152 std::shared_ptr<ccf::crypto::KeyPair> new_account_key_pair)
153 {
154 account_key_pair = new_account_key_pair != nullptr ?
155 new_account_key_pair :
158 "ACME: new account public key: {}",
159 ccf::ds::to_hex(account_key_pair->public_key_der()));
160 }
161
162 bool has_active_orders() const
163 {
164 return !active_orders.empty();
165 }
166
167 protected:
168 virtual void on_challenge(
169 const std::string& token, const std::string& response) = 0;
170 virtual void on_challenge_finished(const std::string& token) = 0;
171 virtual void on_certificate(const std::string& certificate) = 0;
172 virtual void on_http_request(
173 const http::URL& url,
174 http::Request&& req,
175 std::function<bool(
176 http_status status, ccf::http::HeaderMap&&, std::vector<uint8_t>&&)>
177 callback) = 0;
178
180 llhttp_method method,
181 const http::URL& url,
182 const std::vector<uint8_t>& body,
183 http_status expected_status,
184 std::function<bool(
185 const ccf::http::HeaderMap&, const std::vector<uint8_t>&)> ok_callback)
186 {
187 std::unique_lock<ccf::pal::Mutex> guard(req_lock);
188
189 try
190 {
191 auto port = url.port.empty() ? "443" : url.port;
193 "ACME: Requesting https://{}:{}{}", url.host, port, url.path);
194
195 http::Request r(url.path, method);
196 r.set_header(ccf::http::headers::ACCEPT, "*/*");
197 r.set_header(
198 ccf::http::headers::HOST, fmt::format("{}:{}", url.host, url.port));
199 if (!body.empty())
200 {
201 r.set_header(
202 ccf::http::headers::CONTENT_TYPE, "application/jose+json");
203 r.set_body(&body);
204 }
205 auto req = r.build_request();
206 std::string reqs(req.begin(), req.end());
207 LOG_TRACE_FMT("ACME: request:\n{}", reqs);
208
210 url,
211 std::move(r),
212 [this, expected_status, ok_callback](
213 http_status status,
214 ccf::http::HeaderMap&& headers,
215 std::vector<uint8_t>&& data) {
216 for (auto& [k, v] : headers)
217 {
218 LOG_TRACE_FMT("ACME: H: {}: {}", k, v);
219 }
220
221 if (status != expected_status && status != HTTP_STATUS_OK)
222 {
224 "ACME: request failed with status={} and body={}",
225 (int)status,
226 std::string(data.begin(), data.end()));
227 return false;
228 }
229 else
230 {
232 "ACME: data: {}", std::string(data.begin(), data.end()));
233 }
234
235 auto nonce_opt = get_header_value(headers, "replay-nonce");
236 if (nonce_opt)
237 {
238 nonces.push_back(*nonce_opt);
239 }
240
241 try
242 {
243 ok_callback(headers, data);
244 }
245 catch (const std::exception& ex)
246 {
247 LOG_FAIL_FMT("ACME: response callback failed: {}", ex.what());
248 return false;
249 }
250 return true;
251 });
252 }
253 catch (const std::exception& ex)
254 {
255 LOG_FAIL_FMT("ACME: failed to connect to ACME server: {}", ex.what());
256 }
257 }
258
260 llhttp_method method,
261 const http::URL& url,
262 const std::vector<uint8_t>& body,
263 http_status expected_status,
264 std::function<
265 void(const ccf::http::HeaderMap& headers, const nlohmann::json&)>
266 ok_callback)
267 {
269 method,
270 url,
271 body,
272 expected_status,
273 [ok_callback](
274 const ccf::http::HeaderMap& headers,
275 const std::vector<uint8_t>& data) {
276 nlohmann::json jr;
277
278 if (!data.empty())
279 {
280 try
281 {
282 jr = nlohmann::json::parse(data);
283 LOG_TRACE_FMT("ACME: json response: {}", jr.dump());
284 }
285 catch (const std::exception& ex)
286 {
287 LOG_FAIL_FMT("ACME: response parser failed: {}", ex.what());
288 return false;
289 }
290 }
291
292 ok_callback(headers, jr);
293 return true;
294 });
295 }
296
298 const std::string& account_url,
299 const std::string& resource_url,
300 std::function<bool(
301 const ccf::http::HeaderMap&, const std::vector<uint8_t>&)> ok_callback)
302 {
303 if (nonces.empty())
304 {
305 request_new_nonce(
306 [=]() { post_as_get(account_url, resource_url, ok_callback); });
307 }
308 else
309 {
310 auto nonce = nonces.front();
311 nonces.pop_front();
312 auto header = mk_kid_header(account_url, nonce, resource_url);
313 JWS jws(header, *account_key_pair);
314 http::URL url = with_default_port(resource_url);
315 make_request(
316 HTTP_POST, url, json_to_bytes(jws), HTTP_STATUS_OK, ok_callback);
317 }
318 }
319
321 const std::string& account_url,
322 const std::string& resource_url,
323 std::function<bool(const ccf::http::HeaderMap&, const nlohmann::json&)>
324 ok_callback,
325 bool empty_payload = false)
326 {
327 if (nonces.empty())
328 {
329 request_new_nonce([=]() {
330 post_as_get_json(
331 account_url, resource_url, ok_callback, empty_payload);
332 });
333 }
334 else
335 {
336 auto nonce = nonces.front();
337 nonces.pop_front();
338
339 auto header = mk_kid_header(account_url, nonce, resource_url);
340 JWS jws(
341 header, nlohmann::json::object_t(), *account_key_pair, empty_payload);
342 http::URL url = with_default_port(resource_url);
343 make_request(
344 HTTP_POST,
345 url,
346 json_to_bytes(jws),
347 HTTP_STATUS_OK,
348 [ok_callback](
349 const ccf::http::HeaderMap& headers,
350 const std::vector<uint8_t>& data) {
351 try
352 {
353 ok_callback(headers, nlohmann::json::parse(data));
354 return true;
355 }
356 catch (const std::exception& ex)
357 {
358 LOG_FAIL_FMT("ACME: request callback failed: {}", ex.what());
359 return false;
360 }
361 });
362 }
363 }
364
366 std::shared_ptr<ccf::crypto::KeyPair> service_key;
367 std::shared_ptr<ccf::crypto::KeyPair> account_key_pair;
368
369 nlohmann::json directory;
370 nlohmann::json account;
371 std::list<std::string> nonces;
372
375
376 std::optional<std::chrono::system_clock::time_point> last_request =
377 std::nullopt;
378 size_t num_failed_attempts = 0;
379
381 {
382 std::string token;
383 std::string authorization_url;
384 std::string challenge_url;
385 };
386
388 {
391 FAILED
392 };
393
394 struct Order
395 {
396 OrderStatus status = ACTIVE;
397 std::string account_url;
398 std::string order_url;
399 std::string finalize_url;
400 std::string certificate_url;
401 std::unordered_set<std::string> authorizations;
402 std::map<std::string, Challenge> challenges;
403 };
404
405 std::list<Order> active_orders;
406
408 const std::string& url, const std::string& default_port = "443")
409 {
411 if (r.port.empty())
412 {
413 r.port = default_port;
414 }
415 return r;
416 }
417
418 static std::vector<uint8_t> s2v(const std::string& s)
419 {
420 return std::vector<uint8_t>(s.data(), s.data() + s.size());
421 }
422
423 static std::vector<uint8_t> json_to_bytes(const nlohmann::json& j)
424 {
425 return s2v(j.dump());
426 }
427
428 static std::string json_to_b64url(
429 const nlohmann::json& j, bool with_padding = true)
430 {
431 return ccf::crypto::b64url_from_raw(json_to_bytes(j), with_padding);
432 }
433
435 std::vector<uint8_t>& sig, const ccf::crypto::KeyPair& signer)
436 {
437 // Convert signature from ASN.1 format to IEEE P1363
438 const unsigned char* pp = sig.data();
439 ECDSA_SIG* sig_r_s = d2i_ECDSA_SIG(NULL, &pp, sig.size());
440 const BIGNUM* r = ECDSA_SIG_get0_r(sig_r_s);
441 const BIGNUM* s = ECDSA_SIG_get0_s(sig_r_s);
442 size_t sz = signer.coordinates().x.size();
443 sig = std::vector<uint8_t>(2 * sz, 0);
444 BN_bn2binpad(r, sig.data(), sz);
445 BN_bn2binpad(s, sig.data() + sz, sz);
446 ECDSA_SIG_free(sig_r_s);
447 }
448
449 class JWS : public nlohmann::json::object_t
450 {
451 public:
453 const nlohmann::json& header_,
454 const nlohmann::json& payload_,
455 ccf::crypto::KeyPair& signer_,
456 bool empty_payload = false)
457 {
458 LOG_TRACE_FMT("ACME: JWS header: {}", header_.dump());
459 LOG_TRACE_FMT("ACME: JWS payload: {}", payload_.dump());
460 auto header_b64 = json_to_b64url(header_, false);
461 auto payload_b64 = empty_payload ? "" : json_to_b64url(payload_, false);
462 set(header_b64, payload_b64, signer_);
463 }
464
465 JWS(const nlohmann::json& header_, ccf::crypto::KeyPair& signer_) :
466 JWS(header_, nlohmann::json::object_t(), signer_, true)
467 {}
468
469 virtual ~JWS() {}
470
471 protected:
472 void set(
473 const std::string& header_b64,
474 const std::string& payload_b64,
475 ccf::crypto::KeyPair& signer)
476 {
477 auto msg = header_b64 + "." + payload_b64;
478 auto sig = signer.sign(s2v(msg));
479 convert_signature_to_ieee_p1363(sig, signer);
480 auto sig_b64 = ccf::crypto::b64url_from_raw(sig);
481
482 (*this)["protected"] = header_b64;
483 (*this)["payload"] = payload_b64;
484 (*this)["signature"] = sig_b64;
485 }
486 };
487
488 class JWK : public nlohmann::json::object_t
489 {
490 public:
492 const std::string& kty,
493 const std::string& crv,
494 const std::string& x,
495 const std::string& y,
496 const std::optional<std::string>& alg = std::nullopt,
497 const std::optional<std::string>& use = std::nullopt,
498 const std::optional<std::string>& kid = std::nullopt)
499 {
500 (*this)["kty"] = kty;
501 (*this)["crv"] = crv;
502 (*this)["x"] = x;
503 (*this)["y"] = y;
504 if (alg)
505 (*this)["alg"] = *alg;
506 if (use)
507 (*this)["use"] = *use;
508 if (kid)
509 (*this)["kid"] = *kid;
510 }
511 virtual ~JWK() = default;
512 };
513
514 static std::optional<std::string> get_header_value(
515 const ccf::http::HeaderMap& headers, const std::string& name)
516 {
517 for (const auto& [k, v] : headers)
518 {
519 if (k == name)
520 {
521 return v;
522 }
523 }
524
525 return std::nullopt;
526 }
527
528 static void expect(const nlohmann::json& j, const std::string& key)
529 {
530 if (!j.contains(key))
531 {
532 throw std::runtime_error(fmt::format("Missing key '{}'", key));
533 }
534 }
535
536 static void expect_string(
537 const nlohmann::json& j, const std::string& key, const std::string& value)
538 {
539 expect(j, key);
540
541 const auto k = j[key].get<std::string>();
542 if (k != value)
543 {
544 throw std::runtime_error(fmt::format(
545 "Unexpected value for '{}': '{}' while expecting '{}'",
546 key,
547 k,
548 value));
549 }
550 }
551
552 static std::pair<std::string, std::string> get_crv_alg(
553 const std::shared_ptr<ccf::crypto::KeyPair>& key_pair)
554 {
555 std::string crv, alg;
556 if (key_pair->get_curve_id() == ccf::crypto::CurveID::SECP256R1)
557 {
558 crv = "P-256";
559 alg = "ES256";
560 }
561 else if (key_pair->get_curve_id() == ccf::crypto::CurveID::SECP384R1)
562 {
563 crv = "P-384";
564 alg = "ES384";
565 }
566 else
567 throw std::runtime_error("Unsupported curve");
568
569 return std::make_pair(crv, alg);
570 }
571
572 Order* get_order(const std::string& order_url)
573 {
574 auto oit = std::find_if(
575 active_orders.begin(),
576 active_orders.end(),
577 [&order_url](const Order& other) {
578 return order_url == other.order_url;
579 });
580
581 if (oit != active_orders.end())
582 {
583 return &(*oit);
584 }
585
586 LOG_DEBUG_FMT("ACME: no such order {}", order_url);
587
588 return nullptr;
589 }
590
591 void remove_order(const std::string& order_url)
592 {
593 LOG_TRACE_FMT("ACME: removing order {}", order_url);
594
595 std::unique_lock<ccf::pal::Mutex> guard(orders_lock);
596 for (auto it = active_orders.begin(); it != active_orders.end();)
597 {
598 if (it->order_url == order_url)
599 {
600 for (const auto& [_, challenge] : it->challenges)
601 {
602 on_challenge_finished(challenge.token);
603 }
604 it = active_orders.erase(it);
605 break;
606 }
607 else
608 {
609 it++;
610 }
611 }
612 }
613
614 nlohmann::json mk_kid_header(
615 const std::string& account_url,
616 const std::string& nonce,
617 const std::string& resource_url)
618 {
619 // For all other requests, the request is signed using an existing
620 // account, and there MUST be a "kid" field. This field MUST contain the
621 // account URL received by POSTing to the newAccount resource.
622
623 auto crv_alg = get_crv_alg(account_key_pair);
624
625 nlohmann::json r = {
626 {"alg", crv_alg.second},
627 {"kid", account_url},
628 {"nonce", nonce},
629 {"url", resource_url}};
630
631 return r;
632 }
633
635 {
636 http::URL url = with_default_port(config.directory_url);
637 make_json_request(
638 HTTP_GET,
639 url,
640 {},
641 HTTP_STATUS_OK,
642 [this](const ccf::http::HeaderMap&, const nlohmann::json& j) {
643 directory = j;
644 request_new_account();
645 });
646 }
647
648 void request_new_nonce(std::function<void()> ok_callback)
649 {
650 http::URL url = with_default_port(directory.at("newNonce"));
651 make_json_request(
652 HTTP_GET,
653 url,
654 {},
655 HTTP_STATUS_NO_CONTENT,
656 [this, ok_callback](
657 const ccf::http::HeaderMap& headers, const nlohmann::json& j) {
658 ok_callback();
659 return true;
660 });
661 }
662
664 {
665 std::string new_account_url =
666 directory.at("newAccount").get<std::string>();
667
668 if (nonces.empty())
669 {
670 request_new_nonce([this]() { request_new_account(); });
671 }
672 else
673 {
674 auto nonce = nonces.front();
675 nonces.pop_front();
676
677 auto crv_alg = get_crv_alg(account_key_pair);
678 auto key_coords = account_key_pair->coordinates();
679
680 JWK jwk(
681 "EC",
682 crv_alg.first,
683 ccf::crypto::b64url_from_raw(key_coords.x, false),
684 ccf::crypto::b64url_from_raw(key_coords.y, false));
685
686 nlohmann::json header = {
687 {"alg", crv_alg.second},
688 {"jwk", jwk},
689 {"nonce", nonce},
690 {"url", new_account_url}};
691
692 nlohmann::json payload = {
693 {"termsOfServiceAgreed", config.terms_of_service_agreed},
694 {"contact", config.contact}};
695
696 JWS jws(header, payload, *account_key_pair);
697
698 http::URL url = with_default_port(new_account_url);
699 make_json_request(
700 HTTP_POST,
701 url,
702 json_to_bytes(jws),
703 HTTP_STATUS_CREATED,
704 [this](const ccf::http::HeaderMap& headers, const nlohmann::json& j) {
705 expect_string(j, "status", "valid");
706 account = j;
707 auto loc_opt = get_header_value(headers, "location");
708 request_new_order(loc_opt.value_or(""));
709 });
710 }
711 }
712
713 void authorize_next_challenge(const std::string& order_url)
714 {
715 std::unique_lock<ccf::pal::Mutex> guard(orders_lock);
716 auto order = get_order(order_url);
717
718 if (!order)
719 {
720 return;
721 }
722
723 if (!order->authorizations.empty())
724 {
725 request_authorization(*order, *order->authorizations.begin());
726 }
727 }
728
729 void request_new_order(const std::string& account_url)
730 {
731 if (nonces.empty())
732 {
733 request_new_nonce(
734 [this, account_url]() { request_new_order(account_url); });
735 }
736 else
737 {
738 auto nonce = nonces.front();
739 nonces.pop_front();
740
741 auto header =
742 mk_kid_header(account_url, nonce, directory.at("newOrder"));
743
744 nlohmann::json payload = {
745 {"identifiers",
746 nlohmann::json::array(
747 {{{"type", "dns"}, {"value", config.service_dns_name}}})}};
748
749 for (const auto& n : config.alternative_names)
750 payload["identifiers"] += {{"type", "dns"}, {"value", n}};
751
752 if (config.not_before)
753 {
754 payload["notBefore"] = *config.not_before;
755 }
756 if (config.not_after)
757 {
758 payload["notAfter"] = *config.not_after;
759 }
760
761 JWS jws(header, payload, *account_key_pair);
762
763 http::URL url = with_default_port(directory.at("newOrder"));
764 make_json_request(
765 HTTP_POST,
766 url,
767 json_to_bytes(jws),
768 HTTP_STATUS_CREATED,
769 [this, account_url](
770 const ccf::http::HeaderMap& headers, const nlohmann::json& j) {
771 expect(j, "status");
772 expect(j, "finalize");
773
774 auto order_url_opt = get_header_value(headers, "location");
775 if (!order_url_opt)
776 {
777 throw std::runtime_error("Missing order location");
778 }
779
780 std::unique_lock<ccf::pal::Mutex> guard(orders_lock);
781 active_orders.emplace_back(Order{
782 ACTIVE, account_url, *order_url_opt, j["finalize"], "", {}, {}});
783
784 Order& order = active_orders.back();
785
786 const auto status = j["status"].get<std::string>();
787 if (status == "pending" && j.contains("authorizations"))
788 {
789 expect(j, "authorizations");
790 order.authorizations =
791 j["authorizations"].get<std::unordered_set<std::string>>();
792 guard.unlock();
793 authorize_next_challenge(*order_url_opt);
794 }
795 else if (status == "ready")
796 {
797 expect(j, "finalize");
798 guard.unlock();
799 request_finalization(*order_url_opt);
800 }
801 else if (status == "valid")
802 {
803 expect(j, "certificate");
804 order.certificate_url = j["certificate"];
805 guard.unlock();
806 request_certificate(*order_url_opt);
807 }
808 else
809 {
810 LOG_FAIL_FMT("ACME: unknown order status '{}', aborting", status);
811 guard.unlock();
812 remove_order(*order_url_opt);
813 }
814 });
815 }
816 }
817
818 void request_authorization(Order& order, const std::string& authz_url)
819 {
820 post_as_get_json(
821 order.account_url,
822 authz_url,
823 [this, order_url = order.order_url, authz_url](
824 const ccf::http::HeaderMap& headers, const nlohmann::json& j) {
825 LOG_TRACE_FMT("ACME: authorization reply: {}", j.dump());
826 expect_string(j, "status", "pending");
827 expect(j, "challenges");
828
829 {
830 std::unique_lock<ccf::pal::Mutex> guard(orders_lock);
831 auto order = get_order(order_url);
832
833 if (!order)
834 {
835 return false;
836 }
837
838 bool found_match = false;
839 for (const auto& challenge : j["challenges"])
840 {
841 if (
842 challenge.contains("type") &&
843 challenge["type"] == config.challenge_type)
844 {
845 expect_string(challenge, "status", "pending");
846 expect(challenge, "token");
847 expect(challenge, "url");
848
849 std::string token = challenge["token"];
850 std::string challenge_url = challenge["url"];
851
852 add_challenge(*order, token, authz_url, challenge_url);
853 found_match = true;
854 break;
855 }
856 }
857
858 order->authorizations.erase(authz_url);
859
860 if (!found_match)
861 {
862 throw std::runtime_error(fmt::format(
863 "Challenge type '{}' not offered", config.challenge_type));
864 }
865 }
866
867 authorize_next_challenge(order_url);
868
869 return true;
870 },
871 true);
872 }
873
874 std::string make_challenge_response() const
875 {
876 auto crv_alg = get_crv_alg(account_key_pair);
877 auto key_coords = account_key_pair->coordinates();
878
879 JWK jwk(
880 "EC",
881 crv_alg.first,
882 ccf::crypto::b64url_from_raw(key_coords.x, false),
883 ccf::crypto::b64url_from_raw(key_coords.y, false));
884
885 auto thumbprint = ccf::crypto::sha256(s2v(nlohmann::json(jwk).dump()));
886 return ccf::crypto::b64url_from_raw(thumbprint, false);
887 }
888
890 Order& order,
891 const std::string& token,
892 const std::string& authorization_url,
893 const std::string& challenge_url)
894 {
895 auto response = make_challenge_response();
896
897 order.challenges.emplace(
898 token, Challenge{token, authorization_url, challenge_url});
899
900 on_challenge(token, response);
901 }
902
904 {
906 const std::string& order_url, Challenge challenge, Client* client) :
907 order_url(order_url),
908 challenge(challenge),
910 {}
911 std::string order_url;
914 };
915
916 std::unique_ptr<threading::Tmsg<ChallengeWaitMsg>> schedule_check_challenge(
917 const std::string& order_url, Challenge& challenge)
918 {
919 return std::make_unique<threading::Tmsg<ChallengeWaitMsg>>(
920 [](std::unique_ptr<threading::Tmsg<ChallengeWaitMsg>> msg) {
921 std::string& order_url = msg->data.order_url;
922 Challenge& challenge = msg->data.challenge;
923 Client* client = msg->data.client;
924
925 if (client->check_challenge(order_url, challenge))
926 {
927 LOG_TRACE_FMT("ACME: scheduling next challenge check");
929 std::move(msg), std::chrono::seconds(5));
930 }
931 },
932 order_url,
933 challenge,
934 this);
935 }
936
938 const std::string& order_url, const Challenge& challenge)
939 {
940 std::unique_lock<ccf::pal::Mutex> guard(orders_lock);
941 auto order = get_order(order_url);
942
943 if (
944 !order ||
945 order->challenges.find(challenge.token) == order->challenges.end())
946 {
947 return false;
948 }
949
951 "ACME: requesting challenge status for token '{}' ...",
952 challenge.token);
953
954 // This post-as-get with empty body ("", not "{}"), but json response.
955 post_as_get(
956 order->account_url,
957 challenge.authorization_url,
958 [this, order_url, challenge_token = challenge.token](
959 const ccf::http::HeaderMap& headers,
960 const std::vector<uint8_t>& body) {
961 auto j = nlohmann::json::parse(body);
962 LOG_TRACE_FMT("ACME: authorization status: {}", j.dump());
963 expect(j, "status");
964
965 const auto status = j["status"].get<std::string>();
966 if (status == "valid")
967 {
968 finish_challenge(order_url, challenge_token);
969 }
970 else if (status == "pending" || status == "processing")
971 {
972 if (j.contains("error"))
973 {
974 LOG_FAIL_FMT(
975 "ACME: challenge for token '{}' failed with the following "
976 "error: {}",
977 challenge_token,
978 j["error"].dump());
979 finish_challenge(order_url, challenge_token);
980 }
981 else
982 {
983 return true;
984 }
985 }
986 else
987 {
989 "ACME: challenge for token '{}' failed with status '{}' ",
990 challenge_token,
991 status);
992 finish_challenge(order_url, challenge_token);
993 }
994
995 return false;
996 });
997
998 return true;
999 }
1000
1002 const std::string& order_url, const std::string& challenge_token)
1003 {
1004 bool order_done = false;
1005
1006 {
1007 std::unique_lock<ccf::pal::Mutex> guard(orders_lock);
1008 auto order = get_order(order_url);
1009
1010 if (!order)
1011 {
1012 return;
1013 }
1014
1015 auto cit = order->challenges.find(challenge_token);
1016 if (cit == order->challenges.end())
1017 {
1018 throw std::runtime_error(
1019 fmt::format("No active challenge for token '{}'", challenge_token));
1020 }
1021
1022 on_challenge_finished(cit->first);
1023 order->challenges.erase(cit);
1024 order_done = order->challenges.empty();
1025 }
1026
1027 if (order_done)
1028 {
1029 request_finalization(order_url);
1030 }
1031 }
1032
1033 bool check_finalization(const std::string& order_url)
1034 {
1035 std::unique_lock<ccf::pal::Mutex> guard2(orders_lock);
1036 auto order = get_order(order_url);
1037
1038 if (!order)
1039 {
1040 return false;
1041 }
1042
1043 LOG_TRACE_FMT("ACME: checking finalization of {}", order_url);
1044
1045 // This post-as-get with empty body ("", not "{}"), but json response.
1046 post_as_get(
1047 order->account_url,
1048 order->order_url,
1049 [this, order_url](
1050 const ccf::http::HeaderMap& headers,
1051 const std::vector<uint8_t>& body) {
1052 auto j = nlohmann::json::parse(body);
1053 LOG_TRACE_FMT("ACME: finalization status: {}", j.dump());
1054 expect(j, "status");
1055 const auto status = j["status"].get<std::string>();
1056 if (status == "valid")
1057 {
1058 expect(j, "certificate");
1059 {
1060 std::unique_lock<ccf::pal::Mutex> guard(orders_lock);
1061 auto order = get_order(order_url);
1062 if (order)
1063 {
1064 order->certificate_url = j["certificate"];
1065 }
1066 }
1067 request_certificate(order_url);
1068 }
1069 else if (status == "invalid")
1070 {
1071 LOG_TRACE_FMT("ACME: removing failed order");
1072 remove_order(order_url);
1073 }
1074 else if (status != "pending" && status != "processing")
1075 {
1077 "ACME: unknown order status '{}'; aborting order", status);
1078 remove_order(order_url);
1079 }
1080 return true;
1081 });
1082
1083 return true;
1084 }
1085
1087 {
1088 FinalizationWaitMsg(const std::string& order_url, Client* client) :
1089 order_url(order_url),
1090 client(client)
1091 {}
1092 std::string order_url;
1094 };
1095
1096 std::unique_ptr<threading::Tmsg<FinalizationWaitMsg>>
1097 schedule_check_finalization(const std::string& order_url)
1098 {
1099 return std::make_unique<threading::Tmsg<FinalizationWaitMsg>>(
1100 [](std::unique_ptr<threading::Tmsg<FinalizationWaitMsg>> msg) {
1101 Client* client = msg->data.client;
1102 const std::string& order_url = msg->data.order_url;
1103
1104 if (client->check_finalization(order_url))
1105 {
1106 LOG_TRACE_FMT("ACME: scheduling next finalization check");
1108 std::move(msg), std::chrono::seconds(5));
1109 }
1110 },
1111 order_url,
1112 this);
1113 }
1114
1115 virtual std::vector<uint8_t> get_service_csr()
1116 {
1117 std::vector<ccf::crypto::SubjectAltName> alt_names;
1118 alt_names.push_back({config.service_dns_name, false});
1119 for (const auto& an : config.alternative_names)
1120 alt_names.push_back({an, false});
1121 return service_key->create_csr_der(
1122 "CN=" + config.service_dns_name, alt_names);
1123 }
1124
1125 void request_finalization(const std::string& order_url)
1126 {
1127 if (nonces.empty())
1128 {
1129 request_new_nonce(
1130 [this, &order_url]() { request_finalization(order_url); });
1131 }
1132 else
1133 {
1134 std::unique_lock<ccf::pal::Mutex> guard(orders_lock);
1135 auto order = get_order(order_url);
1136
1137 if (!order)
1138 {
1139 return;
1140 }
1141
1142 auto nonce = nonces.front();
1143 nonces.pop_front();
1144
1145 auto header =
1146 mk_kid_header(order->account_url, nonce, order->finalize_url);
1147
1148 auto csr = get_service_csr();
1149
1150 nlohmann::json payload = {
1151 {"csr", ccf::crypto::b64url_from_raw(csr, false)}};
1152
1153 JWS jws(header, payload, *account_key_pair);
1154
1155 http::URL url = with_default_port(order->finalize_url);
1156 make_json_request(
1157 HTTP_POST,
1158 url,
1159 json_to_bytes(jws),
1160 HTTP_STATUS_OK,
1161 [this, order_url = order->order_url](
1162 const ccf::http::HeaderMap& headers, const nlohmann::json& j) {
1163 LOG_TRACE_FMT("ACME: finalization status: {}", j.dump());
1164 expect(j, "status");
1165 const auto status = j["status"].get<std::string>();
1166 if (status == "valid")
1167 {
1168 expect(j, "certificate");
1169
1170 {
1171 std::unique_lock<ccf::pal::Mutex> guard(orders_lock);
1172 auto order = get_order(order_url);
1173 if (order)
1174 {
1175 order->certificate_url = j["certificate"];
1176 }
1177 }
1178 request_certificate(order_url);
1179 }
1180 else
1181 {
1182 LOG_TRACE_FMT("ACME: scheduling finalization check");
1183 threading::ThreadMessaging::instance().add_task_after(
1184 schedule_check_finalization(order_url),
1185 std::chrono::milliseconds(0));
1186 }
1187 });
1188 }
1189 }
1190
1191 void request_certificate(const std::string& order_url)
1192 {
1193 if (nonces.empty())
1194 {
1195 request_new_nonce(
1196 [this, &order_url]() { request_certificate(order_url); });
1197 }
1198 else
1199 {
1200 std::unique_lock<ccf::pal::Mutex> guard(orders_lock);
1201 auto order = get_order(order_url);
1202
1203 if (!order)
1204 {
1205 return;
1206 }
1207
1208 http::URL url = with_default_port(order->certificate_url);
1209 post_as_get(
1210 order->account_url,
1211 order->certificate_url,
1212 [this, order_url](
1213 const ccf::http::HeaderMap& headers,
1214 const std::vector<uint8_t>& data) {
1215 std::string c(data.data(), data.data() + data.size());
1216 LOG_TRACE_FMT("ACME: obtained certificate (chain): {}", c);
1217
1218 on_certificate(c);
1219
1220 remove_order(order_url);
1221
1222 last_request = std::chrono::system_clock::now();
1223 num_failed_attempts = 0;
1224
1225 return true;
1226 });
1227 }
1228 }
1229 };
1230}
Definition acme_client.h:489
virtual ~JWK()=default
JWK(const std::string &kty, const std::string &crv, const std::string &x, const std::string &y, const std::optional< std::string > &alg=std::nullopt, const std::optional< std::string > &use=std::nullopt, const std::optional< std::string > &kid=std::nullopt)
Definition acme_client.h:491
Definition acme_client.h:450
virtual ~JWS()
Definition acme_client.h:469
JWS(const nlohmann::json &header_, ccf::crypto::KeyPair &signer_)
Definition acme_client.h:465
void set(const std::string &header_b64, const std::string &payload_b64, ccf::crypto::KeyPair &signer)
Definition acme_client.h:472
JWS(const nlohmann::json &header_, const nlohmann::json &payload_, ccf::crypto::KeyPair &signer_, bool empty_payload=false)
Definition acme_client.h:452
Definition acme_client.h:64
bool check_challenge(const std::string &order_url, const Challenge &challenge)
Definition acme_client.h:937
void get_certificate(std::shared_ptr< ccf::crypto::KeyPair > service_key_, bool override_time=false)
Definition acme_client.h:76
static std::vector< uint8_t > s2v(const std::string &s)
Definition acme_client.h:418
static void expect_string(const nlohmann::json &j, const std::string &key, const std::string &value)
Definition acme_client.h:536
void add_challenge(Order &order, const std::string &token, const std::string &authorization_url, const std::string &challenge_url)
Definition acme_client.h:889
std::list< std::string > nonces
Definition acme_client.h:371
void post_as_get_json(const std::string &account_url, const std::string &resource_url, std::function< bool(const ccf::http::HeaderMap &, const nlohmann::json &)> ok_callback, bool empty_payload=false)
Definition acme_client.h:320
bool has_active_orders() const
Definition acme_client.h:162
std::unique_ptr< threading::Tmsg< FinalizationWaitMsg > > schedule_check_finalization(const std::string &order_url)
Definition acme_client.h:1097
virtual void on_challenge_finished(const std::string &token)=0
virtual void on_challenge(const std::string &token, const std::string &response)=0
virtual void set_account_key(std::shared_ptr< ccf::crypto::KeyPair > new_account_key_pair)
Definition acme_client.h:151
static std::string json_to_b64url(const nlohmann::json &j, bool with_padding=true)
Definition acme_client.h:428
ccf::pal::Mutex req_lock
Definition acme_client.h:373
void start_challenge(const std::string &token)
Definition acme_client.h:130
void authorize_next_challenge(const std::string &order_url)
Definition acme_client.h:713
static http::URL with_default_port(const std::string &url, const std::string &default_port="443")
Definition acme_client.h:407
std::shared_ptr< ccf::crypto::KeyPair > service_key
Definition acme_client.h:366
nlohmann::json directory
Definition acme_client.h:369
void post_as_get(const std::string &account_url, const std::string &resource_url, std::function< bool(const ccf::http::HeaderMap &, const std::vector< uint8_t > &)> ok_callback)
Definition acme_client.h:297
std::optional< std::chrono::system_clock::time_point > last_request
Definition acme_client.h:376
void request_new_order(const std::string &account_url)
Definition acme_client.h:729
virtual std::vector< uint8_t > get_service_csr()
Definition acme_client.h:1115
size_t num_failed_attempts
Definition acme_client.h:378
static std::optional< std::string > get_header_value(const ccf::http::HeaderMap &headers, const std::string &name)
Definition acme_client.h:514
void request_directory()
Definition acme_client.h:634
virtual void on_certificate(const std::string &certificate)=0
bool check_finalization(const std::string &order_url)
Definition acme_client.h:1033
virtual void on_http_request(const http::URL &url, http::Request &&req, std::function< bool(http_status status, ccf::http::HeaderMap &&, std::vector< uint8_t > &&)> callback)=0
Order * get_order(const std::string &order_url)
Definition acme_client.h:572
static void expect(const nlohmann::json &j, const std::string &key)
Definition acme_client.h:528
void request_certificate(const std::string &order_url)
Definition acme_client.h:1191
nlohmann::json account
Definition acme_client.h:370
void request_authorization(Order &order, const std::string &authz_url)
Definition acme_client.h:818
std::shared_ptr< ccf::crypto::KeyPair > account_key_pair
Definition acme_client.h:367
void request_finalization(const std::string &order_url)
Definition acme_client.h:1125
virtual ~Client()
Definition acme_client.h:74
static void convert_signature_to_ieee_p1363(std::vector< uint8_t > &sig, const ccf::crypto::KeyPair &signer)
Definition acme_client.h:434
void request_new_nonce(std::function< void()> ok_callback)
Definition acme_client.h:648
void make_json_request(llhttp_method method, const http::URL &url, const std::vector< uint8_t > &body, http_status expected_status, std::function< void(const ccf::http::HeaderMap &headers, const nlohmann::json &)> ok_callback)
Definition acme_client.h:259
std::list< Order > active_orders
Definition acme_client.h:405
std::unique_ptr< threading::Tmsg< ChallengeWaitMsg > > schedule_check_challenge(const std::string &order_url, Challenge &challenge)
Definition acme_client.h:916
void finish_challenge(const std::string &order_url, const std::string &challenge_token)
Definition acme_client.h:1001
OrderStatus
Definition acme_client.h:388
@ ACTIVE
Definition acme_client.h:389
@ FINISHED
Definition acme_client.h:390
std::string make_challenge_response() const
Definition acme_client.h:874
static std::pair< std::string, std::string > get_crv_alg(const std::shared_ptr< ccf::crypto::KeyPair > &key_pair)
Definition acme_client.h:552
void make_request(llhttp_method method, const http::URL &url, const std::vector< uint8_t > &body, http_status expected_status, std::function< bool(const ccf::http::HeaderMap &, const std::vector< uint8_t > &)> ok_callback)
Definition acme_client.h:179
ccf::pal::Mutex orders_lock
Definition acme_client.h:374
ClientConfig config
Definition acme_client.h:365
void remove_order(const std::string &order_url)
Definition acme_client.h:591
nlohmann::json mk_kid_header(const std::string &account_url, const std::string &nonce, const std::string &resource_url)
Definition acme_client.h:614
Client(const ClientConfig &config, std::shared_ptr< ccf::crypto::KeyPair > account_key_pair=nullptr)
Definition acme_client.h:66
void request_new_account()
Definition acme_client.h:663
static std::vector< uint8_t > json_to_bytes(const nlohmann::json &j)
Definition acme_client.h:423
Definition key_pair.h:19
virtual PublicKey::Coordinates coordinates() const =0
virtual std::vector< uint8_t > sign(std::span< const uint8_t > d, MDType md_type={}) const =0
void set_body(const std::vector< uint8_t > *b)
Definition http_builder.h:74
void set_header(std::string k, const std::string &v)
Definition http_builder.h:45
Definition http_builder.h:106
std::vector< uint8_t > build_request(bool header_only=false) const
Definition http_builder.h:165
static ThreadMessaging & instance()
Definition thread_messaging.h:278
TaskQueue::TimerEntry add_task_after(std::unique_ptr< Tmsg< Payload > > msg, std::chrono::milliseconds ms)
Definition thread_messaging.h:320
llhttp_status http_status
Definition http_status.h:7
#define LOG_INFO_FMT
Definition logger.h:395
#define LOG_TRACE_FMT
Definition logger.h:378
#define LOG_DEBUG_FMT
Definition logger.h:380
#define LOG_FAIL_FMT
Definition logger.h:396
Definition acme_client.h:30
std::string b64url_from_raw(const uint8_t *data, size_t size, bool with_padding=true)
Definition base64.cpp:49
HashBytes sha256(const std::span< uint8_t const > &data)
Definition hash.cpp:24
@ SECP384R1
The SECP384R1 curve.
@ SECP256R1
The SECP256R1 curve.
KeyPairPtr make_key_pair(CurveID curve_id=service_identity_curve_choice)
Definition key_pair.cpp:35
std::map< std::string, std::string, std::less<> > HeaderMap
Definition http_header_map.h:10
std::mutex Mutex
Definition locking.h:17
Definition perf_client.h:26
URL parse_url_full(const std::string &url)
Definition http_parser.h:145
Definition json_schema.h:15
Definition ledger_secret.h:106
Definition acme_client.h:32
std::string directory_url
Definition acme_client.h:38
bool operator==(const ClientConfig &other) const =default
std::string challenge_type
Definition acme_client.h:54
std::optional< std::string > not_before
Definition acme_client.h:57
std::optional< std::string > not_after
Definition acme_client.h:58
std::vector< std::string > contact
Definition acme_client.h:47
std::string service_dns_name
Definition acme_client.h:41
std::vector< std::string > ca_certs
Definition acme_client.h:35
bool terms_of_service_agreed
Definition acme_client.h:51
std::vector< std::string > alternative_names
Definition acme_client.h:44
Definition acme_client.h:904
Challenge challenge
Definition acme_client.h:912
ChallengeWaitMsg(const std::string &order_url, Challenge challenge, Client *client)
Definition acme_client.h:905
std::string order_url
Definition acme_client.h:911
Client * client
Definition acme_client.h:913
Definition acme_client.h:381
std::string token
Definition acme_client.h:382
std::string challenge_url
Definition acme_client.h:384
std::string authorization_url
Definition acme_client.h:383
Definition acme_client.h:1087
FinalizationWaitMsg(const std::string &order_url, Client *client)
Definition acme_client.h:1088
std::string order_url
Definition acme_client.h:1092
Client * client
Definition acme_client.h:1093
Definition acme_client.h:395
std::map< std::string, Challenge > challenges
Definition acme_client.h:402
std::string finalize_url
Definition acme_client.h:399
std::string certificate_url
Definition acme_client.h:400
std::string account_url
Definition acme_client.h:397
std::unordered_set< std::string > authorizations
Definition acme_client.h:401
std::string order_url
Definition acme_client.h:398
std::vector< uint8_t > x
Definition public_key.h:143
Definition http_parser.h:136
std::string host
Definition http_parser.h:138
std::string port
Definition http_parser.h:139
std::string path
Definition http_parser.h:140