In a previous article, I explained that encryption is not allowed on the radio amateur service, but that we could still use public-key cryptography. In particular, we could use SSH for authentication, but without encryption. This would allow secure remote control of base stations, satellites and more.
In this article, I will discuss how we can do that in practice without reinventing the wheel. The result is the HamSSH project, which forks OpenSSH to provide an encryption-free, but still authentication-secure implementation of SSH.
Building OpenSSH
The most common implementation of the SSH protocol is OpenSSH. To build the project from the sources, we run the commands below:
$ sudo apt install autoconf gcc git libssl-dev make zlib1g-dev
$ git clone https://github.com/openssh/openssh-portable
$ cd openssh-portable
$ autoreconf
$ ./configure
$ make -j$(nproc)
Once this is done, we can generate a server key with ssh-keygen
, start the server and try connecting with the client. You should be able to do that by running the commands below in two terminals:
# Terminal 1
$ ssh-keygen -f host_key -N ''
$ $PWD/sshd -d -f none -o "HostKey $PWD/host_key" -o "Port 2222
# Terminal 2
$ ./ssh -p 2222 localhost
Enabling the Null-Cipher
You may try using -o "Ciphers none"
on the server’s command-line, and -c none
on the client’s, but it will fail because the null-cipher is not enabled. Our first task is to find how to make it available.
Thanks to the -d
option on the server’s command-line, we can check what encryption cipher have been used:
debug1: kex: client->server cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: zlib@openssh.com [preauth]
debug1: kex: server->client cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: zlib@openssh.com [preauth]
From this, we can take a guess and search for the string chacha20
to locate the place where the ciphers are chosen. We quickly find an array named ciphers
, in cipher.c
. It looks promising:
static const struct sshcipher ciphers[] = {
#ifdef WITH_OPENSSL
#ifndef OPENSSL_NO_DES
{ "3des-cbc", 8, 24, 0, 0, CFLAG_CBC, EVP_des_ede3_cbc },
#endif
{ "aes128-cbc", 16, 16, 0, 0, CFLAG_CBC, EVP_aes_128_cbc },
{ "aes192-cbc", 16, 24, 0, 0, CFLAG_CBC, EVP_aes_192_cbc },
{ "aes256-cbc", 16, 32, 0, 0, CFLAG_CBC, EVP_aes_256_cbc },
{ "aes128-ctr", 16, 16, 0, 0, 0, EVP_aes_128_ctr },
{ "aes192-ctr", 16, 24, 0, 0, 0, EVP_aes_192_ctr },
{ "aes256-ctr", 16, 32, 0, 0, 0, EVP_aes_256_ctr },
{ "aes128-gcm@openssh.com",
16, 16, 12, 16, 0, EVP_aes_128_gcm },
{ "aes256-gcm@openssh.com",
16, 32, 12, 16, 0, EVP_aes_256_gcm },
#else
{ "aes128-ctr", 16, 16, 0, 0, CFLAG_AESCTR, NULL },
{ "aes192-ctr", 16, 24, 0, 0, CFLAG_AESCTR, NULL },
{ "aes256-ctr", 16, 32, 0, 0, CFLAG_AESCTR, NULL },
#endif
{ "chacha20-poly1305@openssh.com",
8, 64, 0, 16, CFLAG_CHACHAPOLY, NULL },
{ "none", 8, 0, 0, 0, CFLAG_NONE, NULL },
{ NULL, 0, 0, 0, 0, 0, NULL }
};
We can see that the none
cipher is actually listed. Now, we need to find out how to allow it. The sshcipher struct that make the elements of the array is defined right before:
struct sshcipher {
char *name;
u_int block_size;
u_int key_len;
u_int iv_len; /* defaults to block_size */
u_int auth_len;
u_int flags;
#define CFLAG_CBC (1<<0)
#define CFLAG_CHACHAPOLY (1<<1)
#define CFLAG_AESCTR (1<<2)
#define CFLAG_NONE (1<<3)
#define CFLAG_INTERNAL CFLAG_NONE /* Don't use "none" for packets */
#ifdef WITH_OPENSSL
const EVP_CIPHER *(*evptype)(void);
#else
void *ignored;
#endif
};
The name
field is self-explanatory. The fields block_size
, key_len
, iv_len
and auth_len
are parameters for the cipher. They are not used by the null-cipher, so they are set to zero in ciphers
. The last field, evptype
or ignored
looks optional, so it probably does not matter for what we want to do.
That leaves flags
. Given the definition of CFLAG_INTERNAL
and the associated comment, it seems reasonable to think that this macro controls what ciphers should not be made available in normal operation.
If we look further, we find two functions that use this macro:
/* Returns a comma-separated list of supported ciphers. */
char *
cipher_alg_list(char sep, int auth_only)
{
char *tmp, *ret = NULL;
size_t nlen, rlen = 0;
const struct sshcipher *c;
for (c = ciphers; c->name != NULL; c++) {
if ((c->flags & CFLAG_INTERNAL) != 0)
continue;
if (auth_only && c->auth_len == 0)
continue;
if (ret != NULL)
ret[rlen++] = sep;
nlen = strlen(c->name);
if ((tmp = realloc(ret, rlen + nlen + 2)) == NULL) {
free(ret);
return NULL;
}
ret = tmp;
memcpy(ret + rlen, c->name, nlen + 1);
rlen += nlen;
}
return ret;
}
…
#define CIPHER_SEP ","
int
ciphers_valid(const char *names)
{
const struct sshcipher *c;
char *cipher_list, *cp;
char *p;
if (names == NULL || strcmp(names, "") == 0)
return 0;
if ((cipher_list = cp = strdup(names)) == NULL)
return 0;
for ((p = strsep(&cp, CIPHER_SEP)); p && *p != '\0';
(p = strsep(&cp, CIPHER_SEP))) {
c = cipher_by_name(p);
if (c == NULL || (c->flags & CFLAG_INTERNAL) != 0) {
free(cipher_list);
return 0;
}
}
free(cipher_list);
return 1;
}
The code is pretty straightforward. The first function iterates over the list of ciphers, and filters out those whose flags
field matches the CFLAG_INTERNAL
macro. The second one takes a comma-separated list of cipher names, and check that they all correspond to known ciphers in the list, and that they are not internal ciphers.
If we change CFLAG_INTERNAL
to 0, that should enable none
as a normal cipher.
Note: you might want to set CFLAG_INTERNAL to ~CFLAG_NONE
to disable the real ciphers, but this just makes sshd
segfault. This is because the default list of ciphers becomes invalid. We’ll get back tot his.
After rebuilding, restarting the SSH server, and connecting with the client, we get:
debug1: kex: client->server cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: zlib@openssh.com [preauth]
debug1: kex: server->client cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: zlib@openssh.com [preauth]
We’re still using encryption!
It’s actually normal: we enabled the null-cipher, but it’s not necessarily selected. In fact, it is still not listed as one of the options by either the server of the client. We can force this by adding -o "Ciphers none"
to the server’s command-line, and -o none
to the client’s. Then, we get:
debug1: kex: client->server cipher: none MAC: umac-64-etm@openssh.com compression: zlib@openssh.com [preauth]
debug1: kex: server->client cipher: none MAC: umac-64-etm@openssh.com compression: zlib@openssh.com [preauth]
Success!
But we want this to be the default behavior. In fact, we would like to ensure we do not use encryption by mistake, so we want to disable the other encryption modes.
Disabling Encryption
Let’s look into how the option Ciphers is handled
If we look in the other source files containing the string chacha20, we find this in myproposal.h
:
#define KEX_SERVER_ENCRYPT \
"chacha20-poly1305@openssh.com," \
"aes128-ctr,aes192-ctr,aes256-ctr," \
"aes128-gcm@openssh.com,aes256-gcm@openssh.com"
#define KEX_CLIENT_ENCRYPT KEX_SERVER_ENCRYPT
“KEX” stands for “KEy eXchange”. So KEY_…_ENCRYPT
relates to the stage where the server and the client decide how to encrypt the communications. The macro is used in the servconf.c
and readconf.c
source files, where the server and the client (respectively) detect the default values for the Ciphers
option. Let’s try changing the string to just "none"
, and starting the server and the client without any particular option.
debug1: kex: client->server cipher: none MAC: umac-64-etm@openssh.com compression: zlib@openssh.com [preauth]
debug1: kex: server->client cipher: none MAC: umac-64-etm@openssh.com compression: zlib@openssh.com [preauth]
Good!
But what if we pass -o "Ciphers chacha20-poly1305@openssh.com"
to the server and -c chacha20-poly1305@openssh.com
to the client?
debug1: kex: client->server cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: zlib@openssh.com [preauth]
debug1: kex: server->client cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: zlib@openssh.com [preauth]
Let’s make sure this does not happen. We can now come back to the definition of CFLAG_INTERNAL
and set it to ~CFLAG_NONE
. Since the default cipher list is "none"
, this will not make the server crash. And if we try to use anything but null-cipher, we get:
// server
Bad SSH2 cipher spec 'chacha20-poly1305@openssh.com'.
// client
Unknown cipher type 'chacha20-poly1305@openssh.com'
Done!
Niceties
We now have a null-cipher-only implementation of an SSH server and client. Let’s see what we can do further to avoid mistakes.
Changing the Names
First, let’s make sure we do not confuse the regular SSH binary, and our null-cipher only one. We’ll also want to avoid overwriting the regular ones by mistakes, and we might want to use different configurations files.
HamSSH prepends a “ham” suffix to all the binary and configurations files. So the user configurations files can be found in ~/.hamssh
, and the global configurations files are in /usr/local/etc/ham*
.
Changing the Default Port
Regular SSH uses the port 22 by default. We will want to use another one for our null-cipher-only SSH. Since 21 is already taken by FTP, I opted for 23. It is normally used for Telnet. Since Telnet is even more obsolete than FTP, that should not be a problem. And users can always use another port by using the regular SSH options.
I also like the idea that it reminds us that all communications are in the clear, just like with Telnet.
Disabling Password Authentication
The SSH protocol is still useful without encryption because it gives us public-key encryption. But we do not want to risk the user mistakenly typing a password when trying to log in.
In other words, we want to force the PasswordAuthentication
and KbdInteractiveAuthentication
options to No
. The simplest way is to overwrite options.password_authentication
options.kbd_interactive_authentication
after the command-line arguments and configuration files have been parsed, in sshd.c
(server) and ssh.c
(client):
/* Force public key authentication */
options.password_authentication = 0;
options.kbd_interactive_authentication = 0;
Conclusion
With just a few tweaks, we have a robust and versatile tool for remote control compatible with amateur radio regulations. We have forcibly disabled encryption on both the server and the client to avoid using encryption by mistake, disabled password authentication to avoid spilling secrets carelessly, and changed names and the default port for compatibility with regular SSH. All we need to do now is to use it!