commit: 04d97d9e89926ee785aae78a0e433f2867c103b9
parent b3b5e839121d2389236f085272c37a43c72888b3
Author: Drew DeVault <sir@cmpwn.com>
Date: Mon, 9 May 2022 18:26:04 +0200
hare-ssh
Diffstat:
2 files changed, 941 insertions(+), 4 deletions(-)
diff --git a/.build.yml b/.build.yml
@@ -5,7 +5,6 @@ packages:
sources:
- https://git.sr.ht/~sircmpwn/drewdevault.com
- https://git.sr.ht/~sircmpwn/openring
- - https://git.sr.ht/~sircmpwn/hugo
secrets:
- 160a72cf-34d6-47b7-928b-c13b42b4d4f6
triggers:
@@ -14,9 +13,9 @@ triggers:
to: Drew DeVault <sir@cmpwn.com>
tasks:
- hugo: |
- cd hugo
- go build --tags extended
- sudo cp hugo /usr/local/bin/
+ curl -O https://mirror.drewdevault.com/hugo
+ chmod +x hugo
+ sudo mv hugo /usr/local/bin/
- openring: |
cd openring
go build -o openring
diff --git a/content/blog/hare-ssh.md b/content/blog/hare-ssh.md
@@ -0,0 +1,938 @@
+---
+title: Implementing an SSH agent in Hare
+author: Drew DeVault
+date: 2022-05-09
+---
+
+*Cross-posted from [the Hare blog](https://harelang.org/blog/2022-05-09-hare-ssh/)*
+
+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!
+
+[Himitsu]: https://sr.ht/~sircmpwn/himitsu
+[hare-ssh]: https://sr.ht/~sircmpwn/hare-ssh
+
+**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.
+
+[PEM]: https://docs.harelang.org/encoding/pem
+[RFC 7468]: https://www.rfc-editor.org/rfc/rfc7468
+[pem::newdecoder]: https://docs.harelang.org/encoding/pem#newdecoder
+[pem::next]: https://docs.harelang.org/encoding/pem#next
+
+Based on this, we can examine the contents of this key with a simple program.
+
+```hare
+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:
+
+- Cipher name (aes256-ctr)
+- KDF name (bcrypt)
+- KDF data
+- Public key data
+- Private key data (plus padding)
+
+We parse this information like so:
+
+```hare
+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.
+
+[bcrypt]: https://en.wikipedia.org/wiki/Bcrypt
+[key derivation function]: https://en.wikipedia.org/wiki/Key_derivation_function
+[AES 256]: https://en.wikipedia.org/wiki/Advanced_Encryption_Standard
+[CTR mode]: https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Counter_(CTR)
+
+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.
+
+[AES]: https://docs.harelang.org/crypto/aes
+[CTR]: https://docs.harelang.org/crypto/cipher#ctr
+
+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](https://docs.harelang.org/crypto/blowfish) and
+[implementation](https://git.sr.ht/~sircmpwn/hare/tree/master/crypto/blowfish)
+for your consideration. I also implemented the standard bcrypt hash at
+[crypto::bcrypt], whose implementation is
+[here](https://git.sr.ht/~sircmpwn/hare/tree/master/crypto/bcrypt) (for now).
+This isn't especially relevant for us, however, since OpenSSH uses a modified
+form of bcrypt as a key derivation function.
+
+[Blowfish]: https://en.wikipedia.org/wiki/Blowfish_(cipher)
+[crypto::bcrypt]: https://docs.harelang.org/crypto/bcrypt
+
+The [implementation](https://git.sr.ht/~sircmpwn/hare-ssh/tree/master/item/format/ssh/bcrypt.ha)
+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:
+
+```hare
+// 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:
+
+```hare
+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][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][bufio::fixed] 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.
+
+[ciphers]: https://git.sr.ht/~sircmpwn/hare-ssh/tree/master/item/format/ssh/cipher.ha
+[bufio::fixed]: https://docs.harelang.org/bufio#fixed
+
+We may then use these parameters to decrypt the private key area.
+
+```hare
+ 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:
+
+```hare
+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:
+
+- Key type ("ssh-ed25519" in this case)
+- Public key (in a format specific to each key type)
+- Private key (in a format specific to each key type)
+- Comment
+- Padding up to the cipher's block size (16)
+
+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:
+
+```hare
+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.
+
+[RFC 8709]: https://datatracker.ietf.org/doc/html/rfc8709
+
+```hare
+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.
+
+[draft-miller-ssh-agent-01]: https://tools.ietf.org/id/draft-miller-ssh-agent-01.html
+
+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.
+
+[types.ha]: https://git.sr.ht/~sircmpwn/hare-ssh/tree/master/item/net/ssh/agent/types.ha
+
+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.
+
+```hare
+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:
+
+```hare
+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`:
+
+```hare
+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](https://git.sr.ht/~sircmpwn/hare-ssh/tree/master/item/net/ssh/agent/agent.ha)
+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.
+
+[ssh-agent]: https://git.sr.ht/~sircmpwn/hare-ssh/tree/master/item/cmd/ssh-agent/main.ha
+
+First, we set up a Unix socket, and somewhere to store our application state.
+
+```hare
+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.
+
+```hare
+ 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](https://docs.harelang.org/unix/signal), 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:
+
+```hare
+ 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.
+
+[Himitsu]: https://git.sr.ht/~sircmpwn/himitsu/tree/master/item/cmd/himitsud/socket.ha
+
+The "run" function simply reads SSH agent commands and processes them, until the
+client disconnects.
+
+```hare
+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`:
+
+```hare
+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:
+
+```hare
+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":
+
+```hare
+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.
+
+[himitsu-ssh]: https://git.sr.ht/~sircmpwn/himitsu-ssh
+
+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:
+
+<video src="https://mirror.drewdevault.com/hissh-agent.webm" controls muted></video>
+
+(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!][hiprompt-gtk])
+
+[hiprompt-gtk]: https://git.sr.ht/~sircmpwn/hiprompt-gtk
+
+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](/community)! If you're curious about Hare in general, check out the
+[language introduction][intro] to get started. Good luck!
+
+[intro]: https://harelang.org/tutorials/introduction/