Browse Source

Use `crypto/ecdh`, and kill `crypto/identity` with fire.

Yawning Angel 3 years ago
parent
commit
1bcacc3eb9

+ 7 - 2
basket2proxy/client.go

@@ -17,6 +17,7 @@
 package main
 
 import (
+	"encoding/base64"
 	"encoding/hex"
 	"fmt"
 	"net"
@@ -28,7 +29,7 @@ import (
 	"git.schwanenlied.me/yawning/basket2.git"
 	"git.schwanenlied.me/yawning/basket2.git/basket2proxy/internal/log"
 	"git.schwanenlied.me/yawning/basket2.git/basket2proxy/internal/proxyextras"
-	"git.schwanenlied.me/yawning/basket2.git/crypto/identity"
+	"git.schwanenlied.me/yawning/basket2.git/crypto/ecdh"
 	"git.schwanenlied.me/yawning/basket2.git/handshake"
 
 	"git.torproject.org/pluggable-transports/goptlib.git"
@@ -81,7 +82,11 @@ func (s *clientState) parseBridgeArgs(args *pt.Args) (*basket2.ClientConfig, err
 	}
 
 	// Parse out the bridge's identity key.
-	if cfg.ServerPublicKey, err = identity.PublicKeyFromString(splitArgs[2]); err != nil {
+	b, err := base64.RawStdEncoding.DecodeString(splitArgs[2])
+	if err != nil {
+		return nil, fmt.Errorf("failed to deserialize public key: %v", err)
+	}
+	if cfg.ServerPublicKey, err = ecdh.PublicKeyFromBytes(handshake.IdentityCurve, b); err != nil {
 		return nil, fmt.Errorf("failed to deserialize public key: %v", err)
 	}
 

+ 31 - 11
basket2proxy/server.go

@@ -17,7 +17,10 @@
 package main
 
 import (
+	"encoding/base64"
 	"encoding/json"
+	"encoding/pem"
+	"errors"
 	"flag"
 	"fmt"
 	"io/ioutil"
@@ -29,7 +32,7 @@ import (
 	"git.schwanenlied.me/yawning/basket2.git"
 	"git.schwanenlied.me/yawning/basket2.git/basket2proxy/internal/log"
 	"git.schwanenlied.me/yawning/basket2.git/crypto"
-	"git.schwanenlied.me/yawning/basket2.git/crypto/identity"
+	"git.schwanenlied.me/yawning/basket2.git/crypto/ecdh"
 	"git.schwanenlied.me/yawning/basket2.git/crypto/rand"
 	"git.schwanenlied.me/yawning/basket2.git/framing/tentp"
 	"git.schwanenlied.me/yawning/basket2.git/handshake"
@@ -38,17 +41,22 @@ import (
 )
 
 const (
-	bridgeFile          = "basket2_bridgeline.txt"
-	bridgeParamsFile    = "basket2_params.json"
-	privateIdentityFile = "basket2_x25519.priv"
-	fileMode            = 0600
+	bridgeFile             = "basket2_bridgeline.txt"
+	bridgeParamsFile       = "basket2_params.json"
+	privateIdentityFile    = "basket2_x25519.priv"
+	privateIdentityPEMType = "X25519 PRIVATE KEY"
+	fileMode               = 0600
 
 	serverHandshakeTimeout = time.Duration(30) * time.Second
 	maxCloseDelayBytes     = handshake.MaxHandshakeSize
 	maxCloseDelay          = 60
 )
 
-var useLargeReplayFilter bool
+var (
+	errInvalidKey = errors.New("identity: deserialized key is invalid")
+
+	useLargeReplayFilter bool
+)
 
 type serverState struct {
 	config *basket2.ServerConfig
@@ -62,7 +70,7 @@ type serverState struct {
 }
 
 func (s *serverState) getPtArgs() (*pt.Args, error) {
-	pkStr := s.config.ServerPrivateKey.PublicKey.ToString()
+	pkStr := base64.RawStdEncoding.EncodeToString(s.config.ServerPrivateKey.PublicKey().ToBytes())
 
 	var kexStr string
 	for _, m := range s.config.KEXMethods {
@@ -284,7 +292,7 @@ func (s *serverState) closeAfterDelay(conn net.Conn, establishedAt time.Time) {
 	}
 }
 
-func loadServerPrivateKey() (sk *identity.PrivateKey, err error) {
+func loadServerPrivateKey() (sk ecdh.PrivateKey, err error) {
 	defer func() {
 		if sk != nil {
 			if err != nil {
@@ -310,13 +318,17 @@ func loadServerPrivateKey() (sk *identity.PrivateKey, err error) {
 		}
 
 		// Failed to load a private key, generate it.
-		sk, err = identity.NewPrivateKey(rand.Reader)
+		sk, err = ecdh.New(rand.Reader, handshake.IdentityCurve, false)
 		if err != nil {
 			return
 		}
 
 		// Serialize the private key.
-		blob = sk.ToPEM()
+		block := &pem.Block{
+			Type:  privateIdentityPEMType,
+			Bytes: sk.ToBytes(),
+		}
+		blob = pem.EncodeToMemory(block)
 		defer crypto.Memwipe(blob)
 		err = ioutil.WriteFile(privKeyFile, blob, os.ModeExclusive|fileMode)
 		return
@@ -324,7 +336,15 @@ func loadServerPrivateKey() (sk *identity.PrivateKey, err error) {
 	defer crypto.Memwipe(blob)
 
 	// Deserialize the private key.
-	return identity.PrivateKeyFromPEM(blob)
+	block, _ := pem.Decode(blob)
+	if block == nil {
+		return nil, errInvalidKey
+	}
+	defer crypto.Memwipe(block.Bytes)
+	if block.Type != privateIdentityPEMType {
+		return nil, errInvalidKey
+	}
+	return ecdh.PrivateKeyFromBytes(handshake.IdentityCurve, block.Bytes)
 }
 
 func initServerListener(si *pt.ServerInfo, bindaddr *pt.Bindaddr) (net.Listener, error) {

+ 5 - 2
client.go

@@ -19,7 +19,7 @@ package basket2
 import (
 	"net"
 
-	"git.schwanenlied.me/yawning/basket2.git/crypto/identity"
+	"git.schwanenlied.me/yawning/basket2.git/crypto/ecdh"
 	"git.schwanenlied.me/yawning/basket2.git/crypto/rand"
 	"git.schwanenlied.me/yawning/basket2.git/framing"
 	"git.schwanenlied.me/yawning/basket2.git/handshake"
@@ -30,7 +30,7 @@ import (
 type ClientConfig struct {
 	KEXMethod       handshake.KEXMethod
 	PaddingMethods  []PaddingMethod
-	ServerPublicKey *identity.PublicKey
+	ServerPublicKey ecdh.PublicKey
 
 	// AuthFn is the function called at handshake time to authenticate with
 	// the remote peer.  It is expected to return the authentication request
@@ -186,6 +186,9 @@ func NewClientConn(config *ClientConfig) (*ClientConn, error) {
 	if config.ServerPublicKey == nil {
 		panic("basket2: no server public key")
 	}
+	if config.ServerPublicKey.Curve() != handshake.IdentityCurve {
+		panic("basket2: invalid server public key curve")
+	}
 
 	c := new(ClientConn)
 	c.config = config

+ 7 - 1
crypto/ecdh/ecdh.go

@@ -54,6 +54,9 @@ type Curve byte
 
 // PublicKey is an ECDH public key.
 type PublicKey interface {
+	// Curve returns the Curve that this public key is for.
+	Curve() Curve
+
 	// Size returns the size of the encoded public key in bytes.
 	Size() int
 
@@ -99,6 +102,9 @@ type PrivateKey interface {
 	// attacks.
 	ScalarMult(PublicKey) ([]byte, bool)
 
+	// Curve returns the Curve that this private key is for.
+	Curve() Curve
+
 	// Size returns the size of the encoded private key in bytes.
 	Size() int
 
@@ -126,7 +132,7 @@ func PrivateKeyFromBytes(curve Curve, b []byte) (PrivateKey, error) {
 // New generates a new PrivateKey in the provided curve using the random source
 // rand.  If uniform is true, an Elligator2 representative will also be
 // generated if supported.
-func New(curve Curve, rand io.Reader, uniform bool) (PrivateKey, error) {
+func New(rand io.Reader, curve Curve, uniform bool) (PrivateKey, error) {
 	switch curve {
 	case X25519:
 		return newX25519(rand, uniform)

+ 8 - 0
crypto/ecdh/x25519.go

@@ -33,6 +33,10 @@ type x25519PublicKey struct {
 	uniformBytes *[X25519Size]byte
 }
 
+func (k *x25519PublicKey) Curve() Curve {
+	return X25519
+}
+
 func (k *x25519PublicKey) Size() int {
 	return X25519Size
 }
@@ -95,6 +99,10 @@ func (k *x25519PrivateKey) ScalarMult(publicKey PublicKey) ([]byte, bool) {
 	return sharedSecret[:], ok
 }
 
+func (k *x25519PrivateKey) Curve() Curve {
+	return X25519
+}
+
 func (k *x25519PrivateKey) Size() int {
 	return X25519Size
 }

+ 8 - 0
crypto/ecdh/x448.go

@@ -31,6 +31,10 @@ type x448PublicKey struct {
 	pubBytes [X448Size]byte
 }
 
+func (k *x448PublicKey) Curve() Curve {
+	return X448
+}
+
 func (k *x448PublicKey) Size() int {
 	return X448Size
 }
@@ -74,6 +78,10 @@ func (k *x448PrivateKey) ScalarMult(publicKey PublicKey) ([]byte, bool) {
 	return sharedSecret[:], ok == 0
 }
 
+func (k *x448PrivateKey) Curve() Curve {
+	return X448
+}
+
 func (k *x448PrivateKey) Size() int {
 	return X448Size
 }

+ 0 - 232
crypto/identity/identity.go

@@ -1,232 +0,0 @@
-// identity.go - Identity key routines.
-// Copyright (C) 2015-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 identity provides convinient wrapper types around an X25519
-// keypair.
-package identity
-
-import (
-	"encoding/base64"
-	"encoding/pem"
-	"errors"
-	"io"
-	"runtime"
-
-	"git.schwanenlied.me/yawning/basket2.git/crypto"
-
-	"golang.org/x/crypto/curve25519"
-)
-
-const (
-	// SharedSecretSize is the side of a ECDH shared secret in bytes.
-	SharedSecretSize = 32
-
-	// PublicKeySize is the side of a PublicKey in bytes.
-	PublicKeySize = 32
-
-	// PrivateKeySize is the size of a PrivateKey in bytes.
-	PrivateKeySize = 32
-
-	maxKeygenAttempts = 8
-
-	publicKeyPEMType  = "X25519 PUBLIC KEY"
-	privateKeyPEMType = "X25519 PRIVATE KEY"
-)
-
-var (
-	// ErrInvalidKeySize is the error returned when deserialization failes due
-	// to an invalid length buffer.
-	ErrInvalidKeySize = errors.New("identity: invalid key size")
-
-	// ErrInvalidKey is the error returned when deserialization fails due to
-	// one of the keys consisting of invalid points.
-	ErrInvalidKey = errors.New("identity: deserialized key is invalid")
-
-	identityRandTweak = []byte("basket2-identity-tweak")
-)
-
-// PrivateKey is a X25519 private key.
-type PrivateKey struct {
-	PublicKey
-	PrivateKey [32]byte
-}
-
-func (k *PrivateKey) genPublic() error {
-	curve25519.ScalarBaseMult(&k.PublicKey.PublicKey, &k.PrivateKey)
-	if !crypto.MemIsZero(k.PublicKey.PublicKey[:]) {
-		return nil
-	}
-	return ErrInvalidKey
-}
-
-// ScalarMult derives a shared secret given a peer's public key, suitable
-// for use as an input to a key derivation function, and returns true on
-// success.  The return value MUST be validated to thwart invalid point
-// attacks.
-func (k *PrivateKey) ScalarMult(secret *[SharedSecretSize]byte, publicKey *[PublicKeySize]byte) bool {
-	ok := !crypto.MemIsZero(publicKey[:])
-	curve25519.ScalarMult(secret, &k.PrivateKey, publicKey)
-	ok = ok && !crypto.MemIsZero(secret[:])
-	return ok
-}
-
-// Reset sanitizes private values from the PrivateKey such that they no longer
-// appear in memory.
-func (k *PrivateKey) Reset() {
-	crypto.Memwipe(k.PrivateKey[:])
-}
-
-// ToPEM serializes a private key to PEM format.
-func (k *PrivateKey) ToPEM() []byte {
-	block := &pem.Block{
-		Type:  privateKeyPEMType,
-		Bytes: k.PrivateKey[:],
-	}
-	return pem.EncodeToMemory(block)
-}
-
-// NewPrivateKey generates a X25519 keypair using the random source rand (use
-// crypto/rand.Reader).
-func NewPrivateKey(rand io.Reader) (*PrivateKey, error) {
-	var err error
-	k := new(PrivateKey)
-	h, err := crypto.NewTweakedShake256(rand, identityRandTweak)
-	if err != nil {
-		return nil, err
-	}
-	defer h.Reset()
-
-	runtime.SetFinalizer(k, finalizePrivateKey) // Not always run on exit.
-	for iters := 0; iters < maxKeygenAttempts; iters++ {
-		// Generate the X25519 keypair.
-		if _, err := io.ReadFull(rand, k.PrivateKey[:]); err != nil {
-			return nil, err
-		}
-		if err := k.genPublic(); err == nil {
-			return k, nil
-		}
-	}
-
-	// This should essentially never happen, even with a relatively low
-	// retry count.
-	panic("crypto/identity: failed to generate keypair, broken rng?")
-}
-
-// PrivateKeyFromPEM deserializes a PEM encoded private key.
-func PrivateKeyFromPEM(b []byte) (*PrivateKey, error) {
-	block, _ := pem.Decode(b)
-	if block == nil {
-		return nil, ErrInvalidKey
-	}
-	if block.Type != privateKeyPEMType {
-		return nil, ErrInvalidKey
-	}
-	defer crypto.Memwipe(block.Bytes)
-	// XXX: Just ignore trailing bullshit?
-	return PrivateKeyFromBytes(block.Bytes)
-}
-
-// PrivateKeyFromBytes deserializes a private key.
-func PrivateKeyFromBytes(b []byte) (*PrivateKey, error) {
-	if len(b) != PrivateKeySize {
-		return nil, ErrInvalidKeySize
-	}
-
-	k := new(PrivateKey)
-	runtime.SetFinalizer(k, finalizePrivateKey) // Not always run on exit.
-	copy(k.PrivateKey[:], b)
-	if err := k.genPublic(); err != nil {
-		k.Reset()
-		return nil, err
-	}
-	return k, nil
-}
-
-func finalizePrivateKey(k *PrivateKey) {
-	k.Reset()
-}
-
-// PublicKey is a X25519 public key.
-type PublicKey struct {
-	PublicKey [PublicKeySize]byte
-}
-
-// ScalarMult derives a shared secret given a private key, suitable
-// for use as an input to a key derivation function, and returns true on
-// success.  The return value MUST be validated to thwart invalid point
-// attacks.
-func (k *PublicKey) ScalarMult(secret *[SharedSecretSize]byte, privateKey *[PrivateKeySize]byte) bool {
-	ok := !crypto.MemIsZero(k.PublicKey[:])
-	curve25519.ScalarMult(secret, privateKey, &k.PublicKey)
-	ok = ok && !crypto.MemIsZero(secret[:])
-	return ok
-}
-
-// ToPEM serializes a public key to PEM format.
-func (k *PublicKey) ToPEM() []byte {
-	block := &pem.Block{
-		Type:  publicKeyPEMType,
-		Bytes: k.PublicKey[:],
-	}
-	return pem.EncodeToMemory(block)
-}
-
-// PublicKeyFromPEM deserializes a PEM encoded public key.
-func PublicKeyFromPEM(b []byte) (*PublicKey, error) {
-	block, _ := pem.Decode(b)
-	if block == nil {
-		return nil, ErrInvalidKey
-	}
-	if block.Type != publicKeyPEMType {
-		return nil, ErrInvalidKey
-	}
-	// XXX: Just ignore trailing bullshit?
-	return PublicKeyFromBytes(block.Bytes)
-}
-
-// ToString serializes a public key to a string.
-func (k *PublicKey) ToString() string {
-	return base64.RawStdEncoding.EncodeToString(k.PublicKey[:])
-}
-
-// ToBytes returns the raw internal byte array as a slice.
-func (k *PublicKey) ToBytes() []byte {
-	return k.PublicKey[:]
-}
-
-// PublicKeyFromString deserializes a string encoded public key.
-func PublicKeyFromString(s string) (*PublicKey, error) {
-	b, err := base64.RawStdEncoding.DecodeString(s)
-	if err != nil {
-		return nil, ErrInvalidKey
-	}
-	return PublicKeyFromBytes(b)
-}
-
-// PublicKeyFromBytes deserializes a public key.
-func PublicKeyFromBytes(b []byte) (*PublicKey, error) {
-	if len(b) != PublicKeySize {
-		return nil, ErrInvalidKeySize
-	}
-
-	k := new(PublicKey)
-	copy(k.PublicKey[:], b)
-	if crypto.MemIsZero(k.PublicKey[:]) {
-		return nil, ErrInvalidKey
-	}
-
-	return k, nil
-}

+ 0 - 92
crypto/identity/identity_test.go

@@ -1,92 +0,0 @@
-// identity_test.go - Identity key routines.
-// 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 identity
-
-import (
-	"bytes"
-	"crypto/rand"
-	"errors"
-	"testing"
-)
-
-func comparePublic(a, b *PublicKey) error {
-	if !bytes.Equal(a.PublicKey[:], b.PublicKey[:]) {
-		return errors.New("public key mismatch")
-	}
-	return nil
-}
-
-func comparePrivate(a, b *PrivateKey) error {
-	if err := comparePublic(&a.PublicKey, &b.PublicKey); err != nil {
-		return err
-	}
-	if !bytes.Equal(a.PrivateKey[:], b.PrivateKey[:]) {
-		return errors.New("private key mismatch")
-	}
-
-	return nil
-}
-
-func TestIdentity(t *testing.T) {
-	k0, err := NewPrivateKey(rand.Reader)
-	if err != nil {
-		t.Fatalf("failed to generate identity key: %v", err)
-	}
-
-	k1, err := PrivateKeyFromBytes(k0.PrivateKey[:])
-	if err != nil {
-		t.Fatalf("failed to deseralize identity key: %v", err)
-	}
-	if err = comparePrivate(k0, k1); err != nil {
-		t.Fatalf("byte serialized SK: %v", err)
-	}
-
-	pemEncodedSk := k1.ToPEM()
-	k3, err := PrivateKeyFromPEM(pemEncodedSk)
-	if err != nil {
-		t.Fatalf("failed to deserialize PEM identity key: %v", err)
-	}
-	if err = comparePrivate(k0, k3); err != nil {
-		t.Fatalf("PEM serialized SK: %v", err)
-	}
-
-	pk2, err := PublicKeyFromBytes(k0.PublicKey.PublicKey[:])
-	if err != nil {
-		t.Fatalf("failed to deserialize identity public key: %v", err)
-	}
-	if err = comparePublic(&k0.PublicKey, pk2); err != nil {
-		t.Fatalf("byte serialized PK: %v", err)
-	}
-
-	pemEncodedPk := pk2.ToPEM()
-	pk3, err := PublicKeyFromPEM(pemEncodedPk)
-	if err != nil {
-		t.Fatalf("failed to deserialize PEM public key: %v", err)
-	}
-	if err = comparePublic(&k0.PublicKey, pk3); err != nil {
-		t.Fatalf("PEM serialized PK: %v", err)
-	}
-
-	b64EncodedPk := pk2.ToString()
-	pk4, err := PublicKeyFromString(b64EncodedPk)
-	if err != nil {
-		t.Fatalf("failed to deserialize string public key: %v", err)
-	}
-	if err = comparePublic(&k0.PublicKey, pk4); err != nil {
-		t.Fatalf("String serialized PK: %v", err)
-	}
-}

+ 6 - 3
handshake/handshake.go

@@ -24,7 +24,7 @@ import (
 	"io"
 
 	"git.schwanenlied.me/yawning/basket2.git/crypto"
-	"git.schwanenlied.me/yawning/basket2.git/crypto/identity"
+	"git.schwanenlied.me/yawning/basket2.git/crypto/ecdh"
 	"git.schwanenlied.me/yawning/newhope.git"
 
 	"golang.org/x/crypto/sha3"
@@ -51,6 +51,9 @@ const (
 	// MaxHandshakeSize is the maximum total handshake length.
 	MaxHandshakeSize = 8192
 
+	// IdentityCurve is the curve used for server identities.
+	IdentityCurve = ecdh.X25519
+
 	minReqSize = 1 + 1
 )
 
@@ -204,7 +207,7 @@ func (c *ClientHandshake) Reset() {
 // Note: Due to the rejection sampling in Elligator 2 keypair generation, this
 // should be done offline.  The timing variation only leaks information about
 // the obfuscation method, and does not compromise secrecy or integrity.
-func NewClientHandshake(rand io.Reader, kexMethod KEXMethod, serverPublicKey *identity.PublicKey) (*ClientHandshake, error) {
+func NewClientHandshake(rand io.Reader, kexMethod KEXMethod, serverPublicKey ecdh.PublicKey) (*ClientHandshake, error) {
 	var err error
 	c := new(ClientHandshake)
 	c.rand = rand
@@ -318,7 +321,7 @@ func (s *ServerHandshake) isAllowedKEXMethod(kexMethod KEXMethod) bool {
 
 // NewServerHandshake creates a new ServerHandshake instance suitable for a
 // single handshake to the provided peer identified by a private key.
-func NewServerHandshake(rand io.Reader, kexMethods []KEXMethod, replay ReplayFilter, serverPrivateKey *identity.PrivateKey) (*ServerHandshake, error) {
+func NewServerHandshake(rand io.Reader, kexMethods []KEXMethod, replay ReplayFilter, serverPrivateKey ecdh.PrivateKey) (*ServerHandshake, error) {
 	var err error
 	s := new(ServerHandshake)
 	s.rand = rand

+ 4 - 4
handshake/handshake_test.go

@@ -26,7 +26,7 @@ import (
 	"testing"
 	"time"
 
-	"git.schwanenlied.me/yawning/basket2.git/crypto/identity"
+	"git.schwanenlied.me/yawning/basket2.git/crypto/ecdh"
 )
 
 var (
@@ -42,7 +42,7 @@ type testState struct {
 	sync.WaitGroup
 	kexMethod KEXMethod
 
-	bobKeypair *identity.PrivateKey
+	bobKeypair ecdh.PrivateKey
 	replay     ReplayFilter
 
 	aliceCh, bobCh     chan error
@@ -63,7 +63,7 @@ func (s *testState) aliceRoutine() {
 
 	s.alicePipe.SetDeadline(time.Now().Add(5 * time.Second))
 
-	hs, err := NewClientHandshake(rand.Reader, s.kexMethod, &s.bobKeypair.PublicKey)
+	hs, err := NewClientHandshake(rand.Reader, s.kexMethod, s.bobKeypair.PublicKey())
 	if err != nil {
 		s.aliceCh <- err
 		return
@@ -199,7 +199,7 @@ func newTestState() (*testState, error) {
 	s.aliceCh, s.bobCh = make(chan error), make(chan error)
 	s.kdfCh = make(chan *SessionKeys, 2)
 
-	s.bobKeypair, err = identity.NewPrivateKey(rand.Reader)
+	s.bobKeypair, err = ecdh.New(rand.Reader, IdentityCurve, true)
 	if err != nil {
 		return nil, err
 	}

+ 23 - 44
handshake/handshake_x25519.go

@@ -20,9 +20,8 @@ import (
 	"io"
 
 	"git.schwanenlied.me/yawning/basket2.git/crypto"
+	"git.schwanenlied.me/yawning/basket2.git/crypto/ecdh"
 	"git.schwanenlied.me/yawning/newhope.git"
-
-	"golang.org/x/crypto/curve25519"
 )
 
 const (
@@ -30,10 +29,10 @@ const (
 	X25519NewHope KEXMethod = 0
 
 	x25519ReqSize  = 1 + 1 + newhope.SendASize
-	x25519RespSize = 1 + 1 + 32 + newhope.SendBSize
+	x25519RespSize = 1 + 1 + ecdh.X25519Size + newhope.SendBSize
 
 	x25519RespXOffset  = 2
-	x25519RespNHOffset = 2 + 32
+	x25519RespNHOffset = 2 + ecdh.X25519Size
 	x25519ReqNHOffset  = 2
 
 	// Pad to X448 handshake size...
@@ -41,10 +40,6 @@ const (
 	x25519RespPadNormSize = x448RespSize - x25519RespSize
 )
 
-var (
-	x25519RandTewak = []byte("basket2-x25519-tweak")
-)
-
 func (c *ClientHandshake) handshakeX25519(rw io.ReadWriter, extData []byte, padLen int) (*SessionKeys, []byte, error) {
 	if c.kexMethod != X25519NewHope {
 		panic("handshake: expected X25519")
@@ -79,13 +74,15 @@ func (c *ClientHandshake) handshakeX25519(rw io.ReadWriter, extData []byte, padL
 	}
 
 	// X25519 key exchange with the server's ephemeral key.
-	var xPublicKey, xSharedSecret [32]byte
-	defer crypto.Memwipe(xSharedSecret[:])
-	copy(xPublicKey[:], respBlob[x25519RespXOffset:])
-	curve25519.ScalarMult(&xSharedSecret, &c.obfs.privKey, &xPublicKey)
-	if crypto.MemIsZero(xSharedSecret[:]) {
-		return nil, nil, ErrInvalidPoint
+	xPublicKey, err := ecdh.PublicKeyFromBytes(ecdh.X25519, respBlob[x25519RespXOffset:x25519RespXOffset+ecdh.X25519Size])
+	if err != nil {
+		return nil, nil, err
+	}
+	xSharedSecret, ok := c.obfs.privKey.ScalarMult(xPublicKey)
+	if !ok {
+		return nil, nil, ecdh.ErrInvalidPoint
 	}
+	defer crypto.Memwipe(xSharedSecret)
 
 	// NewHope key exchange with the server's public key.
 	var nhPublicKey newhope.PublicKeyBob
@@ -98,8 +95,8 @@ func (c *ClientHandshake) handshakeX25519(rw io.ReadWriter, extData []byte, padL
 
 	// Derive the session keys.
 	secrets := make([]([]byte), 0, 3)
-	secrets = append(secrets, c.obfs.sharedSecret[:])
-	secrets = append(secrets, xSharedSecret[:])
+	secrets = append(secrets, c.obfs.sharedSecret)
+	secrets = append(secrets, xSharedSecret)
 	secrets = append(secrets, nhSharedSecret)
 	k := newSessionKeys(secrets, c.obfs.transcriptDigest[:])
 
@@ -130,19 +127,19 @@ func (s *ServerHandshake) sendRespX25519(w io.Writer, extData []byte, padLen int
 	respBlob = append(respBlob, byte(s.kexMethod))
 
 	// Generate a new X25519 keypair.
-	var xPublicKey, xPrivateKey, xSharedSecret [32]byte
-	defer crypto.Memwipe(xPrivateKey[:])
-	defer crypto.Memwipe(xSharedSecret[:])
-	if err := newX25519KeyPair(s.rand, &xPublicKey, &xPrivateKey); err != nil {
+	xPrivateKey, err := ecdh.New(s.rand, ecdh.X25519, false)
+	if err != nil {
 		return nil, err
 	}
-	respBlob = append(respBlob, xPublicKey[:]...)
+	defer xPrivateKey.Reset()
+	respBlob = append(respBlob, xPrivateKey.PublicKey().ToBytes()...)
 
 	// X25519 key exchange with both ephemeral keys.
-	curve25519.ScalarMult(&xSharedSecret, &xPrivateKey, &s.obfs.clientPublicKey)
-	if crypto.MemIsZero(xSharedSecret[:]) {
-		return nil, ErrInvalidPoint
+	xSharedSecret, ok := xPrivateKey.ScalarMult(s.obfs.clientPublicKey)
+	if !ok {
+		return nil, ecdh.ErrInvalidPoint
 	}
+	defer crypto.Memwipe(xSharedSecret)
 
 	// NewHope key exchange with the client's public key.
 	var nhPublicKeyAlice newhope.PublicKeyAlice
@@ -171,28 +168,10 @@ func (s *ServerHandshake) sendRespX25519(w io.Writer, extData []byte, padLen int
 
 	// Derive the session keys.
 	secrets := make([]([]byte), 0, 3)
-	secrets = append(secrets, s.obfs.sharedSecret[:])
-	secrets = append(secrets, xSharedSecret[:])
+	secrets = append(secrets, s.obfs.sharedSecret)
+	secrets = append(secrets, xSharedSecret)
 	secrets = append(secrets, nhSharedSecret)
 	k := newSessionKeys(secrets, s.obfs.transcriptDigest[:])
 
 	return k, nil
 }
-
-func newX25519KeyPair(rand io.Reader, publicKey, privateKey *[32]byte) error {
-	rh, err := crypto.NewTweakedShake256(rand, x25519RandTewak)
-	if err != nil {
-		return err
-	}
-	defer rh.Reset()
-
-	if _, err := io.ReadFull(rh, privateKey[:]); err != nil {
-		return err
-	}
-
-	curve25519.ScalarBaseMult(publicKey, privateKey)
-	if !crypto.MemIsZero(publicKey[:]) {
-		return nil
-	}
-	return ErrInvalidPoint
-}

+ 32 - 46
handshake/handshake_x448.go

@@ -20,44 +20,40 @@ import (
 	"io"
 
 	"git.schwanenlied.me/yawning/basket2.git/crypto"
+	"git.schwanenlied.me/yawning/basket2.git/crypto/ecdh"
 	"git.schwanenlied.me/yawning/newhope.git"
-	"git.schwanenlied.me/yawning/x448.git"
 )
 
 const (
 	// X448NewHope is the X448/NewHope based handshake method.
 	X448NewHope KEXMethod = 1
 
-	x448ReqSize  = 1 + 1 + 56 + newhope.SendASize
-	x448RespSize = 1 + 1 + 56 + newhope.SendBSize
+	x448ReqSize  = 1 + 1 + ecdh.X448Size + newhope.SendASize
+	x448RespSize = 1 + 1 + ecdh.X448Size + newhope.SendBSize
 
 	x448XOffset  = 2
-	x448NHOffset = 2 + 56
+	x448NHOffset = 2 + ecdh.X448Size
 
 	x448PadNormSize = newhope.SendBSize - ((obfsClientOverhead - obfsServerOverhead) + newhope.SendASize)
 )
 
-var (
-	x448RandTweak = []byte("basket2-x448-tweak")
-)
-
 func (c *ClientHandshake) handshakeX448(rw io.ReadWriter, extData []byte, padLen int) (*SessionKeys, []byte, error) {
 	if c.kexMethod != X448NewHope {
 		panic("handshake: expected X448")
 	}
 
 	// Generate the ephemeral X448 keypair.
-	var xPublicKey, xPrivateKey [56]byte
-	defer crypto.Memwipe(xPrivateKey[:])
-	if err := newX448KeyPair(c.rand, &xPublicKey, &xPrivateKey); err != nil {
+	xPrivateKey, err := ecdh.New(c.rand, ecdh.X448, false)
+	if err != nil {
 		return nil, nil, err
 	}
+	defer xPrivateKey.Reset()
 
 	// Craft the handshake request blob.
 	reqBlob := make([]byte, 0, x448ReqSize+len(extData))
 	reqBlob = append(reqBlob, HandshakeVersion)
 	reqBlob = append(reqBlob, byte(c.kexMethod))
-	reqBlob = append(reqBlob, xPublicKey[:]...)
+	reqBlob = append(reqBlob, xPrivateKey.PublicKey().ToBytes()...)
 	reqBlob = append(reqBlob, c.nhPublicKey.Send[:]...)
 	reqBlob = append(reqBlob, extData...)
 
@@ -83,12 +79,15 @@ func (c *ClientHandshake) handshakeX448(rw io.ReadWriter, extData []byte, padLen
 	}
 
 	// X448 key exchange with the server's ephemeral key.
-	var xSharedSecret [56]byte
-	defer crypto.Memwipe(xSharedSecret[:])
-	copy(xPublicKey[:], respBlob[x448XOffset:])
-	if x448.ScalarMult(&xSharedSecret, &xPrivateKey, &xPublicKey) != 0 {
-		return nil, nil, ErrInvalidPoint
+	xPublicKey, err := ecdh.PublicKeyFromBytes(ecdh.X448, respBlob[x448XOffset:x448XOffset+ecdh.X448Size])
+	if err != nil {
+		return nil, nil, err
 	}
+	xSharedSecret, ok := xPrivateKey.ScalarMult(xPublicKey)
+	if !ok {
+		return nil, nil, ecdh.ErrInvalidPoint
+	}
+	defer crypto.Memwipe(xSharedSecret)
 
 	// NewHope key exchange with the server's public key.
 	var nhPublicKey newhope.PublicKeyBob
@@ -101,8 +100,8 @@ func (c *ClientHandshake) handshakeX448(rw io.ReadWriter, extData []byte, padLen
 
 	// Derive the session keys.
 	secrets := make([]([]byte), 0, 3)
-	secrets = append(secrets, c.obfs.sharedSecret[:])
-	secrets = append(secrets, xSharedSecret[:])
+	secrets = append(secrets, c.obfs.sharedSecret)
+	secrets = append(secrets, xSharedSecret)
 	secrets = append(secrets, nhSharedSecret)
 	k := newSessionKeys(secrets, c.obfs.transcriptDigest[:])
 
@@ -133,19 +132,23 @@ func (s *ServerHandshake) sendRespX448(w io.Writer, extData []byte, padLen int)
 	respBlob = append(respBlob, byte(s.kexMethod))
 
 	// Generate a new X448 keypair.
-	var xPublicKey, xPrivateKey, xSharedSecret [56]byte
-	defer crypto.Memwipe(xPrivateKey[:])
-	defer crypto.Memwipe(xSharedSecret[:])
-	if err := newX448KeyPair(s.rand, &xPublicKey, &xPrivateKey); err != nil {
+	xPrivateKey, err := ecdh.New(s.rand, ecdh.X448, false)
+	if err != nil {
 		return nil, err
 	}
-	respBlob = append(respBlob, xPublicKey[:]...)
-	copy(xPublicKey[:], s.reqBlob[x448XOffset:])
+	defer xPrivateKey.Reset()
+	respBlob = append(respBlob, xPrivateKey.PublicKey().ToBytes()...)
 
 	// X448 key exchange with both ephemeral keys.
-	if x448.ScalarMult(&xSharedSecret, &xPrivateKey, &xPublicKey) != 0 {
-		return nil, ErrInvalidPoint
+	xPublicKey, err := ecdh.PublicKeyFromBytes(ecdh.X448, s.reqBlob[x448XOffset:x448XOffset+ecdh.X448Size])
+	if err != nil {
+		return nil, err
 	}
+	xSharedSecret, ok := xPrivateKey.ScalarMult(xPublicKey)
+	if !ok {
+		return nil, ecdh.ErrInvalidPoint
+	}
+	defer crypto.Memwipe(xSharedSecret)
 
 	// NewHope key exchange with the client's public key.
 	var nhPublicKeyAlice newhope.PublicKeyAlice
@@ -170,27 +173,10 @@ func (s *ServerHandshake) sendRespX448(w io.Writer, extData []byte, padLen int)
 
 	// Derive the session keys.
 	secrets := make([]([]byte), 0, 3)
-	secrets = append(secrets, s.obfs.sharedSecret[:])
-	secrets = append(secrets, xSharedSecret[:])
+	secrets = append(secrets, s.obfs.sharedSecret)
+	secrets = append(secrets, xSharedSecret)
 	secrets = append(secrets, nhSharedSecret)
 	k := newSessionKeys(secrets, s.obfs.transcriptDigest[:])
 
 	return k, nil
 }
-
-func newX448KeyPair(rand io.Reader, publicKey, privateKey *[56]byte) error {
-	rh, err := crypto.NewTweakedShake256(rand, x448RandTweak)
-	if err != nil {
-		return err
-	}
-	defer rh.Reset()
-
-	if _, err := io.ReadFull(rh, privateKey[:]); err != nil {
-		return err
-	}
-	if x448.ScalarBaseMult(publicKey, privateKey) != 0 {
-		return ErrInvalidPoint
-	}
-
-	return nil
-}

+ 35 - 34
handshake/obfuscation.go

@@ -25,8 +25,7 @@ import (
 	"time"
 
 	"git.schwanenlied.me/yawning/basket2.git/crypto"
-	"git.schwanenlied.me/yawning/basket2.git/crypto/identity"
-	"git.schwanenlied.me/yawning/basket2.git/ext/x25519/elligator2"
+	"git.schwanenlied.me/yawning/basket2.git/crypto/ecdh"
 	"git.schwanenlied.me/yawning/basket2.git/framing"
 	"git.schwanenlied.me/yawning/basket2.git/framing/tentp"
 
@@ -43,11 +42,6 @@ var (
 	obfsKdfTweak   = []byte("basket2-obfs-v1-kdf-tweak")
 	obfsTransTweak = []byte("basket2-obfs-v1-transcript-tweak")
 
-	// ErrInvalidPoint is the error returned when the handshake/obfuscation
-	// crypto fails due to the X25519 point not being on the curve or it's
-	// twist.
-	ErrInvalidPoint = errors.New("handshake: invalid point")
-
 	// ErrInvalidCmd is the error returned on a invalid obfuscated handshake
 	// payload command.
 	ErrInvalidCmd = errors.New("obfs: invalid command")
@@ -66,11 +60,11 @@ var (
 
 // clientObfsCtx is the client handshake obfuscator state.
 type clientObfsCtx struct {
-	serverPublicKey *identity.PublicKey
+	serverPublicKey ecdh.PublicKey
 
-	privKey      [32]byte
-	repr         [32]byte
-	sharedSecret [32]byte
+	privKey      ecdh.PrivateKey
+	repr         []byte
+	sharedSecret []byte
 
 	transcriptDigest [32]byte
 }
@@ -96,11 +90,11 @@ func (o *clientObfsCtx) handshake(rw io.ReadWriter, msg []byte, padLen int) ([]b
 	// Note: The cipherText is structured such that the decoder can determine
 	// the length.
 	reqBlob := make([]byte, 0, obfsClientOverhead+len(msg)+padLen)
-	reqBlob = append(reqBlob, o.repr[:]...)
+	reqBlob = append(reqBlob, o.repr...)
 	macHash := sha3.New256()
 	macHash.Write(obfsMACTweak[:])
 	macHash.Write(o.serverPublicKey.ToBytes())
-	macHash.Write(o.repr[:])
+	macHash.Write(o.repr)
 	macHash.Write(epochHour[:])
 	reqBlob = macHash.Sum(reqBlob)
 
@@ -190,8 +184,11 @@ func (o *clientObfsCtx) handshake(rw io.ReadWriter, msg []byte, padLen int) ([]b
 
 // reset sanitizes private values from the client handshake obfuscator state.
 func (o *clientObfsCtx) reset() {
-	crypto.Memwipe(o.privKey[:])
-	crypto.Memwipe(o.sharedSecret[:])
+	if o.privKey != nil {
+		o.privKey.Reset()
+		o.privKey = nil
+	}
+	crypto.Memwipe(o.sharedSecret)
 }
 
 // newClientObfs creates a new client side handshake obfuscator instance, for
@@ -200,37 +197,36 @@ func (o *clientObfsCtx) reset() {
 // Note: Due to the rejection sampling in Elligator 2 keypair generation, this
 // should be done offline.  The timing variation only leaks information about
 // the obfuscation method, and does not compromise secrecy or integrity.
-func newClientObfs(rand io.Reader, serverPublicKey *identity.PublicKey) (*clientObfsCtx, error) {
+func newClientObfs(rand io.Reader, serverPublicKey ecdh.PublicKey) (*clientObfsCtx, error) {
 	o := new(clientObfsCtx)
 	o.serverPublicKey = serverPublicKey
 
 	// Generate a Curve25519 keypair, along with an Elligator 2 uniform
 	// random representative of the public key.
-	var publicKey [32]byte // Don't need our public key past validation.
-	if err := elligator2.GenerateKey(rand, &publicKey, &o.repr, &o.privKey); err != nil {
+	var err error
+	if o.privKey, err = ecdh.New(rand, IdentityCurve, true); err != nil {
 		return nil, err
 	}
-	if crypto.MemIsZero(publicKey[:]) {
-		return nil, ErrInvalidPoint
-	}
+	o.repr = o.privKey.PublicKey().ToUniformBytes()
 
 	// Calculate a shared secret with our ephemeral key, and the server's
 	// long term public key.
-	ok := serverPublicKey.ScalarMult(&o.sharedSecret, &o.privKey)
+	ok := false
+	o.sharedSecret, ok = o.privKey.ScalarMult(serverPublicKey)
 	if !ok {
-		return nil, ErrInvalidPoint
+		return nil, ecdh.ErrInvalidPoint
 	}
 
 	return o, nil
 }
 
 type serverObfsCtx struct {
-	clientPublicKey [32]byte
+	clientPublicKey ecdh.PublicKey
 
-	keypair      *identity.PrivateKey
+	keypair      ecdh.PrivateKey
 	keyHash      sha3.ShakeHash
 	tHash        hash.Hash
-	sharedSecret [32]byte
+	sharedSecret []byte
 
 	transcriptDigest [32]byte
 
@@ -243,7 +239,7 @@ func (o *serverObfsCtx) reset() {
 	if o.keyHash != nil {
 		o.keyHash.Reset()
 	}
-	crypto.Memwipe(o.sharedSecret[:])
+	crypto.Memwipe(o.sharedSecret)
 }
 
 func (o *serverObfsCtx) recvHandshakeReq(r io.Reader) ([]byte, error) {
@@ -268,7 +264,7 @@ func (o *serverObfsCtx) recvHandshakeReq(r io.Reader) ([]byte, error) {
 
 		macHash := sha3.New256()
 		macHash.Write(obfsMACTweak[:])
-		macHash.Write(o.keypair.PublicKey.ToBytes())
+		macHash.Write(o.keypair.PublicKey().ToBytes())
 		macHash.Write(repr[:])
 		macHash.Write(epochHour[:])
 
@@ -297,9 +293,14 @@ func (o *serverObfsCtx) recvHandshakeReq(r io.Reader) ([]byte, error) {
 
 	// Calculate the shared secret, with the client's representative and our
 	// long term private key.
-	elligator2.RepresentativeToPublicKey(&o.clientPublicKey, &repr)
-	if !o.keypair.ScalarMult(&o.sharedSecret, &o.clientPublicKey) {
-		return nil, ErrInvalidPoint
+	var err error
+	if o.clientPublicKey, err = ecdh.PublicKeyFromUniformBytes(IdentityCurve, repr[:]); err != nil {
+		return nil, err
+	}
+	ok := false
+	o.sharedSecret, ok = o.keypair.ScalarMult(o.clientPublicKey)
+	if !ok {
+		return nil, ecdh.ErrInvalidPoint
 	}
 
 	// Derive the handshake symmetric keys, and initialize the frame decoder
@@ -308,8 +309,8 @@ func (o *serverObfsCtx) recvHandshakeReq(r io.Reader) ([]byte, error) {
 	// sent.
 	o.keyHash = sha3.NewShake256()
 	o.keyHash.Write(obfsKdfTweak)
-	o.keyHash.Write(o.keypair.PublicKey.ToBytes())
-	o.keyHash.Write(o.sharedSecret[:])
+	o.keyHash.Write(o.keypair.PublicKey().ToBytes())
+	o.keyHash.Write(o.sharedSecret)
 	o.keyHash.Write(mac[:]) // Include the MAC in the KDF input.
 	dec, err := tentp.NewDecoderFromKDF(o.keyHash)
 	if err != nil {
@@ -375,7 +376,7 @@ func (o *serverObfsCtx) sendHandshakeResp(w io.Writer, msg []byte, padLen int) e
 	return nil
 }
 
-func newServerObfs(replay ReplayFilter, staticObfsKeypair *identity.PrivateKey) (*serverObfsCtx, error) {
+func newServerObfs(replay ReplayFilter, staticObfsKeypair ecdh.PrivateKey) (*serverObfsCtx, error) {
 	o := new(serverObfsCtx)
 	o.keypair = staticObfsKeypair
 	o.replay = replay

+ 5 - 2
server.go

@@ -19,7 +19,7 @@ package basket2
 import (
 	"net"
 
-	"git.schwanenlied.me/yawning/basket2.git/crypto/identity"
+	"git.schwanenlied.me/yawning/basket2.git/crypto/ecdh"
 	"git.schwanenlied.me/yawning/basket2.git/crypto/rand"
 	"git.schwanenlied.me/yawning/basket2.git/framing"
 	"git.schwanenlied.me/yawning/basket2.git/handshake"
@@ -28,7 +28,7 @@ import (
 // ServerConfig is the server configuration parameters to use when
 // constructing a ServerConn.
 type ServerConfig struct {
-	ServerPrivateKey *identity.PrivateKey
+	ServerPrivateKey ecdh.PrivateKey
 
 	KEXMethods     []handshake.KEXMethod
 	PaddingMethods []PaddingMethod
@@ -196,6 +196,9 @@ func NewServerConn(config *ServerConfig) (*ServerConn, error) {
 	if config.ServerPrivateKey == nil {
 		panic("basket2: no server private key")
 	}
+	if config.ServerPrivateKey.Curve() != handshake.IdentityCurve {
+		panic("basket2: invalid server private key curve")
+	}
 	if config.AuthPolicy == AuthMust && config.AuthFn == nil {
 		panic("basket2: auth required but no AuthFn")
 	}