curvetls

package module
v0.0.0-...-80cba8b Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 28, 2016 License: GPL-3.0 Imports: 10 Imported by: 0

README

curvetls: a simple, robust transport encryption package

curvetls is a Go library that gives you a robust framing and encryption layer for your Go programs, striving to be secure, strict, and simple.

With curvetls, it's dead easy to go from ordinary sockets to secure encrypted channels that support framing. This makes it trivial for you to write secure, robust clients and servers that do not need to implement low-level control flow. curvetls does not use large or unproven libraries, avoids unsafe C bindings, follows well-documented specifications, practices well-understood cryptography, and avoids placing undue trust in peers, even authenticated ones.

This library gives you a layered, stackable wrapper (client / server) for network I/O, which allows you to upgrade regular network sockets to the curvetls protocol. All the wrapper needs is a key pair, a random nonce, and a socket whose underlying transport is of any reliable kind (e.g. a TCP- or file-backed net.Conn).

curvetls is documented with developers' interests in mind. Take a look at the documentation online. Alternatively, clone this repository, then run godoc against it.

Features

  • Simple and robust elliptic curve encryption of communications between peers.
  • Well-defined, robust framing scheme for reliable delivery of whole messages, based on the ZeroMQ ZMTP specification.
  • Robust public key authentication scheme to let servers decide which clients are authorized to proceed, based on the CurveZMQ spec.
  • Straightforward use of the library in your network clients and servers.

Test client programs

In addition to the library, this project ships three demon programs, which can show you how to use the library:

  • curvetls-genkeypair generates keypairs for the use of the other command-line programs
  • curvetls-pingpong-server implements a test ping-pong server
  • curvetls-pingpong-client implements a test ping-pong client

To run these programs, you can simply compile the library after cloning it to the local directory:

    [user@host ~]$ cd /path/to/curvetls
    [user@host curvetls]$ make
    [user@host curvetls]$ 

Generate some key pairs:

    [user@host curvetls]$ bin/curvetls-genkeypair  # note these for the server
    Private key: pT6GGmPNgSPsGKD8UTPdVN50xOGeZr+eb53gfAYoeVm4=
    Public key:  Puwo38S2npQijFuh5cuShYpTnQ+ZupkwveS/A1HjjkSY=
    Tip:         Both keys are encoded in base64 format with a one-character key type prefix
    [user@host curvetls]$ bin/curvetls-genkeypair  # note these for the client
    Private key: paICEhaq2fBJkCRoIMbncQ2sv+LolEvjgM43DYcrQpqM=
    Public key:  Pr59DbWYjUHlj0Z8kAY9LUyP/8hUi5kC+ByX6xvPKIwc=
    Tip:         Both keys are encoded in base64 format with a one-character key type prefix
    [user@host curvetls]$ 

Run the server (in the background):

    [user@host curvetls]$ bin/curvetls-pingpong-server 127.0.0.1:9001 \
        pT6GGmPNgSPsGKD8UTPdVN50xOGeZr+eb53gfAYoeVm4= \
        Puwo38S2npQijFuh5cuShYpTnQ+ZupkwveS/A1HjjkSY= \
        Pr59DbWYjUHlj0Z8kAY9LUyP/8hUi5kC+ByX6xvPKIwc= &

Run the client:

    [user@host curvetls]$ bin/curvetls-pingpong-client 127.0.0.1:9001 \
        paICEhaq2fBJkCRoIMbncQ2sv+LolEvjgM43DYcrQpqM= \
        Pr59DbWYjUHlj0Z8kAY9LUyP/8hUi5kC+ByX6xvPKIwc= \
        Puwo38S2npQijFuh5cuShYpTnQ+ZupkwveS/A1HjjkSY=

And see the ping-pong happen. The server will exit as soon as it is done with the first connection.

Feel free to Wireshark the programs as they execute, to verify that data is, in fact, being encrypted as it goes from program to program.

Run the programs with no arguments to get usage information.

Quality, testing and benchmarking

To run the tests:

    make test

To run a variety of benchmarks (such as message encryption and decryption):

    make bench

