[noise] Does the NK handshake pattern require a separate server pubkey check?

David Fifield david at bamsoftware.com
Tue Apr 21 09:31:00 PDT 2020


On Tue, Apr 21, 2020 at 12:12:07AM -0700, Trevor Perrin wrote:
> > 2. Am I correct that if the pattern were instead NX, that the client
> >    *would* have to check, after the handshake, that the s received from
> >    the server is the expected public key?
> 
> Well, if the client already received the server's public key
> out-of-band it wouldn't have to check it, but in that situation
> there's no reason to be using NX.  I.e. NX could be used just like NK
> and has the same authentication properties, it just additionally
> transmits the public key in case the client doesn't already have it.

There must be something I misunderstand. I don't see what part of the NX
handshake pattern inherently fails when the initiator and responder have
different ideas of what the responder's static key is, for example in
the case of MITM. I get that the NK pattern suffices if the initiator
has out-of-band knowledge of the responder's static key; I just want to
check my understanding that if the NX pattern is used in the same
situation, the initiator needs to check the received static key,
separately from the Noise handshake. I'm thinking by analogy with TLS
applications that use public key pinning in lieu of CA validation: after
the TLS handshake, you still have to check that the server's public key
is as expected.

I'm attaching some test programs. They run either the NK or NX handshake
pattern, with the initiator having a correct ("true") or incorrect
("false") idea of the responder's static key.

With NK, the "true" case works and the "false" case fails:
go run nk.go true
	responder's static key:                      e827557afd4fe75c014bb03b84af68df4976876ddb993386bddf56b70b81692b
	initiator's peer static key, pre-handshake:  e827557afd4fe75c014bb03b84af68df4976876ddb993386bddf56b70b81692b
	initiator's peer static key, post-handshake: e827557afd4fe75c014bb03b84af68df4976876ddb993386bddf56b70b81692b
	responder received "hello"
	responder error: <nil>
	initiator received "world"
	initiator error: <nil>
go run nk.go false
	responder's static key:                      1ff2ce2a5c254c69b91288ab6d9cab3eefeb174ba87e5f05bf1117a6c4d28015
	initiator's peer static key, pre-handshake:  7302e0a398b3496c4de3f01fc2df105d5d8356292fe54ba59e78142e7c97b92b
	responder error: chacha20poly1305: message authentication failed

But with NX, both "true" and "false" cases go through without error:
go run nx.go true
	initiator's peer static key, pre-handshake:  201e3a8647e94806609131afaa608d90c056de084260abb245840feefe933e3a
	responder's static key:                      201e3a8647e94806609131afaa608d90c056de084260abb245840feefe933e3a
	initiator's peer static key, post-handshake: 201e3a8647e94806609131afaa608d90c056de084260abb245840feefe933e3a
	responder received "hello"
	responder error: <nil>
	initiator received "world"
	initiator error: <nil>
go run nx.go false
	responder's static key:                      d6841baf36993777cb4d9dd8995a7979d651889a3c8761919d54694035374625
	initiator's peer static key, pre-handshake:  c87a2f5446aa9dea52a58fece74ea786eb0ae57b3bef05328553d136f0cf4856
	initiator's peer static key, post-handshake: d6841baf36993777cb4d9dd8995a7979d651889a3c8761919d54694035374625
	responder received "hello"
	responder error: <nil>
	initiator received "world"
	initiator error: <nil>

In nx.go there's a commented block of code marked "This check seems to
be necessary." That's the kind of post-handshake static key check that
seems to be necessary. With it uncommented, I get the kind of error I
expect:
go run nx.go false
	responder's static key:                      c121d4d85f048078b3b45f45a2f501ba191d233b361ce6b04bf7911f05981559
	initiator's peer static key, pre-handshake:  734545c920cd1fc7893f40f04f29babead21e97cbb723de559e2894860c49800
	initiator's peer static key, post-handshake: c121d4d85f048078b3b45f45a2f501ba191d233b361ce6b04bf7911f05981559
	initiator error: responder static key was not as expected
-------------- next part --------------
package main

import (
	"crypto/rand"
	"fmt"
	"os"

	"github.com/flynn/noise"
)

