Browse Source

Initial (incomplete) client implementation.

Yawning Angel 4 years ago
parent
commit
bf0c8d0e2d
8 changed files with 635 additions and 5 deletions
  1. 7 4
      README.md
  2. 150 0
      client.go
  3. 319 0
      common.go
  4. 1 0
      framing/const.go
  5. 1 1
      handshake/handshake.go
  6. 104 0
      padding_null.go
  7. 25 0
      version_check.go
  8. 28 0
      version_check_stub.go

+ 7 - 4
README.md

@@ -37,13 +37,14 @@ Notes:
    License") of the AGPL, per the terms of Section 7 ("Additional Terms"),
    for users that:
 
-    * Are using the software to operate a publically accessible Bridge to
-      provide access to the public Tor network as a Tor Pluggable Transport
-      server.  This means:
+    * Are using the software exclusively to operate a publically accessible
+      Bridge to provide access to the public Tor network as a Tor Pluggable
+      Transport server.  This means:
 
         The Bridge publishes a descriptor to the Bridge Authority, and is
         available via BridgeDB OR is a default Bridge pre-configured and
-        distributed with Tor Browser.
+        distributed with Tor Browser, and uses basket2 as a server side
+        Pluggable Transport for said Bridge.
 
  * All other users MUST comply with the AGPL in it's entirety as a general
    rule, though other licensing arrangements may be possible on request.
@@ -69,6 +70,8 @@ TODO:
 
  * Write a formal specification.
 
+ * Write an AVX2 optimized Poly1305 implementation.
+
  * Someone that's not me should write assembly optimized ChaCha20 for ARM and
    i386.  I may do both if I feel bored enough, but no promises.
 

+ 150 - 0
client.go

@@ -0,0 +1,150 @@
+// client.go - Transport client implementation.
+// Copyright (C) 2016  Yawning Angel.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+package basket2
+
+import (
+	"net"
+
+	"git.schwanenlied.me/yawning/basket2.git/crypto/identity"
+	"git.schwanenlied.me/yawning/basket2.git/crypto/rand"
+	"git.schwanenlied.me/yawning/basket2.git/framing/tentp"
+	"git.schwanenlied.me/yawning/basket2.git/handshake"
+)
+
+// ClientConfig is the client configuration parameters to use when
+// constructing a ClientConn.
+type ClientConfig struct {
+	KEXMethod       handshake.KEXMethod
+	PaddingMethods  []PaddingMethod
+	ServerPublicKey *identity.PublicKey
+}
+
+// ClientConn is a client connection instance, that implements the net.Conn
+// interface.
+type ClientConn struct {
+	commonConn
+
+	config         *ClientConfig
+	handshakeState *handshake.ClientHandshake
+}
+
+// Handshake associates a ClientConn with an established net.Conn, and executes
+// the authenticated/encrypted/obfuscated key exchange, and optionally
+// authenticates the client with the server.
+func (c *ClientConn) Handshake(conn net.Conn) (err error) {
+	// Pass or fail, obliterate the handshake state.
+	defer c.handshakeState.Reset()
+	defer func() {
+		if err != nil {
+			c.setState(stateError)
+		}
+	}()
+
+	if err = c.setState(stateHandshaking); err != nil {
+		return
+	}
+	c.conn = conn
+
+	//
+	// Attempt the handshake.
+	//
+
+	// XXX: Build the request extData, for padding negotiation purposes,
+	// and determine the overall handshake request pad length.
+	padLen := 0
+
+	var keys *handshake.SessionKeys
+	var respExtData []byte
+	keys, respExtData, err = c.handshakeState.Handshake(c.conn, nil, padLen)
+	if err != nil {
+		return
+	}
+	defer keys.Reset()
+
+	// XXX: Parse the response extData to see which padding algorithm to use,
+	// and if authentication is possible/required.
+	_ = respExtData
+	shouldAuth := false
+	paddingMethod := PaddingNull
+
+	// Validate that the negotiated padding method is contained in our request.
+	paddingOk := false
+	for _, v := range c.config.PaddingMethods {
+		if paddingMethod == v {
+			paddingOk = true
+			break
+		}
+	}
+	if !paddingOk {
+		return ErrInvalidPadding
+	}
+
+	// Initialize the frame encoder/decoder with the session key material.
+	if c.txEncoder, err = tentp.NewEncoderFromKDF(keys.KDF); err != nil {
+		return
+	}
+	if c.rxDecoder, err = tentp.NewDecoderFromKDF(keys.KDF); err != nil {
+		return
+	}
+
+	// Bring the chosen padding algorithm online.
+	if err = c.setPadding(paddingMethod); err != nil { // XXX: Padding params
+		return
+	}
+
+	// Authenticate if needed.
+	if shouldAuth {
+		if err = c.setState(stateAuthenticate); err != nil {
+			return
+		}
+
+		// XXX: Sign TranscriptDigest, and send the authentication request.
+		// keys.TranscriptDigest  <- sign this shit.
+
+		// XXX: Figure out how to invoke the padding algorithm for auth. :/
+
+		// Receive the authentication response.
+	}
+
+	// The connection is now fully established.
+	if err = c.setState(stateEstablished); err != nil {
+		return
+	}
+
+	return nil
+}
+
+// NewClientConn initializes a ClientConn.  This step should be done offline,
+// as timing variation due to the Elligator 2 rejection sampling may leak
+// information regarding the obfuscation method.
+func NewClientConn(config *ClientConfig) (*ClientConn, error) {
+	var err error
+
+	c := new(ClientConn)
+	c.config = config
+	c.isClient = true
+	if c.mRNG, err = rand.New(); err != nil {
+		return nil, err
+	}
+	if c.handshakeState, err = handshake.NewClientHandshake(rand.Reader, config.KEXMethod, config.ServerPublicKey); err != nil {
+		return nil, err
+	}
+
+	return c, nil
+}
+
+var _ net.Conn = (*ClientConn)(nil)

