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