var cipherSuite = noise.NewCipherSuite(noise.DH25519, noise.CipherChaChaPoly, noise.HashBLAKE2s)

func initiator(r <-chan []byte, w chan<- []byte, respStatic []byte) error {
	hs, err := noise.NewHandshakeState(noise.Config{
		CipherSuite: cipherSuite,
		Pattern:     noise.HandshakeNK,
		Initiator:   true,
		PeerStatic:  respStatic,
	})
	if err != nil {
		return err
	}

	fmt.Printf("initiator's peer static key, pre-handshake:  %x\n", respStatic)

	// -> e, es
	msg, _, _, err := hs.WriteMessage(nil, nil)
	if err != nil {
		return err
	}
	w <- msg

	// <- e, es
	msg = <-r
	_, recvCipher, sendCipher, err := hs.ReadMessage(nil, msg)
	if err != nil {
		return err
	}

	fmt.Printf("initiator's peer static key, post-handshake: %x\n", hs.PeerStatic())

	w <- sendCipher.Encrypt(nil, nil, []byte("hello"))
	p, err := recvCipher.Decrypt(nil, nil, <-r)
	if err != nil {
		return err
	}
	fmt.Printf("initiator received %+q\n", p)

	return nil
}

func responder(r <-chan []byte, w chan<- []byte, keypair noise.DHKey) error {
	hs, err := noise.NewHandshakeState(noise.Config{
		CipherSuite:   cipherSuite,
		Pattern:       noise.HandshakeNK,
		Initiator:     false,
		StaticKeypair: keypair,
	})
	if err != nil {
		return err
	}

	fmt.Printf("responder's static key:                      %x\n", keypair.Public)

	// -> e, es
	msg := <-r
	_, _, _, err = hs.ReadMessage(nil, msg)
	if err != nil {
		return err
	}

	// <- e, es
	msg, sendCipher, recvCipher, err := hs.WriteMessage(nil, nil)
	if err != nil {
		return err
	}
	w <- msg

	p, err := recvCipher.Decrypt(nil, nil, <-r)
	if err != nil {
		return err
	}
	fmt.Printf("responder received %+q\n", p)
	w <- sendCipher.Encrypt(nil, nil, []byte("world"))

	return nil
}

func main() {
	var useCorrectPubkey bool
	switch {
	case len(os.Args) >= 2 && os.Args[1] == "true":
		useCorrectPubkey = true
	case len(os.Args) >= 2 && os.Args[1] == "false":
		useCorrectPubkey = false
	default:
		fmt.Fprintf(os.Stderr, `usage: %s [true|false]
true:  initiator uses the correct public key
false: initiator uses a wrong public key
`, os.Args[0])
		os.Exit(1)
	}

	respKeypair, err := noise.DH25519.GenerateKeypair(rand.Reader)
	if err != nil {
		panic(err)
	}
	var respStatic []byte // the initiator's concept of the responder's public key
	if useCorrectPubkey {
		respStatic = respKeypair.Public
	} else {
		falseKeypair, err := noise.DH25519.GenerateKeypair(rand.Reader)
		if err != nil {
			panic(err)
		}
		respStatic = falseKeypair.Public
	}

	initToResp := make(chan []byte, 10)
	respToInit := make(chan []byte, 10)

	initErrCh := make(chan error)
	respErrCh := make(chan error)
	go func() {
		initErrCh <- initiator(respToInit, initToResp, respStatic)
		close(initToResp)
	}()
	go func() {
		respErrCh <- responder(initToResp, respToInit, respKeypair)
		close(respToInit)
	}()

	select {
	case err := <-initErrCh:
		fmt.Printf("initiator error: %v\n", err)
		fmt.Printf("responder error: %v\n", <-respErrCh)
	case err := <-respErrCh:
		fmt.Printf("responder error: %v\n", err)
		fmt.Printf("initiator error: %v\n", <-initErrCh)
	}
}
-------------- next part --------------
package main

import (
	_ "bytes"
	"crypto/rand"
	"fmt"
	"os"

	"github.com/flynn/noise"
)

