💾 Archived View for rawtext.club › ~sloum › geminilist › 000457.gmi captured on 2020-09-24 at 02:33:26. Gemini links have been rewritten to link to archived content
View Raw
More Information
-=-=-=-=-=-=-
<-- back to the mailing list
Regarding `gemini://` over NaCL (replacing TLS)
Ciprian Dorin Craciun ciprian.craciun at gmail.com
Sun Mar 1 01:31:47 GMT 2020
- - - - - - - - - - - - - - - - - - - ```
So I've taken Sean Conner advice and implemented a proof-of-conceptclient and server (only the protocol, transport and crypto part, notthe actual file serving) in Python by replacing TLS with NaCL /`libsodium`.
The code is available on GitHub:
https://github.com/cipriancraciun/gemini-experiments/blob/e4bbeae01a8e7d2e393ab93317890f5c7f511b09/nacl/sources
The sources are structured as such:
- `protocol_v1.py` builds upon `transport.py`, and actually implementsthe `gemini://` protocol; no surprises here, the `protocol_client`function (that takes the server's address, an optional public key(used for NaCL signatures), and a selector) interacts with the serverand returns the body; similarly the `protocol_server` function takesa listening socket and a handler function (that given a selectorreturns the status, meta and body) and interacts with exactly oneclient;
- `transport.py` builds upon `crypto.py` and `packets.py`; more on this later;
- `crypto.py` wraps various NaCL functions and gives a better idea ofwhat happens; again more on this later;
- `packets.py` basically just handles sockets and reading / writingfull payloads; it also adds support for framed send / receive bywriting a 4 bytes length, followed by a payload of that many bytes (awell known pattern); this basically replaces TLS segments and removesthe need of line splitting, as now each individual protocol item(selector, status and meta, body, and the key exchanges) uses onesingle frame;
- the code is not "pythonic" (whatever that means), uses no globalstate, and relies exclusively on functions that take all requiredinputs as arguments; (this way the functionality of one function canbe assessed only in terms of its inputs and outputs, thus I hopesimplifying the understanding;) (one can think of it as almost purelyfunctional, with the exception of nonces and sockets which are bothmutated by successive calls to the various transport functions;)
Now regarding the transport / crypto and how it replaces TLS:
- as said, the transport module uses exclusively the framed (4 bytelength prefixed) packets, so from now on "send" and "receive" impliessuch frames;
- the bulk of NaCL implementation happens inside `transport_prepare`(https://github.com/cipriancraciun/gemini-experiments/blob/e4bbeae01a8e7d2e393ab93317890f5c7f511b09/nacl/sources/transport.py#L40-L94)which does as follows;
- both the client and server use this function, thus from now on thetransport protocol is identical, thus symmetrical;
- by using `crypto_kx_keypair`(https://libsodium.gitbook.io/doc/key_exchange) the local peer createsan ephemeral session public / private key pair, which it exchangeswith its remote peer; (this is the only piece of information thatdoesn't travel encrypted, and thus available to an attacker;)
- the by using `crypto_kx_client_session_keys` (from the same link asabove) the local peer creates two symmetric secret keys, one forsending and one for receiving; (by now the remote peer has done thesame and obtained exactly the same symmetric secret keys;)
- also the local peer, using its own session public key (known to theremote peer, and also known by an attacker) it creates (in adeterministic manner, thus again known by an attacker) a nonce forreceived packets; using its remote peer session public key (againknown to an attacker) it creates a nonce for sent packets; each timeone of these nonces are used (for sending and receiving), itsrespective value is incremented by 1; (according to the cryptographicproperties described at that page, having known nonces to the attackeris OK, the only requirement is not to reuse them;)
- from here on, all exchanged messages are encrypted by using`crypto_secretbox`(https://libsodium.gitbook.io/doc/secret-key_cryptography/secretbox)which provides both encryption and authentication against tampering;(and coupled with the unique incremental nonces, it also assures thatthe messages haven't been replayed, reordered, or dropped;)
- by using `crypto_sign_keypair`(https://libsodium.gitbook.io/doc/public-key_cryptography/public-key_signatures)the local peer creates a signature public / private key pair used forauthentication; (both the client and server can use a stored publickey, or generated on the fly if `None` is used);
- by using `crypto_sign_detached` (the same link), the local peersigns with its own signature private key the remote peer's sessionpublic key; the local peer then exchanges with the remote peer itsown signature public key and the computed signature; (this provesthat the local peer has control over the signature secret key, andalso that this is not a replay attack as it signs something under thecontrol of the remote peer;)
- then, after receiving the remote peer's signature public key and thedetached signature it verifies it accordingly;
- at this moment the session is considered open, and the peers haveauthenticated themselves to one-another;
A few notes about this encryption scheme:
- I am not a cryptographer or security expert, however I think I'vefollowed the best practices, especially those described in(https://libsodium.gitbook.io/doc/secret-key_cryptography/encrypted-messages); (I bet an experimented cryptographer can take a look at this and sayif there are any issues, and how to fix those;)
- at the moment there is no padding, and give the way NaCL encryptsdata (i.e. the output has the same length as the input plus a fixedconstant), the length of the selector and header can be determined byan attacker by looking at the network traffic; adding a padding toall packets multiple of say 128 bytes, and perhaps randomly adding oneto four such 128 bytes groups, would make these length unguessablewhile impacting very little the network performance;
- at the moment two consecutive packets (like for example thesignature public key and signature verifier) are sent as different`socket.send` calls, and thus perhaps as two different actual TCPsegments; but a more efficient implementation can coalesce these intoa single `socket.send`;
- the overhead of the proposed protocol is minimal, both in terms ofpayload size and latency due to round-trips (remember the protocol issymmetric, thus this applies both to the client and server): * 32+4 bytes for the session public key exchange; * (both peers must read what was sent above before continuing, thusone half-trip); * 32+16+4 bytes for the signature public key exchange; * 64+16+4 bytes for the signature verifier; * (both peers must read what was sent above before continuing, thusanother half-trip); * (so far a single complete round-trip was made;) * for each additional message an extra 16+4 bytes are sent; (16bytes for the `secretbox` authentication, and the 4 bytes for theframing;)
- this schema is compliant with TCP FastOpen(https://en.wikipedia.org/wiki/TCP_Fast_Open), i.e. the session publickey exchange can be done at the same time as the TCPthree-way-handshake happens;
- given the 4 bytes framing, the selector, headers and body can't belarger than 4 GiB; this is not a limitation, as we can implement forthe body an 8 bytes length prefixed framing;
- also given the 4 bytes framing, we solve the `Content-Length` issue,and also the possibility of keep-alive;
- another advantage of the 4 bytes framing is that we can reuse theframes instead of parsing for `CRLF`;
- at the moment TLS-SNI like functionality can be easily implementedby asking the client to send with its signature public key also the"virtual host" it wants to communicate with, thus based on that theserver can respond with that proper public key (this doesn't affectthe packet exchanges); as opposed to TLS-SNI feature, this proposaldoes not expose to an attacker the identity of the server public key(i.e. the virtual host); (granted there is a proposed extension toTLS-SNI that encrypts that information;)
- the protocol should definitively require a version format (both forthe transport but also for the `gemini://` semantic) that should besent first by the client, and based on that the server should choosethe proper implementation if supported; (however such a feature hasto be carefully implemented as not to be used by attackers todowngrade a certain client into a vulnerable version;)
I hope I haven't made too many mistakes, and I hope this is useful asa proof-of-concept that one could replace TLS for such simplerprotocols,Ciprian.