+ 319 - 0
common.go

@@ -0,0 +1,319 @@
+// common.go - Transport common implementation.
+// Copyright (C) 2016  Yawning Angel.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+// Package basket2 implements the basket2 authenticated/encrypted/obfuscated
+// network transport protocol.
+//
+// Note that the package will block during init() if the system entropy pool
+// is not properly initialized on systems where there is support for
+// determining this information.  This is a feature, and "working around" this
+// "bug" will likely totally destroy security.
+package basket2
+
+import (
+	"errors"
+	"fmt"
+	"io"
+	mrand "math/rand"
+	"net"
+	"sync"
+	"time"
+
+	"git.schwanenlied.me/yawning/basket2.git/framing"
+	"git.schwanenlied.me/yawning/basket2.git/framing/tentp"
+)
+
+var (
+	// ErrInvalidState is the error returned on an invalid state or transition.
+	ErrInvalidState = errors.New("basket2: invalid state")
+
+	// ErrInvalidCmd is the error returned on decoding a framing packet with
+	// an invalid command.
+	ErrInvalidCmd = errors.New("basket2: invalid command")
+
+	// ErrInvalidPadding is the error returned when the client requests no
+	// compatible padding methods, or the server specifies a incompatible
+	// padding method.
+	ErrInvalidPadding = errors.New("basket2: invalid padding")
+
+	// ErrNotSupported is the error returned on an unsupported call.
+	ErrNotSupported = errors.New("basket2: operation not supported")
+
+	// ProtocolVersion is the transport protocol version.
+	ProtocolVersion = 0
+)
+
+// PaddingMethod is a given padding algorithm identifier.
+type PaddingMethod byte
+
+type paddingImpl interface {
+	Write([]byte) (int, error)
+	Read([]byte) (int, error)
+	OnClose()
+}
+
+type connState int
+
+const (
+	stateInit connState = iota
+	stateHandshaking
+	stateAuthenticate
+	stateEstablished
+	stateError
+)
+
+type commonConn struct {
+	sync.Mutex
+
+	mRNG  *mrand.Rand
+	state connState
+
+	conn     net.Conn
+	isClient bool
+
+	txEncoder *tentp.Encoder
+	rxDecoder *tentp.Decoder
+	impl      paddingImpl
+}
+
+// Conn returns the raw underlying net.Conn associated with the basket2
+// connection.
+func (c *commonConn) Conn() net.Conn {
+	return c.conn
+}
+
+// Write writes len(p) bytes to the stream, and returns the number of bytes
+// written, or an error.  All errors must be considered fatal.
+func (c *commonConn) Write(p []byte) (n int, err error) {
+	defer func() {
+		if err != nil {
+			c.setState(stateError)
+		}
+	}()
+
+	if !c.stateAllowsIO() {
+		return 0, ErrInvalidState
+	}
+	return c.impl.Write(p)
+}
+
+// Read reads up to len(p) bytes from the stream, and returns the number of
+// bytes read, or an error.  All errors must be considered fatal.
+func (c *commonConn) Read(p []byte) (n int, err error) {
+	defer func() {
+		if err != nil {
+			c.setState(stateError)
+		}
+	}()
+
+	if !c.stateAllowsIO() {
+		return 0, ErrInvalidState
+	}
+	return c.impl.Read(p)
+}
+
+// Close closes the connection and purges cryptographic keying material from
+// memory.
+func (c *commonConn) Close() error {
+	err := c.conn.Close()
+	c.setState(stateError)
+
+	return err
+}
+
+// LocalAddr returns the local address of the connection.
+func (c *commonConn) LocalAddr() net.Addr {
+	return c.conn.LocalAddr()
+}
+
+// RemoteAddr returns the remote address of the connection.
+func (c *commonConn) RemoteAddr() net.Addr {
+	return c.conn.RemoteAddr()
+}
+
+// SetDeadline returns ErrNotSupported.
+func (c *commonConn) SetDeadline(t time.Time) error {
+	return ErrNotSupported
+}
+
+// SetReadDeadline returns ErrNotSupported.
+func (c *commonConn) SetReadDeadline(t time.Time) error {
+	return ErrNotSupported
+}
+
+// SetWriteDeadline returns ErrNotSupported.
+func (c *commonConn) SetWriteDeadline(t time.Time) error {
+	return ErrNotSupported
+}
+
+func (c *commonConn) setState(newState connState) error {
+	c.Lock()
+	defer c.Unlock()
+
+	switch newState {
+	case stateInit:
+		panic("basket2: state transition to Init should NEVER happen")
+	case stateHandshaking:
+		if c.state != stateInit {
+			return ErrInvalidState
+		}
+	case stateAuthenticate:
+		if c.state != stateHandshaking {
+			return ErrInvalidState
+		}
+	case stateEstablished:
+		if c.state != stateHandshaking && c.state != stateAuthenticate {
+			return ErrInvalidState
+		}
+	case stateError:
+		// Transition to stateError is always allowed, and will obliterate
+		// cryptographic material.
+		if c.txEncoder != nil {
+			c.txEncoder.Reset()
+			c.txEncoder = nil
+		}
+		if c.rxDecoder != nil {
+			c.rxDecoder.Reset()
+			c.rxDecoder = nil
+		}
+
+		// If the padding implementation is present, call the termination
+		// handler.
+		if c.impl != nil {
+			c.impl.OnClose()
+			c.impl = nil
+		}
+	default:
+		panic(fmt.Sprintf("basket2: state transition to unknown state: %v", newState))
+	}
+	c.state = newState
+	return nil
+}
+
+func (c *commonConn) stateAllowsIO() bool {
+	c.Lock()
+	defer c.Unlock()
+
+	return c.state == stateAuthenticate || c.state == stateEstablished
+}
+
+// SendRawRecord sends a raw record to the peer with the specified command,
+// payload and padding length.  This call should NOT be interleaved/mixed
+// with the net.Conn Read/Write interface.
+func (c *commonConn) SendRawRecord(cmd byte, msg []byte, padLen int) (err error) {
+	defer func() {
+		if err != nil {
+			c.setState(stateError)
+		}
+	}()
+
+	// Validate the state.
+	if !c.stateAllowsIO() {
+		return ErrInvalidState
+	}
+	if !c.isClient {
+		cmd |= framing.CmdServer
+	}
+
+	// Encode the TENTP record.
+	var rec []byte
+	rec, err = c.txEncoder.EncodeRecord(cmd, msg, padLen)
+	if err != nil {
+		return
+	}
+
+	// Transmit the record.
+	var n int
+	n, err = c.conn.Write(rec)
+	if err != nil {
+		return
+	}
+	if n != len(rec) {
+		return io.ErrShortWrite
+	}
+
+	return
+}
+
+func (c *commonConn) setPadding(method PaddingMethod) error {
+	switch method {
+	case PaddingNull:
+		c.impl = newNullPadding(c)
+	default:
+		return ErrInvalidPadding
+	}
+	return nil
+}
+
+// RecvRawRecord receives a raw record from the peer.  This call should NOT be
+// interleaved/mixed with the net.Conn Read/Write interface.
+func (c *commonConn) RecvRawRecord() (cmd byte, msg []byte, err error) {
+	defer func() {
+		if err != nil {
+			cmd = 0
+			msg = nil
+			c.setState(stateError)
+		}
+	}()
+
+	// Validate the state.
+	if !c.stateAllowsIO() {
+		return 0, nil, ErrInvalidState
+	}
+
+	// Receive/Decode the TENTP header.
+	var recHdr [tentp.FramingOverhead]byte
+	if _, err = io.ReadFull(c.conn, recHdr[:]); err != nil {
+		return
+	}
+	var want int
+	cmd, want, err = c.rxDecoder.DecodeRecordHdr(recHdr[:])
+	if err != nil {
+		return
+	}
+
+	// Validate the command direction bit.
+	cmdCtoS := cmd&framing.CmdServer == 0
+	if c.isClient == cmdCtoS {
+		return 0, nil, ErrInvalidCmd
+	}
+	cmd &= framing.CmdServerMask
+
+	if want == 0 {
+		// Record with no payload, return early.
+		return
+	}
+
+	// Receive/Decode the TENTP record body.
+	recBody := make([]byte, want)
+	if _, err = io.ReadFull(c.conn, recBody); err != nil {
+		return
+	}
+	if msg, err = c.rxDecoder.DecodeRecordBody(recBody); err != nil {
+		return
+	}
+
+	return
+}
+
+func init() {
+	// This check is here for a reason.  If you comment it out, you will
+	// receive absolutely NO SUPPORT, and bug reports that do not contain
+	// patches will be IGNORED.
+	if !isRecentEnoughGo() {
+		panic("basket2: built with a Go version that is too old")
+	}
+}

