Implementing an SSH agent in Hare May 9, 2022 on Drew DeVault's blog

Cross-posted from the Hare blog

In the process of writing an SSH agent for Himitsu, I needed to implement many SSH primitives from the ground up in Hare, now available via hare-ssh. Today, I’m going to show you how it works!

Important: This blog post deals with cryptography-related code. The code you’re going to see today is incomplete, unaudited, and largely hasn’t even seen any code review. Let me begin with a quote from the “crypto” module’s documentation in the Hare standard library:

Cryptography is a difficult, high-risk domain of programming. The life and well-being of your users may depend on your ability to implement cryptographic applications with due care. Please carefully read all of the documentation, double-check your work, and seek second opinions and independent review of your code. Our documentation and API design aims to prevent easy mistakes from being made, but it is no substitute for a good background in applied cryptography.

Do your due diligence before repurposing anything you see here.

Decoding SSH private keys

Technically, you do not need to deal with OpenSSH private keys when implementing an SSH agent. However, my particular use-case includes dealing with this format, so I started here. Unlike much of SSH, the OpenSSH private key format (i.e. the format of the file at ~/.ssh/id_ed25519) is, well, private. It’s not documented and I had to get most of the details from reverse-engineering the OpenSSH C code. The main area of interest is sshkey.c. I’ll spare you from reading it yourself and just explain how it works.

First of all, let’s just consider what an SSH private key looks like:

-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABDTIm/zSI
7zeHAs4rIXaOD1AAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIE7qq/pMk9VrRupn
9j4/tNHclJnKgJAE1pfUecRNT1fAAAAAoEcx6mnJmFlYXx1eYztw6SZ5yuL6T1LWfj+bpg
7zNQBoqJW1j+Q17PUMtXj9wDDOQx+6OE7JT/RrK3Vltp4oXmFI4FgsYbE9RbNXSC2xvLaX
fplmx+eAOir9UTZGTIbOGy1cVho8LzDLLo4WiGYbpxtIvkJE72f0YdTm8RrNVkLlAy7ayV
uFcoq1JBrjIAa7UtqIr9SG8b76ALJZb9jPc3A=
-----END OPENSSH PRIVATE KEY-----

We can immediately tell that this is a PEM file (RFC 7468). The first step to read this file was to implement a decoder for the PEM format, which has been on our to-do list for a while now, and is also needed for many other use-cases. Similar to many other formats provided in the standard library, you can call pem::newdecoder to create a PEM decoder for an arbitrary I/O source, returning the decoder state on the stack. We can then call pem::next to find the next PEM header (-----BEGIN...), which returns a decoder for that specific PEM blob (this design accommodates PEM files which have several PEM segments concatenated together, or intersperse other data in the file alongside the PEM bits. This is common for other PEM use-cases). With this secondary decoder, we can simply read from it like any other I/O source and it decodes the base64-encoded data and returns it to us as bytes.

Based on this, we can examine the contents of this key with a simple program.

use encoding::hex;
use encoding::pem;
use fmt;
use io;
use os;

export fn main() void = {
	const dec = pem::newdecoder(os::stdin);
	defer pem::finish(&dec);

	for (true) {
		const reader = match (pem::next(&dec)) {
		case let reader: (str, pem::pemdecoder) =>
			yield reader;
		case io::EOF =>
			break;
		};
		const name = reader.0, stream = reader.1;
		defer io::close(&stream)!;

		fmt::printfln("PEM data '{}':", name)!;
		const bytes = io::drain(&stream)!;
		defer free(bytes);

		hex::dump(os::stdout, bytes)!;
	};
};

Running this program on our sample key yields the following:

