veyron/runtimes/google/ipc/stream: Implement control-channel encryption.
For VIFs, perform versioning, authentication, and encrypt the control
data (VC data is already encrypted).
In this implementation, authentication/encryption is initiated by the
client.
- The VIF starts off using the standard unencrypted protocol.
- The client calculates the expected IPC version by intersecting
its own supported version range with the server's range that
is published via the endpoint. If the version suports
encryption, the client immediately begins negotiating
with the server.
Negotiation is performed using Encrypt control messages to set up a
bidirectional flow between the client and server. This is like a VC,
but it is hop-by-hop, instead of end-to-end.
- Each party sends its acceptable verson ranges together with
public data for initiating encryption. There is only one
supported encryption scheme now, but the options should
eventually include data for each of the encryption protocols
the party is willing to support.
- Currently the only encryption protocol is based on
code.google.com/p/go.crypto/nacl/box,
which uses Curve25519, XSalsa20 and Poly1305 to encrypt and
authenticate messages. The parties exchange public keys
to create a box cipher.
- Perform the standard Veyron handshake using the cipher for
encryption.
- On success, use the cipher to encrypt the control data.
RULES:
- VIF message formats never change, except in compatible ways.
New developments can only change the VIF protocol by adding
more message types.
- The format of the public data header in the Encrypt
negotiation never changes for all time.
A VIF frame has two parts, a (Type + PayloadSize) header word, and
some payload. The Type+PayloadSize is encrypted with the cipher's
XORKeyStream. The rest of the control data is encrypted with the
cipher's Seal method. This means that none of the data is observable
by an adversary, but the type and length are subject to corruption
(the rest of the data is not). This doesn't matter -- if the Type
or PayloadSize is corrupted by an adversary, the payload will be
misread, and will fail to validate.
We could potentially pass the Type and PayloadSize in the clear,
but then the framing would be observable, a (probably minor)
information leak. There is no reason to do so, we encrypt everything.
TIMES:
$ veyron go test veyron.io/veyron/veyron/runtimes/google/ipc/stream/benchmark -bench=Flow -run=X
Before:
BenchmarkFlow_1VIF_1VC_1Flow 50000 45029 ns/op 1137.02 MB/s
BenchmarkFlow_1VIF_1VC_2Flow 50000 39475 ns/op 1296.99 MB/s
BenchmarkFlow_1VIF_1VC_8Flow 50000 42355 ns/op 1208.82 MB/s
BenchmarkFlow_1VIF_2VC_2Flow 50000 40518 ns/op 1263.62 MB/s
BenchmarkFlow_1VIF_2VC_8Flow 50000 42861 ns/op 1194.55 MB/s
BenchmarkFlow_2VIF_4VC_8Flow 50000 43486 ns/op 1177.38 MB/s
BenchmarkFlow_1VIF_1VC_1FlowTLS 5000 521504 ns/op 98.18 MB/s
BenchmarkFlow_1VIF_1VC_2FlowTLS 5000 503743 ns/op 101.64 MB/s
BenchmarkFlow_1VIF_1VC_8FlowTLS 5000 508953 ns/op 100.60 MB/s
BenchmarkFlow_1VIF_2VC_2FlowTLS 5000 503255 ns/op 101.74 MB/s
BenchmarkFlow_1VIF_2VC_8FlowTLS 5000 510379 ns/op 100.32 MB/s
BenchmarkFlow_2VIF_4VC_8FlowTLS 5000 511113 ns/op 100.17 MB/s
ok veyron.io/veyron/veyron/runtimes/google/ipc/stream/benchmark 32.253s
After:
BenchmarkFlow_1VIF_1VC_1Flow 50000 43946 ns/op 1165.05 MB/s
BenchmarkFlow_1VIF_1VC_2Flow 50000 39394 ns/op 1299.67 MB/s
BenchmarkFlow_1VIF_1VC_8Flow 50000 43537 ns/op 1175.98 MB/s
BenchmarkFlow_1VIF_2VC_2Flow 50000 39945 ns/op 1281.74 MB/s
BenchmarkFlow_1VIF_2VC_8Flow 50000 41531 ns/op 1232.80 MB/s
BenchmarkFlow_2VIF_4VC_8Flow 50000 43568 ns/op 1175.17 MB/s
BenchmarkFlow_1VIF_1VC_1FlowTLS 5000 538491 ns/op 95.08 MB/s
BenchmarkFlow_1VIF_1VC_2FlowTLS 5000 522869 ns/op 97.92 MB/s
BenchmarkFlow_1VIF_1VC_8FlowTLS 5000 526982 ns/op 97.16 MB/s
BenchmarkFlow_1VIF_2VC_2FlowTLS 5000 522381 ns/op 98.01 MB/s
BenchmarkFlow_1VIF_2VC_8FlowTLS 5000 528707 ns/op 96.84 MB/s
BenchmarkFlow_2VIF_4VC_8FlowTLS 5000 530732 ns/op 96.47 MB/s
ok veyron.io/veyron/veyron/runtimes/google/ipc/stream/benchmark 33.302s
Change-Id: I3fcb0346dd23f77d549744fe061695a2dd606215
diff --git a/runtimes/google/ipc/stream/vif/auth.go b/runtimes/google/ipc/stream/vif/auth.go
new file mode 100644
index 0000000..6a05496
--- /dev/null
+++ b/runtimes/google/ipc/stream/vif/auth.go
@@ -0,0 +1,265 @@
+package vif
+
+import (
+ "crypto/rand"
+ "errors"
+ "fmt"
+ "io"
+ "net"
+
+ "code.google.com/p/go.crypto/nacl/box"
+
+ "veyron.io/veyron/veyron/runtimes/google/ipc/stream/crypto"
+ "veyron.io/veyron/veyron/runtimes/google/ipc/stream/message"
+ "veyron.io/veyron/veyron/runtimes/google/ipc/stream/vc"
+ "veyron.io/veyron/veyron/runtimes/google/ipc/version"
+ "veyron.io/veyron/veyron/runtimes/google/lib/iobuf"
+ "veyron.io/veyron/veyron2/ipc/stream"
+ ipcversion "veyron.io/veyron/veyron2/ipc/version"
+ "veyron.io/veyron/veyron2/options"
+ "veyron.io/veyron/veyron2/security"
+)
+
+var (
+ errUnsupportedEncryptVersion = errors.New("unsupported encryption version")
+ errVersionNegotiationFailed = errors.New("encryption version negotiation failed")
+ nullCipher crypto.NullControlCipher
+)
+
+// privateData includes secret data we need for encryption.
+type privateData struct {
+ naclBoxPrivateKey [32]byte
+}
+
+// AuthenticateAsClient sends a HopSetup message if possible. If so, it chooses
+// encryption based on the max supported version.
+//
+// The sequence is initiated by the client.
+//
+// - If the versions include IPCVersion6 or greater, the client sends a
+// HopSetup message to the server, containing the client's supported
+// versions, and the client's crypto options. The HopSetup message
+// is sent in the clear.
+//
+// - When the server receives the HopSetup message, it calls
+// AuthenticateAsServer, which constructs a response HopSetup containing
+// the server's version range, and any crypto options.
+//
+// - For IPCVersion6, the client and server generate fresh public/private key
+// pairs, sending the public key to the peer as a crypto option. The
+// remainder of the communication is encrypted as HopSetupStream messages
+// using NewControlCipherIPC6, which is based on
+// code.google.com/p/go.crypto/nacl/box.
+//
+// - Once the encrypted HopSetupStream channel is setup, the client and
+// server authenticate using the vc.AuthenticateAs{Client,Server} protocol.
+//
+// Note that the HopSetup messages are sent in the clear, so they are subject to
+// modification by a man-in-the-middle, which can currently force a downgrade by
+// modifying the acceptable version ranges downward. This can be addressed by
+// including a hash of the HopSetup message in the encrypted stream. It is
+// likely that this will be addressed in subsequent protocol versions (or it may
+// not be addressed at all if IPCVersion6 becomes the only supported version).
+func AuthenticateAsClient(conn net.Conn, versions *version.Range, principal security.Principal, dc vc.DischargeClient) (crypto.ControlCipher, error) {
+ if versions == nil {
+ versions = version.SupportedRange
+ }
+ if principal == nil {
+ // If there is no principal, we do not support encryption/authentication.
+ var err error
+ versions, err = versions.Intersect(&version.Range{Min: 0, Max: ipcversion.IPCVersion5})
+ if err != nil {
+ return nil, err
+ }
+ }
+ if versions.Max < ipcversion.IPCVersion6 {
+ return nullCipher, nil
+ }
+
+ // The client has not yet sent its public data. Construct it and send it.
+ pvt, pub, err := makeHopSetup(versions)
+ if err != nil {
+ return nil, err
+ }
+ if err := message.WriteTo(conn, &pub, nullCipher); err != nil {
+ return nil, err
+ }
+
+ // Read the server's public data.
+ pool := iobuf.NewPool(0)
+ reader := iobuf.NewReader(pool, conn)
+ pmsg, err := message.ReadFrom(reader, nullCipher)
+ if err != nil {
+ return nil, err
+ }
+ ppub, ok := pmsg.(*message.HopSetup)
+ if !ok {
+ return nil, errVersionNegotiationFailed
+ }
+
+ // Choose the max version in the intersection.
+ vrange, err := pub.Versions.Intersect(&ppub.Versions)
+ if err != nil {
+ return nil, err
+ }
+ v := vrange.Max
+ if v < ipcversion.IPCVersion6 {
+ return nullCipher, nil
+ }
+
+ // Perform the authentication.
+ switch v {
+ case ipcversion.IPCVersion6:
+ return authenticateAsClientIPC6(conn, reader, principal, dc, &pvt, &pub, ppub)
+ default:
+ return nil, errUnsupportedEncryptVersion
+ }
+}
+
+func authenticateAsClientIPC6(writer io.Writer, reader *iobuf.Reader, principal security.Principal, dc vc.DischargeClient,
+ pvt *privateData, pub, ppub *message.HopSetup) (crypto.ControlCipher, error) {
+ pbox := ppub.NaclBox()
+ if pbox == nil {
+ return nil, errVersionNegotiationFailed
+ }
+ c := crypto.NewControlCipherIPC6(&pbox.PublicKey, &pvt.naclBoxPrivateKey, false)
+ sconn := newSetupConn(writer, reader, c)
+ // TODO(jyh): act upon the authentication results.
+ _, _, _, err := vc.AuthenticateAsClient(sconn, principal, dc, crypto.NewNullCrypter(), ipcversion.IPCVersion6)
+ if err != nil {
+ return nil, err
+ }
+ return c, nil
+}
+
+// AuthenticateAsServer handles a HopSetup message, choosing authentication
+// based on the max common version.
+//
+// See AuthenticateAsClient for a description of the negotiation.
+func AuthenticateAsServer(conn net.Conn, versions *version.Range, principal security.Principal, lBlessings security.Blessings,
+ dc vc.DischargeClient, ppub *message.HopSetup) (crypto.ControlCipher, error) {
+ var err error
+ if versions == nil {
+ versions = version.SupportedRange
+ }
+ if principal == nil {
+ // If there is no principal, we don't support encryption/authentication.
+ versions, err = versions.Intersect(&version.Range{Min: 0, Max: ipcversion.IPCVersion5})
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ // Create our public data and send it to the client.
+ pvt, pub, err := makeHopSetup(versions)
+ if err != nil {
+ return nil, err
+ }
+ if err := message.WriteTo(conn, &pub, nullCipher); err != nil {
+ return nil, err
+ }
+
+ // Choose the max version in common.
+ vrange, err := pub.Versions.Intersect(&ppub.Versions)
+ if err != nil {
+ return nil, err
+ }
+ v := vrange.Max
+ if v < ipcversion.IPCVersion6 {
+ return nullCipher, nil
+ }
+
+ // Perform authentication.
+ switch v {
+ case ipcversion.IPCVersion6:
+ return authenticateAsServerIPC6(conn, principal, lBlessings, dc, &pvt, &pub, ppub)
+ default:
+ return nil, errUnsupportedEncryptVersion
+ }
+}
+
+func authenticateAsServerIPC6(conn net.Conn, principal security.Principal, lBlessings security.Blessings, dc vc.DischargeClient,
+ pvt *privateData, pub, ppub *message.HopSetup) (crypto.ControlCipher, error) {
+ box := ppub.NaclBox()
+ if box == nil {
+ return nil, errVersionNegotiationFailed
+ }
+ c := crypto.NewControlCipherIPC6(&box.PublicKey, &pvt.naclBoxPrivateKey, true)
+ pool := iobuf.NewPool(0)
+ reader := iobuf.NewReader(pool, conn)
+ sconn := newSetupConn(conn, reader, c)
+ // TODO(jyh): act upon authentication results.
+ _, _, err := vc.AuthenticateAsServer(sconn, principal, lBlessings, dc, crypto.NewNullCrypter(), ipcversion.IPCVersion6)
+ if err != nil {
+ return nil, err
+ }
+ return c, nil
+}
+
+// serverAuthOptions extracts the Principal from the options list.
+func serverAuthOptions(lopts []stream.ListenerOpt) (principal security.Principal, lBlessings security.Blessings, dischargeClient vc.DischargeClient, err error) {
+ var securityLevel options.VCSecurityLevel
+ for _, o := range lopts {
+ switch v := o.(type) {
+ case vc.DischargeClient:
+ dischargeClient = v
+ case vc.LocalPrincipal:
+ principal = v.Principal
+ case options.VCSecurityLevel:
+ securityLevel = v
+ case options.ServerBlessings:
+ lBlessings = v.Blessings
+ }
+ }
+ switch securityLevel {
+ case options.VCSecurityConfidential:
+ if principal == nil {
+ principal = vc.AnonymousPrincipal
+ }
+ if lBlessings == nil {
+ lBlessings = principal.BlessingStore().Default()
+ }
+ case options.VCSecurityNone:
+ principal = nil
+ default:
+ err = fmt.Errorf("unrecognized VC security level: %v", securityLevel)
+ }
+ return
+}
+
+// clientAuthOptions extracts the client authentication options from the options
+// list.
+func clientAuthOptions(lopts []stream.VCOpt) (principal security.Principal, dischargeClient vc.DischargeClient, err error) {
+ var securityLevel options.VCSecurityLevel
+ for _, o := range lopts {
+ switch v := o.(type) {
+ case vc.DischargeClient:
+ dischargeClient = v
+ case vc.LocalPrincipal:
+ principal = v.Principal
+ case options.VCSecurityLevel:
+ securityLevel = v
+ }
+ }
+ switch securityLevel {
+ case options.VCSecurityConfidential:
+ if principal == nil {
+ principal = vc.AnonymousPrincipal
+ }
+ case options.VCSecurityNone:
+ principal = nil
+ default:
+ err = fmt.Errorf("unrecognized VC security level: %v", securityLevel)
+ }
+ return
+}
+
+// makeHopSetup constructs the options that this process can support.
+func makeHopSetup(versions *version.Range) (pvt privateData, pub message.HopSetup, err error) {
+ pub.Versions = *versions
+ var pubKey, pvtKey *[32]byte
+ pubKey, pvtKey, err = box.GenerateKey(rand.Reader)
+ pub.Options = append(pub.Options, &message.NaclBox{PublicKey: *pubKey})
+ pvt.naclBoxPrivateKey = *pvtKey
+ return
+}