curvetls releases should not come with failing tests. If a test fails, that is a problem and you should report it as an issue right away.

Goals and motivations

As security software, curvetls has the following goals:

  • To enable users of this library to depend on as little code as possible, with special emphasis on reducing unsafe code.
  • To give implementors a simple way to enable encryption between two peers, with as little effort as possible.
  • To make sure that implementors do not have to deal with any low-level details that they may screw up, compromising the security of their programs.
  • To ensure that users of this library do not have to deal with hidden surprises, such as servers allowing clients to allocate unbound resources.

curvetls focuses on getting the low-level security details right, so that you do not have to.

Why curvetls instead of net.tls?

Some people have asked why this library needs to exist, given that Go has net/tls, which is a high-performance crypto library.

The answer is that net/tls is much, much more than just a crypto library, and that has implications for security and complexity. There's a niche in communications where TLS is overkill but plain TCP is irresponsible, and that is a niche which many packages have attempted to fill, from CurveCP to tcpcrypt. curvetls fills this niche quite nicely.

The list-form, practical answer to why you may want to avoid net/tls:

  • A PKI system with certificates imposes on the implementor the additional burden of having to manage the certificate authority that emits the certificates, possibly a revocation infrastructure, both for clients and servers.
  • PKI as implemented in the modern world, including in net/tls, is a bit of a mess in that you have to write extra code if you want to do something that's outside the norm, but still perfectly sensible for certain use cases. Like, say, have clients reject certificates not signed by VeriSign, or have full cert validation without domain name validation. This demands configuration code that you must get right in your program.
  • X.509 certificates are very complex compared to simple base64 strings (what this library uses). There have been vulnerabilities, sometimes years-old, in certificate parsing code.
  • TLS itself is highly complex, because of backwards compatibility reasons and the need to support many ciphers. This complexity has given rise to many security issues as well as many opportunities for the implementor to shoot himself on the foot. This is 100% unneeded complexity if all you want is to send / receive well-encrypted data between two private peers.

TLS is fine and dandy, very well supported in Go via the net/tls package, and many use cases effectively require you to use TLS. However, TLS brings in a lot more complexity than just handshake plus NaCL encryption, and that increases the attack surface. Sometimes all you need is a simple drop-in implementation of peer-to-peer public key crypto. That's what curvetls aims to do well. I think four lines of (non-error handling) code — one for creating a keypair, one for creating a nonce, one for driving the handshake, and one for authorizing the client — is as simple as it can get, and the code that runs underneath is far less complex than anything you get with invoking any of net.tls for the same use case.

Why are you rolling your own crypto code / protocol?

Let's be 100% blunt: curvetls does not roll its own crypto. The crypto in curvetls is the same crypto as the NaCL library, which is fast, well-tested and presumed to be strong.

curvetls also does not roll its own protocol. One of the goals of curvetls is to be interoperable with CurveZMQ DEALER sockets in reliable mode (e.g. TCP). As such, we implement the pertinent specification, which are very good specifications — 100% unambiguous — and enjoy many implementations from competing entities.

curvetls users also enjoy the client / server handshake and send / receive framing that is the great work of the ZeroMQ folks (to my knowledge, primarily Pieter Hintjens). In practical terms, this means you, as a user of curvetls, do not have to worry about authentication / authorization state machines or incomplete messages. A peer is either authorized or not. A message is either fully-received or not.

Why not CurveZMQ instead?

ZeroMQ is great software, but it has three problems, one Go-specific and two more in general w.r.t. security:

Problem numero uno: you can't really send on a ZeroMQ socket in a goroutine while receiving on that same socket in another goroutine. Your program will crash if you do. This is fundamental if you want to have a program that sends and receives at the same time, without having to "take turns", HTTP style. There's ways you can get around that — PAIR inproc socket pairs for in-process communication, pairs of DEALER sockets on each peer, poll loops and reactors — but all of these ways impose extra complexity and a very unnatural and non-idiomatic programming regime for Go programs.

