# SSH keys everywhere

*This blog post is an edited and expanded version of the lightning talk I gave last week at GPN21.*

As a reader of my blog, you probably know what SSH is. If not — the relevant part for this session is that it's a system allowing for secure, authenticated connections between two computers. It allows using passwords to log in, but the interesting part is when the user is logged in based on the provided key. Nearly every Linux hacker has a key pair which is used to log in to remote computers [citation needed].

Normies also enjoy secure systems, but those are based on a different technology. Connecting to web sites is secure and authenticated thanks to the layer called TLS. However, unlike with SSH, pretty much no one uses TLS keys to *log in* to web sites. Instead, the typical login process proceeds by making a TLS connection anonymously, and then a login mechanism built on HTTP or some other protocol (IMAP, XMPP, SMTP, IRC all have one) on top of TLS takes over.

Needless to say, using TLS to authenticate the server, and then a custom protocol to authenticate the client is more complicated than using TLS to authenticate both. SSH wins here by using the same mechanism (a public/private keypair) in both directions.

Another advantage of SSH is the one I mentioned before: many hackers already have a set of keys, and most understand how to use them.

However, there are not many libraries for creating custom services using the SSH protocol, compared to the ubiquitous TLS. What if I wanted to make a custom, secure, authenticated application, but avoid hacking around SSH's niche protocol?

## Would it be possible to use SSH keys in a TLS connection?

You'll be pleased to hear that the answer is **YES**.

I put some effort into building a proof-of-concept pair of programs (server and client) which communicate using a TLS-based connection secured and authenticated using existing SSH keys.

Why is that possible?

Both TLS and SSH protocols are based on a similar set of cryptographic primitives. TLS 1.3 certificates support three signature schemes: RSA keys, ECDSA NIST P-256, P-384, and P-512 elliptic curves, and the pair of elliptic curves called ED25519 and ED448 (which seem to be unsupported by most Web browsers). SSH public keys come in different flavors: dsa | ecdsa | ecdsa-sk | ed25519 | ed25519-sk | rsa. The ecdsa key, once generated, expands to ecdsa-sha2-nistp256.

As you can see, there is some overlap: rsa, ecdsa, ed25519.

If we manage to extract the key data from the SSH public/private key files, and turn them into TLS-compatible public/private key files, then we should be pretty much set!

Except that there is no "public key file" in TLS. The closest equivalent would be the certificate file. The certificate adds some extra data about the certifying authority on top of the key file. But that's fine. We can skip all that, and create a self-signed certificate. (While certificates also exist in SSH, most setups don't use them, so there's no need to bother with them in a proof of concept.)

Now we have a clear path to SSH key support in TLS connections!

## SSH security model

Remember known_hosts and authorized_keys? Those are not things used in most TLS-based applications. TLS applications are usually based on the Web PKI concept, where some entities are trusted by default by everyone to prove the identity of the servers. SSH is different: nothing is trusted by default. When you connect to a server, you get a choice to trust it to be who it promises to be or not. If you don't, the client will prevent the connection. Your choice gets stored in known_hosts. Similarly, the server administrator registers your key as allowed to log in by placing it in the authorized_key file. If you're not registered, the server will reject your connections.

By reusing existing SSH keys, we apply the SSH model to TLS, and throw away centralized certificate authorities.

## Implementation

The proof-of-concept application is simple: the client tries to connect, checks the server public key, presents its own, and if everything goes right, the connection gets accepted by both parties, and the server sends the client a single "hello".

$ cargo run --bin server -- -k foo 
    Finished dev [unoptimized + debuginfo] target(s) in 1.27s
     Running `target/debug/server -k foo`
listening on [::1]:4433
connection incoming
connection remote [::1]:43131, proto "<none>"
$ cargo run --bin client -- -i foo2 --known_hosts known
    Finished dev [unoptimized + debuginfo] target(s) in 0.22s
     Running `target/debug/client -i foo2 --known_hosts known`