PEM data 'OPENSSH PRIVATE KEY':
00000000  6f 70 65 6e 73 73 68 2d  6b 65 79 2d 76 31 00 00  |openssh-key-v1..|
00000010  00 00 0a 61 65 73 32 35  36 2d 63 74 72 00 00 00  |...aes256-ctr...|
00000020  06 62 63 72 79 70 74 00  00 00 18 00 00 00 10 d3  |.bcrypt.........|
00000030  22 6f f3 48 8e f3 78 70  2c e2 b2 17 68 e0 f5 00  |"o.H..xp,...h...|
00000040  00 00 10 00 00 00 01 00  00 00 33 00 00 00 0b 73  |..........3....s|
00000050  73 68 2d 65 64 32 35 35  31 39 00 00 00 20 4e ea  |sh-ed25519... N.|
00000060  ab fa 4c 93 d5 6b 46 ea  67 f6 3e 3f b4 d1 dc 94  |..L..kF.g.>?....|
00000070  99 ca 80 90 04 d6 97 d4  79 c4 4d 4f 57 c0 00 00  |........y.MOW...|
00000080  00 a0 47 31 ea 69 c9 98  59 58 5f 1d 5e 63 3b 70  |..G1.i..YX_.^c;p|
00000090  e9 26 79 ca e2 fa 4f 52  d6 7e 3f 9b a6 0e f3 35  |.&y...OR.~?....5|
000000a0  00 68 a8 95 b5 8f e4 35  ec f5 0c b5 78 fd c0 30  |.h.....5....x..0|
000000b0  ce 43 1f ba 38 4e c9 4f  f4 6b 2b 75 65 b6 9e 28  |.C..8N.O.k+ue..(|
000000c0  5e 61 48 e0 58 2c 61 b1  3d 45 b3 57 48 2d b1 bc  |^aH.X,a.=E.WH-..|
000000d0  b6 97 7e 99 66 c7 e7 80  3a 2a fd 51 36 46 4c 86  |..~.f...:*.Q6FL.|
000000e0  ce 1b 2d 5c 56 1a 3c 2f  30 cb 2e 8e 16 88 66 1b  |..-\V.</0.....f.|
000000f0  a7 1b 48 be 42 44 ef 67  f4 61 d4 e6 f1 1a cd 56  |..H.BD.g.a.....V|
00000100  42 e5 03 2e da c9 5b 85  72 8a b5 24 1a e3 20 06  |B.....[.r..$.. .|
00000110  bb 52 da 88 af d4 86 f1  be fa 00 b2 59 6f d8 cf  |.R..........Yo..|
00000120  73 70                                             |sp|

OpenSSH private keys begin with a magic string, “openssh-key-v1\0”, which we can see here. Following this are a number of binary encoded fields which are represented in a manner similar to the SSH wire protocol, most often as strings prefixed by their length, encoded as a 32-bit big-endian integer. In order, the fields present here are:

We parse this information like so:

export type sshprivkey = struct {
	cipher: str,
	kdfname: str,
	kdf: []u8,
	pubkey: []u8,
	privkey: []u8,
};

export fn decodesshprivate(in: io::handle) (sshprivkey | error) = {
	const pem = pem::newdecoder(in);
	const dec = match (pem::next(&pem)?) {
	case io::EOF =>
		return invalid;
	case let dec: (str, pem::pemdecoder) =>
		if (dec.0 != "OPENSSH PRIVATE KEY") {
			return invalid;
		};
		yield dec.1;
	};

	let magicbuf: [15]u8 = [0...];
	match (io::readall(&dec, magicbuf)?) {
	case size => void;
	case io::EOF =>
		return invalid;
	};
	if (!bytes::equal(magicbuf, strings::toutf8(magic))) {
		return invalid;
	};

	let key = sshprivkey { ... };
	key.cipher = readstr(&dec)?;
	key.kdfname = readstr(&dec)?;
	key.kdf = readslice(&dec)?;

	let buf: [4]u8 = [0...];
	match (io::readall(&dec, buf)?) {
	case size => void;
	case io::EOF =>
		return invalid;
	};
	const nkey = endian::begetu32(buf);
	if (nkey != 1) {
		// OpenSSH currently hard-codes the number of keys to 1
		return invalid;
	};

	key.pubkey = readslice(&dec)?;
	key.privkey = readslice(&dec)?;

	// Add padding bytes
	append(key.privkey, io::drain(&dec)?...);
	return key;
};

However, to get at the actual private key — so that we can do cryptographic operations with it — we first have to decrypt this inner data. Those three fields — cipher name, KDF name, and KDF data — are our hint. In essence, this data is encrypted by OpenSSH by using a variant of bcrypt as a key derivation function, which turns your password (plus a salt) into a symmetric encryption key. Then it uses AES 256 in CTR mode with this symmetric key to encrypt the private key data. With the benefit of hindsight, I might question these primitives, but that’s what they use so we’ll have to work with it.

Prior to starting this work, Hare already had support for AES and CTR, though they gained some upgrades during the course of this work, since using an interface for real-world code is the best way to evaluate its design. This leaves us to implement bcrypt.

bcrypt is a password hashing algorithm invented by OpenBSD based on the Blowfish cipher, and it is pretty badly designed. However, Blowfish was fairly straightforward to implement. I’ll spare you the details, but here’s the documentation and implementation for your consideration. I also implemented the standard bcrypt hash at crypto::bcrypt, whose implementation is here (for now). This isn’t especially relevant for us, however, since OpenSSH uses a modified form of bcrypt as a key derivation function.

The implementation the bcrypt KDF in Hare is fairly straightforward. To write it, I referenced OpenSSH portable’s vendored OpenBSD implementation at openbsd-compat/bcrypt_pbkdf.c, as well as the Go implementation in golang.org/x/crypto. Then, with these primitives done, we can implement the actual key decryption.

First, not all keys are encrypted with a passphrase, so a simple function tells us if this step is required:

// Returns true if this private key is encrypted with a passphrase.
export fn isencrypted(key: *sshprivkey) bool = {
	return key.kdfname != "none";
};

The “decrypt” function is used to perform the actual decryption. It begins by finding the symmetric key, like so:

export fn decrypt(key: *sshprivkey, pass: []u8) (void | error) = {
	assert(isencrypted(key));

	const cipher = getcipher(key.cipher)?;
	let ckey: []u8 = alloc([0...], cipher.keylen + cipher.ivlen);
	defer {
		bytes::zero(ckey);
		free(ckey);
	};

	let kdfbuf = bufio::fixed(key.kdf, io::mode::READ);
	switch (key.kdfname) {
	case "bcrypt" =>
		const salt = readslice(&kdfbuf)?;
		defer free(salt);
		const rounds = readu32(&kdfbuf)?;
		bcrypt_pbkdf(ckey, pass, salt, rounds);
	case =>
		return badcipher;
	};

The “KDF data” field I mentioned earlier uses a format private to each KDF mode, though at the present time the only supported KDF is this bcrypt one. In this case, it serves as the salt. The “getcipher” function returns some data from a static table of supported ciphers, which provides us with the required size of the cipher’s key and IV parameters. We allocate sufficient space to store these, create a bufio reader from the KDF field, read out the salt and hashing rounds, and hand all of this over to the bcrypt function to produce our symmetric key (and I/V) in the “ckey” variable.

We may then use these parameters to decrypt the private key area.

	let secretbuf = bufio::fixed(key.privkey, io::mode::READ);
	const cipher = cipher.init(&secretbuf,
		ckey[..cipher.keylen], ckey[cipher.keylen..]);
	defer cipher_free(cipher);

	let buf: []u8 = alloc([0...], len(key.privkey));
	defer free(buf);
	io::readall(cipher, buf)!;

	const a = endian::begetu32(buf[..4]);
	const b = endian::begetu32(buf[4..8]);
	if (a != b) {
		return badpass;
	};

	key.privkey[..] = buf[..];

	free(key.kdf);
	free(key.kdfname);
	free(key.cipher);
	key.kdfname = strings::dup("none");
	key.cipher = strings::dup("none");
	key.kdf = [];
};

The “cipher.init” function is an abstraction that allows us to support more ciphers in the future. For this particular cipher mode, it’s implemented fairly simply:

type aes256ctr = struct {
	st: cipher::ctr_stream,
	block: aes::ct64_block,
	buf: [aes::CTR_BUFSIZE]u8,
};

fn aes256ctr_init(handle: io::handle, key: []u8, iv: []u8) *io::stream = {
	let state = alloc(aes256ctr {
		block = aes::ct64(),
		...
	});
	aes::ct64_init(&state.block, key);
	state.st = cipher::ctr(handle, &state.block, iv, state.buf);
	return state;
};

Within this private key data section, once decrypted, are several fields. First is a random 32-bit integer which is written twice — comparing that these are equal to one another allows us to verify the user’s password. Once verified, we overwrite the private data field in the key structure with the decrypted data, and update the cipher and KDF information to indicate that the key is unencrypted. We could decrypt it directly into the existing private key buffer, without allocating a second buffer, but this would overwrite the encrypted data with garbage if the password was wrong — you’d have to decode the key all over again if the user wants to try again.

So, what does this private key blob look like once decrypted? The hare-ssh repository includes a little program at cmd/sshkey which dumps all of the information stored in an SSH key, and it provides us with this peek at the private data:

00000000  fb 15 e6 16 fb 15 e6 16  00 00 00 0b 73 73 68 2d  |............ssh-|
00000010  65 64 32 35 35 31 39 00  00 00 20 4e ea ab fa 4c  |ed25519... N...L|
00000020  93 d5 6b 46 ea 67 f6 3e  3f b4 d1 dc 94 99 ca 80  |..kF.g.>?.......|
00000030  90 04 d6 97 d4 79 c4 4d  4f 57 c0 00 00 00 40 17  |.....y.MOW....@.|
00000040  bf 87 74 0b 2a 74 d5 29  d0 14 10 3f 04 5d 88 c6  |..t.*t.)...?.]..|
00000050  32 fa 21 9c e9 97 b0 5a  e7 7e 5c 02 72 35 72 4e  |2.!....Z.~\.r5rN|
00000060  ea ab fa 4c 93 d5 6b 46  ea 67 f6 3e 3f b4 d1 dc  |...L..kF.g.>?...|
00000070  94 99 ca 80 90 04 d6 97  d4 79 c4 4d 4f 57 c0 00  |.........y.MOW..|
00000080  00 00 0e 73 69 72 63 6d  70 77 6e 40 74 61 69 67  |...sircmpwn@taig|
00000090  61 01 02 03 04 05 06 07  08 09 0a 0b 0c 0d 0e 0f  |a...............|

