diff --git a/Makefile b/Makefile index 3a26f42..0ff089b 100644 --- a/Makefile +++ b/Makefile @@ -31,9 +31,13 @@ uninstall: $(DESTDIR)$(BINDIR)/$(BIN) \ $(DESTDIR)$(MANDIR)/man1/$(BIN).1 -test-shs1: - @# %lzzcAZlM21slUIoiH4yd/wgDnXu8raNLvwqjxqrU06k=.sha256 - shs1testclient ./test-shs-inner.sh $(SHS1_TEST_SEED) +test-shs1: test-shs1-client test-shs1-server + +test-shs1-client: + shs1testclient ./test-shs-client-inner.sh $(SHS1_TEST_SEED) + +test-shs1-server: + shs1testserver ./test-shs-server-inner.sh $(SHS1_TEST_SEED) clean: @rm -vf $(BIN) diff --git a/sbotc.1 b/sbotc.1 index 0a741fc..e2bbfe2 100644 --- a/sbotc.1 +++ b/sbotc.1 @@ -30,6 +30,7 @@ | .Fl 6 .Oc +.Op Fl d | .Op Fl u Ar socket_path .Oc @@ -56,8 +57,7 @@ Raw mode. Disables stdin line buffering/editing and echoing. Implies .It Fl e Encode arguments as strings, rather than expecting them to be JSON-encoded. .It Fl T -Test using shs1-testsuite protocol. Instead of connecting to a server and running -a command, connect to stdio. On successful handshake, output concatenation of +Test using shs1-testsuite protocol. Instead of connecting to a peer and calling a RPC method, connect to stdio. On successful handshake, output concatenation of the encryption key, encryption nonce, decryption key and decryption nonce. .It Fl a Passthrough mode. Instead of making a muxrpc call, pass through the box-stream @@ -74,6 +74,8 @@ options have no effect and output a warning if used. Connect to server over IPv4 only. .It Fl 6 Connect to server over IPv6 only. +.It Fl d +Listen for an incoming connection instead of making an outgoing connection. .It Fl c Ar cap Capability key for secret-handshake. Default is SSB's capability key, .Li 1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s= . @@ -90,7 +92,13 @@ and .Fl s . .It Fl k Ar key The key to connect to. Default is your public key, as read from your -private key file. +private key file or the +.Fl K +option. In listen mode ( +.Fl d +), if +.Fl k +is specified, only a connection from the given key is accepted; otherwise a connection from any key is accepted. .It Fl K Ar keypair Private key or private key seed to use for secret-handshake. Default is to use the private key from your diff --git a/sbotc.c b/sbotc.c index 8972a38..07631e8 100644 --- a/sbotc.c +++ b/sbotc.c @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -116,7 +117,7 @@ static void reset_termios() { static void usage() { fputs("usage: sbotc [-j] [-T] [-l] [-r] [-e]\n" " [ -n | [-c ] [-k ] [-K ] ]\n" - " [ [-s ] [-p ] [ -4 | -6 ] | [-u ] ]\n" + " [ [-s ] [-p ] [ -4 | -6 ] [-d] | [-u ] ]\n" " [ -a | [-t ] [...] ]\n", stderr); exit(EXIT_FAILURE); } @@ -156,16 +157,17 @@ static int connect_localhost(const char *port, enum ip_family ip_family) { return fd; } -static int tcp_connect(const char *host, const char *port, enum ip_family ip_family) { +static int tcp_connect(const char *host, const char *port, enum ip_family ip_family, bool server) { struct addrinfo hints; struct addrinfo *result, *rp; int s; int fd; - int err; + int err, rc; memset(&hints, 0, sizeof(hints)); hints.ai_family = ip_family; hints.ai_protocol = IPPROTO_TCP; + if (server) hints.ai_flags = AI_PASSIVE; s = getaddrinfo(host, port, &hints, &result); if (s < 0) errx(1, "unable to resolve host: %s", gai_strerror(s)); @@ -173,7 +175,15 @@ static int tcp_connect(const char *host, const char *port, enum ip_family ip_fam for (rp = result; rp; rp = rp->ai_next) { fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); if (fd < 0) continue; - if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) break; + if (server) { + rc = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int)); + if (rc < 0) goto error; + if (bind(fd, rp->ai_addr, rp->ai_addrlen) < 0) goto error; + if (listen(fd, 1) == 0) break; + } else { + if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) break; + } +error: err = errno; close(fd); errno = err; @@ -182,14 +192,31 @@ static int tcp_connect(const char *host, const char *port, enum ip_family ip_fam freeaddrinfo(result); - if (fd == -1 && errno == ECONNREFUSED && (host == NULL || !strcmp(host, "localhost"))) { + if (!server && fd == -1 && errno == ECONNREFUSED && (host == NULL || !strcmp(host, "localhost"))) { return connect_localhost(port, ip_family); } + if (server && fd > -1) { + int client = accept(fd, NULL, NULL); + err = errno; + close(fd); + errno = err; + return client; + } + return fd; } -static int unix_connect(const char *path) { +static const char *socket_path = NULL; + +void cleanup() { + if (socket_path != NULL) { + int rc = unlink(socket_path); + if (rc < 0) warn("unlink"); + } +} + +static int unix_connect(const char *path, bool server) { struct sockaddr_un name; const size_t path_len = strlen(path); int s, rc; @@ -199,18 +226,38 @@ static int unix_connect(const char *path) { memset(&name, 0, sizeof(struct sockaddr_un)); name.sun_family = AF_UNIX; strncpy(name.sun_path, path, sizeof(name.sun_path) - 1); - rc = connect(s, (const struct sockaddr *)&name, sizeof name); + if (server) { + rc = bind(s, (const struct sockaddr *)&name, sizeof name); + if (rc < 0) return -1; + rc = listen(s, 1); + } else { + rc = connect(s, (const struct sockaddr *)&name, sizeof name); + } if (rc < 0) return -1; + + if (server && rc > -1) { + socket_path = strdup(path); + if (atexit(cleanup) < 0) warn("atexit"); + if (signal(SIGINT, exit) == SIG_ERR) warn("signal"); + int client = accept(s, NULL, NULL); + int err = errno; + close(s); + errno = err; + return client; + } + return s; } -static int get_socket_path(char *buf, size_t len, const char *app_dir) { +static int get_socket_path(char *buf, size_t len, const char *app_dir, bool server) { struct stat st; int sz = snprintf(buf, len-1, "%s/%s", app_dir, "socket"); if (sz < 0 || sz >= (int)len-1) err(1, "failed to get socket path"); int rc = stat(buf, &st); - if (rc < 0) return -1; - if (!(st.st_mode & S_IFSOCK)) { errno = EINVAL; return -1; } + if (!server) { + if (rc < 0) return -1; + if (!(st.st_mode & S_IFSOCK)) { errno = EINVAL; return -1; } + } return 0; } @@ -249,121 +296,226 @@ static int write_all(int fd, const void *buf, size_t count) { return 0; } -static void shs_connect(int sfd, int infd, int outfd, const unsigned char pubkey[32], const unsigned char seckey[64], const unsigned char appkey[32], const unsigned char server_pubkey[32], struct boxs *bs) { +static void shs_connect(int sfd, int infd, int outfd, const unsigned char pubkey[32], const unsigned char seckey[64], const unsigned char appkey[32], const unsigned char remote_pubkey[32], struct boxs *bs, bool client, bool specify_client_key) { int rc; + unsigned char client_pubkey[32]; unsigned char local_app_mac[32], remote_app_mac[32]; + unsigned char kx_pk[32], kx_sk[32], remote_kx_pk[32]; + unsigned char buf[64]; - unsigned char kx_pk[32], kx_sk[32]; - rc = crypto_box_keypair(kx_pk, kx_sk); - if (rc < 0) errx(1, "failed to generate auth keypair"); + if (client) { + rc = crypto_box_keypair(kx_pk, kx_sk); + if (rc < 0) errx(1, "failed to generate auth keypair"); - rc = crypto_auth(local_app_mac, kx_pk, 32, appkey); - if (rc < 0) err(1, "failed to generate app mac"); + rc = crypto_auth(local_app_mac, kx_pk, 32, appkey); + if (rc < 0) err(1, "failed to generate app mac"); - // send challenge - unsigned char buf[64]; - memcpy(buf, local_app_mac, 32); - memcpy(buf+32, kx_pk, 32); - rc = write_all(outfd, buf, sizeof(buf)); - if (rc < 0) err(1, "failed to send challenge"); - - // recv challenge - unsigned char remote_kx_pk[32]; - rc = read_all(infd, buf, sizeof(buf)); - if (rc < 0) err(1, "challenge not accepted"); - memcpy(remote_app_mac, buf, 32); - memcpy(remote_kx_pk, buf+32, 32); - rc = crypto_auth_verify(buf, remote_kx_pk, 32, appkey); - if (rc < 0) errx(1, "wrong protocol (version?)"); - - // send auth + // send challenge + memcpy(buf, local_app_mac, 32); + memcpy(buf+32, kx_pk, 32); + rc = write_all(outfd, buf, sizeof(buf)); + if (rc < 0) err(1, "failed to send challenge"); - unsigned char secret[32]; - rc = crypto_scalarmult(secret, kx_sk, remote_kx_pk); - if (rc < 0) errx(1, "failed to derive shared secret"); + } else { + // recv challenge + rc = read_all(infd, buf, sizeof(buf)); + if (rc < 0) err(1, "expected challenge"); + memcpy(remote_app_mac, buf, 32); + memcpy(remote_kx_pk, buf+32, 32); - unsigned char remote_pk_curve[32]; - rc = crypto_sign_ed25519_pk_to_curve25519(remote_pk_curve, server_pubkey); - if (rc < 0) errx(1, "failed to curvify remote public key"); + rc = crypto_auth_verify(buf, remote_kx_pk, 32, appkey); + if (rc < 0) errx(1, "wrong protocol/version"); - unsigned char a_bob[32]; - rc = crypto_scalarmult(a_bob, kx_sk, remote_pk_curve); - if (rc < 0) errx(1, "failed to derive a_bob"); + } - unsigned char secret2a[96]; - memcpy(secret2a, appkey, 32); - memcpy(secret2a+32, secret, 32); - memcpy(secret2a+64, a_bob, 32); + if (client) { + // recv challenge + rc = read_all(infd, buf, sizeof(buf)); + if (rc < 0) err(1, "challenge not accepted"); + memcpy(remote_app_mac, buf, 32); + memcpy(remote_kx_pk, buf+32, 32); + rc = crypto_auth_verify(buf, remote_kx_pk, 32, appkey); + if (rc < 0) errx(1, "wrong protocol (version?)"); - unsigned char secret2[32]; - rc = crypto_hash_sha256(secret2, secret2a, sizeof(secret2a)); - if (rc < 0) errx(1, "failed to hash secret2"); + } else { + rc = crypto_box_keypair(kx_pk, kx_sk); + if (rc < 0) errx(1, "failed to generate auth keypair"); - unsigned char shash[32]; - rc = crypto_hash_sha256(shash, secret, sizeof(secret)); - if (rc < 0) errx(1, "failed to hash secret"); + rc = crypto_auth(local_app_mac, kx_pk, 32, appkey); + if (rc < 0) err(1, "failed to generate app mac"); - unsigned char signed1[96]; - memcpy(signed1, appkey, 32); - memcpy(signed1+32, server_pubkey, 32); - memcpy(signed1+64, shash, 32); + // send challenge + memcpy(buf, local_app_mac, 32); + memcpy(buf+32, kx_pk, 32); + rc = write_all(outfd, buf, sizeof(buf)); + if (rc < 0) err(1, "failed to send challenge"); - unsigned char sig[64]; - rc = crypto_sign_detached(sig, NULL, signed1, sizeof(signed1), seckey); - if (rc < 0) errx(1, "failed to sign inner hello"); + } + unsigned char remote_pk_curve[32]; unsigned char hello[96]; - memcpy(hello, sig, 64); - memcpy(hello+64, pubkey, 32); - + unsigned char secret2[32]; unsigned char boxed_auth[112]; - rc = crypto_secretbox_easy(boxed_auth, hello, sizeof(hello), zeros, secret2); - if (rc < 0) errx(1, "failed to box hello"); + unsigned char secret[32]; + unsigned char a_bob[32]; + unsigned char secret2a[96]; + unsigned char shash[32]; + unsigned char sig[64]; + unsigned char signed1[96]; + unsigned char local_sk_curve[32]; + if (client) { + // send auth - rc = write_all(outfd, boxed_auth, sizeof(boxed_auth)); - if (rc < 0) errx(1, "failed to send auth"); + rc = crypto_scalarmult(secret, kx_sk, remote_kx_pk); + if (rc < 0) errx(1, "failed to derive shared secret"); - // verify accept + rc = crypto_sign_ed25519_pk_to_curve25519(remote_pk_curve, remote_pubkey); + if (rc < 0) errx(1, "failed to curvify remote public key"); - unsigned char boxed_okay[80]; - rc = read_all(infd, boxed_okay, sizeof(boxed_okay)); - if (rc < 0) err(1, "hello not accepted"); + rc = crypto_scalarmult(a_bob, kx_sk, remote_pk_curve); + if (rc < 0) errx(1, "failed to derive a_bob"); - unsigned char local_sk_curve[32]; - rc = crypto_sign_ed25519_sk_to_curve25519(local_sk_curve, seckey); - if (rc < 0) errx(1, "failed to curvify local secret key"); + memcpy(secret2a, appkey, 32); + memcpy(secret2a+32, secret, 32); + memcpy(secret2a+64, a_bob, 32); - unsigned char b_alice[32]; - rc = crypto_scalarmult(b_alice, local_sk_curve, remote_kx_pk); - if (rc < 0) errx(1, "failed to derive b_alice"); + rc = crypto_hash_sha256(secret2, secret2a, sizeof(secret2a)); + if (rc < 0) errx(1, "failed to hash secret2"); - unsigned char secret3a[128]; - memcpy(secret3a, appkey, 32); - memcpy(secret3a+32, secret, 32); - memcpy(secret3a+64, a_bob, 32); - memcpy(secret3a+96, b_alice, 32); + rc = crypto_hash_sha256(shash, secret, sizeof(secret)); + if (rc < 0) errx(1, "failed to hash secret"); - unsigned char secret3[32]; - rc = crypto_hash_sha256(secret3, secret3a, sizeof(secret3a)); - if (rc < 0) errx(1, "failed to hash secret3"); + unsigned char signed1[96]; + memcpy(signed1, appkey, 32); + memcpy(signed1+32, remote_pubkey, 32); + memcpy(signed1+64, shash, 32); + + rc = crypto_sign_detached(sig, NULL, signed1, sizeof(signed1), seckey); + if (rc < 0) errx(1, "failed to sign inner hello"); + + memcpy(hello, sig, 64); + memcpy(hello+64, pubkey, 32); + + rc = crypto_secretbox_easy(boxed_auth, hello, sizeof(hello), zeros, secret2); + if (rc < 0) errx(1, "failed to box hello"); + + rc = write_all(outfd, boxed_auth, sizeof(boxed_auth)); + if (rc < 0) errx(1, "failed to send auth"); + + } else { + // read auth + rc = read_all(infd, boxed_auth, sizeof(boxed_auth)); + if (rc < 0) err(1, "expected hello"); - rc = crypto_secretbox_open_easy(sig, boxed_okay, sizeof(boxed_okay), zeros, secret3); - if (rc < 0) errx(1, "failed to unbox the okay"); + rc = crypto_scalarmult(secret, kx_sk, remote_kx_pk); + if (rc < 0) errx(1, "failed to derive shared secret"); + rc = crypto_hash_sha256(shash, secret, sizeof(secret)); + if (rc < 0) errx(1, "failed to hash secret"); + + rc = crypto_sign_ed25519_sk_to_curve25519(local_sk_curve, seckey); + if (rc < 0) errx(1, "failed to curvify local secret key"); + + rc = crypto_scalarmult(a_bob, local_sk_curve, remote_kx_pk); + if (rc < 0) errx(1, "failed to derive a_bob"); + + memcpy(secret2a, appkey, 32); + memcpy(secret2a+32, secret, 32); + memcpy(secret2a+64, a_bob, 32); + + rc = crypto_hash_sha256(secret2, secret2a, sizeof(secret2a)); + if (rc < 0) errx(1, "failed to hash secret2"); + + rc = crypto_secretbox_open_easy(hello, boxed_auth, sizeof(boxed_auth), zeros, secret2); + if (rc < 0) errx(1, "failed to unbox client hello"); + + memcpy(sig, hello, 64); + memcpy(client_pubkey, hello+64, 32); + + memcpy(signed1, appkey, 32); + memcpy(signed1+32, pubkey, 32); + memcpy(signed1+64, shash, 32); + + rc = crypto_sign_verify_detached(sig, signed1, sizeof(signed1), client_pubkey); + if (rc < 0) errx(1, "wrong number"); + } + + unsigned char boxed_okay[80]; + unsigned char b_alice[32]; + unsigned char secret3a[128]; + unsigned char secret3[32]; unsigned char signed2[160]; - memcpy(signed2, appkey, 32); - memcpy(signed2+32, hello, 96); - memcpy(signed2+128, shash, 32); + if (client) { + // verify accept + + rc = read_all(infd, boxed_okay, sizeof(boxed_okay)); + if (rc < 0) err(1, "hello not accepted"); + + rc = crypto_sign_ed25519_sk_to_curve25519(local_sk_curve, seckey); + if (rc < 0) errx(1, "failed to curvify local secret key"); + + rc = crypto_scalarmult(b_alice, local_sk_curve, remote_kx_pk); + if (rc < 0) errx(1, "failed to derive b_alice"); - rc = crypto_sign_verify_detached(sig, signed2, sizeof(signed2), server_pubkey); - if (rc < 0) errx(1, "server not authenticated"); + memcpy(secret3a, appkey, 32); + memcpy(secret3a+32, secret, 32); + memcpy(secret3a+64, a_bob, 32); + memcpy(secret3a+96, b_alice, 32); + + rc = crypto_hash_sha256(secret3, secret3a, sizeof(secret3a)); + if (rc < 0) errx(1, "failed to hash secret3"); + + rc = crypto_secretbox_open_easy(sig, boxed_okay, sizeof(boxed_okay), zeros, secret3); + if (rc < 0) errx(1, "failed to unbox the okay"); + + memcpy(signed2, appkey, 32); + memcpy(signed2+32, hello, 96); + memcpy(signed2+128, shash, 32); + + rc = crypto_sign_verify_detached(sig, signed2, sizeof(signed2), remote_pubkey); + if (rc < 0) errx(1, "server not authenticated"); + + } else { + if (specify_client_key && memcmp(client_pubkey, remote_pubkey, 32)) { + errx(1, "unexpected client"); + } + + // send accept + + rc = crypto_sign_ed25519_pk_to_curve25519(remote_pk_curve, client_pubkey); + if (rc < 0) errx(1, "failed to curvify remote public key"); + + rc = crypto_scalarmult(b_alice, kx_sk, remote_pk_curve); + if (rc < 0) errx(1, "failed to derive b_alice"); + + memcpy(secret3a, appkey, 32); + memcpy(secret3a+32, secret, 32); + memcpy(secret3a+64, a_bob, 32); + memcpy(secret3a+96, b_alice, 32); + + rc = crypto_hash_sha256(secret3, secret3a, sizeof(secret3a)); + if (rc < 0) errx(1, "failed to hash secret3"); + + memcpy(signed2, appkey, 32); + memcpy(signed2+32, hello, 96); + memcpy(signed2+128, shash, 32); + + rc = crypto_sign_detached(sig, NULL, signed2, sizeof(signed2), seckey); + if (rc < 0) errx(1, "failed to sign inner accept"); + + rc = crypto_secretbox_easy(boxed_okay, sig, sizeof(sig), zeros, secret3); + if (rc < 0) errx(1, "failed to box accept"); + + rc = write_all(outfd, boxed_okay, sizeof(boxed_okay)); + if (rc < 0) errx(1, "failed to send accept"); + } rc = crypto_hash_sha256(secret, secret3, 32); if (rc < 0) errx(1, "failed to hash secret3"); unsigned char enc_key_hashed[64]; memcpy(enc_key_hashed, secret, 32); - memcpy(enc_key_hashed+32, server_pubkey, 32); + memcpy(enc_key_hashed+32, client ? remote_pubkey : client_pubkey, 32); rc = crypto_hash_sha256(bs->encrypt_key, enc_key_hashed, 64); if (rc < 0) errx(1, "failed to hash the encrypt key"); @@ -1056,6 +1208,7 @@ int main(int argc, char *argv[]) { bool ipv6_arg = false; bool passthrough = false; bool strings = false; + bool daemon = false; enum ip_family ip_family; get_app_dir(app_dir, sizeof(app_dir)); @@ -1089,6 +1242,7 @@ int main(int argc, char *argv[]) { case 'n': noauth = true; break; case '4': ipv4_arg = true; break; case '6': ipv6_arg = true; break; + case 'd': daemon = true; break; case 'a': passthrough = true; break; case 'l': no_newline = true; break; case 'r': raw = true; no_newline = true; break; @@ -1184,24 +1338,24 @@ int main(int argc, char *argv[]) { } else if (socket_path) { if (implied_tcp) errx(1, "-u option conflicts with host/port options"); - s = unix_connect(socket_path); + s = unix_connect(socket_path, daemon); if (s < 0) err(1, "unix_connect"); infd = outfd = s; } else if (!implied_tcp && !implied_auth) { char socket_path_buf[_POSIX_PATH_MAX]; - rc = get_socket_path(socket_path_buf, sizeof(socket_path_buf), app_dir); + rc = get_socket_path(socket_path_buf, sizeof(socket_path_buf), app_dir, daemon); if (rc < 0 && noauth) err(1, "get_socket_path"); - if (rc < 0) goto do_tcp_connect; - s = unix_connect(socket_path_buf); + if (rc < 0) goto do_tcp; + s = unix_connect(socket_path_buf, daemon); if (s < 0 && noauth) err(1, "unix_connect"); - if (s < 0) goto do_tcp_connect; + if (s < 0) goto do_tcp; noauth = true; infd = outfd = s; } else { -do_tcp_connect: - s = tcp_connect(host, port, ip_family); +do_tcp: + s = tcp_connect(host, port, ip_family, daemon); if (s < 0) err(1, "tcp_connect"); infd = outfd = s; } @@ -1212,7 +1366,7 @@ do_tcp_connect: bs.noauth = true; if (implied_auth) errx(1, "-n option conflicts with -k, -K, -c and -T options."); } else { - shs_connect(s, infd, outfd, public_key, private_key, shs_cap_key, remote_key, &bs); + shs_connect(s, infd, outfd, public_key, private_key, shs_cap_key, remote_key, &bs, !daemon, key_arg); } if (test) { diff --git a/test-shs-inner.sh b/test-shs-client-inner.sh similarity index 100% rename from test-shs-inner.sh rename to test-shs-client-inner.sh diff --git a/test-shs-server-inner.sh b/test-shs-server-inner.sh new file mode 100755 index 0000000..a8ee193 --- /dev/null +++ b/test-shs-server-inner.sh @@ -0,0 +1,8 @@ +#!/bin/sh +cap_hex=${1?shs cap key} +sk_hex=${2?server secret key} + +cap_b64="$(echo -n "$cap_hex" | xxd -r -p | base64)" +sk_b64="$(echo -n "$sk_hex" | xxd -r -p | base64 -w 0)" + +exec sbotc -T -d -c "$cap_b64" -K "$sk_b64"