Skip to content

Commit

Permalink
v2: Improve signature and nonce handling
Browse files Browse the repository at this point in the history
  • Loading branch information
lukechampine committed Feb 18, 2025
1 parent abf58b0 commit e057384
Show file tree
Hide file tree
Showing 5 changed files with 45 additions and 28 deletions.
8 changes: 4 additions & 4 deletions mux.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func (m *Mux) DialStream() *Stream {
func Dial(conn net.Conn, theirKey ed25519.PublicKey) (*Mux, error) {
// exchange versions
var theirVersion [1]byte
if _, err := conn.Write([]byte{2}); err != nil {
if _, err := conn.Write([]byte{3}); err != nil {
return nil, fmt.Errorf("could not write our version: %w", err)
} else if _, err := io.ReadFull(conn, theirVersion[:]); err != nil {
return nil, fmt.Errorf("could not read peer version: %w", err)
Expand All @@ -62,7 +62,7 @@ func Dial(conn net.Conn, theirKey ed25519.PublicKey) (*Mux, error) {
m, err := muxv1.Dial(conn, theirKey)
return &Mux{m1: m}, err
}
m, err := muxv2.Dial(conn, theirKey)
m, err := muxv2.Dial(conn, theirKey, theirVersion[0])
return &Mux{m2: m}, err
}

Expand All @@ -72,7 +72,7 @@ func Accept(conn net.Conn, ourKey ed25519.PrivateKey) (*Mux, error) {
var theirVersion [1]byte
if _, err := io.ReadFull(conn, theirVersion[:]); err != nil {
return nil, fmt.Errorf("could not read peer version: %w", err)
} else if _, err := conn.Write([]byte{2}); err != nil {
} else if _, err := conn.Write([]byte{3}); err != nil {
return nil, fmt.Errorf("could not write our version: %w", err)
} else if theirVersion[0] == 0 {
return nil, errors.New("peer sent invalid version")
Expand All @@ -81,7 +81,7 @@ func Accept(conn net.Conn, ourKey ed25519.PrivateKey) (*Mux, error) {
m, err := muxv1.Accept(conn, ourKey)
return &Mux{m1: m}, err
}
m, err := muxv2.Accept(conn, ourKey)
m, err := muxv2.Accept(conn, ourKey, theirVersion[0])
return &Mux{m2: m}, err
}

Expand Down
19 changes: 10 additions & 9 deletions spec_v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,18 @@ The *dialing peer* generates an X25519 keypair and sends:

| Length | Type | Description |
|--------|--------|---------------|
| 0 | uint8 | Version |
| 1 | uint8 | Version |
| 32 | []byte | X25519 pubkey |

The current version is 2.
The current version is 3.

The *accepting* peer derives the shared X25519 secret, hashes it with BLAKE2b
for use as a ChaCha20-Poly1305 key, hashes that key *again* to derive the
initial nonce value, and responds with:
The *accepting* peer generates an X25519 keypair, derives the shared X25519
secret, hashes it with BLAKE2b for use as a ChaCha20-Poly1305 key, hashes that
key *again* to derive the initial nonce value, and responds with:

| Length | Type | Description |
|--------|--------|--------------------|
| 0 | uint8 | Version |
| 1 | uint8 | Version |
| 32 | []byte | X25519 pubkey |
| 64 | []byte | Ed25519 signature |
| 24 | | Encrypted settings |
Expand All @@ -60,7 +60,7 @@ The settings are:
| 4 | uint32 | Max timeout | 120000-7200000 |

Settings are encrypted in the same manner as [Packets](#packets): a ciphertext
(8 bytes in this case) followed by a 16-byte tag.
(8 bytes in this case) followed by a 16-byte authentication tag.

Peers agree upon settings by choosing the minimum of the two packet sizes and
the maximum of the two timeouts. The timeout is an integer number of
Expand Down Expand Up @@ -116,8 +116,9 @@ must not be split across packet boundaries. (In other words, the maximum size of
a frame's payload is `n - (4 + 2 + 2)`.)

A separate nonce is tracked for both the dialing and accepting peer, incremented
after each use. The initial value for both nonces is the first 12 bytes of
BLAKE2b(BLAKE2b(shared secret)). To increment a nonce, interpret its leading 8
after each use. The initial value for the nonces is the first 12 bytes of
BLAKE2b(BLAKE2b(shared secret)), but the most significant bit of the accepting
peer's nonce is flipped. To increment a nonce, interpret its least-significant 8
bytes as a 64-bit unsigned integer.

### Covert Frames
Expand Down
30 changes: 23 additions & 7 deletions v2/handshake.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func mergeSettings(ours, theirs connSettings) (connSettings, error) {
return merged, nil
}

func initiateHandshake(conn net.Conn, theirKey ed25519.PublicKey, ourSettings connSettings) (*seqCipher, connSettings, error) {
func initiateHandshake(conn net.Conn, theirKey ed25519.PublicKey, theirVersion uint8, ourSettings connSettings) (*seqCipher, connSettings, error) {
xsk, xpk := generateX25519KeyPair()

// write pubkey
Expand All @@ -142,12 +142,16 @@ func initiateHandshake(conn net.Conn, theirKey ed25519.PublicKey, ourSettings co
return nil, connSettings{}, fmt.Errorf("could not read handshake response: %w", err)
}

// verify signature
// verify signature and derive shared cipher
var rxpk [32]byte
copy(rxpk[:], buf[:32])
msg := append(xpk[:], rxpk[:]...)
if theirVersion == 2 {
sigHash := blake2b.Sum256(msg)
msg = sigHash[:]
}
sig := buf[32:][:64]
sigHash := blake2b.Sum256(append(xpk[:], rxpk[:]...))
if !ed25519.Verify(theirKey, sigHash[:], sig) {
if !ed25519.Verify(theirKey, msg, sig) {
return nil, connSettings{}, errors.New("invalid signature")
}

Expand All @@ -156,6 +160,10 @@ func initiateHandshake(conn net.Conn, theirKey ed25519.PublicKey, ourSettings co
if err != nil {
return nil, connSettings{}, fmt.Errorf("failed to derive shared cipher: %w", err)
}
if theirVersion > 2 {
// flip the most significant bit of their nonce
cipher.theirNonce[len(cipher.theirNonce)-1] ^= 0x80
}

// decrypt settings
var mergedSettings connSettings
Expand All @@ -175,7 +183,7 @@ func initiateHandshake(conn net.Conn, theirKey ed25519.PublicKey, ourSettings co
return cipher, mergedSettings, nil
}

func acceptHandshake(conn net.Conn, ourKey ed25519.PrivateKey, ourSettings connSettings) (*seqCipher, connSettings, error) {
func acceptHandshake(conn net.Conn, ourKey ed25519.PrivateKey, theirVersion uint8, ourSettings connSettings) (*seqCipher, connSettings, error) {
xsk, xpk := generateX25519KeyPair()

// read pubkey
Expand All @@ -191,10 +199,18 @@ func acceptHandshake(conn net.Conn, ourKey ed25519.PrivateKey, ourSettings connS
if err != nil {
return nil, connSettings{}, fmt.Errorf("failed to derive shared cipher: %w", err)
}
if theirVersion > 2 {
// flip the most significant bit of our nonce
cipher.ourNonce[len(cipher.ourNonce)-1] ^= 0x80
}

// write pubkey, signature, and settings
sigHash := blake2b.Sum256(append(rxpk[:], xpk[:]...))
sig := ed25519.Sign(ourKey, sigHash[:])
msg := append(rxpk[:], xpk[:]...)
if theirVersion == 2 {
sigHash := blake2b.Sum256(msg)
msg = sigHash[:]
}
sig := ed25519.Sign(ourKey, msg)
copy(buf, xpk[:])
copy(buf[32:], sig)
encodeConnSettings(buf[32+64:], ourSettings)
Expand Down
12 changes: 6 additions & 6 deletions v2/mux.go
Original file line number Diff line number Diff line change
Expand Up @@ -383,17 +383,17 @@ func newMux(conn net.Conn, cipher *seqCipher, settings connSettings) *Mux {
}

// Dial initiates a mux protocol handshake on the provided conn.
func Dial(conn net.Conn, theirKey ed25519.PublicKey) (*Mux, error) {
cipher, settings, err := initiateHandshake(conn, theirKey, defaultConnSettings)
func Dial(conn net.Conn, theirKey ed25519.PublicKey, theirVersion uint8) (*Mux, error) {
cipher, settings, err := initiateHandshake(conn, theirKey, theirVersion, defaultConnSettings)
if err != nil {
return nil, fmt.Errorf("handshake failed: %w", err)
}
return newMux(conn, cipher, settings), nil
}

// Accept reciprocates a mux protocol handshake on the provided conn.
func Accept(conn net.Conn, ourKey ed25519.PrivateKey) (*Mux, error) {
cipher, settings, err := acceptHandshake(conn, ourKey, defaultConnSettings)
func Accept(conn net.Conn, ourKey ed25519.PrivateKey, theirVersion uint8) (*Mux, error) {
cipher, settings, err := acceptHandshake(conn, ourKey, theirVersion, defaultConnSettings)
if err != nil {
return nil, fmt.Errorf("handshake failed: %w", err)
}
Expand All @@ -408,12 +408,12 @@ var anonPubkey = anonPrivkey.Public().(ed25519.PublicKey)
// DialAnonymous initiates a mux protocol handshake to a party without a
// pre-established identity. The counterparty must reciprocate the handshake with
// AcceptAnonymous.
func DialAnonymous(conn net.Conn) (*Mux, error) { return Dial(conn, anonPubkey) }
func DialAnonymous(conn net.Conn) (*Mux, error) { return Dial(conn, anonPubkey, 3) }

// AcceptAnonymous reciprocates a mux protocol handshake without a
// pre-established identity. The counterparty must initiate the handshake with
// DialAnonymous.
func AcceptAnonymous(conn net.Conn) (*Mux, error) { return Accept(conn, anonPrivkey) }
func AcceptAnonymous(conn net.Conn) (*Mux, error) { return Accept(conn, anonPrivkey, 3) }

// A Stream is a duplex connection multiplexed over a net.Conn. It implements
// the net.Conn interface.
Expand Down
4 changes: 2 additions & 2 deletions v2/mux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func TestMux(t *testing.T) {
if err != nil {
return err
}
m, err := Accept(conn, serverKey)
m, err := Accept(conn, serverKey, 3)
if err != nil {
return err
}
Expand All @@ -111,7 +111,7 @@ func TestMux(t *testing.T) {
if err != nil {
t.Fatal(err)
}
m, err := Dial(conn, serverKey.Public().(ed25519.PublicKey))
m, err := Dial(conn, serverKey.Public().(ed25519.PublicKey), 3)
if err != nil {
t.Fatal(err)
}
Expand Down

0 comments on commit e057384

Please sign in to comment.