We can see upfront these two 32-bit verification numbers I mentioned, and following this are several fields in a similar format to earlier — length-prefixed strings. The fields are:

This is a little bit weird in my opinion — the public key field is redundant with the unencrypted data in this file, and the comment field is probably not so secret as to demand encryption. I think these are just consequences of the file format being private to OpenSSH’s implementation; not much thought has gone into it and implementation details (like the ability to call the same “dump private key” function here as OpenSSH uses elsewhere) have probably leaked through.

We can decode this data with the following Hare code:

export fn decodeprivate(src: *sshprivkey) (key | error) = {
	assert(!isencrypted(src));
	const buf = bufio::fixed(src.privkey, io::mode::READ);

	let verify: [8]u8 = [0...];
	io::read(&buf, verify)!;
	const a = endian::begetu32(verify[..4]);
	const b = endian::begetu32(verify[4..8]);
	if (a != b) {
		return badpass;
	};

	const keytype = readstr(&buf)?;
	defer free(keytype);

	switch (keytype) {
	case "ssh-ed25519" =>
		let key = ed25519key { ... };
		decode_ed25519_sk(&key, &buf)?;
		return key;
	case =>
		// TODO: Support additional key types
		return badcipher;
	};
};

// An ed25519 key pair.
export type ed25519key = struct {
	pkey: ed25519::publickey,
	skey: ed25519::privatekey,
	comment: str,
};

