veyron/runtimes/google/ipc/stream/crypto: use nacl/crypto_box
nacl/box is a well-studied, simple and robust public key encryption
scheme. A plain C implementation of the box function is less than 400
lines without any external dependencies (tweetnacl), compared to current
TLS implementations that consist of tens of thousands of lines. Chrome,
Quic, Tor, OpenSSH, ZeroMQ, End-to-End and other applications have recently
adopted a variant of nacl/box as several dreadful bugs have been found
in common implementations of either TLS or the cryptographic primitives
it uses. In particular, the Go crypto/tls package is not considered
production ready because timing-based attacks are not prevented.
Currently, Veyron is essentially using TLS as a glorified Diffie-Hellman
key exchange implementation -- all the complicated
public-key-infrastructure-related features are not needed at all.
This change implements the connection encryption hinted at in the end of
the spec (http://nacl.cr.yp.to/box.html). This change is here mostly for
experimentation, but it could in principle replace our use of TLS
entirely. Currently, replacing all uses of TLS in the codebase with the
implementation results in all non-TLS-specifc tests passing.
BenchmarkTLSEncrypt_1B: 0.57 MB/s; BenchmarkBoxEncrypt_1B: 1.23 MB/s
BenchmarkTLSEncrypt_5M: 272.84 MB/s; BenchmarkBoxEncrypt_5M: 596.14 MB/s.
BenchmarkRoundTrip results are essentially the same (134 MB/s at 5M).
TLS has 25 bytes of per-message overhead, box has 16.
Change-Id: Ia313b1a78467317e994d66897b23fd1a7888320f
diff --git a/runtimes/google/ipc/stream/crypto/box.go b/runtimes/google/ipc/stream/crypto/box.go
new file mode 100644
index 0000000..3ab2c33
--- /dev/null
+++ b/runtimes/google/ipc/stream/crypto/box.go
@@ -0,0 +1,87 @@
+package crypto
+
+import (
+ "bytes"
+ "code.google.com/p/go.crypto/nacl/box"
+ "crypto/rand"
+ "encoding/binary"
+ "fmt"
+ "io"
+ "net"
+ "veyron/runtimes/google/lib/iobuf"
+)
+
+type boxcrypter struct {
+ conn net.Conn
+ alloc *iobuf.Allocator
+ sharedKey [32]byte
+ writeNonce, readNonce uint64
+}
+
+// NewBoxCrypter uses Curve25519, XSalsa20 and Poly1305 to encrypt and
+// authenticate messages (as defined in http://nacl.cr.yp.to/box.html).
+// An ephemeral Diffie-Hellman key exchange is performed per invocation
+// of NewBoxCrypter; the data sent has forward security with connection
+// granularity. One round-trip is required before any data can be sent.
+// BoxCrypter does NOT do anything to verify the identity of the peer.
+func NewBoxCrypter(conn net.Conn, pool *iobuf.Pool) (Crypter, error) {
+ pk, sk, err := box.GenerateKey(rand.Reader)
+ if err != nil {
+ return nil, err
+ }
+ var theirPK [32]byte
+ errChan := make(chan error)
+ defer close(errChan)
+ go func() { _, err := conn.Write(pk[:]); errChan <- err }()
+ go func() { _, err := io.ReadFull(conn, theirPK[:]); errChan <- err }()
+ if err := <-errChan; err != nil {
+ return nil, err
+ }
+ if err := <-errChan; err != nil {
+ return nil, err
+ }
+ ret := &boxcrypter{conn: conn, alloc: iobuf.NewAllocator(pool, 0)}
+ box.Precompute(&ret.sharedKey, &theirPK, sk)
+ // Distinct messages between the same {sender, receiver} set are required
+ // to have distinct nonces. The server with the lexicographically smaller
+ // public key will be sending messages with 0, 2, 4... and the other will
+ // be using 1, 3, 5...
+ if bytes.Compare(pk[:], theirPK[:]) < 0 {
+ ret.writeNonce, ret.readNonce = 0, 1
+ } else {
+ ret.writeNonce, ret.readNonce = 1, 0
+ }
+ return ret, nil
+}
+
+func (c *boxcrypter) Encrypt(src *iobuf.Slice) (*iobuf.Slice, error) {
+ defer src.Release()
+ var nonce [24]byte
+ binary.LittleEndian.PutUint64(nonce[:], c.writeNonce)
+ c.writeNonce += 2
+ ret := c.alloc.Alloc(uint(len(src.Contents) + box.Overhead))
+ ret.Contents = box.SealAfterPrecomputation(ret.Contents[:0], src.Contents, &nonce, &c.sharedKey)
+ return ret, nil
+}
+
+func (c *boxcrypter) Decrypt(src *iobuf.Slice) (*iobuf.Slice, error) {
+ defer src.Release()
+ var nonce [24]byte
+ binary.LittleEndian.PutUint64(nonce[:], c.readNonce)
+ c.readNonce += 2
+ retLen := len(src.Contents) - box.Overhead
+ if retLen < 0 {
+ return nil, fmt.Errorf("ciphertext too short")
+ }
+ ret := c.alloc.Alloc(uint(retLen))
+ var ok bool
+ ret.Contents, ok = box.OpenAfterPrecomputation(ret.Contents[:0], src.Contents, &nonce, &c.sharedKey)
+ if !ok {
+ return nil, fmt.Errorf("message authentication failed")
+ }
+ return ret, nil
+}
+
+func (c *boxcrypter) String() string {
+ return fmt.Sprintf("%#v", *c)
+}
diff --git a/runtimes/google/ipc/stream/crypto/crypto_test.go b/runtimes/google/ipc/stream/crypto/crypto_test.go
index 063d222..4bbdd45 100644
--- a/runtimes/google/ipc/stream/crypto/crypto_test.go
+++ b/runtimes/google/ipc/stream/crypto/crypto_test.go
@@ -37,8 +37,8 @@
crypter.String() // Only to test that String does not crash.
}
-func TestTLS(t *testing.T) {
- c1, c2 := tlsCrypters(t)
+func testSimple(t *testing.T, crypters func(testing.TB) (Crypter, Crypter)) {
+ c1, c2 := crypters(t)
// Execute String just to check that it does not crash.
c1.String()
c2.String()
@@ -63,6 +63,9 @@
t.Logf("Byte overhead of encryption: %v", overhead)
}
+func TestTLS(t *testing.T) { testSimple(t, tlsCrypters) }
+func TestBox(t *testing.T) { testSimple(t, boxCrypters) }
+
func TestTLSNil(t *testing.T) {
c1, c2 := tlsCrypters(t)
if t.Failed() {
@@ -123,12 +126,27 @@
return c1, c2
}
-func benchmarkEncrypt(b *testing.B, size int) {
+func boxCrypters(t testing.TB) (Crypter, Crypter) {
+ serverConn, clientConn := net.Pipe()
+ crypters := make(chan Crypter)
+ for _, conn := range []net.Conn{serverConn, clientConn} {
+ go func(conn net.Conn) {
+ crypter, err := NewBoxCrypter(conn, iobuf.NewPool(0))
+ if err != nil {
+ t.Fatal(err)
+ }
+ crypters <- crypter
+ }(conn)
+ }
+ return <-crypters, <-crypters
+}
+
+func benchmarkEncrypt(b *testing.B, crypters func(testing.TB) (Crypter, Crypter), size int) {
plaintext := make([]byte, size)
if _, err := rand.Read(plaintext); err != nil {
b.Fatal(err)
}
- e, _ := tlsCrypters(b)
+ e, _ := crypters(b)
b.SetBytes(int64(size))
b.ResetTimer()
for i := 0; i < b.N; i++ {
@@ -140,13 +158,19 @@
}
}
-func BenchmarkEncrypt_1B(b *testing.B) { benchmarkEncrypt(b, 1) }
-func BenchmarkEncrypt_1K(b *testing.B) { benchmarkEncrypt(b, 1<<10) }
-func BenchmarkEncrypt_10K(b *testing.B) { benchmarkEncrypt(b, 10<<10) }
-func BenchmarkEncrypt_1M(b *testing.B) { benchmarkEncrypt(b, 1<<20) }
-func BenchmarkEncrypt_5M(b *testing.B) { benchmarkEncrypt(b, 5<<20) }
+func BenchmarkTLSEncrypt_1B(b *testing.B) { benchmarkEncrypt(b, tlsCrypters, 1) }
+func BenchmarkTLSEncrypt_1K(b *testing.B) { benchmarkEncrypt(b, tlsCrypters, 1<<10) }
+func BenchmarkTLSEncrypt_10K(b *testing.B) { benchmarkEncrypt(b, tlsCrypters, 10<<10) }
+func BenchmarkTLSEncrypt_1M(b *testing.B) { benchmarkEncrypt(b, tlsCrypters, 1<<20) }
+func BenchmarkTLSEncrypt_5M(b *testing.B) { benchmarkEncrypt(b, tlsCrypters, 5<<20) }
-func benchmarkRoundTrip(b *testing.B, size int) {
+func BenchmarkBoxEncrypt_1B(b *testing.B) { benchmarkEncrypt(b, boxCrypters, 1) }
+func BenchmarkBoxEncrypt_1K(b *testing.B) { benchmarkEncrypt(b, boxCrypters, 1<<10) }
+func BenchmarkBoxEncrypt_10K(b *testing.B) { benchmarkEncrypt(b, boxCrypters, 10<<10) }
+func BenchmarkBoxEncrypt_1M(b *testing.B) { benchmarkEncrypt(b, boxCrypters, 1<<20) }
+func BenchmarkBoxEncrypt_5M(b *testing.B) { benchmarkEncrypt(b, boxCrypters, 5<<20) }
+
+func benchmarkRoundTrip(b *testing.B, crypters func(testing.TB) (Crypter, Crypter), size int) {
plaintext := make([]byte, size)
if _, err := rand.Read(plaintext); err != nil {
b.Fatal(err)
@@ -166,8 +190,14 @@
plainslice.Release()
}
}
-func BenchmarkRoundTrip_1B(b *testing.B) { benchmarkRoundTrip(b, 1) }
-func BenchmarkRoundTrip_1K(b *testing.B) { benchmarkRoundTrip(b, 1<<10) }
-func BenchmarkRoundTrip_10K(b *testing.B) { benchmarkRoundTrip(b, 10<<10) }
-func BenchmarkRoundTrip_1M(b *testing.B) { benchmarkRoundTrip(b, 1<<20) }
-func BenchmarkRoundTrip_5M(b *testing.B) { benchmarkRoundTrip(b, 5<<20) }
+func BenchmarkTLSRoundTrip_1B(b *testing.B) { benchmarkRoundTrip(b, tlsCrypters, 1) }
+func BenchmarkTLSRoundTrip_1K(b *testing.B) { benchmarkRoundTrip(b, tlsCrypters, 1<<10) }
+func BenchmarkTLSRoundTrip_10K(b *testing.B) { benchmarkRoundTrip(b, tlsCrypters, 10<<10) }
+func BenchmarkTLSRoundTrip_1M(b *testing.B) { benchmarkRoundTrip(b, tlsCrypters, 1<<20) }
+func BenchmarkTLSRoundTrip_5M(b *testing.B) { benchmarkRoundTrip(b, tlsCrypters, 5<<20) }
+
+func BenchmarkBoxRoundTrip_1B(b *testing.B) { benchmarkRoundTrip(b, boxCrypters, 1) }
+func BenchmarkBoxRoundTrip_1K(b *testing.B) { benchmarkRoundTrip(b, boxCrypters, 1<<10) }
+func BenchmarkBoxRoundTrip_10K(b *testing.B) { benchmarkRoundTrip(b, boxCrypters, 10<<10) }
+func BenchmarkBoxRoundTrip_1M(b *testing.B) { benchmarkRoundTrip(b, boxCrypters, 1<<20) }
+func BenchmarkBoxRoundTrip_5M(b *testing.B) { benchmarkRoundTrip(b, boxCrypters, 5<<20) }