curvetls sockets, in contrast, are safe to Read() from one goroutine while another goroutine Write()s to them. They work in the expected manner and do not require you to implement any bespoke multiplexing solutions.

Problem numero dos: if you use the existing ZeroMQ implementations, then you are bringing into your process a lot of unsafe code, plus a lot of code you don't need just to do peer-to-peer encryption and authentication.

curvetls effectively implements the most basic use case of ZeroMQ plus CurveZMQ, without the extra dependency of ZeroMQ, or any unsafe C code. This package depends on no unsafe libraries, beyond perhaps the Go NaCL implementation or the Go standard library itself.

Problem numero tres: did you know that ZeroMQ happily lets clients send 1 GB buffers, and allocates that memory on the server to receive them?

We have a high-priority item on our roadmap which involves giving implementors a knob that lets them limit the amount of memory a single frame can consume. Because in ZeroMQ a frame can be effectively as large as you can imagine, and the frame will not be delivered to the peer until the peer has read all of it into memory, malicious clients which have successfully completed the handshake — perhaps they stole a keypair, perhaps the server Allow()s all peers — can bring a server down by making it allocate inordinately large amounts of memory.

Additionally, curvetls — unlike CurveZMQ — will not accept any metadata from a peer during the handshake (which happens before the peer has been authenticated). CurveZMQ metadata is effectively specified to be as big as you can imagine, which lets clients (and servers!) fill memory on your server before the CurveZMQ handshake completes. On the roadmap we have an item which involves adding support for metadata during handshake, but not before we can provide you, the implementor, with a knob that limits the amount of metadata a peer is allowed to send.

Problem numero cuatro: ZeroMQ happily accepts as many connections as peers, including hostile peers, will send its way. You are not in control of the Accept() call — your code only gets notified of messages, not of peer connections and disconnections. These are some odd socket semantics which work well in many use cases that involve trustworthy peers, but these semantics work badly outside of it. Additionally, you have to write extra code in order to track identities — ZeroMQ will not, by default, let you track of peers by key identity, mostly assuming that a message is a message is a message, irrespective of which peer is sending it. Effectively, you have reduced control over the low-level connection and authentication process, when you implement a ZeroMQ server. You can solve the authentication and authorization issue, but the low-level connection acceptance and throttling part is strictly off-limits to you as a programmer.

In curvetls, you are in charge of connecting / listening / accepting / tracking / closing sockets. This lets you implement custom throttling policies based on which peer is connecting prior to the handshake itself, and it lets you know verifiably which peer has connected as soon as the handshake is over. You want these properties when writing robust servers. Have a traffic storm or more clients than your program wants to handle? Throttle the socket Accept(). Have a peer that is already active and authenticated but wants to connect for a second time? Close the socket on it as soon as the handshake is over. Have a peer that is relentlessly connecting when you don't want it to? Close the socket on it as soon as the Accept() returns, or run a firewall rule change — you have the peer's IP address right after Accept(), after all.

These were the security concerns I needed to address when I set out to write curvetls, and I'm happy to report they have either been addressed or been considered high-priority and active work.

Why not CurveCP?

The first reason is that there are no complete implementations of CurveCP for Go. You can take the existing implementation and write a binding for it, but that was much more work than implementing a well-documented specification in a memory-safe language.

The second reason is that, even if you do a binding to CurveCP, the full power of the CurveCP security mechanism would then be available to implementors but with the burden of having to rely on unsafe code that is basically abandoned.

The third reason: CurveCP brings with it the extra code of implementing a reliable protocol over UDP. This aspect of CurveCP is truly a noble project that can revolutionize the Internet — if it hasn't already, as CurveCP was the forefather of Google's QUIC — but it's still extra code that is less tested than TCP, and it puts more complexity in the path between peer and your program's processing code.