fn decode_ed25519_pk(key: *ed25519key, buf: io::handle) (void | error) = {
	const l = readu32(buf)?;
	if (l != ed25519::PUBLICKEYSZ) {
		return invalid;
	};
	io::readall(buf, key.pkey)?;
};

fn decode_ed25519_sk(key: *ed25519key, buf: io::handle) (void | error) = {
	decode_ed25519_pk(key, buf)?;

	const l = readu32(buf)?;
	if (l != ed25519::PRIVATEKEYSZ) {
		return invalid;
	};
	io::readall(buf, key.skey)?;

	// Sanity check
	const pkey = ed25519::skey_getpublic(&key.skey);
	if (!bytes::equal(pkey, key.pkey)) {
		return invalid;
	};

	key.comment = readstr(buf)?;
};

Fairly straightforward! Finally, we have extracted the actual private key from the file. For this SSH key, in base64, the cryptographic keys are:

Public key:  Tuqr+kyT1WtG6mf2Pj+00dyUmcqAkATWl9R5xE1PV8A=
Private key: F7+HdAsqdNUp0BQQPwRdiMYy+iGc6ZewWud+XAJyNXJO6qv6TJPVa0bqZ/Y+P7TR3JSZyoCQBNaX1HnETU9XwA==

Signing and verification with ed25519

