blob: 6b69707cb87ff9cd49580e2255f918f8993f55d2 [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 {};
36 std::filesystem::path id {};
37 std::filesystem::path privatekey {};
38 bool pkid {false};
39 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'},
46 {"CA", required_argument, nullptr, 'c'},
Amnab5f0a682024-02-08 16:21:12 -050047 {"id", required_argument, nullptr, 'o'},
Amna423b25b2024-02-06 13:21:19 -050048 {"privatekey", required_argument, nullptr, 'p'},
49 {"name", required_argument, nullptr, 'n'},
Louis Maillardd887d3b2024-07-23 14:29:31 -040050 {"pkid", 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;
Louis Maillardd887d3b2024-07-23 14:29:31 -040060 while ((opt = getopt_long(argc, argv, "hasvi:c: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':
Amna423b25b2024-02-06 13:21:19 -050072 params.id = optarg;
73 break;
74 case 'p':
75 params.privatekey = optarg;
76 break;
Louis Maillardd887d3b2024-07-23 14:29:31 -040077 case 'a':
Amna423b25b2024-02-06 13:21:19 -050078 params.pkid = true;
79 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
Louis Maillard9737f532024-07-23 14:26:33 -040095 if (params.id.empty() && !params.pkid && !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";
138 }
Louis Maillard9737f532024-07-23 14:26:33 -0400139 yaml_file.close();
140 fmt::print("Configuration file created in {}\n", file);
141 } else {
142 fmt::print(stderr, "Error: Could not create configuration file {}.\n", file);
143 return 1;
144 }
145 return 0;
146}
147
148int configure_ssh_config(std::filesystem::path yaml_config)
149{
150 std::filesystem::path home_dir = getenv("HOME");
151 if (home_dir.empty()) {
152 fmt::print(stderr, "Error: HOME environment variable is not set. Cannot configure SSH.\n");
153 return 1;
154 }
155 std::filesystem::path ssh_dir = home_dir / ".ssh";
156 if (!std::filesystem::exists(ssh_dir)) {
157 fmt::print(stderr, "Error: {} folder doesn't exist. Install and configure ssh client first.\n", ssh_dir);
158 return 1;
159 }
160 std::filesystem::path ssh_config = ssh_dir / "config";
161 if (std::filesystem::exists(ssh_config)) {
162 std::ifstream ssh_file(ssh_config);
163 std::string line;
164 while (std::getline(ssh_file, line)) {
165 if (line.find("Host dnc") != std::string::npos) {
166 fmt::print("Info: dnc configuration already exists in ssh config. File is left untouched\n");
167 return 0;
168 }
169 }
170 }
171 std::ofstream ssh_file(ssh_config, std::ios::app);
172 if (ssh_file.is_open()) {
173 ssh_file << "\nHost dnc/*\n";
174 ssh_file << " ProxyCommand dnc -d " << yaml_config << " $(basename %h)\n";
175 ssh_file.close();
176 fmt::print("SSH configuration added to {}\n", ssh_config);
177 } else {
178 fmt::print(stderr, "Error: Could not open ssh config file.\n");
179 return 1;
180 }
181 return 0;
182}
183
184// https://en.cppreference.com/w/cpp/string/byte/tolower
185std::string str_tolower(std::string s)
186{
187 std::transform(s.begin(), s.end(), s.begin(),
188 [](unsigned char c){ return std::tolower(c); } // correct
189 );
190 return s;
191}
192
Amna423b25b2024-02-06 13:21:19 -0500193int
194main(int argc, char** argv)
195{
196 auto params = parse_args(argc, argv);
197
198 if (params.help) {
199 fmt::print("Usage: dhtnet-crtmgr [options]\n"
200 "\nOptions:\n"
Louis Maillardd887d3b2024-07-23 14:29:31 -0400201 " -h, --help Display this help message and then exit.\n"
202 " -v, --version Show the version of the program.\n"
203 " -p, --privatekey [PATH] Provide the path to the private key as an argument.\n"
204 " -c, --certificate [PATH] Provide the path to the certificate as an argument.\n"
205 " -o, --output [FOLDER] Provide the path where the generated certificate should be saved as an argument.\n"
206 " -a, --identifier Display the user identifier.\n"
207 " -n, --name [NAME] Provide the name of the certificate to be generated.\n"
208 " -s, --setup Create an CA and a certificate.\n"
209 " -i, --interactive Interactively create and setup identities.\n");
Amna423b25b2024-02-06 13:21:19 -0500210 return EXIT_SUCCESS;
211 }
212
213 if (params.version) {
214 fmt::print("dhtnet-crtmgr v1.0\n");
215 return EXIT_SUCCESS;
216 }
217 // check if the public key id is requested
218 if (params.pkid) {
219 if (params.ca.empty() || params.privatekey.empty()) {
Louis Maillardd887d3b2024-07-23 14:29:31 -0400220 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 -0500221 exit(EXIT_FAILURE);
222 }
223 auto identity = dhtnet::loadIdentity(params.privatekey, params.ca);
224 fmt::print("Public key id: {}\n", identity.second->getId());
225 return EXIT_SUCCESS;
226 }
227
Louis Maillard9737f532024-07-23 14:26:33 -0400228 // check if the interactive mode is requested
229 if (params.interactive) {
230 // Ask user if he want to setup client or server config
231 std::string usage = "";
232 do {
233 fmt::print("Generate identity for server or client? [(C)lient/(s)erver] (default: client): ");
234 std::getline(std::cin, usage);
235 usage = str_tolower(usage);
236 if (usage == "s") usage = "server";
237 if (usage == "c") usage = "client";
238 if (usage.empty()) usage = "client";
239 } while (usage != "server" && usage != "client");
240
241 // In case user select client mode, Ask if we should sign using server CA (required for anonymous: false)
242 std::string use_server_ca = "";
243 if (usage == "client") {
244 do {
245 fmt::print("Sign client certificate using server CA? [Y/n] (default: yes): ");
246 std::getline(std::cin, use_server_ca);
247 use_server_ca = str_tolower(use_server_ca);
248 if (use_server_ca == "y") use_server_ca = "yes";
249 if (use_server_ca == "n") use_server_ca = "no";
250 if (use_server_ca.empty()) use_server_ca = "yes";
251 } while (use_server_ca != "yes" && use_server_ca != "no");
252 }
253
254 // Before asking for save folder, pre-compute default locations
255 std::filesystem::path home_dir = getenv("HOME");
256 if (home_dir.empty()) home_dir = "/tmp/.dnc";
257 else if (usage == "server") home_dir = "/etc/dhtnet";
258 else home_dir = home_dir / ".dnc";
259
260 std::string input_folder;
261
262 // Ask where to store identity files
263 std::filesystem::path folder;
264 fmt::print("Enter the path to save identities and config [{}]: ", home_dir);
265 std::getline(std::cin, input_folder);
266 if (input_folder.empty()) {
267 folder = home_dir;
268 } else {
269 folder = input_folder;
270 }
271 folder = std::filesystem::absolute(folder);
Amnac8354042024-07-24 15:31:32 -0400272
273 std::error_code e;
274 std::filesystem::create_directories(folder, e);
275 if (e) {
276 fmt::print(stderr, "Error: Could not create directory {}. {}\n", folder, e.message());
277 return EXIT_FAILURE;
278 }
Louis Maillard9737f532024-07-23 14:26:33 -0400279
280 if (usage == "client") {
281 // Use existing CA or generate new CA
282 dht::crypto::Identity ca;
283 if (use_server_ca == "yes") {
284 try {
285 std::filesystem::path server_ca = "/etc/dhtnet/CA";
286 ca = dhtnet::loadIdentity(server_ca / "ca-server.pem", server_ca / "ca-server.crt");
287 if (!ca.first || !ca.second) {
288 throw std::runtime_error("Failed to load server CA");
289 }
290 }
291 catch (const std::exception& e) {
292 fmt::print(stderr, "Error: Could not load server CA. Please generate server CA first.\n");
293 return EXIT_FAILURE;
294 }
295 } else {
296 ca = dhtnet::generateIdentity(folder, "ca");
Amnac8354042024-07-24 15:31:32 -0400297 if (!ca.first || !ca.second) {
298 fmt::print(stderr, "Error: Could not generate CA.\n");
299 return EXIT_FAILURE;
300 }
Louis Maillard9737f532024-07-23 14:26:33 -0400301 fmt::print("Generated CA in {}: {} {}\n", folder, "ca", ca.second->getId());
302 }
303
304 // Generate client certificate
305 auto id = dhtnet::generateIdentity(folder, "certificate", ca);
Amnac8354042024-07-24 15:31:32 -0400306 if (!id.first || !id.second) {
307 fmt::print(stderr, "Error: Could not generate certificate.\n");
308 return EXIT_FAILURE;
309 }
Louis Maillard9737f532024-07-23 14:26:33 -0400310 fmt::print("Generated certificate in {}: {} {}\n", folder, "certificate", id.second->getId());
311
312 // Create configuration file with generated keys
313 std::filesystem::path yaml_config{folder / "config.yml"};
314 if (create_yaml_config(yaml_config, folder / "certificate.crt", folder / "certificate.pem", true) != 0) {
315 return EXIT_FAILURE;
316 }
317
318 // Ask user if he want to configure SSH
319 std::string ssh_setup = "";
320 do {
321 fmt::print("Configure SSH to support dnc protocol? [Y/n] (default: yes): ");
322 std::getline(std::cin, ssh_setup);
323 ssh_setup = str_tolower(ssh_setup);
324 if (ssh_setup == "y") ssh_setup = "yes";
325 if (ssh_setup == "n") ssh_setup = "no";
326 if (ssh_setup.empty()) ssh_setup = "yes";
327 } while (ssh_setup != "yes" && ssh_setup != "no");
328
329 if (ssh_setup == "yes") {
330 if (configure_ssh_config(yaml_config) != 0) {
331 return EXIT_FAILURE;
332 }
333 }
334
335 return EXIT_SUCCESS;
336 } else {
337 // Create configuration file with generated keys
Amnac8354042024-07-24 15:31:32 -0400338 std::filesystem::path yaml_config{folder / "dnc.yaml"};
Louis Maillard9737f532024-07-23 14:26:33 -0400339 std::string overwrite = "";
340 if (std::filesystem::exists(yaml_config)) {
341 do {
342 fmt::print("Configuration file already exists in {}. Overwrite it? [y/N] (default: no): \n", yaml_config);
343 std::getline(std::cin, overwrite);
344 overwrite = str_tolower(overwrite);
345 if (overwrite == "y") overwrite = "yes";
346 if (overwrite == "n") overwrite = "no";
347 if (overwrite.empty()) overwrite = "no";
348 } while (overwrite != "yes" && overwrite != "no");
349 } else {
350 overwrite = "yes"; // File doesn't exist, create it
351 }
352 if (overwrite == "yes") {
Amnac8354042024-07-24 15:31:32 -0400353 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 -0400354 return EXIT_FAILURE;
355 }
356 }
357 params.setup = true;
358 params.id = folder;
359 }
360 }
361
Amna423b25b2024-02-06 13:21:19 -0500362 // check if the setup is requested
363 if (params.setup) {
364 // create CA with name ca-server
365 std::filesystem::path path_ca = params.id / "CA";
366 auto ca = dhtnet::generateIdentity(path_ca, "ca-server");
Amnac8354042024-07-24 15:31:32 -0400367 if (!ca.first || !ca.second) {
368 fmt::print(stderr, "Error: Could not generate CA.\n");
369 return EXIT_FAILURE;
370 }
Amna423b25b2024-02-06 13:21:19 -0500371 fmt::print("Generated CA in {}: {} {}\n", path_ca, "ca-server", ca.second->getId());
372 // create identity with name id-server
373 std::filesystem::path path_id = params.id / "id";
374 auto identity = dhtnet::generateIdentity(path_id, "id-server", ca);
Amnac8354042024-07-24 15:31:32 -0400375 if (!identity.first || !identity.second) {
376 fmt::print(stderr, "Error: Could not generate certificate.\n");
377 return EXIT_FAILURE;
378 }
Amnab5f0a682024-02-08 16:21:12 -0500379 fmt::print("Generated certificate in {}: {} {}\n", path_id,"id-server", identity.second->getId());
Amna423b25b2024-02-06 13:21:19 -0500380 return EXIT_SUCCESS;
381 }
382
383 if (params.ca.empty() || params.privatekey.empty()) {
384 if (params.name.empty()) {
385 auto ca = dhtnet::generateIdentity(params.id, "ca");
Amnac8354042024-07-24 15:31:32 -0400386 if (!ca.first || !ca.second) {
387 fmt::print(stderr, "Error: Could not generate CA.\n");
388 return EXIT_FAILURE;
389 }
Amnab5f0a682024-02-08 16:21:12 -0500390 fmt::print("Generated certificate in {}: {} {}\n", params.id, "ca", ca.second->getId());
Amna423b25b2024-02-06 13:21:19 -0500391 }else{
Louis Maillardd887d3b2024-07-23 14:29:31 -0400392 auto ca = dhtnet::generateIdentity(params.id, params.name);
Amnac8354042024-07-24 15:31:32 -0400393 if (!ca.first || !ca.second) {
394 fmt::print(stderr, "Error: Could not generate CA.\n");
395 return EXIT_FAILURE;
396 }
Louis Maillardd887d3b2024-07-23 14:29:31 -0400397 fmt::print("Generated certificate in {}: {} {}\n", params.id, params.name, ca.second->getId());
Amna423b25b2024-02-06 13:21:19 -0500398 }
399 }else{
400 auto ca = dhtnet::loadIdentity(params.privatekey, params.ca);
401 if (params.name.empty()) {
Amnab5f0a682024-02-08 16:21:12 -0500402 auto id = dhtnet::generateIdentity(params.id, "certificate", ca);
Amnac8354042024-07-24 15:31:32 -0400403 if (!id.first || !id.second) {
404 fmt::print(stderr, "Error: Could not generate certificate.\n");
405 return EXIT_FAILURE;
406 }
Amnab5f0a682024-02-08 16:21:12 -0500407 fmt::print("Generated certificate in {}: {} {}\n", params.id, "certificate", id.second->getId());
Amna423b25b2024-02-06 13:21:19 -0500408 }else{
409 auto id = dhtnet::generateIdentity(params.id, params.name, ca);
Amnac8354042024-07-24 15:31:32 -0400410 if (!id.first || !id.second) {
411 fmt::print(stderr, "Error: Could not generate certificate.\n");
412 return EXIT_FAILURE;
413 }
Amnab5f0a682024-02-08 16:21:12 -0500414 fmt::print("Generated certificate in {}: {} {}\n", params.id, params.name, id.second->getId());
Amna423b25b2024-02-06 13:21:19 -0500415 }
416 }
417 return EXIT_SUCCESS;
418}