From 268ffbfd14baed7503e7be7cccc3cf19a6e0f7f5 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Mon, 9 Oct 2023 16:44:07 +0100 Subject: [PATCH 1/2] Add authenticated handshake, support for passwords --- src/core/link.go | 23 ++++++++++++++++-- src/core/version.go | 41 ++++++++++++++++++++++++++++---- src/core/version_test.go | 50 ++++++++++++++++++++++++---------------- 3 files changed, 88 insertions(+), 26 deletions(-) diff --git a/src/core/link.go b/src/core/link.go index 9e0b15fd..bc4f191b 100644 --- a/src/core/link.go +++ b/src/core/link.go @@ -17,6 +17,7 @@ import ( "github.com/Arceliar/phony" "github.com/yggdrasil-network/yggdrasil-go/src/address" + "golang.org/x/crypto/blake2b" ) type linkType int @@ -65,6 +66,7 @@ type linkOptions struct { pinnedEd25519Keys map[keyArray]struct{} priority uint8 tlsSNI string + password []byte } type Listener struct { @@ -129,6 +131,7 @@ func (e linkError) Error() string { return string(e) } const ErrLinkAlreadyConfigured = linkError("peer is already configured") const ErrLinkPriorityInvalid = linkError("priority value is invalid") const ErrLinkPinnedKeyInvalid = linkError("pinned public key is invalid") +const ErrLinkPasswordInvalid = linkError("password is invalid") const ErrLinkUnrecognisedSchema = linkError("link schema unknown") 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) } + 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 // 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) } + if p := u.Query().Get("password"); p != "" { + if len(p) > blake2b.Size { + return nil, ErrLinkPasswordInvalid + } + options.password = []byte(p) + } go func() { 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.publicKey = l.core.public 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 { 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{} base := version_getBaseMetadata() - if !meta.decode(conn) { + if !meta.decode(conn, options.password) { return conn.Close() } if !meta.check() { diff --git a/src/core/version.go b/src/core/version.go index 0820fbdd..e01fe10c 100644 --- a/src/core/version.go +++ b/src/core/version.go @@ -8,7 +8,10 @@ import ( "bytes" "crypto/ed25519" "encoding/binary" + "fmt" "io" + + "golang.org/x/crypto/blake2b" ) // This is the version-specific metadata exchanged at the start of a connection. @@ -26,6 +29,8 @@ const ( 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 ( metaVersionMajor uint16 = iota // uint16 metaVersionMinor // uint16 @@ -42,7 +47,7 @@ func version_getBaseMetadata() version_metadata { } // 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 = append(bs, 'm', 'e', 't', 'a') 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 = 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)) - return bs + return bs, nil } // 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{} if _, err := io.ReadFull(r, bh[:]); err != nil { return false @@ -81,6 +100,10 @@ func (m *version_metadata) decode(r io.Reader) bool { if _, err := io.ReadFull(r, bs); err != nil { return false } + + sig := bs[len(bs)-ed25519.SignatureSize:] + bs = bs[:len(bs)-ed25519.SignatureSize] + for len(bs) >= 4 { op := binary.BigEndian.Uint16(bs[:2]) oplen := binary.BigEndian.Uint16(bs[2:4]) @@ -103,7 +126,17 @@ func (m *version_metadata) decode(r io.Reader) bool { } 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. diff --git a/src/core/version_test.go b/src/core/version_test.go index 1c1f673a..b71010fb 100644 --- a/src/core/version_test.go +++ b/src/core/version_test.go @@ -3,33 +3,43 @@ package core import ( "bytes" "crypto/ed25519" - "crypto/rand" "reflect" "testing" ) func TestVersionRoundtrip(t *testing.T) { - for _, test := range []*version_metadata{ - {majorVer: 1}, - {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}, + for _, password := range [][]byte{ + nil, []byte(""), []byte("foo"), } { - // Generate a random public key for each time, since it is - // a required field. - test.publicKey = make(ed25519.PublicKey, ed25519.PublicKeySize) - _, _ = rand.Read(test.publicKey) + for _, test := range []*version_metadata{ + {majorVer: 1}, + {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 + // a required field. + pk, sk, err := ed25519.GenerateKey(nil) + if err != nil { + t.Fatal(err) + } - encoded := bytes.NewBuffer(test.encode()) - decoded := &version_metadata{} - if !decoded.decode(encoded) { - t.Fatalf("failed to decode") - } - if !reflect.DeepEqual(test, decoded) { - t.Fatalf("round-trip failed\nwant: %+v\n got: %+v", test, decoded) + test.publicKey = pk + meta, err := test.encode(sk, password) + if err != nil { + t.Fatal(err) + } + encoded := bytes.NewBuffer(meta) + 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) + } } } } From bd7e699130ef0c647eaec30f2c5fc8c5c55a7b14 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Mon, 9 Oct 2023 22:28:20 +0100 Subject: [PATCH 2/2] Add unit test for password auth --- src/core/version_test.go | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/core/version_test.go b/src/core/version_test.go index b71010fb..7efe2f35 100644 --- a/src/core/version_test.go +++ b/src/core/version_test.go @@ -7,6 +7,39 @@ import ( "testing" ) +func TestVersionPasswordAuth(t *testing.T) { + for _, tt := range []struct { + password1 []byte // The password on node 1 + password2 []byte // The password on node 2 + allowed bool // Should the connection have been allowed? + }{ + {nil, nil, true}, // Allow: No passwords (both nil) + {nil, []byte(""), true}, // Allow: No passwords (mixed nil and empty string) + {nil, []byte("foo"), false}, // Reject: One node has a password, the other doesn't + {[]byte("foo"), []byte(""), false}, // Reject: One node has a password, the other doesn't + {[]byte("foo"), []byte("foo"), true}, // Allow: Same password + {[]byte("foo"), []byte("bar"), false}, // Reject: Different passwords + } { + pk1, sk1, err := ed25519.GenerateKey(nil) + if err != nil { + t.Fatalf("Node 1 failed to generate key: %s", err) + } + + metadata1 := &version_metadata{ + publicKey: pk1, + } + encoded, err := metadata1.encode(sk1, tt.password1) + if err != nil { + t.Fatalf("Node 1 failed to encode metadata: %s", err) + } + + var decoded version_metadata + if allowed := decoded.decode(bytes.NewBuffer(encoded), tt.password2); allowed != tt.allowed { + t.Fatalf("Permutation %q -> %q should have been %v but was %v", tt.password1, tt.password2, tt.allowed, allowed) + } + } +} + func TestVersionRoundtrip(t *testing.T) { for _, password := range [][]byte{ nil, []byte(""), []byte("foo"),