blob: 4ad8dfe830ca2c7d74825a1938f7170f1567b391 [file] [log] [blame]
Amna423b25b2024-02-06 13:21:19 -05001/*
2 * Copyright (C) 2023 Savoir-faire Linux Inc.
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 */
17#include "dhtnet_crtmgr.h"
18
19
20#include <iostream>
Louis Maillard9737f532024-07-23 14:26:33 -040021#include <fstream>
Amna423b25b2024-02-06 13:21:19 -050022#include <unistd.h>
23#include <getopt.h>
24#if __has_include(<fmt/std.h>)
25#include <fmt/std.h>
26#else
27#include <fmt/ostream.h>
28#endif
29
30
31struct dhtnet_crtmgr_params
32{
33 bool help {false};
34 bool version {false};
35 std::filesystem::path ca {};
Amna77e27dc2024-07-24 13:09:40 -040036 std::filesystem::path output {};
Amna423b25b2024-02-06 13:21:19 -050037 std::filesystem::path privatekey {};
Amna77e27dc2024-07-24 13:09:40 -040038 bool identifier {false};
Amna423b25b2024-02-06 13:21:19 -050039 std::string name {};
40 bool setup {false};
Louis Maillard9737f532024-07-23 14:26:33 -040041 bool interactive {false};
Amna423b25b2024-02-06 13:21:19 -050042};
43static const constexpr struct option long_options[]
44 = {{"help", no_argument, nullptr, 'h'},
45 {"version", no_argument, nullptr, 'v'},
Amna77e27dc2024-07-24 13:09:40 -040046 {"certificate", required_argument, nullptr, 'c'},
47 {"output", required_argument, nullptr, 'o'},
Amna423b25b2024-02-06 13:21:19 -050048 {"privatekey", required_argument, nullptr, 'p'},
49 {"name", required_argument, nullptr, 'n'},
Amna77e27dc2024-07-24 13:09:40 -040050 {"identifier", no_argument, nullptr, 'a'},
Amna423b25b2024-02-06 13:21:19 -050051 {"setup", no_argument, nullptr, 's'},
Louis Maillard9737f532024-07-23 14:26:33 -040052 {"interactive", no_argument, nullptr, 'i'},
Amna423b25b2024-02-06 13:21:19 -050053 {nullptr, 0, nullptr, 0}};
54
55dhtnet_crtmgr_params
56parse_args(int argc, char** argv)
57{
58 dhtnet_crtmgr_params params;
59 int opt;
Amna77e27dc2024-07-24 13:09:40 -040060 while ((opt = getopt_long(argc, argv, "hvasic:o:p:n:", long_options, nullptr)) != -1) {
Amna423b25b2024-02-06 13:21:19 -050061 switch (opt) {
62 case 'h':
63 params.help = true;
64 break;
65 case 'v':
66 params.version = true;
67 break;
68 case 'c':
69 params.ca = optarg;
70 break;
Amnab5f0a682024-02-08 16:21:12 -050071 case 'o':
Amna77e27dc2024-07-24 13:09:40 -040072 params.output = optarg;
Amna423b25b2024-02-06 13:21:19 -050073 break;
74 case 'p':
75 params.privatekey = optarg;
76 break;
Louis Maillardd887d3b2024-07-23 14:29:31 -040077 case 'a':
Amna77e27dc2024-07-24 13:09:40 -040078 params.identifier = true;
Amna423b25b2024-02-06 13:21:19 -050079 break;
80 case 'n':
81 params.name = optarg;
82 break;
83 case 's':
84 params.setup = true;
85 break;
Louis Maillard9737f532024-07-23 14:26:33 -040086 case 'i':
87 params.interactive = true;
88 break;
Amna423b25b2024-02-06 13:21:19 -050089 default:
90 std::cerr << "Invalid option" << std::endl;
91 exit(EXIT_FAILURE);
92 }
93 }
94
Amna77e27dc2024-07-24 13:09:40 -040095 if (params.output.empty() && !params.identifier && !params.help && !params.version && !params.interactive) {
Louis Maillard2da67252024-07-23 14:17:16 -040096 std::cerr << "Error: The path to save the generated certificate is not provided.\n Please specify the path using the -o option.\n";
Amnab5f0a682024-02-08 16:21:12 -050097 exit(EXIT_FAILURE);
Amna423b25b2024-02-06 13:21:19 -050098 }
99 return params;
100}
101
102
Louis Maillard9737f532024-07-23 14:26:33 -0400103int create_yaml_config(std::filesystem::path file, std::filesystem::path certificate, std::filesystem::path privateKey, bool is_client)
104{
105 std::ofstream yaml_file (file);
106 if (yaml_file.is_open()) {
107 yaml_file << "# The bootstrap node serves as the entry point to the DHT network.\n";
108 yaml_file << "# By default, bootstrap.jami.net is configured for the public DHT network and should be used for personal use only.\n";
109 yaml_file << "# For production environments, it is recommended to set up your own bootstrap node to establish your own DHT network.\n";
110 yaml_file << "# Documentation: https://docs.jami.net/en_US/user/lan-only.html#boostraping\n";
111 yaml_file << "bootstrap: \"bootstrap.jami.net\"\n";
112
113 yaml_file << "\n# TURN server is used as a fallback for connections if the NAT block all possible connections.\n";
114 yaml_file << "# By default is turn.jami.net (which uses coturn) but can be any TURN.\n";
115 yaml_file << "# Developer must set up their own TURN server.\n";
116 yaml_file << "# Documentation: https://docs.jami.net/en_US/developer/going-further/setting-up-your-own-turn-server.html\n";
117 yaml_file << "turn_host: \"turn.jami.net\"\n";
118 yaml_file << "turn_user: \"ring\"\n";
119 yaml_file << "turn_pass: \"ring\"\n";
120 yaml_file << "turn_realm: \"ring\"\n";
Louis Maillardf81d36b2024-07-23 14:38:28 -0400121
122 yaml_file << "\n# When verbose is set to true, the server logs all incoming connections\n";
123 yaml_file << "verbose: false\n";
124
Louis Maillard9737f532024-07-23 14:26:33 -0400125 yaml_file << "\n# On server, identities are saved in /etc/dhtnet/id/\n";
126 yaml_file << "# On client, they are generaly saved in ~/.dnc/\n";
127 yaml_file << "certificate: " << certificate << "\n";
128 yaml_file << "privateKey: " << privateKey << "\n";
Louis Maillardf81d36b2024-07-23 14:38:28 -0400129 if (is_client) {
130 yaml_file << "\n# When dnc server receives connexions, it forwards them to service at specified IP:port requested by CLIENT\n";
131 yaml_file << "# By default, it forwards them to SSH server running on localhost at port 22\n";
132 yaml_file << "ip: \"127.0.0.1\"\n";
133 yaml_file << "port: 22\n";
134 } else {
Louis Maillard9737f532024-07-23 14:26:33 -0400135 yaml_file << "\n# When anonymous is set to true, the server accepts any connection without checking CA\n";
136 yaml_file << "# When anonymous is set to false, the server allows only connection which are issued by the same CA as the server\n";
137 yaml_file << "anonymous: false\n";
Amna2ee14f02024-07-24 15:15:55 -0400138
139 yaml_file << "\n# List of authorized services\n";
140 yaml_file << "# Each service is defined by an IP and a port\n";
141 yaml_file << "# If no authorized services are defined, the server will accept any connection.\n";
142 yaml_file << "authorized_services:\n";
143 yaml_file << " - ip: \"127.0.0.1\"\n";
144 yaml_file << " port: 22\n";
145 yaml_file << " # - ip: \"127.0.0.1\"\n";
146 yaml_file << " # port: 80\n";
147 yaml_file << " # - ip: \"127.0.0.1\"\n";
148 yaml_file << " # port: 443\n";
Louis Maillard9737f532024-07-23 14:26:33 -0400149 }
Louis Maillard9737f532024-07-23 14:26:33 -0400150 yaml_file.close();
151 fmt::print("Configuration file created in {}\n", file);
152 } else {
153 fmt::print(stderr, "Error: Could not create configuration file {}.\n", file);
154 return 1;
155 }
156 return 0;
157}
158
159int configure_ssh_config(std::filesystem::path yaml_config)
160{
161 std::filesystem::path home_dir = getenv("HOME");
162 if (home_dir.empty()) {
163 fmt::print(stderr, "Error: HOME environment variable is not set. Cannot configure SSH.\n");
164 return 1;
165 }
166 std::filesystem::path ssh_dir = home_dir / ".ssh";
167 if (!std::filesystem::exists(ssh_dir)) {
168 fmt::print(stderr, "Error: {} folder doesn't exist. Install and configure ssh client first.\n", ssh_dir);
169 return 1;
170 }
171 std::filesystem::path ssh_config = ssh_dir / "config";
172 if (std::filesystem::exists(ssh_config)) {
173 std::ifstream ssh_file(ssh_config);
174 std::string line;
175 while (std::getline(ssh_file, line)) {
176 if (line.find("Host dnc") != std::string::npos) {
177 fmt::print("Info: dnc configuration already exists in ssh config. File is left untouched\n");
178 return 0;
179 }
180 }
181 }
182 std::ofstream ssh_file(ssh_config, std::ios::app);
183 if (ssh_file.is_open()) {
184 ssh_file << "\nHost dnc/*\n";
185 ssh_file << " ProxyCommand dnc -d " << yaml_config << " $(basename %h)\n";
186 ssh_file.close();
187 fmt::print("SSH configuration added to {}\n", ssh_config);
188 } else {
189 fmt::print(stderr, "Error: Could not open ssh config file.\n");
190 return 1;
191 }
192 return 0;
193}
194
195// https://en.cppreference.com/w/cpp/string/byte/tolower
196std::string str_tolower(std::string s)
197{
198 std::transform(s.begin(), s.end(), s.begin(),
199 [](unsigned char c){ return std::tolower(c); } // correct
200 );
201 return s;
202}
203
Amna423b25b2024-02-06 13:21:19 -0500204int
205main(int argc, char** argv)
206{
207 auto params = parse_args(argc, argv);
208
209 if (params.help) {
210 fmt::print("Usage: dhtnet-crtmgr [options]\n"
211 "\nOptions:\n"
Louis Maillardd887d3b2024-07-23 14:29:31 -0400212 " -h, --help Display this help message and then exit.\n"
213 " -v, --version Show the version of the program.\n"
Amna77e27dc2024-07-24 13:09:40 -0400214 " -p, --privatekey [FILE] Provide the path to the private key as an argument.\n"
215 " -c, --certificate [FILE] Provide the path to the certificate as an argument.\n"
Louis Maillardd887d3b2024-07-23 14:29:31 -0400216 " -o, --output [FOLDER] Provide the path where the generated certificate should be saved as an argument.\n"
217 " -a, --identifier Display the user identifier.\n"
218 " -n, --name [NAME] Provide the name of the certificate to be generated.\n"
219 " -s, --setup Create an CA and a certificate.\n"
220 " -i, --interactive Interactively create and setup identities.\n");
Amna423b25b2024-02-06 13:21:19 -0500221 return EXIT_SUCCESS;
222 }
223
224 if (params.version) {
225 fmt::print("dhtnet-crtmgr v1.0\n");
226 return EXIT_SUCCESS;
227 }
228 // check if the public key id is requested
Amna77e27dc2024-07-24 13:09:40 -0400229 if (params.identifier) {
Amna423b25b2024-02-06 13:21:19 -0500230 if (params.ca.empty() || params.privatekey.empty()) {
Louis Maillardd887d3b2024-07-23 14:29:31 -0400231 fmt::print(stderr, "Error: The path to the private key and the certificate is not provided.\n Please specify the path for the private key and the certificate using the -p and -c options.\n");
Amna423b25b2024-02-06 13:21:19 -0500232 exit(EXIT_FAILURE);
233 }
234 auto identity = dhtnet::loadIdentity(params.privatekey, params.ca);
235 fmt::print("Public key id: {}\n", identity.second->getId());
236 return EXIT_SUCCESS;
237 }
238
Louis Maillard9737f532024-07-23 14:26:33 -0400239 // check if the interactive mode is requested
240 if (params.interactive) {
241 // Ask user if he want to setup client or server config
242 std::string usage = "";
243 do {
244 fmt::print("Generate identity for server or client? [(C)lient/(s)erver] (default: client): ");
245 std::getline(std::cin, usage);
246 usage = str_tolower(usage);
247 if (usage == "s") usage = "server";
248 if (usage == "c") usage = "client";
249 if (usage.empty()) usage = "client";
250 } while (usage != "server" && usage != "client");
251
252 // In case user select client mode, Ask if we should sign using server CA (required for anonymous: false)
253 std::string use_server_ca = "";
254 if (usage == "client") {
255 do {
256 fmt::print("Sign client certificate using server CA? [Y/n] (default: yes): ");
257 std::getline(std::cin, use_server_ca);
258 use_server_ca = str_tolower(use_server_ca);
259 if (use_server_ca == "y") use_server_ca = "yes";
260 if (use_server_ca == "n") use_server_ca = "no";
261 if (use_server_ca.empty()) use_server_ca = "yes";
262 } while (use_server_ca != "yes" && use_server_ca != "no");
263 }
264
265 // Before asking for save folder, pre-compute default locations
266 std::filesystem::path home_dir = getenv("HOME");
267 if (home_dir.empty()) home_dir = "/tmp/.dnc";
268 else if (usage == "server") home_dir = "/etc/dhtnet";
269 else home_dir = home_dir / ".dnc";
270
271 std::string input_folder;
272
273 // Ask where to store identity files
274 std::filesystem::path folder;
275 fmt::print("Enter the path to save identities and config [{}]: ", home_dir);
276 std::getline(std::cin, input_folder);
277 if (input_folder.empty()) {
278 folder = home_dir;
279 } else {
280 folder = input_folder;
281 }
282 folder = std::filesystem::absolute(folder);
Amnac8354042024-07-24 15:31:32 -0400283
284 std::error_code e;
285 std::filesystem::create_directories(folder, e);
286 if (e) {
287 fmt::print(stderr, "Error: Could not create directory {}. {}\n", folder, e.message());
288 return EXIT_FAILURE;
289 }
Louis Maillard9737f532024-07-23 14:26:33 -0400290
291 if (usage == "client") {
292 // Use existing CA or generate new CA
293 dht::crypto::Identity ca;
294 if (use_server_ca == "yes") {
295 try {
296 std::filesystem::path server_ca = "/etc/dhtnet/CA";
297 ca = dhtnet::loadIdentity(server_ca / "ca-server.pem", server_ca / "ca-server.crt");
298 if (!ca.first || !ca.second) {
299 throw std::runtime_error("Failed to load server CA");
300 }
301 }
302 catch (const std::exception& e) {
303 fmt::print(stderr, "Error: Could not load server CA. Please generate server CA first.\n");
304 return EXIT_FAILURE;
305 }
306 } else {
307 ca = dhtnet::generateIdentity(folder, "ca");
Amnac8354042024-07-24 15:31:32 -0400308 if (!ca.first || !ca.second) {
309 fmt::print(stderr, "Error: Could not generate CA.\n");
310 return EXIT_FAILURE;
311 }
Louis Maillard9737f532024-07-23 14:26:33 -0400312 fmt::print("Generated CA in {}: {} {}\n", folder, "ca", ca.second->getId());
313 }
314
315 // Generate client certificate
316 auto id = dhtnet::generateIdentity(folder, "certificate", ca);
Amnac8354042024-07-24 15:31:32 -0400317 if (!id.first || !id.second) {
318 fmt::print(stderr, "Error: Could not generate certificate.\n");
319 return EXIT_FAILURE;
320 }
Louis Maillard9737f532024-07-23 14:26:33 -0400321 fmt::print("Generated certificate in {}: {} {}\n", folder, "certificate", id.second->getId());
322
323 // Create configuration file with generated keys
324 std::filesystem::path yaml_config{folder / "config.yml"};
325 if (create_yaml_config(yaml_config, folder / "certificate.crt", folder / "certificate.pem", true) != 0) {
326 return EXIT_FAILURE;
327 }
328
329 // Ask user if he want to configure SSH
330 std::string ssh_setup = "";
331 do {
332 fmt::print("Configure SSH to support dnc protocol? [Y/n] (default: yes): ");
333 std::getline(std::cin, ssh_setup);
334 ssh_setup = str_tolower(ssh_setup);
335 if (ssh_setup == "y") ssh_setup = "yes";
336 if (ssh_setup == "n") ssh_setup = "no";
337 if (ssh_setup.empty()) ssh_setup = "yes";
338 } while (ssh_setup != "yes" && ssh_setup != "no");
339
340 if (ssh_setup == "yes") {
341 if (configure_ssh_config(yaml_config) != 0) {
342 return EXIT_FAILURE;
343 }
344 }
345
346 return EXIT_SUCCESS;
347 } else {
348 // Create configuration file with generated keys
Amnac8354042024-07-24 15:31:32 -0400349 std::filesystem::path yaml_config{folder / "dnc.yaml"};
Louis Maillard9737f532024-07-23 14:26:33 -0400350 std::string overwrite = "";
351 if (std::filesystem::exists(yaml_config)) {
352 do {
Louis Maillardc1f7da22024-07-24 15:13:50 -0400353 fmt::print("Configuration file already exists in {}. Overwrite it? [y/N] (default: no): ", yaml_config);
Louis Maillard9737f532024-07-23 14:26:33 -0400354 std::getline(std::cin, overwrite);
355 overwrite = str_tolower(overwrite);
356 if (overwrite == "y") overwrite = "yes";
357 if (overwrite == "n") overwrite = "no";
358 if (overwrite.empty()) overwrite = "no";
359 } while (overwrite != "yes" && overwrite != "no");
360 } else {
361 overwrite = "yes"; // File doesn't exist, create it
362 }
363 if (overwrite == "yes") {
Amnac8354042024-07-24 15:31:32 -0400364 if (create_yaml_config(yaml_config, folder / "id" / "id-server.crt", folder / "id" / "id-server.pem", false) != 0) {
Louis Maillard9737f532024-07-23 14:26:33 -0400365 return EXIT_FAILURE;
366 }
367 }
368 params.setup = true;
Amna77e27dc2024-07-24 13:09:40 -0400369 params.output = folder;
Louis Maillard9737f532024-07-23 14:26:33 -0400370 }
371 }
372
Amna423b25b2024-02-06 13:21:19 -0500373 // check if the setup is requested
374 if (params.setup) {
375 // create CA with name ca-server
Amna77e27dc2024-07-24 13:09:40 -0400376 std::filesystem::path path_ca = params.output / "CA";
Amna423b25b2024-02-06 13:21:19 -0500377 auto ca = dhtnet::generateIdentity(path_ca, "ca-server");
Amnac8354042024-07-24 15:31:32 -0400378 if (!ca.first || !ca.second) {
379 fmt::print(stderr, "Error: Could not generate CA.\n");
380 return EXIT_FAILURE;
381 }
Amna423b25b2024-02-06 13:21:19 -0500382 fmt::print("Generated CA in {}: {} {}\n", path_ca, "ca-server", ca.second->getId());
383 // create identity with name id-server
Amna77e27dc2024-07-24 13:09:40 -0400384 std::filesystem::path path_id = params.output / "id";
Amna423b25b2024-02-06 13:21:19 -0500385 auto identity = dhtnet::generateIdentity(path_id, "id-server", ca);
Amnac8354042024-07-24 15:31:32 -0400386 if (!identity.first || !identity.second) {
387 fmt::print(stderr, "Error: Could not generate certificate.\n");
388 return EXIT_FAILURE;
389 }
Amnab5f0a682024-02-08 16:21:12 -0500390 fmt::print("Generated certificate in {}: {} {}\n", path_id,"id-server", identity.second->getId());
Amna423b25b2024-02-06 13:21:19 -0500391 return EXIT_SUCCESS;
392 }
393
394 if (params.ca.empty() || params.privatekey.empty()) {
395 if (params.name.empty()) {
Amna77e27dc2024-07-24 13:09:40 -0400396 auto ca = dhtnet::generateIdentity(params.output, "ca");
Amnac8354042024-07-24 15:31:32 -0400397 if (!ca.first || !ca.second) {
398 fmt::print(stderr, "Error: Could not generate CA.\n");
399 return EXIT_FAILURE;
400 }
Amna77e27dc2024-07-24 13:09:40 -0400401 fmt::print("Generated certificate in {}: {} {}\n", params.output, "ca", ca.second->getId());
Amna423b25b2024-02-06 13:21:19 -0500402 }else{
Amna77e27dc2024-07-24 13:09:40 -0400403 auto ca = dhtnet::generateIdentity(params.output, params.name);
Amnac8354042024-07-24 15:31:32 -0400404 if (!ca.first || !ca.second) {
405 fmt::print(stderr, "Error: Could not generate CA.\n");
406 return EXIT_FAILURE;
407 }
Amna77e27dc2024-07-24 13:09:40 -0400408 fmt::print("Generated certificate in {}: {} {}\n", params.output, params.name, ca.second->getId());
Amna423b25b2024-02-06 13:21:19 -0500409 }
410 }else{
411 auto ca = dhtnet::loadIdentity(params.privatekey, params.ca);
412 if (params.name.empty()) {
Amna77e27dc2024-07-24 13:09:40 -0400413 auto id = dhtnet::generateIdentity(params.output, "certificate", ca);
Amnac8354042024-07-24 15:31:32 -0400414 if (!id.first || !id.second) {
415 fmt::print(stderr, "Error: Could not generate certificate.\n");
416 return EXIT_FAILURE;
417 }
Amna77e27dc2024-07-24 13:09:40 -0400418 fmt::print("Generated certificate in {}: {} {}\n", params.output, "certificate", id.second->getId());
Amna423b25b2024-02-06 13:21:19 -0500419 }else{
Amna77e27dc2024-07-24 13:09:40 -0400420 auto id = dhtnet::generateIdentity(params.output, params.name, ca);
Amnac8354042024-07-24 15:31:32 -0400421 if (!id.first || !id.second) {
422 fmt::print(stderr, "Error: Could not generate certificate.\n");
423 return EXIT_FAILURE;
424 }
Amna77e27dc2024-07-24 13:09:40 -0400425 fmt::print("Generated certificate in {}: {} {}\n", params.output, params.name, id.second->getId());
Amna423b25b2024-02-06 13:21:19 -0500426 }
427 }
428 return EXIT_SUCCESS;
429}