Using these private keys, implementing signatures and signature verification are pretty straightforward. We can stop reading the OpenSSH code at this point — RFC 8709 standardizes this format for ed25519 signatures.

use crypto::ed25519;
use io;

// Signs a message using the provided key, writing the message signature in the
// SSH format to the provided sink.
export fn sign(
	sink: io::handle,
	key: *key,
	msg: []u8,
) (void | io::error) = {
	const signature = ed25519::sign(&key.skey, msg);
	writestr(sink, "ssh-ed25519")?;
	writeslice(sink, signature)?;
};

// Reads an SSH wire signature from the provided I/O handle and verifies that it
// is a valid signature for the given message and key. If valid, void is
// returned; otherwise [[badsig]] is returned.
export fn verify(
	source: io::handle,
	key: *key,
	msg: []u8,
) (void | error) = {
	const sigtype = readstr(source)?;
	defer free(sigtype);
	if (sigtype != keytype(key)) {
		return badsig;
	};
	const sig = readslice(source)?;
	defer free(sig);

	assert(sigtype == "ssh-ed25519"); // TODO: other key types
	if (len(sig) != ed25519::SIGNATURESZ) {
		return badsig;
	};

	const sig = sig: *[*]u8: *[ed25519::SIGNATURESZ]u8;
	if (!ed25519::verify(&key.pkey, msg, sig)) {
		return badsig;
	};
};

This implementation writes and reads signatures in the SSH wire format, which is generally how they will be most useful in this context. This code will be expanded in the future with additional keys, such as RSA, once the necessary primitives are implemented for Hare’s standard library.

The SSH agent protocol

The agent protocol is also standardized (albeit in draft form), so we refer to draft-miller-ssh-agent-01 from this point onwards. It’s fairly straightforward. The agent communicates over an unspecified protocol (Unix sockets in practice) by sending messages in the SSH wire format, which, again, mainly comes in the form of strings prefixed by their 32-bit length in network order.

The first step for implementing net::ssh::agent starts with adding types for all of the data structures and enums for all of the constants, which you can find in types.ha. Each message begins with its length, then a message type (one byte) and a message payload; the structure of the latter varies with the message type.

I started to approach this by writing some functions which, given a byte buffer that contains an SSH agent message, either parses it or asks for more data.

export fn parse(msg: []u8) (message | size | invalid) = {
	if (len(msg) < 5) {
		return 5 - len(msg);
	};
	const ln = endian::begetu32(msg[..4]);
	if (len(msg) < 4 + ln) {
		return 4 + ln - len(msg);
	};

	const mtype = msg[4];
	const buf = bufio::fixed(msg[5..], io::mode::READ);
	switch (mtype) {
	case messagetype::REQUEST_IDENTITIES =>
		return request_identities;
	case messagetype::SIGN_REQUEST =>
		return parse_sign_request(&buf)?;
	case messagetype::ADD_IDENTITY =>
		return parse_add_identity(&buf)?;
	// ...trimmed for brevity, and also because it's full of TODOs...
	case =>
		return invalid.
	};
};

Each individual message payload includes its own parser, except for some messages (such as REQUEST_IDENTITIES), which have no payload. Here’s what the parser for SIGN_REQUEST looks like:

