HamSSH

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!

One thought on “HamSSH

Leave a Reply

Your email address will not be published. Required fields are marked *