Add authenticated handshake, support for passwords

This commit is contained in:
Neil Alexander 2023-10-09 16:44:07 +01:00
parent 490c11c29e
commit 268ffbfd14
No known key found for this signature in database
GPG Key ID: A02A2019A2BB0944
3 changed files with 88 additions and 26 deletions

View File

@ -17,6 +17,7 @@ import (
"github.com/Arceliar/phony" "github.com/Arceliar/phony"
"github.com/yggdrasil-network/yggdrasil-go/src/address" "github.com/yggdrasil-network/yggdrasil-go/src/address"
"golang.org/x/crypto/blake2b"
) )
type linkType int type linkType int
@ -65,6 +66,7 @@ type linkOptions struct {
pinnedEd25519Keys map[keyArray]struct{} pinnedEd25519Keys map[keyArray]struct{}
priority uint8 priority uint8
tlsSNI string tlsSNI string
password []byte
} }
type Listener struct { type Listener struct {
@ -129,6 +131,7 @@ func (e linkError) Error() string { return string(e) }
const ErrLinkAlreadyConfigured = linkError("peer is already configured") const ErrLinkAlreadyConfigured = linkError("peer is already configured")
const ErrLinkPriorityInvalid = linkError("priority value is invalid") const ErrLinkPriorityInvalid = linkError("priority value is invalid")
const ErrLinkPinnedKeyInvalid = linkError("pinned public key is invalid") const ErrLinkPinnedKeyInvalid = linkError("pinned public key is invalid")
const ErrLinkPasswordInvalid = linkError("password is invalid")
const ErrLinkUnrecognisedSchema = linkError("link schema unknown") const ErrLinkUnrecognisedSchema = linkError("link schema unknown")
func (l *links) add(u *url.URL, sintf string, linkType linkType) error { func (l *links) add(u *url.URL, sintf string, linkType linkType) error {
@ -166,6 +169,13 @@ func (l *links) add(u *url.URL, sintf string, linkType linkType) error {
} }
options.priority = uint8(pi) options.priority = uint8(pi)
} }
if p := u.Query().Get("password"); p != "" {
if len(p) > blake2b.Size {
retErr = ErrLinkPasswordInvalid
return
}
options.password = []byte(p)
}
// If we think we're already connected to this peer, load up // If we think we're already connected to this peer, load up
// the existing peer state. Try to kick the peer if possible, // the existing peer state. Try to kick the peer if possible,
@ -351,6 +361,12 @@ func (l *links) listen(u *url.URL, sintf string) (*Listener, error) {
} }
options.priority = uint8(pi) options.priority = uint8(pi)
} }
if p := u.Query().Get("password"); p != "" {
if len(p) > blake2b.Size {
return nil, ErrLinkPasswordInvalid
}
options.password = []byte(p)
}
go func() { go func() {
l.core.log.Printf("%s listener started on %s", strings.ToUpper(u.Scheme), listener.Addr()) l.core.log.Printf("%s listener started on %s", strings.ToUpper(u.Scheme), listener.Addr())
@ -476,7 +492,10 @@ func (l *links) handler(linkType linkType, options linkOptions, conn net.Conn) e
meta := version_getBaseMetadata() meta := version_getBaseMetadata()
meta.publicKey = l.core.public meta.publicKey = l.core.public
meta.priority = options.priority meta.priority = options.priority
metaBytes := meta.encode() metaBytes, err := meta.encode(l.core.secret, options.password)
if err != nil {
return fmt.Errorf("failed to generate handshake: %w", err)
}
if err := conn.SetDeadline(time.Now().Add(time.Second * 6)); err != nil { if err := conn.SetDeadline(time.Now().Add(time.Second * 6)); err != nil {
return fmt.Errorf("failed to set handshake deadline: %w", err) return fmt.Errorf("failed to set handshake deadline: %w", err)
} }
@ -489,7 +508,7 @@ func (l *links) handler(linkType linkType, options linkOptions, conn net.Conn) e
} }
meta = version_metadata{} meta = version_metadata{}
base := version_getBaseMetadata() base := version_getBaseMetadata()
if !meta.decode(conn) { if !meta.decode(conn, options.password) {
return conn.Close() return conn.Close()
} }
if !meta.check() { if !meta.check() {

View File

@ -8,7 +8,10 @@ import (
"bytes" "bytes"
"crypto/ed25519" "crypto/ed25519"
"encoding/binary" "encoding/binary"
"fmt"
"io" "io"
"golang.org/x/crypto/blake2b"
) )
// This is the version-specific metadata exchanged at the start of a connection. // This is the version-specific metadata exchanged at the start of a connection.
@ -26,6 +29,8 @@ const (
ProtocolVersionMinor uint16 = 5 ProtocolVersionMinor uint16 = 5
) )
// Once a major/minor version is released, it is not safe to change any of these
// (including their ordering), it is only safe to add new ones.
const ( const (
metaVersionMajor uint16 = iota // uint16 metaVersionMajor uint16 = iota // uint16
metaVersionMinor // uint16 metaVersionMinor // uint16
@ -42,7 +47,7 @@ func version_getBaseMetadata() version_metadata {
} }
// Encodes version metadata into its wire format. // Encodes version metadata into its wire format.
func (m *version_metadata) encode() []byte { func (m *version_metadata) encode(privateKey ed25519.PrivateKey, password []byte) ([]byte, error) {
bs := make([]byte, 0, 64) bs := make([]byte, 0, 64)
bs = append(bs, 'm', 'e', 't', 'a') bs = append(bs, 'm', 'e', 't', 'a')
bs = append(bs, 0, 0) // Remaining message length bs = append(bs, 0, 0) // Remaining message length
@ -63,12 +68,26 @@ func (m *version_metadata) encode() []byte {
bs = binary.BigEndian.AppendUint16(bs, 1) bs = binary.BigEndian.AppendUint16(bs, 1)
bs = append(bs, m.priority) bs = append(bs, m.priority)
hasher, err := blake2b.New512(password)
if err != nil {
return nil, err
}
n, err := hasher.Write(m.publicKey)
if err != nil {
return nil, err
}
if n != ed25519.PublicKeySize {
return nil, fmt.Errorf("hash writer only wrote %d bytes", n)
}
hash := hasher.Sum(nil)
bs = append(bs, ed25519.Sign(privateKey, hash)...)
binary.BigEndian.PutUint16(bs[4:6], uint16(len(bs)-6)) binary.BigEndian.PutUint16(bs[4:6], uint16(len(bs)-6))
return bs return bs, nil
} }
// Decodes version metadata from its wire format into the struct. // Decodes version metadata from its wire format into the struct.
func (m *version_metadata) decode(r io.Reader) bool { func (m *version_metadata) decode(r io.Reader, password []byte) bool {
bh := [6]byte{} bh := [6]byte{}
if _, err := io.ReadFull(r, bh[:]); err != nil { if _, err := io.ReadFull(r, bh[:]); err != nil {
return false return false
@ -81,6 +100,10 @@ func (m *version_metadata) decode(r io.Reader) bool {
if _, err := io.ReadFull(r, bs); err != nil { if _, err := io.ReadFull(r, bs); err != nil {
return false return false
} }
sig := bs[len(bs)-ed25519.SignatureSize:]
bs = bs[:len(bs)-ed25519.SignatureSize]
for len(bs) >= 4 { for len(bs) >= 4 {
op := binary.BigEndian.Uint16(bs[:2]) op := binary.BigEndian.Uint16(bs[:2])
oplen := binary.BigEndian.Uint16(bs[2:4]) oplen := binary.BigEndian.Uint16(bs[2:4])
@ -103,7 +126,17 @@ func (m *version_metadata) decode(r io.Reader) bool {
} }
bs = bs[oplen:] bs = bs[oplen:]
} }
return true
hasher, err := blake2b.New512(password)
if err != nil {
return false
}
n, err := hasher.Write(m.publicKey)
if err != nil || n != ed25519.PublicKeySize {
return false
}
hash := hasher.Sum(nil)
return ed25519.Verify(m.publicKey, hash, sig)
} }
// Checks that the "meta" bytes and the version numbers are the expected values. // Checks that the "meta" bytes and the version numbers are the expected values.

View File

@ -3,33 +3,43 @@ package core
import ( import (
"bytes" "bytes"
"crypto/ed25519" "crypto/ed25519"
"crypto/rand"
"reflect" "reflect"
"testing" "testing"
) )
func TestVersionRoundtrip(t *testing.T) { func TestVersionRoundtrip(t *testing.T) {
for _, test := range []*version_metadata{ for _, password := range [][]byte{
{majorVer: 1}, nil, []byte(""), []byte("foo"),
{majorVer: 256},
{majorVer: 2, minorVer: 4},
{majorVer: 2, minorVer: 257},
{majorVer: 258, minorVer: 259},
{majorVer: 3, minorVer: 5, priority: 6},
{majorVer: 260, minorVer: 261, priority: 7},
} { } {
// Generate a random public key for each time, since it is for _, test := range []*version_metadata{
// a required field. {majorVer: 1},
test.publicKey = make(ed25519.PublicKey, ed25519.PublicKeySize) {majorVer: 256},
_, _ = rand.Read(test.publicKey) {majorVer: 2, minorVer: 4},
{majorVer: 2, minorVer: 257},
{majorVer: 258, minorVer: 259},
{majorVer: 3, minorVer: 5, priority: 6},
{majorVer: 260, minorVer: 261, priority: 7},
} {
// Generate a random public key for each time, since it is
// a required field.
pk, sk, err := ed25519.GenerateKey(nil)
if err != nil {
t.Fatal(err)
}
encoded := bytes.NewBuffer(test.encode()) test.publicKey = pk
decoded := &version_metadata{} meta, err := test.encode(sk, password)
if !decoded.decode(encoded) { if err != nil {
t.Fatalf("failed to decode") t.Fatal(err)
} }
if !reflect.DeepEqual(test, decoded) { encoded := bytes.NewBuffer(meta)
t.Fatalf("round-trip failed\nwant: %+v\n got: %+v", test, decoded) decoded := &version_metadata{}
if !decoded.decode(encoded, password) {
t.Fatalf("failed to decode")
}
if !reflect.DeepEqual(test, decoded) {
t.Fatalf("round-trip failed\nwant: %+v\n got: %+v", test, decoded)
}
} }
} }
} }