fn parse_sign_request(src: io::handle) (sign_request | invalid) = {
	return sign_request {
		key = readslice(src)?,
		data = readslice(src)?,
		flags = readu32(src)?: sigflag,
	};
};

Pretty straightforward! A more complex one is ADD_IDENTITY:

fn parse_add_identity(src: io::handle) (add_identity | invalid) = {
	const keytype = readstr(src)?;
	// TODO: Support more key types
	const key: ssh::key = switch (keytype) {
	case "ssh-ed25519" =>
		let key = ssh::ed25519key { ... };
		const npub = readu32(src)?;
		if (npub != len(key.pkey)) {
			return invalid;
		};
		io::readall(src, key.pkey)!;
		const npriv = readu32(src)?;
		if (npriv != len(key.skey)) {
			return invalid;
		};
		io::readall(src, key.skey)!;
		yield key;
	case =>
		return invalid;
	};
	return add_identity {
		keytype = keytype,
		key = key,
		comment = readstr(src)?,
	};
};

One thing I’m not thrilled with in this code is memory management. In Hare, libraries like this one are not supposed to allocate memory if they can get away with it, and if they must, they should do it as conservatively as possible. This implementation does a lot of its own allocations, which is unfortunate. I might refactor it in the future to avoid this. A more subtle issue here is the memory leaks on errors — each of the readslice/readstr functions allocates data for its return value, but if they return an error, the ? operator will return immediately without freeing them. This is a known problem with Hare’s language design, and while we have some ideas for addressing it, we have not completed any of them yet. This is one of a small number of goals for Hare which will likely require language changes prior to 1.0.

We have a little bit more code in net::ssh::agent, which you can check out if you like, but this covers most of it — time to move onto the daemon implementation.

Completing our SSH agent

The ssh-agent command in the hare-ssh tree is a simple (and non-production) implementation of an SSH agent based on this work. Let’s go over its code to see how this all comes together to make it work.

First, we set up a Unix socket, and somewhere to store our application state.

let running: bool = true;

type identity = struct {
	comment: str,
	privkey: ssh::key,
	pubkey: []u8,
};

type state = struct {
	identities: []identity,
};

export fn main() void = {
	let state = state { ... };
	const sockpath = "./socket";

	const listener = unix::listen(sockpath)!;
	defer {
		net::shutdown(listener);
		os::remove(sockpath)!;
	};
	os::chmod(sockpath, 0o700)!;
	log::printfln("Listening at {}", sockpath);

We also need a main loop, but we need to clean up that Unix socket when we terminate, so we’ll also set up some signal handlers.

	signal::handle(signal::SIGINT, &handle_signal);
	signal::handle(signal::SIGTERM, &handle_signal);

	for (running) {
		// ...stay tuned...
	};

	for (let i = 0z; i < len(state.identities); i += 1) {
		const ident = state.identities[i];
		ssh::key_finish(&ident.privkey);
		free(ident.pubkey);
		free(ident.comment);
	};

	log::printfln("Terminated.");
};

// ...elsewhere...
fn handle_signal(sig: int, info: *signal::siginfo, ucontext: *void) void = {
	running = false;
};

The actual clean-up is handled by our “defer” statement at the start of “main”. The semantics of signal handling on Unix are complex (and bad), and beyond the scope of this post, so hopefully you already grok them. Our stdlib provides docs, if you care to learn more, but also includes this warning:

Signal handling is stupidly complicated and easy to get wrong. The standard library makes little effort to help you deal with this. Consult your local man pages, particularly signal-safety(7) on Linux, and perhaps a local priest as well. We advise you to get out of the signal handler as soon as possible, for example via the “self-pipe trick”.

We also provide signalfds on platforms that support them (such as Linux), which is less fraught with issues. Good luck.

Next: the main loop. This code accepts new clients, prepares an agent for them, and hands them off to a second function:

		const client = match (net::accept(listener)) {
		case errors::interrupted =>
			continue;
		case let err: net::error =>
			log::fatalf("Error: accept: {}", net::strerror(err));
		case let fd: io::file =>
			yield fd;
		};
		const agent = agent::new(client);
		defer agent::agent_finish(&agent);
		run(&state, &agent);

