├── .gitignore ├── IMPLEMENTATION.md ├── LICENSE ├── Makefile ├── PROTOCOL.md ├── README.md ├── rebar.config ├── rebar.lock ├── src ├── Makefile ├── curve_tun.app.src ├── curve_tun.erl ├── curve_tun_app.erl ├── curve_tun_connection.erl ├── curve_tun_connection_sup.erl ├── curve_tun_cookie.erl ├── curve_tun_simple_registry.erl ├── curve_tun_sup.erl └── curve_tun_vault_dummy.erl └── test └── curve_tun_SUITE.erl /.gitignore: -------------------------------------------------------------------------------- 1 | .rebar3 2 | _* 3 | .eunit 4 | *.o 5 | *.beam 6 | *.plt 7 | *.swp 8 | *.swo 9 | .erlang.cookie 10 | ebin 11 | log 12 | erl_crash.dump 13 | .rebar 14 | _rel 15 | _deps 16 | _plugins 17 | _tdeps 18 | logs 19 | -------------------------------------------------------------------------------- /IMPLEMENTATION.md: -------------------------------------------------------------------------------- 1 | # Implementation details 2 | 3 | The Erlang code uses the `enacl` library for all its low-level cryptographic work. These are bindings to the NaCl/libsodium libraries. On top of this, the code implements a high-level packet-oriented protocol which uses the cryptographic primitives to secure the socket connection on the wire. See the document PROTOCOL.md for the protocol description and its security properties. 4 | 5 | This document describes the construction of the Erlang system. 6 | 7 | # General considerations 8 | 9 | Currently, our system does a miserable job at throwing out old key data. In particular, if an attacker can gain memory access to a system, he may be able to grab even very old key data and obtain it. We plan on creating more safe vaults with overwriting capability in the future, but for now, we use the Erlang subsystem for ease of use. 10 | 11 | # Specific processes 12 | 13 | ## Vault 14 | 15 | A clients key is kept in a specialized process called the *vault*. This process is the only one who stores the secret key of the system. No provision has been made to protect this process against scrutiny from the shell, so if you have shell-access to the Erlang system, the secret key is known. 16 | 17 | The design, however, is such that you can hide the secret key in various ways in your system. Either by implementing a hidden C node, or by hiding the key material behind a NIF, or something like that. By keeping the *vault* separate from the rest of the code, and by doing all secret-key cryptographic work in the vault, we make sure we can store the key differently later on. A good candidate is a hardware security module for instance. 18 | 19 | The vault can box and open boxes pertaining to the secret key of `curve_tun`. While doing so, it also constructs long-term Nonce's according to the PROTOCOL.md specification. It always returns the box and the used Nonce to make it easy to incorporate into packets. 20 | 21 | The vault tracks: 22 | 23 | * Public/Secret key pairs of long-term keys. 24 | * Counters for the long-term keys for nonce generation. 25 | * The nonce block-key for scrambling the counter so it doesn't leak out. 26 | 27 | ## Cookie key process 28 | 29 | A separate process maintain a cookie key generated at random. The key is cycled once in a minute, but older keys are kept for a minute. This key is used to decrypt cookies when they come in from the client. If the key has been cycled in the meantime, the older keys are tried. 30 | 31 | The brilliance of the protocol is that cookies contain all information to construct the connection. So if used over a datagram protocol like UDP, the cookie acts like a SYN-cookie of TCP but encrypted. The client bears the burden of establishing the connection. 32 | 33 | The security implications are that once a key is recycled, it is gone. This means that while the server needs protection, it doesn't track keys long-term and thus is less of a problem. 34 | 35 | ## The registry 36 | 37 | There is a process which handles the peer registry. This is akin to the file `$HOME/.ssh/known_hosts` in the SSH system. It maps from IP addresses into Public keys for those addresses. In a future variant of the system, it is possible to use different kinds of registries. For instance a registry mapping into DNS and thus binding the keys into the infrastructure. 38 | 39 | The registry must be protected against forgery on the endpoint. But apart from that, there are few security considerations. The registry only contains public keys of other clients, and possessing these should not damage anyone. 40 | 41 | ## Connection 42 | 43 | Connection processes implement an FSM which runs the connection system. There are the following two possible transition chains: 44 | 45 | server: ready -> accepting -> connected 46 | client: ready -> initiating -> connected 47 | 48 | for servers and clients respectively. Furthermore, there is a 'closed' state we can transition to from any state if the TCP connection is shut down. 49 | 50 | The process implements something looks a *lot* like a gen_tcp connection, but it does differ in that it provides a message-oriented interface over the socket. Future implementations might reinvigorate the stream nature on top of this protocol. 51 | 52 | Multiple receivers are processed in FIFO order of their call to the `recv/1` function. 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Jesper Louis Andersen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REBAR=rebar3 2 | 3 | .DEFAULT_GOAL: compile 4 | 5 | compile: 6 | $(REBAR) compile 7 | 8 | ct: 9 | $(REBAR) ct 10 | 11 | test: 12 | $(REBAR) ct 13 | -------------------------------------------------------------------------------- /PROTOCOL.md: -------------------------------------------------------------------------------- 1 | # Protocol Specification of CurveTun 2 | 3 | Current version 1.0: 4 | 5 | The purpose of `curve_tun` is to provide a secure socket communication channel over TCP. That is, the goal we are trying to fulfill is the same as the `ssl` application, but without using the complexity of `ssl`. The approach we take is to leverage the work in `CurveCP` by Dan J. Bernstein in order to provide a secure channel over TCP. Another important inspiration are OTR and its ratchet construction for forward secrecy. 6 | 7 | This document describes the protocol specification itself. It is split into two parts. The first part describes the high-level cryptographic construction in the protocol which gives enough information to validate the protocol design itself from a cryptographic perspective. Next follows the actual data contents which describes further low level handling, which is not important for a cryptographic perspective. The specification and protocol is kept Erlang-agnostic, so it can be implemented easily by other languages. In particular, we have opted for a protocol that is easy to parse with a binary parser, and we have tried hard to eliminate any kind of parsing ambiguity, as this usually means fewer venues for errors. 8 | 9 | Everywhere in this document, we use Erlang notation for the binary specification on the wire. The Erlang notation is succinct and precise, while providing an isomorphic description of what is on the wire, and how Erlang will be constructing/parsing the data. 10 | 11 | # Deviations from TCP 12 | 13 | This protocol is *NOT* a stream protocol. It works as a messaging protocol, where parties exchange messages between two endpoints. That is, a message M of K bytes sent over the connection is guaranteed to arrive in one piece M of K bytes in the other end. This choice is deliberate. While it removes the ability to use `curve_tun` as a replacement for TCP in the first place, it is usually a far better kind of messaging construction for Erlang programs. 14 | 15 | Later versions of the protocol may define a `stream` option which can reinstate the stream-oriented messaging if we so please, on top of the underlying cryptographic messaging system. 16 | 17 | # Bucket list 18 | 19 | There are a number of things I'd like to address at some point in the protocol: 20 | 21 | * The ephemeral keys are in-memory for long-running connections. If we have a BGP connection, it will last for days. This runs the problem that the ephemeral key never ever changes which means we keep the key material around for a long time. This is a security problem. A future version of the protocol will ratchet the key material forward in order to cripple these attacks. 22 | * I would like to implement ideas of Axolotl into the protocol. This provides an excellent way to ratchet the key material while also protecting keys. 23 | * Handle processing of large messages (i.e., messages that can't fit into a single packet). 24 | 25 | # Protocol overview 26 | 27 | The communication protocol proceeds, by first handshaking the connection and setting up the cryptographic channel. Then it exchanges messages on the channel. The handshake initializes a second ephermeral key-set in order to achieve forward secrecy. 28 | 29 | A keypair is defined as `(K, Ks)` where `K` is the public part and `Ks` is the secret part. Everywhere, a key ending in the "s" character designate a secret key. We define the notation `Box[X](Cs -> S)` to mean a secure *box* primitive which *encrypts* and *authenticates* the message `X`from a client to a server. The client uses `Cs` to sign the message and uses `S` to encrypt the message destined for the server. For secret-key cryptography we define `SecretBox[X](Ks)` as a secret box encrypted (and authenticated) by the (secret) key `Ks`. 30 | 31 | Our implementation uses the `crypto_box` primitive of NaCl/libsodium to implement `Box[…](K1s -> K2)` and uses `crypto_secretbox` to implement `SecretBox[…](KS)`. 32 | 33 | Throughout the description, we assume a keypair `(C, Cs)` for the client and `(S, Ss)` for the server. We also use ephermeral keys for the client, `(EC, ECs)` and for the server, `(ES, ESs)`. The protocol also uses *nonces* in quite a few places and their generation are described below. First the general communication. Details follow. 34 | 35 | It assumed the client already have access to the public key of the server, `S` and that the server already has access to the clients public key `C`. 36 | 37 | | Client | Server | 38 | |---------|------------| 39 | | 1. Generate `(EC, ECs)` | | 40 | | 2. Hello: send `(EC, Box[0'](ECs -> S))` | | 41 | | | 3. Generate `(ES, ESs)` | 42 | | | 4. Cookie ack: send `Box[ES, K](Ss -> EC)` | 43 | | 5. Vouch: send `(K, Box[C,V](EC -> ES))` | | 44 | | *bi-directional flow from here on out* | | 45 | | 6. Msg: send `Box[…](ECs -> ES)` | | 46 | | | 7. Msg: send `Box[…](ESs -> EC)` | 47 | 48 | 1. The client generates a new keypair. This keypair is ephemeral for the lifetime of the connection. Once the connection dies, the secret key of this connection is thrown away and since it never leaves the client, it means that nobody is able to understand messages on the connection from then on. This construction provides forward secrecy for the client. 49 | 50 | 2. The client advertises the ephemeral public key and boxes a set zero-values. 51 | 3. The server generates a keypair. This is also ephemeral, but on the server side. It provides forward secrecy for the server-end. 52 | 4. The server generates a cookie `K = SecretBox[EC,ESs](Ts)` where `Ts` is a secret minute key only the server knows. In other words, this is a cryptographic box which can only be understood by the holder of `Ts`. In principle, this protocol doesn't really need these kind of SYN-cookies, but it does protect the protocol against an eventual weakness in TCP and also it makes it easier to adapt the code base to CurveCP later if we want to do that. So it is kept in this protocol. The cookie doesn't need storage on the server side, which means it can't be flooded. The key `Ts` changes from time to time. 53 | 5. The client reflects the cookie and *vouches* for its key. Here `V = Box[EC](Cs -> S)`. 54 | 6. A message can be sent from the client to the server. It has to be boxed properly. 55 | 7. A message can be sent from the server to the client. 56 | 57 | From step 6 and onwards, the message flow is bidirectional. Until connection termination, which is simply just terminating the TCP connection like one would normally do. 58 | 59 | # Detailed protocol messaging: 60 | 61 | This part describes the protocol contents in detail. Here we address some of the typical low-level protocol details, which are not that necessary to understand the high-level protocol construction. 62 | 63 | Throughout this section, we use Erlang-notation for packet formats. This has the advantage packet formats are isomorphic to the code in place. Also, it means the format is formally specified and has an unambigous construction for parsing as well as unparsing. 64 | 65 | ## General protocol packet structure: 66 | 67 | All packets are encoded with `{packet, 2}` (for non-Erlangers, this means packets are encoded as: `<>`, that is 2 bytes of big-endian length followed by that many bytes of payload). Thus, the maximal packet size is 64k, and this puts limits on the size of the message in a packet. The precise message size is mentioned in the section for packets carrying messages. The 2 bytes length is the *only* length given in packets. The rest of the packet contains fixed-size lengths and everything else can be derived from the general message length. The reason for this is to avoid typical heartbleed-like attacks, where sizes are misinterpreted. 68 | 69 | Keys in the protocol: 70 | 71 | * `C and Cs` are the clients long-term keys. 72 | * `S and Ss` are the servers long-term keys. 73 | * `EC and ECs` are *ephemeral* keys generated for the connection by the client. 74 | * `ES and ESs` are *ephemeral* keys generated for the connection by the server. 75 | 76 | ### Hello packets: 77 | 78 | The initial packet has the following structure: 79 | 80 | N = 0, 81 | Nonce = st_nonce(hello, client, N), 82 | Box = enacl:box(binary:copy(<<0>>, 64), Nonce, S, ECs), 83 | H = <<108,9,175,178,138,169,250,252, EC:32/binary, N:64/integer, Box/binary>> 84 | 85 | The first 8 bytes are randomly picked and identifies the connection type as a Version 1.0. It identifies we are speaking the protocol correctly from the client side. Then follows the pubkey and then follows the box, encoding 512 bits of 0. This allows graceful protocol extension in the future. 86 | 87 | ### Cookie packets: 88 | 89 | The cookie packet has the following structure: 90 | 91 | Ts = curve_tun_cookie:key(), 92 | SafeNonce = curve_tun_vault:safe_nonce(), 93 | CookieNonce = <<"minute-k", SafeNonce/binary>>, 94 | 95 | KBox = enacl:secret_box(<>, CookieNonce, Ts), 96 | K = <>, 97 | Box = curve_tun_vault:box(<>, SafeNonce, EC), 98 | Cookie = <<28,69,220,185,65,192,227,246, SafeNonce:16/binary, Box/binary>>, 99 | 100 | The 8 bytes are randomly picked and identifies the stream in the other direction as version 1.0. It allows us to roll new versions of the protocol later if needed. *Note* The long-term generated nonce is used twice in this packet with different prefixes. It is used once to make sure the cookie is protected, and once to make sure the packet is protected. The safety hinges on the safety of typical long_term nonce values, see further down for their construction. 101 | 102 | *Note*: Once the `ES` key is in the hands of the client, the server has no need for the key anymore and it is thrown away. 103 | 104 | ### Vouch (Initiate) packets 105 | 106 | Vouch packets from the client to the server have the following structure: 107 | 108 | K = cookie(), 109 | Nonce = short_term_nonce(initiate, client), 110 | NonceLT = long_term_nonce(), 111 | V = box(<>, NonceLT:16/binary, S, Cs), 112 | Box = box(<>, ES, ECs), 113 | Initiate = <<108,9,175,178,138,169,250,253, K:96/binary, Nonce:8/binary, Box/binary>> 114 | 115 | ### Message packets 116 | 117 | Once the connection has been established, the messaging structure is much simpler. Messages have the obvious structure: 118 | 119 | Nonce = short_term_nonce(msg, Side), 120 | Box = box(M, Nonce:8/binary, ES, ECs), 121 | Msg = <<109,27,57,203,246,90,17,180, Nonce:64/integer, Box/binary>> 122 | 123 | The header of a message is `8+8+16 = 32` bytes. This makes the maximally sized message in the procotol `256 * 256 - 32 = 65504` bytes in size. Sending larger messages are possible if a higher-level implementation embeds chunking inside packets, but it is of no concern to the security structure of the protocol. 124 | 125 | The value `Side` is either `server` or `client`. It affects how short-term nonce values are generated. By having the client and server generate different nonce values, we guarantee the servera and client can't use the same nonce value with a keypair, thus avoiding overlap problems. 126 | 127 | # Nonce handling 128 | 129 | The protocols security is hinging on the correct usage of a number of Nonce's or number-used-just-once. If ever a nonce is reused, the security of the protocol is greatly diminished to the point of breakage. Hence, this section lays out in detail how the nonce-values are generated. 130 | 131 | Like in CurveCP, there are four different nonce types involved: 132 | 133 | | Key Pair | Nonce Format | 134 | | ------------| ------------| 135 | | The servers long-term keypair `(S, Ss)`. The client knows `S` before making a connection | The string `<<"CurveCPK">>` follow by a 16 byte compressed nonce | 136 | | The clients long-term keypair `(C, Cs)`. Some servers can differentiate connections based on `C` | The string `<<"CurveCPV">>` followed by a 16 byte compressed nonce | 137 | | The servers short-term keypair `(ES, ESs)`. This keypair provides forward secrecy. | The string `<<"CurveCP-server-M">>` followed by a 8 byte compressed nonce. This nonce represents a 64-bit *big-endian* number | 138 | | The clients short-term keypair `(EC, ECs)`. Specific to the connection. | The string `<<"CurveCP-client-">>` followed by `<<"H">>`, `<<"I">>` and `<<"M">>` for Hello, Initiate and Message packets respectively. Then a 8 byte compressed nonce representing a 64 bit *big-endian* number | 139 | 140 | ## Short term keys 141 | 142 | For short-term client keys you generate the following nonce for a message type `T`. See below for the rules about `N`: 143 | 144 | msg_type(hello) -> <<"H">>; 145 | msg_type(initiate) -> <<"I">>; 146 | msg_type(msg) -> <<"M">>. 147 | 148 | Type = msg_type(T), 149 | <<"CurveCP-client-", Type:1/binary, N:64/integer>> 150 | 151 | Server-keys are likewise, but replaces `<<"CurveCP-client-">>` with `<<"CurveCP-server-">>`. The `N` is a counter counting from 0, 1, 2, … and so on. The rule is that if you reach the number `2^64` you must immediately close the connection. Note that this number is so large that a rate of 1 billion packets a second takes nearly 600 years to go through, so it should be ample. 152 | 153 | ## Long term keys 154 | 155 | Nonces for the long-term keys are far slower moving. There are two such keys being exchanged at the moment. One for the cookie packet. And one for the vouching initiate packet from the client. They are currently generated in the same way, but they needn't be in a future protocol. 156 | 157 | The server generates a cookie packet nonce by the following method: 158 | 159 | <<"CurveCPK", NonceVal:16/binary>> 160 | 161 | The server is not required to generate these in order. Client messages are likewise generated: 162 | 163 | <<"CurveCPV", NonceVal:16/binary>> 164 | 165 | Now, the `NonceVal` is generated by the following construction: 166 | 167 | Val = <>, 168 | encrypt(Val, Key) 169 | 170 | Where the `encrypt` primitive is the TEA cipher extended to 16 byte blocks and 32 byte keys (Tiny Encryption Algorithm, Needham & Wheeler). The reason we encrypt the data is to avoid leaking the `Counter`. The counter starts from 0 and increases over time for each connection. If the system terminates in a wrong way, then the counter is not trustworthy. Hence, the system stores a counter on disk next to the key from which to start up next time around. The rule is whenever the counter C passes a multiple of 1048576 we store C+2097152 on disk and start from there if the system dies by some bad means. 171 | 172 | *Discussion:* The forced counter increase is a protection against the problem where a server is suddenly shut down. The randomness provides further protection as even *if* a counter gets to be re-used as a nonce, the random component makes it highly unlikely that the counter will class with an earlier one. This means that even if you make the mistake of using old key material from a backup, the system should be safe. It *is* better to use proper key material however. 173 | 174 | The choice of TEA is perhaps not the best. The cipher has certain weaknesses in its properties. CurveCP uses it however, and furthermore we can at the worst leak the counters. A future variant of the protocol will use a more safe encryption algorithm in this place. 175 | 176 | ## Nonce rejection 177 | 178 | A client rejects all short-term nonces which moves backward in time. That is, the nonce counter is strictly monotonically increasing. Old messages can be ignored since it means somebody is seriously messing with TCP and trying to replay packets. 179 | 180 | Increments does not have to be 1 and the stream doesn't have to start from 0. Clients must be prepared for this. 181 | 182 | For long-term keys, you can't reject the nonce, since encryption makes them indistinguishable from random values. 183 | 184 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | curve_tun - TCP tunnels based on Curve25519 2 | ===== 3 | 4 | (WARNING: This is alpha-code. Do not use it in a project. In particular, error paths in the code are not implemented) 5 | 6 | This document describes the `curve_tun` application. It provides cryptographic tunnels over TCP in order to build secure communication between endpoints which provide *confidentiality* and *integrity* while also providing a certain amount of *availability*. 7 | 8 | The protocol also provides *active forward secrecy* against attackers. I.e., an attacker who is a man-in-the-middle and has the ability to act and change data. Even in that case, the protocol provides forward secrecy. Naturally, this also means the protocol is safe against a *passive* adversary. 9 | 10 | Build 11 | ----- 12 | 13 | This project requires the `enacl` bindings which in turn requires an installed `libsodium` library. Do note that the sodium library is not a package by default in Debian/Ubuntu for instance, so you may have to build a package yourself through the use of e.g., `checkinstall`. From there on, it should be as easy as compiling with rebar: 14 | 15 | $ rebar3 compile 16 | 17 | The project is using rebar3, because we need to move forward and rebar3 is a much better tool than rebar ever was. 18 | 19 | Security 20 | ------------- 21 | 22 | A cryptographic system is no more safe than its weakest link. There are two documents next to this one describing the used primitives and how the cryptographic safety is achieved. 23 | 24 | * PROTOCOL.md — describes the protocol design, which owes much, if not everything, to Dan J. Bernstein. 25 | * IMPLEMENTATION.md — describes the Erlang implementation and its internal structure. 26 | 27 | Current status 28 | ------------------ 29 | 30 | We are currently constructing the curve_tun application. This means a lot of things doesn't work like they are intended to do. For many primitives, we opt to get something to the point of working first, and then attack the details later on. Most notably, there are currently no strong security guarantees provided by the code: 31 | 32 | * There is only a single vault, the dummy vault and it always use the same key material every time we want to encrypt something. This is chosen for simplicity, while we are focusing on other parts of the code base. 33 | * The current implementation opts to leak internal counters rather than encrypt them. This will be fixed in a future version. 34 | * safe_nonce() generation does not yet block-encrypt its counters. 35 | * There is only a single registry, and that registry is very simplistic. 36 | * The code has seen no testing at all, and spews dialyzer warnings left and right. 37 | 38 | The code itself is at a stage where it can be tested for connectivity and message transfer. But we have still to provide for error paths in the code, optimizations, robustness, verification and Erlang QuickCheck. 39 | 40 | Background 41 | ------------------ 42 | 43 | When people want to secure communication between endpoints, the ubiquituous solution is to apply SSL on the tunnel, mostly implemented by use of the OpenSSL application. While easy, it poses many problems. SSL is notoriously hard to implement correctly and furthermore, most implementations are written in C, an unsafe language in many ways. While the Erlang SSL implementation is implemented in Erlang, thus avoiding some of low-level problems, it still implements TLS, which is far from a simple protocol to implement correctly. 44 | 45 | CurveTun or `curve_tun` implements tunnels over elliptic curve cryptography by means of the NaCl/libsodium library. It draws inspiration from Dan J. Bernsteins CurveCP implementation, but provides its tunnels over TCP—for better or worse. By being a *vastly* simpler protocol design, the hope is that it is easier to implement, which should provide better security. Also, the protocol is deliberately constructed such that it can be parsed easily. There should be no gotchas the like of Heartbleed in this protocol design. 46 | 47 | Security considerations 48 | ------------------------------ 49 | 50 | This section addresses the security of the `curve_tun` implementation and protocol. As with everything using crypto, the biggest problem lies in the right implementation of the cryptographic primitives moreso than if the low-level facilities work. See the PROTOCOL.md description for an explanation of how the protocol is built. The document is recent and up-to-date. 51 | 52 | As for the implementation, I can't guarantee there won't be security errors in there. The plan is to test against errors by fuzzing the implementation via Erlang QuickCheck in the future. The primary author, Jesper Louis Andersen, does have *some* cryptographic experience and is probably in the better half of the bell curve. Yet, he is no *expert* and the protocol, while being derived by one from Dan J. Bernstein, has not been thoroughly verified. Thus, the best I can do is to list the attack vectors which have been considered and mitigated. 53 | 54 | ### Requirements for correct operation: 55 | 56 | Security protocols require a number of prerequisites to be safe. These are the ones listed for `curve_tun`. 57 | 58 | * The random source of data must be a CSPRNG. This is true for libsodium on at least OpenBSD, FreeBSD, and Linux. OSX and iOS should also be secure with their yarrow-based generator. libsodium currently (Dec 2014) uses `RtlGenRandom()` on Windows. You will have to make sure this is safe. Likewise for other operating systems. We rely on the kernel to provide safe randomness. 59 | * The key store vault must be writable. In order to provide safe nonces for the protocol, counters are periodically written next to the key material. While the system is safe even if using an older backup, the practice is not recommended. Furthermore, a scrambling key is written into the vault to provide protection against counters leaking to an attacker. 60 | * The endpoints must be safe. This is a standard encrypted tunnel implementation. It does not provide any point of endpoint security, though it does provide active forward secrecy. 61 | 62 | ### Specific attack vectors: 63 | 64 | The specific security considerations and mitigations goes here in the future. System description is not entirely done, and there are parts which have been fully implemented or verified yet. 65 | 66 | * Server authentication: An attacker pretends to be the server. These packets are immediately rejected because they don't have either the servers long-term signature or the servers short-term signature. And the short term key is vouched for by the long-term key. 67 | * Client authentication: An attacker pretends to be a client. Since all client communication is authenticated, with either the long-term key or a (vouched) short-term key, this is rejected by the implementation. 68 | * No way to disable or downgrade encryption: The protocol doesn not allow for any kind of protocol downgrade, either to an earlier variant of the protocol, nor to an earlier or less safe suite of ciphers. The ciphers used are selected by Schwabe, Lange and Bernstein (between 2007–2011) and they are used as-is. 69 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [debug_info]}. 2 | {deps, [ 3 | {enacl, {git, "https://github.com/jlouis/enacl.git", {tag, "v0.12.1"}}} 4 | ]}. -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | [{<<"enacl">>, 2 | {git,"https://github.com/jlouis/enacl.git", 3 | {ref,"2979503a7fbd45fd402ac2262a84f8fabf5152eb"}}, 4 | 0}]. 5 | -------------------------------------------------------------------------------- /src/Makefile: -------------------------------------------------------------------------------- 1 | compile: 2 | $(MAKE) -C .. compile 3 | -------------------------------------------------------------------------------- /src/curve_tun.app.src: -------------------------------------------------------------------------------- 1 | {application, curve_tun, 2 | [{description, "Socket tunnels encrypted via the NaCl library (Curve 25519)"} 3 | ,{vsn, "0.1.0"} 4 | ,{registered, [curve_tun_sup]} 5 | ,{mod, {'curve_tun_app', []}} 6 | ,{applications, 7 | [kernel 8 | ,stdlib 9 | 10 | ,enacl 11 | ]} 12 | ,{env,[ 13 | {vault_providers, [curve_tun_vault_dummy]}, 14 | {registry_providers, [curve_tun_simple_registry]}]} 15 | ,{modules, []} 16 | ]}. 17 | -------------------------------------------------------------------------------- /src/curve_tun.erl: -------------------------------------------------------------------------------- 1 | -module(curve_tun). 2 | 3 | -export([connect/3, accept/1, accept/2, listen/2, send/2, close/1, recv/1, recv/2, controlling_process/2]). 4 | 5 | connect(Host, Port, Opts) -> 6 | curve_tun_connection:connect(Host, Port, Opts). 7 | 8 | accept(LSock) -> 9 | curve_tun_connection:accept(LSock). 10 | 11 | accept(LSock, Timeout) -> 12 | curve_tun_connection:accept(LSock, Timeout). 13 | 14 | listen(Port, Opts) -> 15 | curve_tun_connection:listen(Port, Opts). 16 | 17 | send(Sock, Msg) -> 18 | curve_tun_connection:send(Sock, Msg). 19 | 20 | close(Sock) -> 21 | curve_tun_connection:close(Sock). 22 | 23 | recv(Sock) -> 24 | curve_tun_connection:recv(Sock). 25 | 26 | recv(Sock, Timeout) -> 27 | curve_tun_connection:recv(Sock, Timeout). 28 | 29 | controlling_process(Sock, Pid) -> 30 | curve_tun_connection:controlling_process(Sock, Pid). 31 | -------------------------------------------------------------------------------- /src/curve_tun_app.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @doc curve_tun public API 3 | %% @end 4 | %%%------------------------------------------------------------------- 5 | 6 | -module(curve_tun_app). 7 | 8 | -behaviour(application). 9 | 10 | %% Application callbacks 11 | -export([start/2, stop/1]). 12 | 13 | %%==================================================================== 14 | %% API 15 | %%==================================================================== 16 | 17 | start(_StartType, _StartArgs) -> 18 | curve_tun_sup:start_link(). 19 | 20 | %%-------------------------------------------------------------------- 21 | stop(_State) -> 22 | ok. 23 | 24 | %%==================================================================== 25 | %% Internal functions 26 | %%==================================================================== 27 | -------------------------------------------------------------------------------- /src/curve_tun_connection.erl: -------------------------------------------------------------------------------- 1 | -module(curve_tun_connection). 2 | -behaviour(gen_fsm). 3 | 4 | -export([connect/3, accept/1, accept/2, listen/2, send/2, close/1, recv/1, recv/2, controlling_process/2]). 5 | 6 | %% Private callbacks 7 | -export([start_fsm/0, start_link/1]). 8 | 9 | %% FSM callbacks 10 | -export([init/1, code_change/4, terminate/3, handle_info/3, handle_event/3, handle_sync_event/4]). 11 | 12 | -export([ 13 | closed/2, closed/3, 14 | connected/2, connected/3, 15 | initiating/2, initiating/3, 16 | ready/2, ready/3 17 | ]). 18 | 19 | -record(curve_tun_lsock, { lsock :: port () }). 20 | 21 | -record(curve_tun_socket, { pid :: pid() }). 22 | 23 | %% Maximal number of messages that can be sent on the line before we crash. 24 | %% I don't expect code to ever hit this limit. As an example, you exhaust this in 25 | %% a year if you manage to send 584 billion messages per second on a single 26 | %% connection. 27 | -define(COUNT_LIMIT, 18446744073709551616 - 1). 28 | 29 | connect(Address, Port, Options) -> 30 | {ok, Pid} = start_fsm(), 31 | case gen_fsm:sync_send_event(Pid, {connect, Address, Port, Options}) of 32 | ok -> 33 | {ok, #curve_tun_socket { pid = Pid }}; 34 | {error, Reason} -> 35 | {error, Reason} 36 | end. 37 | 38 | send(#curve_tun_socket { pid = Pid }, Msg) -> 39 | gen_fsm:sync_send_event(Pid, {send, Msg}). 40 | 41 | recv(#curve_tun_socket { pid = Pid }, Timeout) -> 42 | gen_fsm:sync_send_event(Pid, recv, Timeout). 43 | 44 | recv(State) -> 45 | recv(State, 5000). 46 | 47 | close(#curve_tun_socket { pid = Pid }) -> 48 | gen_fsm:sync_send_event(Pid, close). 49 | 50 | listen(Port, Opts) -> 51 | Options = [binary, {packet, 2}, {active, false} | Opts], 52 | case gen_tcp:listen(Port, Options) of 53 | {ok, LSock} -> {ok, #curve_tun_lsock { lsock = LSock }}; 54 | {error, Reason} -> {error, Reason} 55 | end. 56 | 57 | accept(#curve_tun_lsock { lsock = LSock}, Timeout) -> 58 | {ok, Pid} = start_fsm(), 59 | case gen_fsm:sync_send_event(Pid, {accept, LSock}, Timeout) of 60 | ok -> 61 | {ok, #curve_tun_socket { pid = Pid }}; 62 | {error, Reason} -> 63 | {error, Reason} 64 | end. 65 | 66 | accept(State) -> 67 | accept(State, 5000). 68 | 69 | controlling_process(#curve_tun_socket { pid = Pid }, Controller) -> 70 | gen_fsm:sync_send_all_state_event(Pid, {controlling_process, Controller}). 71 | 72 | %% @private 73 | start_fsm() -> 74 | Controller = self(), 75 | curve_tun_connection_sup:start_child([Controller]). 76 | 77 | %% @private 78 | start_link(Controller) -> 79 | gen_fsm:start_link(?MODULE, [Controller], []). 80 | 81 | %% @private 82 | init([Controller]) -> 83 | Ref = erlang:monitor(process, Controller), 84 | State = #{ 85 | vault => curve_tun_vault_dummy, 86 | registry => curve_tun_simple_registry, 87 | controller => {Controller, Ref} 88 | }, 89 | {ok, ready, State}. 90 | 91 | 92 | %% @private 93 | ready({accept, LSock}, From, #{ vault := Vault} = State) -> 94 | case gen_tcp:accept(LSock) of 95 | {error, Reason} -> 96 | {stop, normal, {error, Reason}, ready, State}; 97 | {ok, Socket} -> 98 | InitState = State#{ socket => Socket }, 99 | ok = inet:setopts(Socket, [{active, once}]), 100 | {ok, EC} = recv_hello(InitState), 101 | %% Once ES is in the hands of the client, the server doesn't need it anymore 102 | #{ public := ES, secret := ESs } = enacl:box_keypair(), 103 | case gen_tcp:send(Socket, e_cookie(EC, ES, ESs, Vault)) of 104 | ok -> 105 | ok = inet:setopts(Socket, [{active, once}]), 106 | {next_state, accepting, InitState#{ from => From }}; 107 | {error, Reason} -> 108 | {stop, normal, {error, Reason}, State} 109 | end 110 | end; 111 | ready({connect, Address, Port, Options}, From, State) -> 112 | TcpOpts = lists:keydelete(key, 1, [{packet, 2}, binary, {active, false} | Options]), 113 | S = proplists:get_value(key, Options), 114 | case gen_tcp:connect(Address, Port, TcpOpts) of 115 | {error, Reason} -> 116 | {stop, normal, {error, Reason}, State}; 117 | {ok, Socket} -> 118 | #{ public := EC, secret := ECs } = enacl:box_keypair(), 119 | case gen_tcp:send(Socket, e_hello(S, EC, ECs, 0)) of 120 | ok -> 121 | ok = inet:setopts(Socket, [{active, once}]), 122 | {next_state, initiating, State#{ 123 | from => From, 124 | peer_lt_public_key => S, 125 | public_key => EC, 126 | secret_key => ECs, 127 | socket => Socket }}; 128 | {error, Reason} -> 129 | {stop, normal, {error, Reason}, State} 130 | end 131 | end. 132 | 133 | ready(_Msg, ready) -> 134 | {stop, argh, ready}. 135 | 136 | initiating(_Msg, _From, _State) -> 137 | {stop, argh, ready}. 138 | 139 | initiating(_Msg, _) -> 140 | {stop, argh, ready}. 141 | 142 | closed(_Msg, _State) -> 143 | {stop, argh, closed}. 144 | 145 | closed({send, _}, _From, State) -> 146 | {reply, {error, closed}, State}. 147 | 148 | connected(_M, _) -> 149 | {stop, argh, connected}. 150 | 151 | connected(close, _From, #{ socket := Sock } = State) -> 152 | ok = gen_tcp:close(Sock), 153 | {stop, normal, ok, maps:remove(socket, State)}; 154 | connected(recv, From, #{ socket := Sock, recv_queue := Q } = State) -> 155 | ok = inet:setopts(Sock, [{active, once}]), 156 | {next_state, connected, State#{ recv_queue := queue:in(From, Q) }}; 157 | connected({send, M}, _From, #{ socket := Socket, secret_key := Ks, peer_public_key := P, c := NonceCount, side := Side } = State) -> 158 | case gen_tcp:send(Socket, e_msg(M, Side, NonceCount, P, Ks)) of 159 | ok -> {reply, ok, connected, State#{ c := NonceCount + 1}}; 160 | {error, _Reason} = Err -> {reply, Err, connected, State} 161 | end. 162 | 163 | handle_sync_event({controlling_process, Controller}, {PrevController, _Tag}, Statename, 164 | #{ controller := {PrevController, MRef} } = State) -> 165 | erlang:demonitor(MRef, [flush]), 166 | NewRef = erlang:monitor(process, Controller), 167 | {reply, ok, Statename, State#{ controller := {Controller, NewRef}}}; 168 | handle_sync_event({controlling_process, _Controller}, _From, Statename, State) -> 169 | {reply, {error, not_owner}, Statename, State}; 170 | handle_sync_event(Event, _From, Statename, State) -> 171 | error_logger:info_msg("Unknown sync_event ~p in state ~p", [Event, Statename]), 172 | {next_state, Statename, State}. 173 | 174 | handle_event(Event, Statename, State) -> 175 | error_logger:info_msg("Unknown event ~p in state ~p", [Event, Statename]), 176 | {next_state, Statename, State}. 177 | 178 | handle_info({'DOWN', _Ref, process, Pid, _Info}, _Statename, #{ controller := Pid, socket := Socket } = State) -> 179 | ok = gen_tcp:close(Socket), 180 | {stop, tcp_closed, maps:remove(socket, State)}; 181 | handle_info({tcp, Sock, Data}, Statename, #{ socket := Sock } = State) -> 182 | handle_tcp(Data, Statename, State); 183 | handle_info({tcp_closed, Sock}, Statename, #{ socket := Sock } = State) -> 184 | handle_tcp_closed(Statename, State); 185 | handle_info(Info, Statename, State) -> 186 | error_logger:info_msg("Unknown info msg ~p in state ~p", [Info, Statename]), 187 | {next_state, Statename, State}. 188 | 189 | terminate(_Reason, _Statename, _State) -> 190 | ok. 191 | 192 | code_change(_OldVsn, Statename, State, _Aux) -> 193 | {ok, Statename, State}. 194 | 195 | %% INTERNAL HANDLERS 196 | 197 | unpack_cookie(<>) -> 198 | CNonce = lt_nonce(minute_k, Nonce), 199 | Keys = curve_tun_cookie:recent_keys(), 200 | unpack_cookie_(Keys, CNonce, Cookie). 201 | 202 | unpack_cookie_([], _, _) -> {error, ecookie}; 203 | unpack_cookie_([K | Ks], CNonce, Cookie) -> 204 | case enacl:secretbox_open(Cookie, CNonce, K) of 205 | {ok, <>} -> {ok, EC, ESs}; 206 | {error, failed_verification} -> 207 | unpack_cookie_(Ks, CNonce, Cookie) 208 | end. 209 | 210 | reply(M, #{ from := From } = State) -> 211 | gen_fsm:reply(From, M), 212 | maps:remove(from, State). 213 | 214 | %% @doc process_recv_queue/1 sends messages back to waiting receivers 215 | %% Analyze the current waiting receivers and the buffer state. If there is a receiver for the buffered 216 | %% message, then send the message back the receiver. 217 | %% @end 218 | process_recv_queue(#{ recv_queue := Q, buf := Buf, socket := Sock } = State) -> 219 | case {queue:out(Q), Buf} of 220 | {{{value, _Receiver}, _Q2}, undefined} -> 221 | ok = inet:setopts(Sock, [{active, once}]), 222 | {next_state, connected, State}; 223 | {{{value, Receiver}, Q2}, Msg} -> 224 | gen_fsm:reply(Receiver, Msg), 225 | process_recv_queue(State#{ recv_queue := Q2, buf := undefined }); 226 | {{empty, _Q2}, _} -> 227 | {next_state, connected, State} 228 | end. 229 | 230 | handle_msg(?COUNT_LIMIT, _Box, _State) -> exit(count_limit); 231 | handle_msg(N, Box, #{ 232 | peer_public_key := P, 233 | secret_key := Ks, 234 | buf := undefined, 235 | side := Side, 236 | rc := N } = State) -> 237 | Nonce = case Side of 238 | client -> st_nonce(msg, server, N); 239 | server -> st_nonce(msg, client, N) 240 | end, 241 | {ok, Msg} = enacl:box_open(Box, Nonce, P, Ks), 242 | process_recv_queue(State#{ buf := Msg, rc := N+1 }). 243 | 244 | handle_vouch(K, 1, Box, #{ socket := Sock, vault := Vault, registry := Registry } = State) -> 245 | case unpack_cookie(K) of 246 | {ok, EC, ESs} -> 247 | Nonce = st_nonce(initiate, client, 1), 248 | {ok, <>} = enacl:box_open(Box, Nonce, EC, ESs), 249 | true = Registry:verify(Sock, C), 250 | VNonce = lt_nonce(client, NonceLT), 251 | {ok, <>} = Vault:box_open(Vouch, VNonce, C), 252 | %% Everything seems to be in order, go to connected state 253 | NState = State#{ recv_queue => queue:new(), buf => undefined, 254 | secret_key => ESs, peer_public_key => EC, c => 2, rc => 2, side => server }, 255 | {next_state, connected, reply(ok, NState)}; 256 | {error, _Reason} = Err -> 257 | {stop, Err, State} 258 | end. 259 | 260 | handle_cookie(N, Box, #{ public_key := EC, secret_key := ECs, peer_lt_public_key := S, socket := Socket, vault := Vault } = State) -> 261 | Nonce = lt_nonce(server, N), 262 | {ok, <>} = enacl:box_open(Box, Nonce, S, ECs), 263 | case gen_tcp:send(Socket, e_vouch(K, EC, S, Vault, 1, ES, ECs)) of 264 | ok -> 265 | {next_state, connected, reply(ok, State#{ 266 | peer_public_key => ES, 267 | recv_queue => queue:new(), 268 | buf => undefined, 269 | c => 2, 270 | side => client, 271 | rc => 2 })}; 272 | {error, _Reason} = Err -> 273 | {stop, normal, reply(Err, State)} 274 | end. 275 | 276 | handle_tcp(Data, StateName, State) -> 277 | case {d_packet(Data), StateName} of 278 | {{msg, N, Box}, connected} -> handle_msg(N, Box, State); 279 | {{vouch, K, N, Box}, accepting} -> handle_vouch(K, N, Box, State); 280 | {{cookie, N, Box}, initiating} -> handle_cookie(N, Box, State) 281 | end. 282 | 283 | handle_tcp_closed(_Statename, State) -> 284 | {next_state, closed, maps:remove(socket, State)}. 285 | 286 | %% NONCE generation 287 | %% 288 | %% There are two types of nonces: short-term (st) and long-term (lt) 289 | 290 | st_nonce(hello, client, N) -> <<"CurveCP-client-H", N:64/integer>>; 291 | st_nonce(initiate, client, N) -> <<"CurveCP-client-I", N:64/integer>>; 292 | st_nonce(msg, client, N) -> <<"CurveCP-client-M", N:64/integer>>; 293 | st_nonce(hello, server, N) -> <<"CurveCP-server-H", N:64/integer>>; 294 | st_nonce(initiate, server, N) -> <<"CurveCP-server-I", N:64/integer>>; 295 | st_nonce(msg, server, N) -> <<"CurveCP-server-M", N:64/integer>>. 296 | 297 | lt_nonce(minute_k, N) -> <<"minute-k", N/binary>>; 298 | lt_nonce(client, N) -> <<"CurveCPV", N/binary>>; 299 | lt_nonce(server, N) -> <<"CurveCPK", N/binary>>. 300 | 301 | %% RECEIVING expected messages 302 | recv_hello(#{ socket := Socket, vault := Vault}) -> 303 | receive 304 | {tcp, Socket, Data} -> 305 | case d_packet(Data) of 306 | {hello, EC, 0, Box} -> 307 | STNonce = st_nonce(hello, client, 0), 308 | {ok, <<0:512/integer>>} = Vault:box_open(Box, STNonce, EC), 309 | {ok, EC}; 310 | Otherwise -> 311 | error_logger:info_report([received, Otherwise]), 312 | {error, ehello} 313 | end 314 | after 5000 -> 315 | {error, timeout} 316 | end. 317 | 318 | 319 | %% COMMAND GENERATION 320 | %% 321 | %% The e_* functions produce messages for the wire. They are kept here 322 | %% for easy perusal. Note that while the arguments are terse, they do have 323 | %% meaning since they reflect the meaning of the protocol specification. For 324 | %% instance, the argument ECs means (E)phermeral (C)lient (s)ecret key. 325 | e_hello(S, EC, ECs, N) -> 326 | Nonce = st_nonce(hello, client, N), 327 | Box = enacl:box(binary:copy(<<0>>, 64), Nonce, S, ECs), 328 | <<108,9,175,178,138,169,250,252, EC:32/binary, N:64/integer, Box/binary>>. 329 | 330 | e_cookie(EC, ES, ESs, Vault) -> 331 | Ts = curve_tun_cookie:current_key(), 332 | SafeNonce = Vault:safe_nonce(), 333 | CookieNonce = lt_nonce(minute_k, SafeNonce), 334 | 335 | KBox = enacl:secretbox(<>, CookieNonce, Ts), 336 | K = <>, 337 | BoxNonce = lt_nonce(server, SafeNonce), 338 | Box = Vault:box(<>, BoxNonce, EC), 339 | <<28,69,220,185,65,192,227,246, SafeNonce:16/binary, Box/binary>>. 340 | 341 | e_vouch(Kookie, VMsg, S, Vault, N, ES, ECs) when byte_size(Kookie) == 96 -> 342 | NonceBase = Vault:safe_nonce(), 343 | 344 | %% Produce the box for the vouch 345 | VouchNonce = lt_nonce(client, NonceBase), 346 | VouchBox = Vault:box(VMsg, VouchNonce, S), 347 | C = Vault:public_key(), 348 | 349 | STNonce = st_nonce(initiate, client, N), 350 | Box = enacl:box(<>, STNonce, ES, ECs), 351 | <<108,9,175,178,138,169,250,253, Kookie/binary, N:64/integer, Box/binary>>. 352 | 353 | e_msg(M, Side, NonceCount, PK, SK) -> 354 | Nonce = st_nonce(msg, Side, NonceCount), 355 | Box = enacl:box(M, Nonce, PK, SK), 356 | <<109,27,57,203,246,90,17,180, NonceCount:64/integer, Box/binary>>. 357 | 358 | %% PACKET DECODING 359 | %% 360 | %% To make it easy to understand what is going on, keep the packet decoder 361 | %% close the to encoding of messages. The above layers then handle the 362 | %% semantics of receiving and sending commands/packets over the wire 363 | d_packet(<<109,27,57,203,246,90,17,180, N:64/integer, Box/binary>>) -> 364 | {msg, N, Box}; 365 | d_packet(<<108,9,175,178,138,169,250,253, K:96/binary, N:64/integer, Box/binary>>) -> 366 | {vouch, K, N, Box}; 367 | d_packet(<<28,69,220,185,65,192,227,246, N:16/binary, Box/binary>>) -> 368 | {cookie, N, Box}; 369 | d_packet(<<108,9,175,178,138,169,250,252, EC:32/binary, N:64/integer, Box/binary>>) -> 370 | {hello, EC, N, Box}; 371 | d_packet(_) -> 372 | unknown. 373 | 374 | -------------------------------------------------------------------------------- /src/curve_tun_connection_sup.erl: -------------------------------------------------------------------------------- 1 | -module(curve_tun_connection_sup). 2 | -behaviour(supervisor). 3 | 4 | -export([start_link/0]). 5 | -export([start_child/1]). 6 | 7 | -export([init/1]). 8 | 9 | start_link() -> 10 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 11 | 12 | start_child(Args) -> 13 | supervisor:start_child(?MODULE, Args). 14 | 15 | init(_O) -> 16 | RestartStrategy = simple_one_for_one, 17 | MaxR = 50, 18 | MaxT = 3600, 19 | 20 | Name = undefined, 21 | StartFunc = {curve_tun_connection, start_link, []}, 22 | Restart = temporary, 23 | Shutdown = 4000, 24 | Modules = [curve_tun_connection], 25 | Type = worker, 26 | 27 | ChildSpec = {Name, StartFunc, Restart, Shutdown, Type, Modules}, 28 | {ok, 29 | {{RestartStrategy, MaxR, MaxT}, 30 | [ChildSpec]}}. 31 | 32 | -------------------------------------------------------------------------------- /src/curve_tun_cookie.erl: -------------------------------------------------------------------------------- 1 | -module(curve_tun_cookie). 2 | -behaviour(gen_server). 3 | 4 | -export([start_link/0]). 5 | -export([current_key/0, recent_keys/0]). 6 | 7 | -export([init/1, code_change/3, terminate/2, handle_info/2, handle_cast/2, handle_call/3]). 8 | 9 | -define(SERVER, ?MODULE). 10 | -define(ROTATE_PERIOD, 60 * 1000). 11 | 12 | -record(state, { 13 | keys :: [binary()] 14 | }). 15 | 16 | %% API 17 | start_link() -> 18 | gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). 19 | 20 | current_key() -> 21 | gen_server:call(?SERVER, current_key). 22 | 23 | recent_keys() -> 24 | gen_server:call(?SERVER, recent_keys). 25 | 26 | %% Callbacks 27 | init([]) -> 28 | OldKey = minute_key(), 29 | Key = minute_key(), 30 | erlang:send_after(?ROTATE_PERIOD, self(), recompute_key), 31 | {ok, #state{ keys = [Key, OldKey] }}. 32 | 33 | handle_call(current_key, _From, #state { keys = [K | _] } = State) -> 34 | {reply, K, State}; 35 | handle_call(recent_keys, _From, #state { keys = Keys } = State) -> 36 | {reply, Keys, State}; 37 | handle_call(_Msg, _From, State) -> 38 | {reply, {error, not_implemented}, State}. 39 | 40 | handle_cast(Msg, State) -> 41 | error_logger:info_report([{wrong_handle_cast, Msg}]), 42 | {noreply, State}. 43 | 44 | handle_info(recompute_key, #state { keys = [Existing, _Old] } = State) -> 45 | erlang:send_after(?ROTATE_PERIOD, self(), recompute_key), 46 | Key = minute_key(), 47 | {noreply, State#state{ keys = [Key, Existing] }}; 48 | handle_info(Msg, State) -> 49 | error_logger:info_report([{wrong_handle_cast, Msg}]), 50 | {noreply, State}. 51 | 52 | terminate(_Reason, _State) -> 53 | ok. 54 | 55 | code_change(_OldVsn, State, _Aux) -> 56 | {ok, State}. 57 | 58 | %% Internal functions 59 | minute_key() -> 60 | enacl:randombytes(enacl:secretbox_key_size()). 61 | -------------------------------------------------------------------------------- /src/curve_tun_simple_registry.erl: -------------------------------------------------------------------------------- 1 | %%% @doc module curve_tun_simple_registry implements a simple memory-based registry 2 | %%% @end 3 | -module(curve_tun_simple_registry). 4 | -behaviour(gen_server). 5 | 6 | %% Lifetime API 7 | -export([start_link/0]). 8 | -export([lookup/1, register/2, verify/2]). 9 | 10 | %% API 11 | -export([]). 12 | 13 | %% Callbacks 14 | -export([init/1, code_change/3, terminate/2, handle_call/3, handle_cast/2, handle_info/2]). 15 | 16 | -define(SERVER, ?MODULE). 17 | -record(state, { 18 | dict :: dict:dict(inet:ip_address(), binary())}). 19 | 20 | %% API 21 | start_link() -> 22 | gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). 23 | 24 | register(IP, PubKey) -> 25 | gen_server:call(?SERVER, {register, IP, PubKey}). 26 | 27 | lookup(IP) -> 28 | gen_server:call(?SERVER, {lookup, IP}). 29 | 30 | verify(Socket, PubKey) -> 31 | {ok, {Address, _Port}} = inet:peername(Socket), 32 | case lookup(Address) of 33 | {ok, Key} -> PubKey =:= Key; 34 | {error, not_found} -> false 35 | end. 36 | 37 | %% Callbacks 38 | 39 | %% @private 40 | init([]) -> 41 | {ok, #state{ dict = dict:new() }}. 42 | 43 | %% @private 44 | handle_cast(_M, State) -> 45 | {noreply, State}. 46 | 47 | %% @private 48 | handle_call({register, IP, PubKey}, _From, #state { dict = Dict } = State) -> 49 | {reply, ok, State#state { dict = dict:store(IP, PubKey, Dict) }}; 50 | handle_call({lookup, IP}, _From, #state { dict = Dict } = State) -> 51 | {reply, lookup(IP, Dict), State}; 52 | handle_call(_M, _From, State) -> 53 | {reply, {error, bad_call}, State}. 54 | 55 | %% @private 56 | handle_info(_M, State) -> 57 | {noreply, State}. 58 | 59 | %% @private 60 | code_change(_OldVsn, State, _Aux) -> 61 | {ok, State}. 62 | 63 | %% @private 64 | terminate(_Reason, _State) -> 65 | ok. 66 | 67 | lookup(IP, Dict) -> 68 | case dict:find(IP, Dict) of 69 | {ok, PubKey} -> {ok, PubKey}; 70 | error -> {error, not_found} 71 | end. 72 | -------------------------------------------------------------------------------- /src/curve_tun_sup.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @doc curve_tun top level supervisor. 3 | %% @end 4 | %%%------------------------------------------------------------------- 5 | 6 | -module(curve_tun_sup). 7 | 8 | -behaviour(supervisor). 9 | 10 | %% API 11 | -export([start_link/0]). 12 | 13 | %% Supervisor callbacks 14 | -export([init/1]). 15 | 16 | -define(SERVER, ?MODULE). 17 | -define(CHILD(I), {I, {I, start_link, []}, permanent, 5000, worker, [I]}). 18 | -define(CHILDW(I, W), {I, {I, start_link, []}, permanent, W, worker, [I]}). 19 | 20 | %%==================================================================== 21 | %% API functions 22 | %%==================================================================== 23 | 24 | start_link() -> 25 | supervisor:start_link({local, ?SERVER}, ?MODULE, []). 26 | 27 | %%==================================================================== 28 | %% Supervisor callbacks 29 | %%==================================================================== 30 | 31 | %% Child :: {Id,StartFunc,Restart,Shutdown,Type,Modules} 32 | init([]) -> 33 | ConnectionSup = connection_sup_child_spec(), 34 | CookieSpec = ?CHILDW(curve_tun_cookie, 2000), 35 | Registries = registry_providers(), 36 | Vaults = vault_providers(), 37 | {ok, { {one_for_all, 10, 3600}, [ConnectionSup, CookieSpec] ++ Vaults ++ Registries} }. 38 | 39 | %%==================================================================== 40 | %% Internal functions 41 | %%==================================================================== 42 | vault_providers() -> 43 | {ok, Modules} = application:get_env(curve_tun, vault_providers), 44 | lists:map(fun child/1, Modules). 45 | 46 | registry_providers() -> 47 | {ok, Modules} = application:get_env(curve_tun, registry_providers), 48 | lists:map(fun child/1, Modules). 49 | 50 | child(M) -> ?CHILD(M). 51 | 52 | connection_sup_child_spec() -> 53 | Name = connection_sup, 54 | StartFunc = {curve_tun_connection_sup, start_link, []}, 55 | Restart = permanent, 56 | Shutdown = 4000, 57 | Modules = [curve_tun_connection_sup], 58 | Type = supervisor, 59 | {Name, StartFunc, Restart, Shutdown, Type, Modules}. 60 | 61 | -------------------------------------------------------------------------------- /src/curve_tun_vault_dummy.erl: -------------------------------------------------------------------------------- 1 | -module(curve_tun_vault_dummy). 2 | 3 | -behaviour(gen_server). 4 | -define(SERVER, ?MODULE). 5 | 6 | -export([start_link/0]). 7 | -export([public_key/0, box/3, box_open/3, safe_nonce/0]). 8 | 9 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). 10 | 11 | -record(state, { 12 | public_key :: binary(), 13 | secret_key :: binary(), 14 | counter :: non_neg_integer(), 15 | nonce_key :: binary() 16 | }). 17 | 18 | start_link() -> 19 | gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). %% @todo mark the process as sensitive 20 | 21 | public_key() -> 22 | gen_server:call(?SERVER, public_key). 23 | 24 | box_open(Box, Nonce, SignatureKey) -> 25 | gen_server:call(?SERVER, {box_open, Box, Nonce, SignatureKey}). 26 | 27 | box(Msg, Nonce, PublicKey) -> 28 | gen_server:call(?SERVER, {box, Msg, Nonce, PublicKey}). 29 | 30 | safe_nonce() -> 31 | gen_server:call(?SERVER, safe_nonce). 32 | 33 | init([]) -> 34 | #{ public := Public, secret := Secret } = keypair(), 35 | NonceKey = scramble_key(), 36 | {ok, #state { public_key = Public, secret_key = Secret, counter = 0, nonce_key = NonceKey }}. 37 | 38 | handle_call({box_open, Box, Nonce, SignatureKey}, _From, #state { secret_key = SK } = State) -> 39 | {reply, enacl:box_open(Box, Nonce, SignatureKey, SK), State}; 40 | handle_call({box, Msg, Nonce, PublicKey}, _From, #state { secret_key = SK } = State) -> 41 | {reply, enacl:box(Msg, Nonce, PublicKey, SK), State}; 42 | handle_call(safe_nonce, _From, #state { nonce_key = NK, counter = C } = State) -> 43 | RandomBytes = enacl:randombytes(8), 44 | SafeNonce = enacl_ext:scramble_block_16(<>, NK), 45 | {reply, SafeNonce, State#state { counter = C+1 }}; 46 | handle_call(public_key, _From, #state { public_key = PK } = State) -> 47 | {reply, PK, State}. 48 | 49 | handle_cast(Msg, State) -> 50 | error_logger:info_msg("Unknown handle_cast message: ~p", [Msg]), 51 | {noreply, State}. 52 | 53 | handle_info(Msg, State) -> 54 | error_logger:info_msg("Unknown handle_info message: ~p", [Msg]), 55 | {noreply, State}. 56 | 57 | terminate(_Reason, _State) -> 58 | ok. 59 | 60 | code_change(_OldVsn, State, _Aux) -> 61 | {ok, State}. 62 | 63 | scramble_key() -> 64 | enacl:randombytes(32). 65 | 66 | %% For now, we always use the same keypair in the dummy vault. 67 | keypair() -> 68 | #{ 69 | public => <<81,13,101,52,29,109,136,196,86,91,34,91,3,19,150,3,215, 70 | 43,210,9,242,146,119,188,153,245,78,232,94,113,37,47>>, 71 | secret => <<79,5,69,119,45,58,176,227,13,41,218,168,234,190,227,142, 72 | 160,217,229,207,248,33,10,84,184,133,218,238,93,40,44, 157>> 73 | }. 74 | -------------------------------------------------------------------------------- /test/curve_tun_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(curve_tun_SUITE). 2 | 3 | -include_lib("common_test/include/ct.hrl"). 4 | 5 | -export([suite/0, all/0, groups/0, 6 | init_per_group/2, end_per_group/2, 7 | init_per_suite/1, end_per_suite/1, 8 | init_per_testcase/2, end_per_testcase/2]). 9 | 10 | -export([send_recv/1]). 11 | 12 | -define(TIMEOUT, 5000). 13 | 14 | suite() -> 15 | [{timetrap, {seconds, 10}}]. 16 | 17 | %% Setup/Teardown 18 | %% ---------------------------------------------------------------------- 19 | init_per_group(_Group, Config) -> 20 | Config. 21 | 22 | end_per_group(_Group, _Config) -> 23 | ok. 24 | 25 | init_per_suite(Config) -> 26 | error_logger:tty(false), %% Disable the error loggers tty output for tests 27 | {ok, Apps} = application:ensure_all_started(curve_tun), 28 | PK = <<81,13,101,52,29,109,136,196,86,91,34,91,3,19,150,3,215, 29 | 43,210,9,242,146,119,188,153,245,78,232,94,113,37,47>>, 30 | [{apps, Apps}, {host, "localhost"}, {port, 1337}, {receiver_pk, PK}| Config]. 31 | 32 | end_per_suite(Config) -> 33 | Apps = proplists:get_value(apps, Config), 34 | [ok = application:stop(A) || A <- Apps], 35 | ok. 36 | 37 | init_per_testcase(_Case, Config) -> 38 | Config. 39 | 40 | end_per_testcase(_Case, _Config) -> 41 | ok. 42 | 43 | %% Tests 44 | %% ---------------------------------------------------------------------- 45 | groups() -> 46 | [{basic, [shuffle, {repeat, 30}], [ 47 | send_recv 48 | ]}]. 49 | 50 | all() -> 51 | [{group, basic}]. 52 | 53 | send_recv(Config) -> 54 | SPid = sender(Config), 55 | RPid = receiver(Config), 56 | ok = join([SPid, RPid]), 57 | ok. 58 | 59 | %% ------------------------------------- 60 | join([]) -> ok; 61 | join([P | Next]) -> 62 | receive 63 | {P, ok} -> join(Next); 64 | {P, {error, Err}} -> ct:fail(Err) 65 | after ?TIMEOUT -> 66 | ct:fail(timeout) 67 | end. 68 | 69 | sender(Config) -> 70 | Ctl = self(), 71 | spawn(fun() -> 72 | random:seed(erlang:now()), 73 | ct:sleep(10), 74 | sleep(), 75 | {ok, Sock} = curve_tun:connect(?config(host, Config), ?config(port, Config), 76 | [{key, ?config(receiver_pk, Config)}]), 77 | sleep(), 78 | ok = curve_tun:send(Sock, <<"1">>), 79 | sleep(), 80 | ok = curve_tun:send(Sock, <<"2">>), 81 | sleep(), 82 | ok = curve_tun:send(Sock, <<"3">>), 83 | sleep(), 84 | ok = curve_tun:close(Sock), 85 | Ctl ! {self(), ok} 86 | end). 87 | 88 | receiver(Config) -> 89 | Ctl = self(), 90 | spawn(fun() -> 91 | random:seed(erlang:now()), 92 | ok = curve_tun_simple_registry:register({127,0,0,1},<<81,13,101,52,29,109,136,196,86,91,34,91,3,19,150,3,215, 43,210,9,242,146,119,188,153,245,78,232,94,113,37,47>>), 93 | {ok, LSock} = curve_tun:listen(?config(port, Config), [{reuseaddr, true}]), 94 | sleep(), 95 | {ok, Sock} = curve_tun:accept(LSock), 96 | sleep(), 97 | <<"1">> = curve_tun:recv(Sock), 98 | sleep(), 99 | <<"2">> = curve_tun:recv(Sock), 100 | sleep(), 101 | <<"3">> = curve_tun:recv(Sock), 102 | sleep(), 103 | ok = curve_tun:close(Sock), 104 | Ctl ! {self(), ok} 105 | end). 106 | 107 | sleep() -> 108 | ct:sleep(random:uniform(100)). 109 | --------------------------------------------------------------------------------