unet-cli: strip initial newline in usage message
[project/unetd.git] / cli.c
1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /*
3 * Copyright (C) 2022 Felix Fietkau <nbd@nbd.name>
4 */
5 #include <sys/stat.h>
6 #include <sys/time.h>
7 #include <sys/mman.h>
8 #include <sys/types.h>
9 #include <sys/socket.h>
10 #include <arpa/inet.h>
11 #include <stdio.h>
12 #include <stdint.h>
13 #include <stdlib.h>
14 #include <fcntl.h>
15 #include <errno.h>
16 #include <libubox/utils.h>
17 #include <libubox/uloop.h>
18 #include <libubox/blobmsg.h>
19 #include <libubox/blobmsg_json.h>
20 #include "edsign.h"
21 #include "ed25519.h"
22 #include "curve25519.h"
23 #include "auth-data.h"
24 #include "pex-msg.h"
25
26 static uint8_t peerkey[EDSIGN_PUBLIC_KEY_SIZE];
27 static uint8_t pubkey[EDSIGN_PUBLIC_KEY_SIZE];
28 static uint8_t seckey[EDSIGN_PUBLIC_KEY_SIZE];
29 static void *net_data;
30 static size_t net_data_len;
31 static uint64_t net_data_version;
32 static struct blob_attr *net_data_hosts;
33 static uint64_t req_id;
34 static struct blob_buf b;
35 static FILE *out_file;
36 static bool quiet;
37 static bool sync_done;
38 static enum {
39 CMD_UNKNOWN,
40 CMD_GENERATE,
41 CMD_PUBKEY,
42 CMD_HOST_PUBKEY,
43 CMD_VERIFY,
44 CMD_SIGN,
45 CMD_DOWNLOAD,
46 CMD_UPLOAD,
47 } cmd;
48
49 #define INFO(...) \
50 do { \
51 if (quiet) \
52 break; \
53 fprintf(stderr, ##__VA_ARGS__); \
54 } while (0)
55
56 static void print_key(const uint8_t *key)
57 {
58 char keystr[B64_ENCODE_LEN(EDSIGN_PUBLIC_KEY_SIZE)];
59
60 if (b64_encode(key, EDSIGN_PUBLIC_KEY_SIZE, keystr, sizeof(keystr)) < 0)
61 return;
62
63 fprintf(out_file, "%s\n", keystr);
64 }
65
66 static int usage(const char *progname)
67 {
68 fprintf(stderr, "Usage: %s [command|options] [<file>]\n"
69 "Commands:\n"
70 " -S Sign file\n"
71 " -V Verify file\n"
72 " -P Get public signing key from secret key\n"
73 " -H Get public host key from secret key\n"
74 " -G Generate new private key\n"
75 " -D <host>[:<port>] Download network data from unetd\n"
76 " -U <host>[:<port>] Upload network data to unetd\n"
77 "\n"
78 "Options:\n"
79 " -q: Quiet mode - suppress error/info messages\n"
80 " -o <file>: Set output file to <file> (defaults to stdout)\n"
81 " -k <keyfile>|-: Set public key from file or stdin\n"
82 " -K <keyfile>|-: Set secret key from file or stdin\n"
83 " -h <keyfile>|- Set peer private key from file or stdin\n"
84 " (for network data down-/upload)\n"
85 "\n", progname);
86 return 1;
87 }
88
89 static void pex_timeout(struct uloop_timeout *timeout)
90 {
91 uloop_end();
92 }
93
94 static void
95 pex_recv_update_response(const uint8_t *data, size_t len, enum pex_opcode op)
96 {
97 int net_data_len = 0;
98 void *net_data;
99
100 net_data = pex_msg_update_response_recv(data, len, op, &net_data_len, NULL);
101 if (net_data_len < 0)
102 goto out;
103
104 if (!net_data)
105 return;
106
107 if (cmd == CMD_DOWNLOAD) {
108 fwrite(net_data, net_data_len, 1, out_file);
109 sync_done = true;
110 }
111
112 free(net_data);
113
114 out:
115 if (cmd == CMD_DOWNLOAD)
116 uloop_end();
117 }
118
119 static bool
120 pex_get_pubkey(uint8_t *pubkey, const uint8_t *id)
121 {
122 static const struct blobmsg_policy policy = { "key", BLOBMSG_TYPE_STRING };
123 struct blob_attr *cur, *key;
124 int rem;
125
126 blobmsg_for_each_attr(cur, net_data_hosts, rem) {
127 const char *keystr;
128
129 blobmsg_parse(&policy, 1, &key, blobmsg_data(cur), blobmsg_len(cur));
130
131 if (!key)
132 continue;
133
134 keystr = blobmsg_get_string(key);
135 if (b64_decode(keystr, pubkey, CURVE25519_KEY_SIZE) != CURVE25519_KEY_SIZE)
136 continue;
137
138 if (!memcmp(pubkey, id, PEX_ID_LEN))
139 return true;
140 }
141
142 return false;
143 }
144
145 static void
146 pex_handle_update_request(struct sockaddr_in6 *addr, const uint8_t *id, void *data, size_t len)
147 {
148 struct pex_msg_update_send_ctx ctx = {};
149 static uint8_t empty_key[EDSIGN_PUBLIC_KEY_SIZE] = {};
150 uint8_t peerpubkey[EDSIGN_PUBLIC_KEY_SIZE];
151 bool done = false;
152
153 if (!pex_get_pubkey(peerpubkey, id)) {
154 INFO("Could not find public key\n");
155 return;
156 }
157
158 pex_msg_update_response_init(&ctx, empty_key, pubkey,
159 peerpubkey, true, data, net_data, net_data_len);
160 while (!done) {
161 __pex_msg_send(-1, NULL, NULL, 0);
162 done = !pex_msg_update_response_continue(&ctx);
163 }
164 sync_done = true;
165 uloop_end();
166 }
167
168 static void pex_recv(void *msg, size_t msg_len, struct sockaddr_in6 *addr)
169 {
170 struct pex_hdr *hdr;
171 struct pex_ext_hdr *ehdr;
172 uint64_t *msg_req_id;
173 void *data;
174
175 hdr = pex_rx_accept(msg, msg_len, true);
176 if (!hdr)
177 return;
178
179 ehdr = (void *)(hdr + 1);
180 data = (void *)(ehdr + 1);
181 msg_req_id = data;
182
183 if (hdr->version != 0)
184 return;
185
186 if (memcmp(ehdr->auth_id, pubkey, sizeof(ehdr->auth_id)) != 0)
187 return;
188
189 *(uint64_t *)hdr->id ^= pex_network_hash(pubkey, ehdr->nonce);
190
191 switch (hdr->opcode) {
192 case PEX_MSG_UPDATE_REQUEST:
193 if (cmd != CMD_UPLOAD)
194 break;
195
196 pex_handle_update_request(addr, hdr->id, data, hdr->len);
197 break;
198 case PEX_MSG_UPDATE_RESPONSE:
199 case PEX_MSG_UPDATE_RESPONSE_DATA:
200 case PEX_MSG_UPDATE_RESPONSE_NO_DATA:
201 if (hdr->len < sizeof(*msg_req_id) || *msg_req_id != req_id)
202 break;
203
204 if (cmd == CMD_DOWNLOAD &&
205 hdr->opcode == PEX_MSG_UPDATE_RESPONSE_NO_DATA) {
206 INFO("No network data available\n");
207 uloop_end();
208 }
209
210 if (cmd == CMD_UPLOAD &&
211 hdr->opcode != PEX_MSG_UPDATE_RESPONSE_NO_DATA) {
212 INFO("Server has newer network data\n");
213 uloop_end();
214 }
215
216 pex_recv_update_response(data, hdr->len, hdr->opcode);
217 break;
218 }
219 }
220
221 static int load_network_data(const char *file)
222 {
223 static const struct blobmsg_policy policy = { "hosts", BLOBMSG_TYPE_TABLE };
224 struct unet_auth_hdr *hdr;
225 struct unet_auth_data *data;
226 const char *json;
227
228 net_data_len = UNETD_NET_DATA_SIZE_MAX;
229 net_data = unet_read_file(file, &net_data_len);
230 if (!net_data) {
231 INFO("failed to read input file %s\n", file);
232 return 1;
233 }
234
235 if (unet_auth_data_validate(NULL, net_data, net_data_len, &net_data_version, &json) < 0) {
236 INFO("input data validation failed\n");
237 return 1;
238 }
239
240 hdr = net_data;
241 data = (struct unet_auth_data *)(hdr + 1);
242 memcpy(pubkey, data->pubkey, sizeof(pubkey));
243
244 blob_buf_init(&b, 0);
245 blobmsg_add_json_from_string(&b, json);
246
247 blobmsg_parse(&policy, 1, &net_data_hosts, blobmsg_data(b.head), blobmsg_len(b.head));
248 if (!net_data_hosts) {
249 INFO("network data is missing the hosts attribute\n");
250 return 1;
251 }
252
253 return 0;
254 }
255
256
257 static int cmd_sync(const char *endpoint, int argc, char **argv)
258 {
259 uint8_t peerpubkey[EDSIGN_PUBLIC_KEY_SIZE];
260 struct uloop_timeout timeout = {
261 .cb = pex_timeout
262 };
263 struct pex_update_request *req;
264 union network_endpoint ep = {};
265 int len;
266
267 if (cmd == CMD_UPLOAD) {
268 if (argc < 1) {
269 INFO("missing file argument\n");
270 return 1;
271 }
272
273 if (load_network_data(argv[0]))
274 return 1;
275 }
276
277 if (network_get_endpoint(&ep, AF_UNSPEC, endpoint, UNETD_GLOBAL_PEX_PORT, 0) < 0) {
278 INFO("Invalid hostname/port %s\n", endpoint);
279 return 1;
280 }
281
282 len = ep.sa.sa_family == AF_INET6 ? sizeof(ep.in6) : sizeof(ep.in);
283
284 uloop_init();
285
286 if (pex_open(&ep, len, pex_recv, false) < 0)
287 return 1;
288
289 uloop_timeout_set(&timeout, 5000);
290
291 curve25519_generate_public(peerpubkey, peerkey);
292 req = pex_msg_update_request_init(peerpubkey, peerkey, pubkey, &ep,
293 net_data_version, true);
294 if (!req)
295 return 1;
296
297 req_id = req->req_id;
298 if (__pex_msg_send(-1, NULL, NULL, 0) < 0) {
299 if (!quiet)
300 perror("send");
301 return 1;
302 }
303
304 uloop_run();
305
306 return !sync_done;
307 }
308
309 static int cmd_sign(int argc, char **argv)
310 {
311 struct unet_auth_hdr hdr = {
312 .magic = cpu_to_be32(UNET_AUTH_MAGIC),
313 };
314 struct unet_auth_data *data;
315 struct timeval tv;
316 struct stat st;
317 off_t len;
318 FILE *f;
319
320 if (argc != 1) {
321 INFO("Missing filename\n");
322 return 1;
323 }
324
325 if (gettimeofday(&tv, NULL)) {
326 if (!quiet)
327 perror("gettimeofday");
328 return 1;
329 }
330
331 if (stat(argv[0], &st) ||
332 (f = fopen(argv[0], "r")) == NULL) {
333 INFO("Input file not found\n");
334 return 1;
335 }
336
337 data = calloc(1, sizeof(*data) + st.st_size + 1);
338 data->timestamp = cpu_to_be64(tv.tv_sec);
339 len = fread(data + 1, 1, st.st_size, f);
340 fclose(f);
341
342 if (len != st.st_size) {
343 INFO("Error reading from input file\n");
344 return 1;
345 }
346
347 len += sizeof(*data) + 1;
348
349 memcpy(data->pubkey, pubkey, sizeof(pubkey));
350 edsign_sign(hdr.signature, pubkey, seckey, (const void *)data, len);
351
352 fwrite(&hdr, sizeof(hdr), 1, out_file);
353 fwrite(data, len, 1, out_file);
354
355 free(data);
356
357 return 0;
358 }
359
360 static int cmd_verify(int argc, char **argv)
361 {
362 struct unet_auth_data *data;
363 struct unet_auth_hdr *hdr;
364 struct stat st;
365 off_t len;
366 FILE *f;
367 int ret = 1;
368
369 if (argc != 1) {
370 INFO("Missing filename\n");
371 return 1;
372 }
373
374 if (stat(argv[0], &st) ||
375 (f = fopen(argv[0], "r")) == NULL) {
376 INFO("Input file not found\n");
377 return 1;
378 }
379
380 if (st.st_size <= sizeof(*hdr) + sizeof(*data)) {
381 INFO("Input file too small\n");
382 fclose(f);
383 return 1;
384 }
385
386 hdr = calloc(1, st.st_size);
387 len = fread(hdr, 1, st.st_size, f);
388 fclose(f);
389
390 if (len != st.st_size) {
391 INFO("Error reading from input file\n");
392 return 1;
393 }
394
395 ret = unet_auth_data_validate(pubkey, hdr, len, NULL, NULL);
396 switch (ret) {
397 case -1:
398 INFO("Invalid input data\n");
399 break;
400 case -2:
401 INFO("Public key does not match\n");
402 break;
403 case -3:
404 INFO("Signature verification failed\n");
405 break;
406 }
407
408 free(hdr);
409 return ret;
410 }
411
412 static int cmd_host_pubkey(int argc, char **argv)
413 {
414 curve25519_generate_public(pubkey, seckey);
415 print_key(pubkey);
416
417 return 0;
418 }
419
420 static int cmd_pubkey(int argc, char **argv)
421 {
422 print_key(pubkey);
423
424 return 0;
425 }
426
427 static int cmd_generate(int argc, char **argv)
428 {
429 FILE *f;
430 int ret;
431
432 f = fopen("/dev/urandom", "r");
433 if (!f) {
434 INFO("Can't open /dev/urandom\n");
435 return 1;
436 }
437
438 ret = fread(seckey, sizeof(seckey), 1, f);
439 fclose(f);
440
441 if (ret != 1) {
442 INFO("Can't read data from /dev/urandom\n");
443 return 1;
444 }
445
446 ed25519_prepare(seckey);
447 print_key(seckey);
448
449 return 0;
450 }
451
452 static bool parse_key(uint8_t *dest, const char *str)
453 {
454 char keystr[B64_ENCODE_LEN(EDSIGN_PUBLIC_KEY_SIZE) + 2];
455 FILE *f;
456 int len;
457
458 if (!strcmp(str, "-"))
459 f = stdin;
460 else
461 f = fopen(str, "r");
462
463 if (!f) {
464 INFO("Can't open key file for reading\n");
465 return false;
466 }
467
468 len = fread(keystr, 1, sizeof(keystr) - 1, f);
469 if (f != stdin)
470 fclose(f);
471
472 keystr[len] = 0;
473
474 if (b64_decode(keystr, dest, EDSIGN_PUBLIC_KEY_SIZE) != EDSIGN_PUBLIC_KEY_SIZE) {
475 INFO("Failed to parse key data\n");
476 return false;
477 }
478
479 return true;
480 }
481
482 static bool cmd_needs_peerkey(void)
483 {
484 switch (cmd) {
485 case CMD_DOWNLOAD:
486 return true;
487 default:
488 return false;
489 }
490 }
491
492 static bool cmd_needs_pubkey(void)
493 {
494 switch (cmd) {
495 case CMD_DOWNLOAD:
496 case CMD_VERIFY:
497 return true;
498 default:
499 return false;
500 }
501 }
502
503 static bool cmd_needs_key(void)
504 {
505 switch (cmd) {
506 case CMD_SIGN:
507 case CMD_PUBKEY:
508 case CMD_HOST_PUBKEY:
509 return true;
510 default:
511 return false;
512 }
513 }
514
515 static bool cmd_needs_outfile(void)
516 {
517 switch (cmd) {
518 case CMD_SIGN:
519 case CMD_PUBKEY:
520 case CMD_GENERATE:
521 case CMD_DOWNLOAD:
522 return true;
523 default:
524 return false;
525 }
526 }
527
528 int main(int argc, char **argv)
529 {
530 const char *progname = argv[0];
531 const char *out_filename = NULL;
532 const char *cmd_arg = NULL;
533 bool has_key = false, has_pubkey = false;
534 bool has_peerkey = false;
535 int ret, ch;
536
537 while ((ch = getopt(argc, argv, "h:k:K:o:qD:GHPSU:V")) != -1) {
538 switch (ch) {
539 case 'D':
540 case 'U':
541 case 'G':
542 case 'H':
543 case 'S':
544 case 'P':
545 case 'V':
546 if (cmd != CMD_UNKNOWN)
547 return usage(progname);
548 break;
549 default:
550 break;
551 }
552
553 switch (ch) {
554 case 'q':
555 quiet = true;
556 break;
557 case 'o':
558 out_filename = optarg;
559 break;
560 case 'h':
561 if (has_peerkey)
562 return usage(progname);
563
564 if (!parse_key(peerkey, optarg)) {
565 return 1;
566 }
567
568 has_peerkey = true;
569 break;
570 case 'k':
571 if (has_pubkey)
572 return usage(progname);
573
574 if (!parse_key(pubkey, optarg)) {
575 return 1;
576 }
577
578 has_pubkey = true;
579 break;
580 case 'K':
581 if (has_pubkey)
582 return usage(progname);
583
584 if (!parse_key(seckey, optarg)) {
585 return 1;
586 }
587
588 has_key = true;
589
590 edsign_sec_to_pub(pubkey, seckey);
591 has_pubkey = true;
592 break;
593 case 'U':
594 cmd = CMD_UPLOAD;
595 cmd_arg = optarg;
596 break;
597 case 'D':
598 cmd = CMD_DOWNLOAD;
599 cmd_arg = optarg;
600 break;
601 case 'G':
602 cmd = CMD_GENERATE;
603 break;
604 case 'S':
605 cmd = CMD_SIGN;
606 break;
607 case 'P':
608 cmd = CMD_PUBKEY;
609 break;
610 case 'H':
611 cmd = CMD_HOST_PUBKEY;
612 break;
613 case 'V':
614 cmd = CMD_VERIFY;
615 break;
616 default:
617 return usage(progname);
618 }
619 }
620
621 if (!has_peerkey && cmd_needs_peerkey()) {
622 INFO("Missing -h <key> argument\n");
623 return 1;
624 }
625
626 if (!has_key && cmd_needs_key()) {
627 INFO("Missing -K <key> argument\n");
628 return 1;
629 }
630
631 if (!has_pubkey && cmd_needs_pubkey()) {
632 INFO("Missing -k <key> argument\n");
633 return 1;
634 }
635
636 argc -= optind;
637 argv += optind;
638
639 if (out_filename && cmd_needs_outfile()) {
640 out_file = fopen(out_filename, "w");
641 if (!out_file) {
642 INFO("Failed to open output file\n");
643 return 1;
644 }
645 } else {
646 out_file = stdout;
647 }
648
649 ret = -1;
650 switch (cmd) {
651 case CMD_UPLOAD:
652 case CMD_DOWNLOAD:
653 ret = cmd_sync(cmd_arg, argc, argv);
654 break;
655 case CMD_GENERATE:
656 ret = cmd_generate(argc, argv);
657 break;
658 case CMD_SIGN:
659 ret = cmd_sign(argc, argv);
660 break;
661 case CMD_PUBKEY:
662 ret = cmd_pubkey(argc, argv);
663 break;
664 case CMD_HOST_PUBKEY:
665 ret = cmd_host_pubkey(argc, argv);
666 break;
667 case CMD_VERIFY:
668 ret = cmd_verify(argc, argv);
669 break;
670 case CMD_UNKNOWN:
671 ret = usage(progname);
672 break;
673 }
674
675 if (net_data)
676 free(net_data);
677
678 blob_buf_free(&b);
679
680 if (out_file != stdout) {
681 fclose(out_file);
682 if (ret)
683 unlink(out_filename);
684 }
685
686 return ret;
687 }