This is a really simple event loop for a network daemon, and comes with one major limitation: no support for serving multiple clients connecting at once. If you’re curious what a more robust network daemon looks like in Hare, consult the Himitsu code.

The “run” function simply reads SSH agent commands and processes them, until the client disconnects.

fn run(state: *state, agent: *agent::agent) void = {
	for (true) {
		const msg = match (agent::readmsg(agent)) {
		case (io::EOF | agent::error) =>
			break;
		case void =>
			continue;
		case let msg: agent::message =>
			yield msg;
		};
		defer agent::message_finish(&msg);

		const res = match (msg) {
		case agent::request_identities =>
			yield handle_req_ident(state, agent);
		case let msg: agent::add_identity =>
			yield handle_add_ident(state, &msg, agent);
		case let msg: agent::sign_request =>
			yield handle_sign_request(state, &msg, agent);
		case agent::extension =>
			const answer: agent::message = agent::extension_failure;
			agent::writemsg(agent, &answer)!;
		case => abort();
		};
		match (res) {
		case void => yield;
		case agent::error => abort();
		};
	};
};

Again, this is non-production code, and, among other things, is missing good error handling. The handlers for each message are fairly straightforward, however. Here’s the handler for REQUEST_IDENTITIES:

fn handle_req_ident(
	state: *state,
	agent: *agent::agent,
) (void | agent::error) = {
	let idents: agent::identities_answer = [];
	defer free(idents);

	for (let i = 0z; i < len(state.identities); i += 1) {
		const ident = &state.identities[i];
		append(idents, agent::identity {
			pubkey = ident.pubkey,
			comment = ident.comment,
		});
	};

	const answer: agent::message = idents;
	agent::writemsg(agent, &answer)!;
};

The first one to do something interesting is ADD_IDENTITY, which allows the user to supply SSH private keys to the agent to work with:

fn handle_add_ident(
	state: *state,
	msg: *agent::add_identity,
	agent: *agent::agent,
) (void | agent::error) = {
	let sink = bufio::dynamic(io::mode::WRITE);
	ssh::encode_pubkey(&sink, &msg.key)!;
	append(state.identities, identity {
		comment = strings::dup(msg.comment),
		privkey = msg.key,
		pubkey = bufio::buffer(&sink),
	});
	const answer: agent::message = agent::agent_success;
	agent::writemsg(agent, &answer)?;
	log::printfln("Added key {}", msg.comment);
};

With these two messages, we can start to get the agent to do something relatively interesting: accepting and listing keys.

$ hare run cmd/ssh-agent/
[2022-05-09 17:39:12] Listening at ./socket
^Z[1]+  Stopped                    hare run cmd/ssh-agent/
$ bg
[1] hare run cmd/ssh-agent/
$ export SSH_AUTH_SOCK=./socket
$ ssh-add -l
The agent has no identities.
$ ssh-add ~/.ssh/id_ed25519
Enter passphrase for /home/sircmpwn/.ssh/id_ed25519: 
Identity added: /home/sircmpwn/.ssh/id_ed25519 (sircmpwn@homura)
2022-05-09 17:39:31] Added key sircmpwn@homura
$ ssh-add -l
256 SHA256:kPr5ZKTNE54TRHGSaanhcQYiJ56zSgcpKeLZw4/myEI sircmpwn@homura (ED25519)

With the last message handler, we can upgrade from something “interesting” to something “useful”:

fn handle_sign_request(
	state: *state,
	msg: *agent::sign_request,
	agent: *agent::agent,
) (void | agent::error) = {
	let key: nullable *identity = null;
	for (let i = 0z; i < len(state.identities); i += 1) {
		let ident = &state.identities[i];
		if (bytes::equal(ident.pubkey, msg.key)) {
			key = ident;
			break;
		};
	};
	const key = match (key) {
	case let key: *identity =>
		yield key;
	case null =>
		const answer: agent::message = agent::agent_failure;
		agent::writemsg(agent, &answer)?;
		return;
	};

	let buf = bufio::dynamic(io::mode::WRITE);
	defer io::close(&buf)!;
	ssh::sign(&buf, &key.privkey, msg.data)!;

	const answer: agent::message = agent::sign_response {
		signature = bufio::buffer(&buf),
	};
	agent::writemsg(agent, &answer)?;
	log::printfln("Signed challenge with key {}", key.comment);
};

