CurveCP Review – Part 1 Crypto
CurveCP is a secure transport protocol, that can be used instead of the common TCP+TLS combination. This article assumes familiarity with CurveCP. You should at least read the overview and the packets description.
In this post I’ll look only at the cryptographic part of CurveCP, including the implementation hints.
But I’ll ignore engineering issues, such as the choice of UDP over TCP, congestion control and the per-message overhead. I’ll probably talk about these in a later post.
I’m not a professional cryptographer, just a programmer with an interest in crypto.
So it’s easily possible that my “improvement” suggestions actually make the protocol less secure.
I like the basic design of CurveCP a lot:
- It’s much simpler than TLS
- It always offers forward secrecy
- It’s fast without needing to resort to ugly hacks like session resumption. Each connection costs the server four Curve25519 operations.
It doesn’t use certificates, but assumes that you obtained the public key through a different channel, such as DNS. This fits my usage, but might be a problem for others.
Adding an additional step, where the client requests the server’s certificate is easy. This can happen in the clear before the actual CurveCP connection is established. This step would add one round-trip latency, but can be cached. It’s stateless for the server.
- Client authentication is done using asymmetric crypto, where it keeps the client’s public key confidential. (TLS on the other hand sends the client certificate in plain, unless you use renegotiation.)
- Connections are not bound to an IP. For example they’ll survive switching between W-LAN and mobile networks without connection interruptions.
Issue 1: Nonces
CurveCP relies heavily on nonces. It uses a “box” operation that combines the stream cipher XSalsa20 with the Poly1305 MAC. Poly1305 is a Wegman-Carter style MAC, i.e. it is based on universal hashing.
As the name implies, a nonce should only be used once for each key. One interesting question here is what happens if a nonce is reused by accident:
- The stream cipher will leak the xor of the plaintexts. In many cases this allows the reconstruction of both plaintexts.
- The MAC is broken as well, since universal hashing relies on unique nonces
CurveCP uses nonces in two contexts:
- 64 bit counters with short term keys. If distinct short term keys are used, nonce reuse seems very unlikely. The damage is limited to the confidentiality and integrity of the sessions with reused keys. It’s possible that confidential data from such a compromized session can be used in further attacks on higher level protocols, or thanks to Issue 2 even on CurveCP itself.
- 128 bit nonces with long term keys.
For the long term nonces one obvious approach is using random 128 bit values. I see no problems with that approach, provided the RNG works correctly. A good RNG is already required for the short-term keys, so relying on a RNG for nonces doesn’t seem to make the situation much worse.
The CurveCP website on the other hand, recommends using a persistent counter. IMO that’s a very fragile approach, and it causes considerable annoyances in deployment. For example:
- When sharing a key between multiple computers, you need to implement nonce-separation. i.e. you need to make sure that the counters work on disjoint intervals
- Restoring from backup, or cloning a virtual machine can cause counter-rollbacks.
Thus I believe random nonces are preferable. I’d also consider switching to a nonce-less MAC(such as HMAC) for authentication with a long-term key.
Edit: I couldn’t find any practical attack, even if nonce reuse happens in the context of the CurveCP handshake. In particular I found no way to impersonate the server in such a situation, since to do that you’d need to know
S', the server’s short-term publickey. If the server reuses both short-term key and nonce, impersonation becomes possible.
Issue 2: Client Authentication
The client authenticates by sending a value
V = Box[C'](C->S) to the server. I think that this is a bad choice.
Problem 1: If an attacker learns client short term secret c’ of a single session, he will be able to impersonate that user to that server at any point in the future. This shouldn’t happen normally, but I think higher robustness for this failure case would be nice.
The attacker just opens a connection, using the same
V does not depend on
S', it’s still valid.
One scenario where this is a relevant attack is keeping the long term key in a secure hardware module, but handling the actual session from a lower trust computer.
This problem could be fixed by making the authenticated message dependent on the server’s short term key, which cannot be chosen by the attacker:
V = Box[C',S'](C->S). This adds a per-connection overhead of 32 bytes, which is negligible. It’s also very unlikely that this introduces any vulnerabilities not present in the original protocol.
Problem 2: If the server’s long term key gets compromized, it’s possible to impersonate arbitrary clients to it.
Anybody who knows s, can trivially calculate the key
C->S, and impersonate arbitrary clients. Once again this should not happen, but makes a failure much bigger than it should be.
To fix both problems at the same time, we could use
S' instead of
S as the second key. i.e.
V = Box[C',S](C->S') or
V = Box[C'](C->S').
Personally I’d like to include all four public keys and the servername into this authentication, and I’d replace crypto_box with a simple MAC, such as HMAC with
KeyExchange(C->S') as key. Switching to HMAC avoids nonce issues, and it also reduces the size of
V to 16 bytes. Including additional information, such as the servername allows a secure hardware device to ask a question like “Do you want to authenticate to ServerX?”. If ServerX is your bank’s website when you only want to check your mails, you notice that something is wrong.
Interestingly, in some contexts, such as an OTR like protocol, using
C->S' instead of
C->S is undesirable, since it weakens deniability for the client. But I still think it’s preferable in most situations.
Issue 3: Replay Attacks
This is an implementation pitfall, not an inherent problem of the protocol.
A naive server will accept an Initiate-Packet, as long as the associated minute-key is still valid, allowing an attacker to replay a full connection. To avoid this, you need to put
C' on a blacklist when initiating a connection. You can expire a blacklist entry when the associated minute key expires. It’s not enough to just prevent connections with a client short-term key that’s already in use (You need to do that too, since
C' doubles as connection identifier).
Edit: Locking at the implementation in NaCl-20110221 (an alpha version), it seems to be vulnerable to this. A related code section has the comment
/* XXX: cache cookie if it's recent */, which when implemented would probably fix the issue.
My conclusion is that CurveCP has one actual undesirable property in the protocol: The weak client authentication. The other issues can be avoided by a careful implementation.