connecting from [::]:43131

The only supported key type on both sides is ED25519. Make sure to create new keys for the client and the server: this is a proof of concept, full of cut corners. It's possible that some of the corners became a security hole!

ssh-keygen -t ed25519 -f my_special_key

This is what happens if a client with an unknown key tries to connect:

     Running `target/debug/server -k foo`
listening on [::1]:4433
connection incoming
Unknown pubkey with fingerprint: [F0, 34, FB, EA, 59, B, 2D, 40, BD, 37, C1, 24, B1, 7D, 4C, D7, E4, 49, E3, 86, 7, A8, A9, 23, 1B, 1A, 9B, 6A, 5B, C5, 74, D6], rejecting connection.
IF THIS KEY SHOULD GAIN ACCESS, add this entry to your authorized_keys file:
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOD833SxhcyNayIdwoZfNJ12PK7heXfl5iHHKJWU/okB
connection failed: the cryptographic handshake failed: error 40: invalid peer certificate contents: Public key unknown

### ssh-agent

The best way to ensure security of keys is to not have direct access to them. This is the job of ssh-agent: it loads a key, and signs everything you throw at it. The application never sees the key, which means it cannot leak it. This is perfect for applications written by someone so unskilled as me, so I implemented ssh-agent support in the client. I skipped it for the server, but it should be easy to do as well.

### ssh-keygen and openssl

Without those tools, I would not have gotten anywhere. The first one can make a PEM public key from a SSH private key, and the other can inspect created keys and certificates.

ssh-keygen -e -m pem -f my_private_key > my_pub.pem
openssl asn1parse -inform pem -in my_pub.pem
    0:d=0  hl=2 l=  89 cons: SEQUENCE          
    2:d=1  hl=2 l=  19 cons: SEQUENCE          
    4:d=2  hl=2 l=   7 prim: OBJECT            :id-ecPublicKey
   13:d=2  hl=2 l=   8 prim: OBJECT            :prime256v1
   23:d=1  hl=2 l=  66 prim: BIT STRING        

Those help verify that the keys converted inside the application are indeed those which openssh would convert itself.

### My eyes are bleeding

Have you seen the source code? Yes, I'm using 3 different ssh-related libraries. Each has parts which the two others don't, and which I needed to pull through. Also, the project came up because I was trying to learn QUIC, so it's made more convoluted because of that. As you can see, it's possible to write garbage, duplicates, unstructured code in Rust as well. I swear I will fix some of those things once I come back to the project.

Contributions welcome ;)

## Lost SSH goodies

The SSH network protocol is not just security and key exchange. There are also things like channels and some awareness of X11/port forwarding specified in the RFC. Most standalone applications would not feel their lack after switching to TLS, but what about those which would? Those who open multiple channels and forward data around?

TLS can run over TCP which is single-stream-at-a-time, but I took another approach. QUIC incorporates TLS, and also [supports streams[(https://www.rfc-editor.org/rfc/rfc9000.html#name-streams) natively. Best of both worlds!

That being said, there exists an expired draft for SSH over QUIC already. According to my reading, it does not reuse SSH keys to establish the TLS layer, so in the end, it encrypts everything twice: once on the TLS, and once on the SSH layer (please correct me if I'm wrong). Not great for my server which is already limited by CPU, rather than network throughput.

There's also an implementation of SSH over QUIC, but it looks more like a simple proxy, than anything more involved. Twice encrypted again.

## Future

Quishka, the project I wrote, is intended to become a library supporting arbitrary applications. If you decide that it's worth exploring the idea with me, get in touch!

With QUIC, there's few remaining reasons to stick to the vanilla SSH protocol, and I would definitely want to see the official server support it. Multiple nonblocking streams would make multiple file transfers in sshfs so much smoother, for example. I would be totally stoked if this toy project revived the idea!

Written on .


dcz's projects

Thoughts on software and society.

Atom feed