For performance reasons, it may be better to use a hash map in a production Hare program (and, as many commenters will be sure to point out, Hare does not provide a built-in hash map or generics). We select the desired key with a linear search, sign the provided payload, and return the signature to the client. Finally, the big pay-off:

$ ssh git@git.sr.ht
[2022-05-09 17:41:42] Signed challenge with key sircmpwn@homura
PTY allocation request failed on channel 0
Hi sircmpwn! You've successfully authenticated, but I do not provide an interactive shell. Bye!
Connection to git.sr.ht closed.

Incorporating it into Himitsu

Himitsu was the motivation for all of this work, and I have yet to properly introduce it to the public. I will go into detail later, but in essence, Himitsu is a key-value store that stores some keys in plaintext and some keys encrypted, and acts as a more general form of a password manager. One of the things it can do (at least as of this week) is store your SSH private keys and act as an SSH agent, via a helper called himitsu-ssh. The user can import their private key from OpenSSH’s private key format via the “hissh-import” tool, and then the “hissh-agent” daemon provides agent functionality via the Himitsu key store.

The user can import their SSH key like so:

$ hissh-import < ~/.ssh/id_ed25519
Enter SSH key passphrase: 
key proto=ssh type=ssh-ed25519 pkey=pF7SljE25sVLdWvInO4gfqpJbbjxI6j+tIUcNWzVTHU= skey! comment=sircmpwn@homura

# Query the key store for keys matching proto=ssh:
$ hiq proto=ssh
proto=ssh type=ssh-ed25519 pkey=pF7SljE25sVLdWvInO4gfqpJbbjxI6j+tIUcNWzVTHU= skey! comment=sircmpwn@homura

Then, when running the agent:

(Yes, I know that the GUI has issues. I slapped it together in C in an afternoon and it needs a lot of work. Help wanted!)

Ta-da!

What’s next?

I accomplished my main goal, which was getting my SSH setup working with Himitsu. The next steps for expanding hare-ssh are:

  1. Expanding the supported key types and ciphers (RSA, DSA, etc), which first requires implementing the primitives in the standard library
  2. Implement the SSH connection protocol, which requires primitives like ECDH in the standard library. Some required primitives, like ChaCha, are already supported.
  3. Improve the design of the networking code. hare-ssh is one of a very small number of network-facing Hare libraries, and it’s treading new design ground here.

SSH is a relatively small target for a cryptography implementation to aim for. I’m looking forward to using it as a testbed for our cryptographic suite. If you’re interested in helping with any of these, please get in touch! If you’re curious about Hare in general, check out the language introduction to get started. Good luck!

Have a comment on one of my posts? Start a discussion in my public inbox by sending an email to ~sircmpwn/public-inbox@lists.sr.ht [mailing list etiquette]

Articles from blogs I read Generated by openring

Status update, May 2022

Hi all! This month’s status update will be shorter than usual, because I’ve taken some time off to visit Napoli. Discovering the city and the surrounding region was great! Of course the main reason to visit is to taste true Neapolitan pizza. I must admit, th…

via emersion May 24, 2022

What's cooking on SourceHut? May 2022

Hello everyone! We’re back at it for another month of news in the SourceHut sphere. Of our now 29,036 users, 630 are new this month: please offer them a warm welcome, and your patience, as they learn about the new platform. todo.sr.ht Comprehensive GraphQL-na…

via Blogs on Sourcehut May 16, 2022

Summary of changes for April

Hey everyone! This is the list of all the changes we've done to our projects and apps during the month of April. We'll also be reporting in our on position in the world, and on our future plans. Summary Of Changes Bicycle, released an Uxntal pla…

via Hundred Rabbits May 1, 2022