var cipherSuite = noise.NewCipherSuite(noise.DH25519, noise.CipherChaChaPoly, noise.HashBLAKE2s)

func initiator(r <-chan []byte, w chan<- []byte, respStatic []byte) error {
	hs, err := noise.NewHandshakeState(noise.Config{
		CipherSuite: cipherSuite,
		Pattern:     noise.HandshakeNX,
		Initiator:   true,
	})
	if err != nil {
		return err
	}

	fmt.Printf("initiator's peer static key, pre-handshake:  %x\n", respStatic)

	// -> e
	msg, _, _, err := hs.WriteMessage(nil, nil)
	if err != nil {
		return err
	}
	w <- msg

	// <- e, ee, s, es
	msg = <-r
	_, recvCipher, sendCipher, err := hs.ReadMessage(nil, msg)
	if err != nil {
		return err
	}

	fmt.Printf("initiator's peer static key, post-handshake: %x\n", hs.PeerStatic())

	/*
		// This check seems to be necessary.
		if !bytes.Equal(respStatic, hs.PeerStatic()) {
			return fmt.Errorf("responder static key was not as expected")
		}
	*/

	w <- sendCipher.Encrypt(nil, nil, []byte("hello"))
	p, err := recvCipher.Decrypt(nil, nil, <-r)
	if err != nil {
		return err
	}
	fmt.Printf("initiator received %+q\n", p)

	return nil
}

func responder(r <-chan []byte, w chan<- []byte, keypair noise.DHKey) error {
	hs, err := noise.NewHandshakeState(noise.Config{
		CipherSuite:   cipherSuite,
		Pattern:       noise.HandshakeNX,
		Initiator:     false,
		StaticKeypair: keypair,
	})
	if err != nil {
		return err
	}

	fmt.Printf("responder's static key:                      %x\n", keypair.Public)

	// -> e
	msg := <-r
	_, _, _, err = hs.ReadMessage(nil, msg)
	if err != nil {
		return err
	}

	// <- e, ee, s, es
	msg, sendCipher, recvCipher, err := hs.WriteMessage(nil, nil)
	if err != nil {
		return err
	}
	w <- msg

	p, err := recvCipher.Decrypt(nil, nil, <-r)
	if err != nil {
		return err
	}
	fmt.Printf("responder received %+q\n", p)
	w <- sendCipher.Encrypt(nil, nil, []byte("world"))

	return nil
}

func main() {
	var useCorrectPubkey bool
	switch {
	case len(os.Args) >= 2 && os.Args[1] == "true":
		useCorrectPubkey = true
	case len(os.Args) >= 2 && os.Args[1] == "false":
		useCorrectPubkey = false
	default:
		fmt.Fprintf(os.Stderr, `usage: %s [true|false]
true:  initiator uses the correct public key
false: initiator uses a wrong public key
`, os.Args[0])
		os.Exit(1)
	}

	respKeypair, err := noise.DH25519.GenerateKeypair(rand.Reader)
	if err != nil {
		panic(err)
	}
	var respStatic []byte // the initiator's concept of the responder's public key
	if useCorrectPubkey {
		respStatic = respKeypair.Public
	} else {
		falseKeypair, err := noise.DH25519.GenerateKeypair(rand.Reader)
		if err != nil {
			panic(err)
		}
		respStatic = falseKeypair.Public
	}

	initToResp := make(chan []byte, 10)
	respToInit := make(chan []byte, 10)

	initErrCh := make(chan error)
	respErrCh := make(chan error)
	go func() {
		initErrCh <- initiator(respToInit, initToResp, respStatic)
		close(initToResp)
	}()
	go func() {
		respErrCh <- responder(initToResp, respToInit, respKeypair)
		close(respToInit)
	}()

	select {
	case err := <-initErrCh:
		fmt.Printf("initiator error: %v\n", err)
		fmt.Printf("responder error: %v\n", <-respErrCh)
	case err := <-respErrCh:
		fmt.Printf("responder error: %v\n", err)
		fmt.Printf("initiator error: %v\n", <-initErrCh)
	}
}


More information about the Noise mailing list