Why not (this thing I haven't heard of)?

I'm happy to read the code of that thing and talk to you about it. Who knows, maybe that thing will render curvetls entirely unnecessary?

Technical and compatibility information

Compatibility:

Any deviations from the CurveZMQ handshake specification, or interoperability problems with CurveZMQ implementations, as well as deviations and problems from / with the ZeroMQ framing scheme, are bugs. You should report them, so we can fix them.

Note that, if you choose to use unreliable transports such as UDP, you must roll your own congestion and retransmission features on each net.Conn you intend to wrap. Perhaps the right way to go about it, is to write a similar wrapping library which will wrap (let's say, UDP) network I/O sockets using the CurveCP congestion algorithm as specified in its documentation. Such a wrapper, if it returns net.Conn instances, will be compatible with this work.

The license of this library is GPLv3 or later. See file COPYING for details. For relicensing inquiries, contact the author.

Documentation

Overview

Package curvetls is a simple, robust transport encryption library.

This is a pluggable wrapper (client / server) for network I/O, which allows you to upgrade your regular network sockets to a protocol that supports robust framing, transport security and authentication, so long as your net.Conn is of any reliable kind (e.g. a TCP- or file-backed net.Conn).

Usage Instructions

(a) Generate keypairs for clients and server, persisting them to disk if you want to, so you can later load them again.

(b) Distribute, however you see fit, the public keys of the server to the clients, and the public keys of the clients to the server.

(c) Generate one long nonce per server keypair, and one long nonce per client keypair. You can do this at runtime. Never reuse the same long nonce for two different keypairs.

(d) Make your server Listen() on a TCP socket, and Accept() incoming connections to obtain one or more server net.Conn.

(e) Make your clients Connect() on a TCP socket to the Listen() address of the server.

(f) On your client, right after Connect(), wrap the net.Conn you received by using WrapClient() on that client net.Conn, and giving it the client keypair, its corresponding client long nonce, and the server public key. WrapClient() will return an encrypted socket you can use to talk to the server.

(g) On your server, right after Accept(), wrap the net.Conn you received by using WrapServer() on that server net.Conn, and giving it the server keypair together with its corresponding server long nonce. Use the authorizer and the public key that WrapServer() returns to decide whether to call Allow() or Deny() on the authorizer. Allow() will return an encrypted socket you can use to talk to the client.

Congratulations, at this point you have a connection between peers that is encrypted with (a limited version of) the CurveZMQ protocol.

Sending and receiving traffic is covered by the documentation of the Read(), ReadFrame() and Write() methods of EncryptedConn. Two example programs are included in the cmd/ directory of this package.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func GenKeyPair

func GenKeyPair() (Privkey, Pubkey, error)

GenKeyPair generates a pair of private and public keys as Privkey and Pubkey structs.

It is safe to invoke this function concurrently.

func IsAuthenticationError

func IsAuthenticationError(e error) bool

IsAuthenticationError returns true when the error returned by WrapClient() was caused by the server rejecting the client for authentication reasons with Deny().

func NewLongNonce

func NewLongNonce() (*longNonce, error)

NewLongNonce generates a long nonce for use with curvetls.WrapServer and curvetls.WrapClient. A long nonce is needed and must be unique per long-term private key, whether the private key belongs to the server or the client. Long nonces must not be reused for new private keys.

func WrapServer

func WrapServer(conn net.Conn,
	serverprivkey Privkey,
	serverpubkey Pubkey,
	long_nonce *longNonce) (*Authorizer, Pubkey, error)

WrapServer wraps an existing, connected net.Conn with encryption and framing.

Returned Values

An Authorizer object with two methods Allow() and Deny(), one of which you must call. Allow() will return an EncryptedConn (a net.Conn compatible object) that you can use to send and receive data. See the documentation of Authorizer for more information.

The public key of the client; use this key to authenticate the client and decide whether it is authorized to continue the conversation, then either call Allow() to signal to the client that it is authorized, or call Deny() to signal to the client that it is not authorized and terminate the connection.

An error. It can be an underlying socket error, an internal error produced by a bug in the library, or a protocol error indicating that the communication encountered corrupt or malformed data from the peer.

Lifecycle Information

If WrapServer() returns an error, the passed socket will have been closed by the time this function returns.

If you read or write any data to the underlying socket rather than go through the returned socket, your data will be transmitted in plaintext and the endpoint will become confused and close the connection. Don't do that.

Types

type Authorizer

type Authorizer struct {
	// contains filtered or unexported fields
}

Authorizer is returned by WrapServer() together with the connecting client's public key. It lets your server make an authorization decision with respect to the client.

By the time the caller of WrapServer() has received the authorizer and the client's public key, the client has been authenticated (to mean: the server knows the client truthfully holds its keypair). At that point your server can invoke an authorization service of your choice to decide whether the client is authorized to proceed.

To signal to the client that it is authorized to proceed, call Allow() on the authorizer. This returns an EncryptedConn that lets your server communicate with the client securely.

Conversely, to signal to the client that it is not authorized, call Deny() on the authorizer.

You must call one of the two methods. Failure to do so will leave the client hanging, and will leak file descriptors on the server.

See the documentation of Allow() and Deny() for important information.

func (*Authorizer) Allow

func (c *Authorizer) Allow() (*EncryptedConn, error)

Allow signals the client that it is authorized, finishing the handshake and returning an EncryptedConn to talk to the client.

Returned Values

An EncryptedConn (a net.Conn compatible object) that you can use to send and receive data. Data sent and received will be framed and encrypted.

Upon successful return of this function, the Close() method of the returned net.Conn will also Close() the underlying net.Conn.

Lifecycle Information

If Allow() returns an error, the passed socket will have been closed by the time this function returns.

func (*Authorizer) Deny

func (c *Authorizer) Deny() error

Deny, signals the client that it is not authorized to continue, and closes the underlying socket passed to WrapServer.

Lifecycle Information

When Deny() returns, the underlying socket will have been closed too.

Clients which receive a Deny() denial SHALL NOT reconnect with the same credentials, but wise implementors know that hostile clients can do what they want, so they will need to implement throttling based on public key. WrapServer() returns the verified public key of the client before the server has made an authentication policy decision, so the server can implement throttling based on client public key.

type EncryptedConn

type EncryptedConn struct {
	net.Conn // FIXME reorg struct to make it more efficient, and that which is accessed frequenly should be clustered together, and measure perf diff
	// contains filtered or unexported fields
}

EncryptedConn is the opaque structure representing an encrypted connection.

On the client, use WrapClient() to obtain one. On the server, use WrapServer() to obtain an Authorizer and then invoke Allow() on the authorizer to obtain an EncryptedConn.

Then, use the EncryptedConn methods to engage in secure communication.

EncryptedConn implemenets the net.Conn interface.

Lifecycle Information

In general, it is not thread safe to perform reads or writes on an EncryptedConn while any part of a handshake (WrapClient(), WrapServer(), Allow() or Deny()) is going on in a different goroutine. You should complete the handshakes on a single goroutine. It is also not safe to perform reads simultaneously on two or more goroutines. It is also not safe to perform writes simultaneously on two or more goroutines. It is also not safe to intersperse calls to Read() and ReadFrame(), even from the same goroutine.

Concurrent things that are safe: (1) one read and one write each on a distinct goroutine (2) same as (1) while Close() is invoked on another goroutine (the ongoing read and write should return normally with an EOF or UnexpectedEOF in that case). (3) performing any operation on one EncryptedConn in a single goroutine, while any other operation on another EncryptedConn is ongoing in another goroutine. There is no global mutable state shared among EncryptedConn instances.

func WrapClient

func WrapClient(conn net.Conn,
	clientprivkey Privkey, clientpubkey Pubkey,
	permServerPubkey Pubkey,
	long_nonce *longNonce) (*EncryptedConn, error)

WrapClient wraps an existing, connected net.Conn with encryption and framing.

Returned Values

An EncryptedConn (a net.Conn compatible object) that you can use to send and receive data. Data sent and received will be framed and encrypted.

An error. It can be an underlying socket error, an internal error produced by a bug in the library, a protocol error indicating that the communication encountered corrupt or malformed data from the peer, or an authentication error. A method to distinguish authentication errors is provided by the IsAuthenticationError() function. No method is provided to distinguish among the other errors because the only sane thing to do at that point is to close the connection.

Lifecycle Information

If WrapClient() returns an error, the passed socket will have been closed by the time this function returns.

Upon successful return of this function, the Close() method of the returned net.Conn will also Close() the passed net.Conn.

Upon unauthorized use (the server Authorizer rejects the client with Deny()) this function will return an error which can be checked with the function IsAuthenticationError(). See note on Deny() to learn more about reconnection policy.

If you read or write any data to the underlying socket rather than go through the returned socket, your data will be transmitted in plaintext and the endpoint will become confused and close the connection. Don't do that.

func (*EncryptedConn) Read

func (w *EncryptedConn) Read(b []byte) (int, error)

Read reads one frame from the other side, decrypts the encrypted frame, then copies the bytes read to the passed slice.

If the destination buffer is not large enough to contain the whole received frame, then a partial read is made and written to the buffer, and subsequent Read() calls will continue reading the remainder of that frame.

When the peer has closed the socket, Read() will return a standard EOF.

Lifecycle Information

If Read() returns an error, the socket remains technically open, but (much like TLS) it is highly unlikely that, after your program receives the error, the connection will continue working.

It is an error to invoke an EncryptedConn's Read() from a goroutine while another goroutine is invoking Read() or ReadFrame() on the same EncryptedConn. Even with plain old sockets, you'd get nothing but corrupted reads that way. It should, however, be safe to invoke Read() on an EncryptedConn within one goroutine while another goroutine invokes Write() on the same EncryptedConn.

func (*EncryptedConn) ReadFrame

func (w *EncryptedConn) ReadFrame() ([]byte, error)

ReadFrame reads one frame from the other side, decrypts the encrypted frame, then returns the whole frame as a slice of bytes.

When the peer has closed the socket, ReadFrame() will return a standard EOF.

Lifecycle Information

If ReadFrame() returns an error, the socket remains technically open, but (much like TLS) it is highly unlikely that, after your program receives the error, the connection will continue working.

It is an error to call ReadFrame when a previous Read was only partially written to its output buffer.

It is an error to invoke an EncryptedConn's ReadFrame() from a goroutine while another goroutine is invoking ReadFrame() or Read() on the same EncryptedConn. Even with plain old sockets, you'd get nothing but corruption that way. It should, however, be safe to invoke ReadFrame() on an EncryptedConn within one goroutine while another goroutine invokes Write() on the same EncryptedConn.

func (*EncryptedConn) Write

func (w *EncryptedConn) Write(b []byte) (int, error)

Write frames, encrypts and sends to the other side the passed bytes.

If this function returns an error, the socket remains open, but (much like TLS) it is highly unlikely that, after returning an error, the connection will continue working.

It is an error to invoke Write() on the same EncryptedConn simultaneously from two goroutines. Even with plain old sockets, you'd get nothing but corruption that way. It should, however, be safe to invoke Write() on an EncryptedConn within one goroutine while another goroutine invokes Read() or ReadFrame() on the same EncryptedConn.

type Privkey

type Privkey key

Privkey is an opaque type representing a private key as used in curvetls.

func PrivkeyFromString

func PrivkeyFromString(s string) (p Privkey, err error)

PrivkeyFromString deserializes a Privkey as supplied in the string. See Privkey.String() for information on the string format of Privkeys.

func (Privkey) String

func (k Privkey) String() string

String format of Privkey is the letter "p" plus a base64 rendering of 32 bytes.

type Pubkey

type Pubkey key

Pubkey is an opaque type representing a public key as used in curvetls.

func PubkeyFromString

func PubkeyFromString(s string) (p Pubkey, err error)

PubkeyFromString deserializes a Pubkey as supplied in the string. See Pubkey.String() for information on the string format of Pubkeys.

String format of Privkey is the letter p plus a base64 rendering of 32 bytes.

func (Pubkey) String

func (k Pubkey) String() string

String format of Privkey is the letter "P" plus a base64 rendering of 32 bytes.

Directories

Path Synopsis
cmd

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL