Implementing SecretHandshake secure-connection support in C++

53 views
Skip to first unread message

Jens Alfke

unread,
Dec 9, 2021, 1:32:22 PM12/9/21
to Cap'n Proto
I'm starting to implement support for SecretHandshake, a "secure-channel based on a a mutually authenticating key agreement handshake, with forward secure identity metadata". shs1-c implements the crypto part, resulting in a pair of symmetric stream-cipher keys; beyond that I'm going to copy and paste and hack the C++ Cap'n Proto TLS code, despite being a total newbie at kj.

Basically all I need to do is create a Cap'n Proto RPC connection that splices into the TCP I/O and initially does a couple of data exchanges via shs1-c, then filters the data streams through the ciphers.

I'm writing in case anyone has knowledge about the kj side of this that they'd like to share.

I'll reply here once I've got this working, and I plan to release the code as open source.

--Jens

Kenton Varda

unread,
Dec 10, 2021, 2:09:50 PM12/10/21
to Jens Alfke, Cap'n Proto
Sounds cool!

Note that the KJ TLS implementation is pretty weird largely to work with OpenSSL's API being weird. Hopefully the library you're using has a nicer API and so the implementation will be easier...

-Kenton

--
You received this message because you are subscribed to the Google Groups "Cap'n Proto" group.
To unsubscribe from this group and stop receiving emails from it, send an email to capnproto+...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/capnproto/fe6b6564-3f08-478e-af5c-2bf461ea0e81n%40googlegroups.com.

Jens Alfke

unread,
Dec 11, 2021, 9:09:23 PM12/11/21
to Kenton Varda, Cap'n Proto
I just got it working! Once I got the code finished and compiling, there were just a few runtime issues before it succeeded.

The protocol is pretty simple — each side sends a challenge, receives the other’s challenge, then sends a response; the result is two shared symmetric keys for a stream cipher. So it wasn’t hard to splice into an AsyncStream.

I hacked at the TLS code and EzRpc. The result isn’t pretty and could probably have been done in half the code if I knew what I was doing, but at least it works. I’ll try to factor it out and publish it as a Git repo, then perhaps experts can suggest how to clean it up.

—Jens

Jens Alfke

unread,
Dec 14, 2021, 3:05:21 PM12/14/21
to Cap'n Proto
After some more work and cleanup, I’ve published my source code at

Suggestions and fixes gratefully accepted, especially since much of the code is taken from Cap’n Proto itself and was “adapted for radio by putting it on a board and banging a few nails through it”, as Monty Python put it.

Here’s the gist of the README:

SecretHandshake For Cap’n Proto

C++ implementation of the SecretHandshake protocol for the awesome Cap’n Proto RPC library. This lets you upgrade your network connections with encryption and mutual authentication, without all the overhead of OpenSSL.

(You don’t actually need Cap’n Proto to use this, but if so you’ll need to provide your own networking code.)

About SecretHandshake

SecretHandshake is “a mutually authenticating key agreement handshake, with forward secure identity metadata.” It was designed by Dominic Tarr and is used in the Secure Scuttlebutt P2P social network.

It’s based on 256-bit elliptic Ed25519 key-pairs. The peers each maintain a long-term key pair, whose public key serves as a global identifier. The peer making the connection (“client”) must know the public key of the other peer (“server”) to be able to connect, and the server learns the client’s public key during the handshake. Each peer receives proof that the other has the matching private key. Much more detail is available in the design paper.

The handshake also produces two session keys, which are then used to encrypt the channel with the 256-bit symmetric XSalsa20 cipher. (This is not strictly speaking part of the SecretHandshake protocol, which ends after key agreement. Scuttlebutt uses a different encryption scheme based on libSodium’s “secret box”.)

Kenton Varda

unread,
Jan 12, 2022, 12:22:44 PM1/12/22
to Jens Alfke, Cap'n Proto
Sorry for the long delay in replying, I had a baby the day you sent this!

This is neat! How many round trips are needed to set up a connection?

When Cap'n Proto gets three-party handoff support, I'm hoping we can do 0-RTT encrypted session setup after introductions. Not many protocols seem to consider this use case though.

On Tue, Dec 14, 2021 at 2:05 PM Jens Alfke <je...@mooseyard.com> wrote:

The handshake also produces two session keys, which are then used to encrypt the channel with the 256-bit symmetric XSalsa20 cipher. (This is not strictly speaking part of the SecretHandshake protocol, which ends after key agreement. Scuttlebutt uses a different encryption scheme based on libSodium’s “secret box”.)

Hmm if you're using a plain xsalsa20 stream and not secret boxes, does that mean you're implementing only encryption, not authentication? Note that XSalsa20 and related ciphers work by generating a random stream, and then XORing it with the plaintext. So although the attacker can't decrypt the bytes, they can flip individual bits in the ciphertext and this will result in the same bit being flipped in the plaintext. Secret boxes add a MAC to each block which allows the receiver to verify that the bits haven't been tampered with.

-Kenton

Jens Alfke

unread,
Jan 12, 2022, 1:01:31 PM1/12/22
to Kenton Varda, Cap'n Proto


> On Jan 12, 2022, at 9:22 AM, Kenton Varda <ken...@cloudflare.com> wrote:
>
> Sorry for the long delay in replying, I had a baby the day you sent this!

Congratulations! 🎉 I remember those days...