+ 1 - 0
framing/const.go

@@ -21,6 +21,7 @@ package framing
 const (
 	CmdData = iota
 	CmdHandshake
+	CmdAuthenticate
 
 	CmdServer     = 0x80
 	CmdServerMask = 0x7f

+ 1 - 1
handshake/handshake.go

@@ -31,7 +31,7 @@ import (
 
 // KEXMethod is a supported/known handshake key exchange primitive set
 // approximately analagous to a TLS ciphersuite.
-type KEXMethod int
+type KEXMethod byte
 
 const (
 	// HandshakeVersion is the current handshake version.

+ 104 - 0
padding_null.go

@@ -0,0 +1,104 @@
+// padding_null.go - Null padding implementation.
+// Copyright (C) 2016  Yawning Angel.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+package basket2
+
+import (
+	"bytes"
+
+	"git.schwanenlied.me/yawning/basket2.git/framing"
+	"git.schwanenlied.me/yawning/basket2.git/framing/tentp"
+)
+
+const (
+	// PaddingNull is the "NULL" padding algorithm.  No packet length or
+	// timing obfuscation will be done beyond the standard handshake
+	// obfuscation.  This method SHOULD NOT currently be used, and is only
+	// provided for testing, and in anticipation of Tor getting it's own
+	// circuit level padding implementation.
+	PaddingNull PaddingMethod = 0
+)
+
+type nullPadding struct {
+	conn *commonConn
+
+	recvBuf bytes.Buffer
+}
+
+func (c *nullPadding) Write(p []byte) (int, error) {
+	// Break the write up into records, and send them out on the wire.  The
+	// kernel is better at breaking writes into appropriate sized packets than
+	// any userland app will be (at least with TCP), so use the maximum record
+	// size permitted by the framing layer as padding isn't a concern.
+	for off, left := 0, len(p); left > 0; {
+		wrSize := tentp.MaxPlaintextRecordSize
+		if left < wrSize {
+			wrSize = left
+		}
+
+		if err := c.conn.SendRawRecord(framing.CmdData, p[off:off+wrSize], 0); err != nil {
+			return 0, err
+		}
+
+		off += wrSize
+		left -= wrSize
+	}
+
+	return len(p), nil
+}
+
+func (c *nullPadding) Read(p []byte) (n int, err error) {
+	// This buffering strategy will return short reads, since a new record
+	// is only consumed off the network once the entirety of the previous
+	// record has been returned.  A goroutine that comsumes off the network
+	// instead would minimize this, but this is simple and prevents rampant
+	// runaway buffer growth.
+
+	// Refill the receive buffer as needed...
+	for c.recvBuf.Len() == 0 && err == nil {
+		// ... by reading the next record off the network...
+		var cmd byte
+		var msg []byte
+		cmd, msg, err = c.conn.RecvRawRecord()
+		if err != nil {
+			break
+		}
+		if cmd != framing.CmdData {
+			return 0, ErrInvalidCmd
+		}
+
+		// ... and stashing it in the buffer.
+		if msg != nil && len(msg) > 0 {
+			c.recvBuf.Write(msg)
+		}
+	}
+
+	// Service the Read using buffered payload.
+	if c.recvBuf.Len() > 0 && err == nil {
+		n, _ = c.recvBuf.Read(p)
+	}
+	return
+}
+
+func (c *nullPadding) OnClose() {
+	c.recvBuf.Reset()
+}
+
+func newNullPadding(conn *commonConn) paddingImpl {
+	c := new(nullPadding)
+	c.conn = conn
+	return c
+}

+ 25 - 0
version_check.go

@@ -0,0 +1,25 @@
+// version_check.go - Version check
+// Copyright (C) 2016  Yawning Angel.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+// +build go1.6
+// +build !gccgo
+
+package basket2
+
+// Congratulations, your Go is sufficiently recent, and isn't gccgo.
+func isRecentEnoughGo() bool {
+	return true
+}

+ 28 - 0
version_check_stub.go

@@ -0,0 +1,28 @@
+// version_check.go - Version check
+// Copyright (C) 2016  Yawning Angel.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+// +build !go1.6 gccgo
+
+package basket2
+
+// If you are hitting this at test/runtime, this means your Go is kind of old.
+// How to work around this check is blatantly obvious, however the authors
+// will provide even less than the expected non-existent support for binaries
+// built that way, though clean patches that do not massively complicate the
+// code will likely be merged.
+func isRecentEnoughGo() bool {
+	return false
+}