From 97ce0bee159fca5d71ee940dabfcc3d4b0acfb4a Mon Sep 17 00:00:00 2001 From: Mario Gottardo Date: Tue, 29 Jul 2025 19:22:40 +0200 Subject: [PATCH 1/8] Added CHALLENGE_SIZE constant --- include/self-cert-bot/cert_utils.h | 2 ++ src/server/Server.cpp | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/include/self-cert-bot/cert_utils.h b/include/self-cert-bot/cert_utils.h index e69552c..2f62c8e 100644 --- a/include/self-cert-bot/cert_utils.h +++ b/include/self-cert-bot/cert_utils.h @@ -9,6 +9,8 @@ #include namespace certbot { +#define CHALLENGE_SIZE 24 + typedef struct CertFieldsStruct { std::string C; std::string ST; diff --git a/src/server/Server.cpp b/src/server/Server.cpp index 8883b11..9ccaf02 100644 --- a/src/server/Server.cpp +++ b/src/server/Server.cpp @@ -72,7 +72,7 @@ namespace certbot { const int clientChallengeSocket = setup_socket_client(ipv4->sin_addr.s_addr, port); char buffer[32] = {}; - recv(clientChallengeSocket, buffer, 24, 0); + recv(clientChallengeSocket, buffer, CHALLENGE_SIZE, 0); close(clientChallengeSocket); std::cout << "received challenge " << buffer << std::endl; From c576676f84329ebb8efc91ab64b0ed34c4567096 Mon Sep 17 00:00:00 2001 From: Mario Gottardo Date: Tue, 29 Jul 2025 19:24:18 +0200 Subject: [PATCH 2/8] Server code cleanup --- src/server/Server.cpp | 68 +++++++++++--------------------------- src/utils/utils.cpp | 2 +- tests/utils/test_utils.cpp | 10 +++++- 3 files changed, 29 insertions(+), 51 deletions(-) diff --git a/src/server/Server.cpp b/src/server/Server.cpp index 9ccaf02..3ab589c 100644 --- a/src/server/Server.cpp +++ b/src/server/Server.cpp @@ -38,13 +38,13 @@ namespace certbot { } const char *connected_ip = inet_ntoa(clientAddress.sin_addr); + std::cout << "New client connected: " << connected_ip << std::endl; - std::vector domainVector = receiveSocketMessage(ssl).value(); - const std::string domain(domainVector.begin(), domainVector.end()); + const std::string domain(receiveSocketMessage(ssl).value().data()); std::cout << "requested domain " << domain << std::endl; - const std::string challenge = generate_random_string(24); + const std::string challenge = generate_random_string(CHALLENGE_SIZE); sendSocketMessage(ssl, challenge); std::cout << "challenge sent " << challenge << ", size: " << challenge.size() << std::endl; @@ -56,29 +56,30 @@ namespace certbot { addrinfo *result = resolve_domain(domain); - const sockaddr_in *ipv4 = nullptr; - for (const addrinfo *p = result; p != nullptr; p = p->ai_next) { - char ipStr[INET_ADDRSTRLEN]; - // const sockaddr_in *ipv4 = reinterpret_cast(p->ai_addr); - ipv4 = reinterpret_cast(p->ai_addr); - - inet_ntop(AF_INET, &ipv4->sin_addr, ipStr, sizeof(ipStr)); - std::cout << ipStr << std::endl; - break; + if (result == nullptr) { + // failed resolution + return; } + const sockaddr_in *ipv4 = nullptr; + + char ipStr[INET_ADDRSTRLEN]; + ipv4 = reinterpret_cast(result->ai_addr); + + inet_ntop(AF_INET, &ipv4->sin_addr, ipStr, sizeof(ipStr)); + std::cout << ipStr << std::endl; + if (ipv4 == nullptr) return; const int clientChallengeSocket = setup_socket_client(ipv4->sin_addr.s_addr, port); + freeaddrinfo(result); char buffer[32] = {}; recv(clientChallengeSocket, buffer, CHALLENGE_SIZE, 0); close(clientChallengeSocket); std::cout << "received challenge " << buffer << std::endl; - const bool isChallengeCorrect = challenge == std::string(buffer); - - if (isChallengeCorrect) { + if (challenge == std::string(buffer)) { std::cout << "Challenge matches" << std::endl; auto cert_fields_buffer = receiveSocketMessage(ssl).value(); @@ -95,10 +96,11 @@ namespace certbot { X509_free(child_cert); EVP_PKEY_free(child_pkey); + } else { + // TODO: blacklist client + std::cout << "Challenge does not match" << std::endl; } - freeaddrinfo(result); - SSL_shutdown(ssl); SSL_free(ssl); close(clientSocket); @@ -172,24 +174,6 @@ namespace certbot { std::cerr << "Error reading certificate!" << std::endl; exit(EXIT_FAILURE); } - - /* - if (const X509_NAME *subject = X509_get_issuer_name(ca_cert)) { - char buffer[256]; - const auto entry = X509_NAME_get_entry(subject, 3); - - const auto s = X509_NAME_ENTRY_get_data(entry); - - X509_NAME_oneline(subject, buffer, sizeof(buffer)); - std::cout << "Cert Subject: " << buffer << std::endl << s->data << std::endl; - } else { - std::cerr << "Failed to get subject name!" << std::endl; - } - */ - - // BIO_free(bio_cert); - - // this->root_cert; } Server::~Server() { @@ -199,20 +183,6 @@ namespace certbot { } void Server::start() { - /* - addrinfo *result = resolve_domain(domain); - - for (const addrinfo *p = result; p != nullptr; p = p->ai_next) { - char ipStr[INET_ADDRSTRLEN]; - const sockaddr_in *ipv4 = reinterpret_cast(p->ai_addr); - - inet_ntop(AF_INET, &ipv4->sin_addr, ipStr, sizeof(ipStr)); - std::cout << ipStr << std::endl; - } - - freeaddrinfo(result); - */ - const int serverSocket = configureServer(this->conf.port); configure_SSL_context(); std::cout << generate_random_string(64) << std::endl; diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp index d40f468..25d7931 100644 --- a/src/utils/utils.cpp +++ b/src/utils/utils.cpp @@ -80,7 +80,7 @@ addrinfo *resolve_domain(const std::string &domain) { if (const int s = getaddrinfo(domain.c_str(), nullptr, &hints, &result); s != 0) { std::cerr << "getaddrinfo: " << gai_strerror(s); - //exit(EXIT_FAILURE); + return nullptr; } return result; diff --git a/tests/utils/test_utils.cpp b/tests/utils/test_utils.cpp index 4f1cfd9..1f6f980 100644 --- a/tests/utils/test_utils.cpp +++ b/tests/utils/test_utils.cpp @@ -26,7 +26,9 @@ BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE(resolve_domainTestSuite) BOOST_AUTO_TEST_CASE(DomainResolution) { - const addrinfo* addrinfo = certbot::resolve_domain("local.mostserene.eu"); + const addrinfo *addrinfo = certbot::resolve_domain("local.mostserene.eu"); + + BOOST_CHECK(addrinfo != nullptr); char ipStr[INET_ADDRSTRLEN]; const auto *ipv4 = reinterpret_cast(addrinfo->ai_addr); @@ -36,6 +38,12 @@ BOOST_AUTO_TEST_SUITE(resolve_domainTestSuite) BOOST_CHECK(strcmp(ipStr, "127.0.0.1") == 0); } + BOOST_AUTO_TEST_CASE(NonExistentDomainResolution) { + const addrinfo *addrinfo = certbot::resolve_domain("non-existent.mostserene.eu"); + + BOOST_CHECK(addrinfo == nullptr); + } + BOOST_AUTO_TEST_SUITE_END() From 02e152350ffd157b9940ef5e812be32d442941c5 Mon Sep 17 00:00:00 2001 From: Mario Gottardo Date: Tue, 29 Jul 2025 21:20:00 +0200 Subject: [PATCH 3/8] Improved server thread body structure --- src/server/Server.cpp | 83 +++++++++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 34 deletions(-) diff --git a/src/server/Server.cpp b/src/server/Server.cpp index 3ab589c..8633faf 100644 --- a/src/server/Server.cpp +++ b/src/server/Server.cpp @@ -21,44 +21,40 @@ #include "self-cert-bot/protocol_utils.hpp" namespace certbot { - - void thread_body(const int clientSocket, const sockaddr_in &clientAddress, SSL_CTX *ctx, - const X509* ca_cert, EVP_PKEY* ca_pkey, const char* ca_passkey - ) { - SSL *ssl = SSL_new(ctx); - - if (SSL_set_fd(ssl, clientSocket) == 0) { - std::cerr << "Failed to bound the file descriptor\n"; + int setup_secure_connection(SSL *ssl, const int clientSocket, const sockaddr_in &clientAddress) { + if (const int ret = SSL_set_fd(ssl, clientSocket); ret == 0) { + std::cerr << "Failed to bound the file descriptor: " << ret << std::endl; ERR_print_errors_fp(stderr); + return 1; } if (const int ret = SSL_accept(ssl); ret != 1) { std::cerr << "Failed to accept a connection: " << ret << std::endl; ERR_print_errors_fp(stderr); + return 1; } const char *connected_ip = inet_ntoa(clientAddress.sin_addr); std::cout << "New client connected: " << connected_ip << std::endl; - const std::string domain(receiveSocketMessage(ssl).value().data()); + return 0; + } + int execute_challenge(SSL *ssl) { + const std::string domain(receiveSocketMessage(ssl).value().data()); std::cout << "requested domain " << domain << std::endl; const std::string challenge = generate_random_string(CHALLENGE_SIZE); sendSocketMessage(ssl, challenge); - std::cout << "challenge sent " << challenge << ", size: " << challenge.size() << std::endl; unsigned short port; std::memcpy(&port, receiveSocketMessage(ssl).value().data(), sizeof(unsigned short)); - std::cout << "port: " << port << std::endl; addrinfo *result = resolve_domain(domain); - if (result == nullptr) { - // failed resolution - return; + return 2; } const sockaddr_in *ipv4 = nullptr; @@ -69,7 +65,7 @@ namespace certbot { inet_ntop(AF_INET, &ipv4->sin_addr, ipStr, sizeof(ipStr)); std::cout << ipStr << std::endl; - if (ipv4 == nullptr) return; + if (ipv4 == nullptr) return 2; const int clientChallengeSocket = setup_socket_client(ipv4->sin_addr.s_addr, port); freeaddrinfo(result); @@ -79,26 +75,47 @@ namespace certbot { close(clientChallengeSocket); std::cout << "received challenge " << buffer << std::endl; - if (challenge == std::string(buffer)) { - std::cout << "Challenge matches" << std::endl; + return challenge == std::string(buffer) ? 0 : 1; + } + + int issue_certificate(SSL *ssl, const X509 *ca_cert, EVP_PKEY *ca_pkey) { + std::cout << "Challenge matches" << std::endl; + + auto cert_fields_buffer = receiveSocketMessage(ssl).value(); + const CertFields *p_obj = reinterpret_cast(cert_fields_buffer.data()); + + const auto serialized_p_key = receiveSocketMessage(ssl).value(); + EVP_PKEY *child_pkey = deserializePublicKey( + std::vector(serialized_p_key.begin(), serialized_p_key.end())); + + X509 *child_cert = craft_certificate(ca_cert, ca_pkey, child_pkey, *p_obj); + + if (child_cert == nullptr) { + return 1; + } - auto cert_fields_buffer = receiveSocketMessage(ssl).value(); - const CertFields* p_obj = reinterpret_cast(cert_fields_buffer.data()); + const std::vector serializedCert = serializeX509ToDER(child_cert); - const auto serialized_p_key = receiveSocketMessage(ssl).value(); - EVP_PKEY* child_pkey = deserializePublicKey(std::vector(serialized_p_key.begin(), serialized_p_key.end())); + sendSocketMessageRaw(ssl, serializedCert); - X509* child_cert = craft_certificate(ca_cert, ca_pkey, child_pkey, *p_obj); + X509_free(child_cert); + EVP_PKEY_free(child_pkey); - const std::vector serializedCert = serializeX509ToDER(child_cert); + return 0; + } - sendSocketMessageRaw(ssl, serializedCert); + void thread_body(const int clientSocket, const sockaddr_in &clientAddress, SSL_CTX *ctx, + const X509 *ca_cert, EVP_PKEY *ca_pkey + ) { + SSL *ssl = SSL_new(ctx); - X509_free(child_cert); - EVP_PKEY_free(child_pkey); - } else { - // TODO: blacklist client - std::cout << "Challenge does not match" << std::endl; + if (const int ret = setup_secure_connection(ssl, clientSocket, clientAddress); ret == 0) { + if (execute_challenge(ssl) == 0) { + issue_certificate(ssl, ca_cert, ca_pkey); + } else { + // TODO: blacklist client + std::cout << "Challenge does not match" << std::endl; + } } SSL_shutdown(ssl); @@ -123,8 +140,7 @@ namespace certbot { } [[noreturn]] void serverBody(const int serverSocket, SSL_CTX *ctx, - const X509* ca_cert, EVP_PKEY* ca_pkey, const std::string &passkey - ) { + const X509 *ca_cert, EVP_PKEY *ca_pkey) { while (true) { sockaddr_in clientAddress = {}; socklen_t clientAddressLength = sizeof clientAddress; @@ -133,7 +149,7 @@ namespace certbot { &clientAddressLength); auto thread = std::thread(thread_body, clientSocket, clientAddress, ctx, - ca_cert, ca_pkey, passkey.c_str() + ca_cert, ca_pkey ); thread.detach(); } @@ -185,9 +201,8 @@ namespace certbot { void Server::start() { const int serverSocket = configureServer(this->conf.port); configure_SSL_context(); - std::cout << generate_random_string(64) << std::endl; - serverBody(serverSocket, ctx, ca_cert, ca_pkey, ca_passkey); + serverBody(serverSocket, ctx, ca_cert, ca_pkey); } void Server::configure_SSL_context() { From 85a96eae2b52f08cc8fa1c7bf8de90a799362b05 Mon Sep 17 00:00:00 2001 From: Mario Gottardo Date: Wed, 30 Jul 2025 00:18:55 +0200 Subject: [PATCH 4/8] README.md draft --- CMakeLists.txt | 10 +++++---- README.md | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index cfc79d5..13cded3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,9 +1,10 @@ cmake_minimum_required(VERSION 3.30) -project(self_cert_bot VERSION 0.1.0) +project(self_cert_bot VERSION 1.0.0) set(CMAKE_CXX_STANDARD 20) +set(EXECUTABLE_NAME self-cert) -add_library(self_cert_lib +add_library(self_cert_lib STATIC src/client/Client.cpp src/server/Server.cpp src/utils/cert_utils.cpp @@ -27,6 +28,7 @@ target_link_libraries(self_cert_lib nlohmann_json::nlohmann_json ) -add_executable(${PROJECT_NAME} src/main.cpp) +add_executable(${EXECUTABLE_NAME} src/main.cpp) + +target_link_libraries(${EXECUTABLE_NAME} self_cert_lib) -target_link_libraries(${PROJECT_NAME} self_cert_lib) diff --git a/README.md b/README.md index 283cf80..3b80b06 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,64 @@ _A library to enable automatic certificate issuing and renewal for self-hosted architectures_ +## Description + + +## JASC - Just a simple challenge +Domain ownership is proven by solving a simple challenge. + + +## Installation + +### External dependencies: +- [OpenSSL](https://openssl-library.org/) +- [nlohmann_json](https://json.nlohmann.me/) + +### Build + +```bash +$ mkdir build +$ cd build +$ cmake .. +$ make +``` + +## Usage + +### Server +Configuration: +```json +{ + "domain": "example.com", + "outPath": "./data/generated", + "port": 14024, + "serverIp": "10.0.0.1", + "serverPort": 8080, + "C": "CN", + "ST": "ST", + "O": "Organization", + "OU": "Organization Unit" +} +``` +Usage: +```bash +$ self-cert -m server -c ./settings/server.json +``` + +### Client +Configuration: +```json +{ + "port": 8080, + "ca_cert_path": "./data/yourCA.pem", + "ca_key_path": "./data/yourCA.key", + "ca_passkey_path": "./data/passkey.txt" +} +``` +Usage: +```bash +$ self-cert -m client --parallel +``` +```bash +$ self-cert -m client -c ./settings/client.json +``` From 773e9c5f3496ffd80c9f650aac2f344a5c03761a Mon Sep 17 00:00:00 2001 From: Mario Gottardo Date: Mon, 25 Aug 2025 20:19:05 +0200 Subject: [PATCH 5/8] Config case standardization --- src/client/Client.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/Client.cpp b/src/client/Client.cpp index 3c028dd..cd3225f 100644 --- a/src/client/Client.cpp +++ b/src/client/Client.cpp @@ -27,13 +27,13 @@ namespace certbot { nlohmann::json data = nlohmann::json::parse(json_file); conf.domain = data["domain"].get(); conf.challengePort = data["port"].get(); - conf.outPath = data["outPath"].get(); + conf.outPath = data["out_path"].get(); conf.C = data["C"].get(); conf.ST = data["ST"].get(); conf.O = data["O"].get(); conf.OU = data["OU"].get(); - serverIp = inet_addr(data["serverIp"].get().c_str()); - serverPort = data["serverPort"].get(); + serverIp = inet_addr(data["server_ip"].get().c_str()); + serverPort = data["server_port"].get(); } Client::Client(const std::string &conf_path) { From b0298aa2ab20becf46af1e69cb4a8b117e3041b0 Mon Sep 17 00:00:00 2001 From: Mario Gottardo Date: Mon, 25 Aug 2025 20:20:19 +0200 Subject: [PATCH 6/8] Version set to 0.1.0 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 13cded3..42e3332 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.30) -project(self_cert_bot VERSION 1.0.0) +project(self_cert_bot VERSION 0.1.0) set(CMAKE_CXX_STANDARD 20) set(EXECUTABLE_NAME self-cert) From 246e1bbe41b29bb3abb4b8f470d66eed6970dfd3 Mon Sep 17 00:00:00 2001 From: Mario Gottardo Date: Mon, 25 Aug 2025 20:20:56 +0200 Subject: [PATCH 7/8] Improved README documentation --- README.md | 61 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 43 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 3b80b06..db0c6b4 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,43 @@ # Self Certification Bot -_A library to enable automatic certificate issuing and renewal for self-hosted architectures_ +_A library to enable automatic certificate issuance and renewal for self-hosted architectures_ -## Description +## Introduction +The Self certification bot is a reimplementation of the popular certification bot core feature, +designed to be minimal and to be deployed into self-hosted architectures. +The project is structured to be built as a library, enabling seamless integration with other projects, +with an additional main for building it as a binary. +Further details on the project will soon be available in my blog. -## JASC - Just a simple challenge -Domain ownership is proven by solving a simple challenge. +_*Disclaimer*: the project does not feature rate limiting or other types of security measures yet. +It is meant to make HTTPS easier to use in closed-network setups where Certbot is not reachable. +## JASC - Just A Simple Challenge +Domain ownership is proven by solving a simple challenge. +It works as follows: +1. The claimant connects to the server and communicates the requested domain, its public key and certificate fields. +2. The server generates a secret and sends it to the claimant. +3. The server resolves the requested domain using the DNS and opens a connection, +expecting to receive the challenge back. +4. - If the secret matches the challenge is considered solved, and the certificate is signed and issued. + - Otherwise, the challenge fails and the connection eventually closed. ## Installation +The project will soon be packaged for common Linux distributions. +Until then, it requires manual compilation and installation. +Moreover, a Docker image for both the server and the client side is scheduled to be developed. ### External dependencies: - [OpenSSL](https://openssl-library.org/) - [nlohmann_json](https://json.nlohmann.me/) +- [Boost](https://www.boost.org) + +In the near future I'm considering managing dependencies using [Conan](https://conan.io), +till then, they must be included either using your distro's package manager or by building them locally. ### Build +The build process is handled with cmake, hence it can be built as follows: ```bash $ mkdir build @@ -30,15 +52,10 @@ $ make Configuration: ```json { - "domain": "example.com", - "outPath": "./data/generated", - "port": 14024, - "serverIp": "10.0.0.1", - "serverPort": 8080, - "C": "CN", - "ST": "ST", - "O": "Organization", - "OU": "Organization Unit" + "port": 8080, + "ca_cert_path": "./data/yourCA.pem", + "ca_key_path": "./data/yourCA.key", + "ca_passkey_path": "./data/passkey.txt" } ``` Usage: @@ -47,18 +64,26 @@ $ self-cert -m server -c ./settings/server.json ``` ### Client +The client supports both an interactive mode and a configuration-based mode, +the latter being useful for enforcing automatic renewal. + Configuration: ```json { - "port": 8080, - "ca_cert_path": "./data/yourCA.pem", - "ca_key_path": "./data/yourCA.key", - "ca_passkey_path": "./data/passkey.txt" + "domain": "example.com", + "out_path": "./data/generated", + "port": 14024, + "server_ip": "10.0.0.1", + "server_port": 8080, + "C": "CN", + "ST": "ST", + "O": "Organization", + "OU": "Organization Unit" } ``` Usage: ```bash -$ self-cert -m client --parallel +$ self-cert -m client --interactive ``` ```bash $ self-cert -m client -c ./settings/client.json From f6fa1fa9be1c7ac287a98ea427d63b7bdeab64f6 Mon Sep 17 00:00:00 2001 From: Mario Gottardo Date: Mon, 25 Aug 2025 21:12:09 +0200 Subject: [PATCH 8/8] Changed the library to be shared --- CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 42e3332..7943bf4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,12 +4,13 @@ project(self_cert_bot VERSION 0.1.0) set(CMAKE_CXX_STANDARD 20) set(EXECUTABLE_NAME self-cert) -add_library(self_cert_lib STATIC +add_library(self_cert_lib SHARED src/client/Client.cpp src/server/Server.cpp src/utils/cert_utils.cpp src/utils/utils.cpp ) +set_target_properties(self_cert_lib PROPERTIES PREFIX "") target_include_directories(self_cert_lib PUBLIC include)