> This is neat! How many round trips are needed to set up a connection?

There are four messages: C->S, S->C, C->S, S->C. In the current implementation they happen one at a time, so I guess that’s two round trips? But it looks as though messages 1 & 2 can be sent concurrently, and also 3 & 4.

I dimly recall that there are some additional TCP hacks one can do, to piggyback a small payload on the initial packets that open the connection, but I don’t feel like cracking open TCP/IP Illustrated right now :-p

> Hmm if you're using a plain xsalsa20 stream and not secret boxes, does that mean you're implementing only encryption, not authentication? Note that XSalsa20 and related ciphers work by generating a random stream, and then XORing it with the plaintext. So although the attacker can't decrypt the bytes, they can flip individual bits in the ciphertext and this will result in the same bit being flipped in the plaintext.

Yeah, there are no integrity checks in the data stream, and I agree that’s a weakness*. Adding MACs requires adding a block- or message-oriented layer on top, like SecretBox, the way that Scuttlebutt does. This feels like redundant effort since Cap’nP also is itself message-oriented; my guess is that there’s a higher level API inside Cap’nP that exposes the message framing, and the MAC could be added there, but I have not yet delved deeper into the way Cap’nP works. (Hints welcome.)

(Or if there’s a clever stream-based way to insert MACs without having to build a framing layer, that would be even better. I’ve read my share of crypto textbooks, maybe I’ve just forgotten that bit.)

—Jens

* I should probably call this out in the readme, shouldn’t I.

Kenton Varda

unread,
Jan 12, 2022, 4:55:47 PM1/12/22
to Jens Alfke, Cap'n Proto
On Wed, Jan 12, 2022 at 12:01 PM Jens Alfke <je...@mooseyard.com> wrote:
Yeah, there are no integrity checks in the data stream, and I agree that’s a weakness*. Adding MACs requires adding a block- or message-oriented layer on top, like SecretBox, the way that Scuttlebutt does. This feels like redundant effort since Cap’nP also is itself message-oriented; my guess is that there’s a higher level API inside Cap’nP that exposes the message framing, and the MAC could be added there, but I have not yet delved deeper into the way Cap’nP works. (Hints welcome.)

You might want to look at the `capnp::MessageStream` abstraction, instead of `kj::AsyncIoStream`. It lets you see whole messages, which makes it easier to customize the framing.

-Kenton

Jens Alfke

unread,
Jan 12, 2022, 7:59:32 PM1/12/22
to Kenton Varda, Cap'n Proto

Hmm if you're using a plain xsalsa20 stream and not secret boxes, does that mean you're implementing only encryption, not authentication? Note that XSalsa20 and related ciphers work by generating a random stream, and then XORing it with the plaintext.

FYI: It turns out that my stream-encryption code is totally broken anyway. I naively believed that Sodium’s `crypto_stream_xor` implemented a stream cipher, as the name implies — but it doesn’t. The key and nonce parameters are both const, so it’s stateless, and just xor’s the buffer with the same bit-stream every time it’s called.

I am not a cryptographer, but I find this baffling and pointless. Why call this a “stream cipher” when the API only allows you to encrypt a single (variable-size) block of data?

Looks like I’m forced to implement a chunk-based protocol after all. Good news is it’ll be tamper-proof.

—Jens

Kenton Varda

unread,
Jan 13, 2022, 12:31:46 PM1/13/22
to Jens Alfke, Cap'n Proto
It looks like the chacha20 functions have variants with an "ic" parameter, which lets you specify the block counter, but the salsa20 functions don't have this for some reason.

-Kenton

Jens Alfke

unread,
Jan 13, 2022, 1:19:14 PM1/13/22
to Kenton Varda, Cap'n Proto


> On Jan 13, 2022, at 9:31 AM, Kenton Varda <ken...@cloudflare.com> wrote:
>
> It looks like the chacha20 functions have variants with an "ic" parameter, which lets you specify the block counter, but the salsa20 functions don't have this for some reason.

A block counter would still require dividing the stream into blocks.
Fixed-size blocks won’t work because the codec will stall until a block is completed, which would deadlock most interactive protocols.
Variable-size blocks depend on the byte counts passed to the writer, which then means writing the block size into the output, and assembling a block on the read side. This turns out to be just as much work as using the higher level APIs like crypto_secretstream_xchacha20poly1305, or for that matter crypto_secretbox, both of which authenticate; so might as well just use them. (As does Scuttlebutt.)

Bizarrely, there appears to be no actual streaming API where your data gets encrypted with successive portions of the infinite cipher stream. This is further confirmation of my belief that cryptographers should never be allowed to design APIs.

So. Current plan is to write a stream wrapper around crypto_secretbox. This involves annoying stuff like buffering data, but it’s not rocket science.

—Jens

Kenton Varda

unread,
Jan 13, 2022, 2:01:09 PM1/13/22
to Jens Alfke, Cap'n Proto
No no, this is "block" in the cryptography sense. It's the unit of computation of the cipher. For salsa20 and chacha20, the block size is 512 bits. You can still start the stream at an arbitrary byte by rounding to the closest block boundary and then throwing away the extra bytes. (You'd have to do the XOR'ing manually though, to start at the specific byte you want.)

But I definitely agree it'd be better to use boxes with authentication.

-Kenton
Reply all
Reply to author
Forward
0 new messages