nox

package module
v0.0.0-...-c266a4d Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 29, 2022 License: MIT Imports: 17 Imported by: 1

README

nox - bidirectional streaming transport protocol, mutually authenticated and secured by the noise protocol variant Noise_XX_25519_ChaChaPoly_BLAKE2b, with simple framing.

For documentation, see https://godoc.org/github.com/mjl-/nox.
For an example client & server, see cmd/nox/.

The nox protocol is described in PROTOCOL.txt.
See https://noiseprotocol.org/ for the noise protocol families.
This code uses the noise Go library at https://github.com/flynn/noise.

Design inspiration for nox:
- Looking for a nearby .nox directory: hg & git.
- Trust-on-first use with known_hosts file: ssh.
- Go library interface: "net" and "crypto/tls" packages.
- Using noise, small base64 keys, simple setup: wireguard.
- X.509, for what not to want.

# TODO

- need an implementation in another language
- test with go-fuzz
- clear crypto state on close, https://github.com/golang/go/issues/21865
- audit the code
- add benchmark tests, can probably be made more efficient with less data copying
- more tests, counterparty with invalid protocol messages, use a transcript of successful communication.

Documentation

Overview

Package nox implements the nox protocol, a bidirectional streaming transport protocol, mutually authenticated and secured by the noise protocol variant Noise_XX_25519_ChaChaPoly_BLAKE2b, with simple framing.

The nox protocol uses Curve25519 for static and ephemeral keys.A 32-byte public key is generated from a 32-byte private key. The static public key serves as an identity. In this package, keys are stored in base64-raw-url encoding, making them easy to handle and embed in config files and even URLs.

In nox, both parties typically have trust configured out of bounds, like with SSH and WireGuard. Nox does not use a PKI (public key infrastructure). No certificate authorities are involved. Keys do not expire. No X.509 certificates are involved.

This package provides a programming interface similar to "net" and "crypto/tls" for making secure connections. Dial and Listen parse extended nox addresses that optionally contain public and private keys (described below), making integration of nox in existing Go code extremely easy.

Errors returned by nox are typically wrapped with additional information. Use errors.Is() or Unwrap to check for errors.

Security

Nox uses the Noise XX handshake. The ephemeral key exchange provides forward secrecy. The static key exchange provides mutual authentication. The static public key is the identity of the remote party. The identity of the client remains hidden. Server identities can be probed by any connection. BLAKE2b is used to hash the handshake into keying material used for symmetric authenticated encryption (AEAD) used in the transport phase. Nox uses ChaCha20 with Poly1305 as AEAD algorithm.

Nox addresses

Nox uses an address format that can include keys, or specify where the keys should be read from:

host:port+local+remote

Host and port are like in regular dial addresses. Local specifies the (source of) the local private key, remote for the remote's public key.

Local can be a literal private key. Remote can be a comma-separate list of literal public keys.

Alternatively, nox can read keys from the nearest ".nox" directory. If local is "fs", the key is read from ".nox/private_key". If remote is "known", a list of trusted public keys is read from ".nox/known_hosts". Nox uses uses "+fs+known" as default policy for connections. Nox also recognizes "tofu" for remote, trusting remote public keys on first use for the dialed address. See ParseAddress for details.

Use cmd/nox to initialize a ".nox" directory and to create simple servers and clients.

Index

Examples

Constants

View Source
const (

	// Nox0 is the fist version identifier for the hello message in protocol negotiation.
	Nox0 = "nox0"
)

Variables

View Source
var (
	// Versions holds the nox protocol versions supported by this package.
	Versions = []string{Nox0}

	// ErrVersionMismatch is returned when no mutually supported nox version could be
	// negotiated.
	ErrVersionMismatch = errors.New("protocol version mismatch")

	// ErrNoPrivateKey indicates no private key was found, either in the config or
	// through the nox address.
	ErrNoPrivateKey = errors.New("no private key")

	// ErrBadKey indicates a key is not valid, either public or private. Possibly
	// invalid base64-raw-url-encoded data, or not 32 bytes.
	ErrBadKey = errors.New("bad key")

	// ErrBadAddress is returned when a nox address is malformed.
	ErrBadAddress = errors.New("malformed nox address")

	// ErrBadConfig is returned when a config and address cannot be turned into a
	// usable Config.
	ErrBadConfig = errors.New("invalid configuration/address combination")

	// ErrHandshakeAborted is returned when the client disconnects after seeing the
	// server's static key. This may indicate the client does not trust the server, or
	// that the client is probing the server for its static key.
	ErrHandshakeAborted = errors.New("handshake aborted by client")

	// ErrRemoteUntrusted is returned when the remote host did not have a trusted
	// static public key.
	ErrRemoteUntrusted = errors.New("remote untrusted")

	// ErrProtocol is returned for protocol-level errors, like malformed messages.
	ErrProtocol = errors.New("protocol error")

	// ErrNoHandshake is returned for operations before having completed the handshake.
	ErrNoHandshake = errors.New("handshake not completed yet")

	// ErrConnClosed is returned when calling functions on a closed connection.
	ErrConnClosed = errors.New("connection closed")

	// ErrNoNoxDir indicates no .nox directory was found.
	ErrNoNoxDir = errors.New("no .nox directory found")

	// ErrNoKnownHosts indicates no .nox/known_hosts file was found.
	ErrNoKnownHosts = errors.New("no .nox/known_hosts file was found")
)

Functions

func CheckKnownhosts

func CheckKnownhosts(address string, pubKey PublicKey, conn *Conn) error

CheckKnownhosts looks up address in the "known_hosts" file in the nearest ".nox" directory. If it is present and the public key matches, CheckKnownhosts returns nil.

CheckKnownhosts implements the remote specifier "known" in nox addresses.

func CheckTrustOnFirstUse

func CheckTrustOnFirstUse(address string, pubKey PublicKey, conn *Conn) error

CheckTrustOnFirstUse is like CheckKnownhosts. If a public key is associated with the address in the "known_hosts" file, the check passes. If the address is not in the "known_hosts" file, CheckTrustOnFirstUse adds the remote public key to the file. Future connections to the same address require the same public key from remote.

It is an error if the "known_hosts" file does not exist yet.

CheckTrustOnFirstUse implements the remote specifier "tofu" in nox addresses.

func Listen

func Listen(network, address string, config *Config) (net.Listener, error)

Listen creates a new listener for incoming connections. Accept on the returned listener returns a *Conn with the handshake not yet completed. The first read or write performs the handshake, as does calling RemoteAddress.

Listen calls ParseAddress on address, which can be a nox address.

Example
package main

import (
	"io"
	"log"

	"github.com/mjl-/nox"
)

func main() {
	// Use defaults "+fs+known" to make server read ".nox/private_key" and ".nox/known_hosts".
	address := "localhost:1047"

	config := &nox.Config{}
	l, err := nox.Listen("tcp", address, config)
	if err != nil {
		log.Fatalf("listen: %s", err)
	}

	log.Printf("listening on %s, local static public key %s\n", config.Address, config.LocalStaticPublic())

	serve := func(conn *nox.Conn) {
		defer conn.Close()
		io.Copy(conn, conn)
	}

	for {
		conn, err := l.Accept()
		if err != nil {
			log.Fatalf("accept: %s", err)
		}

		go serve(conn.(*nox.Conn))
	}
}
Output:

func NearestNoxDir

func NearestNoxDir() (string, error)

NearestNoxDir locates the nearest directory named ".nox", starting at the current directory, walking up to the root. If no directory was found, ErrNoNoxDir is returned.

func ParseAddress

func ParseAddress(address string, config *Config) (rerr error)

ParseAddress parses a regular "host:port" address, or a nox address of the form "host:port+local+remote". Config is updated with information from "local" and "remote". The leftover regular address is stored in config.Address.

"Local" specifies the local static private key, and must be one of:

  • a literal base64-raw-url-encoded key. Keep in mind this address may be printed or logged, revealing it unintentionally.
  • "fs", read the key from the file "private_key" from the nearest ".nox" directory. The default for regular addresses.
  • "new", a new private key is created and used for the lifetime of the program.
  • "" (empty string), nothing is done, in which case the "config" parameter must contain a private key.

"Remote" specifies the remote static public keys, and must be a comma-separated list of:

  • a literal base64-raw-url-encoded key.
  • "known", read the file "known_hosts" for known public keys from the nearest ".nox" directory. The default for regular addresses.
  • "tofu", for trust on first use, like "known", but adds a line to the known hosts file for a previously unseen address, returning an error if no known hosts file was found.
  • "any", for trusting any remote public key, this is unsafe and should only be used for testing or merely fetching the remote's public key.

Example addresses:

localhost:1047
localhost:1047+fs+known
localhost:1047+fs+tofu
localhost:1047+EzckHRK9zMVib3vIHYc17LztyyabLGaV5F7Z-ye5yRQ+S1KCaHr7wHI4f06GY4uPstZnPC6UIDzwkYq48B3lhG8
localhost:1047+new+any

Types

type Config

type Config struct {
	// Rand is used as source of cryptographic randomness in Nox. If nil, Reader from
	// crypto/rand is used.
	Rand io.Reader

	// Address to dial or listen after parsing nox address. Set by ParseAddress, which
	// is also called by Dial and Listen.
	Address string

	// LocalStaticPrivateKey is the private key used in the Noise protocol.
	// Can be set by direct assignment, through a nox address containing a private key,
	// or through the "fs" keyword.
	LocalStaticPrivateKey *noise.DHKey

	// CheckPublicKey is called (if set) to verify a remote public key for an address.
	// For server connections, the address passed to CheckPublicKey is "*".
	// See CheckKnownhosts and CheckTrustOnFirstUse.
	CheckPublicKey func(address string, pubKey PublicKey, conn *Conn) error
	// contains filtered or unexported fields
}

Config holds the authentication credentials for the secure nox connection.

func (*Config) LocalStaticPublic

func (c *Config) LocalStaticPublic() PublicKey

LocalStaticPublic returns the local public static key.

If no private key has been configured, LocalStaticPublic calls panic.

type Conn

type Conn struct {
	// contains filtered or unexported fields
}

Conn is a nox connection.

func Client

func Client(conn net.Conn, config *Config) (*Conn, error)

Client turns an existing (non-nox) connection in a nox connection. On failure, the existing connection is not closed.

func Dial

func Dial(network, address string, config *Config) (*Conn, error)

Dial connects to the remote and performs the nox handshake and checks if the remote is trusted.

Dial calls ParseAddress on address, which can be a nox address.

Example
package main

import (
	"log"

	"github.com/mjl-/nox"
)

func main() {
	// Connecting with the default "+fs+known" policy. Requires having a ".nox"
	// directory with "known_hosts" and "private_key" files.
	config := &nox.Config{}
	conn, err := nox.Dial("tcp", "localhost:1047", config)
	if err != nil {
		log.Fatalf("dial: %s", err)
	}
	// Handshake was completed, remote is trusted.

	conn.Close()
}
Output:

Example (Keys)
package main

import (
	"log"

	"github.com/mjl-/nox"
)

func main() {
	// Connecting with an address that includes private & public keys.
	address := "localhost:1047+9Raaywe4hLyJT7olZjwbjuGShPmqV0YD6aiX9r2uwps+nwpSVXwaGB5EpsRQvNyAzG1CYAGdJr5MrDhAvsdTyCs"
	config := &nox.Config{}
	conn, err := nox.Dial("tcp", address, config)
	if err != nil {
		log.Fatalf("dial: %s", err)
	}
	// Handshake was completed, remote is trusted.

	conn.Close()
}
Output:

func Server

func Server(conn net.Conn, config *Config) (*Conn, error)

Server turns an existing (non-nox) connection in a nox connection. On failure, the existing connection is not closed.

func (*Conn) Close

func (c *Conn) Close() error

Close closes the connection. If still connected, Close sends a message to signal EOF to remote, as in CloseWrite. Close then closes the underlying connection. If a write is in progress, Close immediately closes the underlying connection, assuming Close was called to abort.

func (*Conn) CloseWrite

func (c *Conn) CloseWrite() error

CloseWrite sends a zero-sized write to remote to indicate the end of data to remote. Data can still be read from remote until an EOF from remote is read. CloseWrite does not close the underlying connection.

func (*Conn) Handshake

func (c *Conn) Handshake() error

Handshake performs the protocol handshake: first version negotiation, then noise handshake. Read, Write or RemoteAddress on a new connection ensures a handshake is done.

Handshake returns an error if a handshake has already completed or failed.

func (*Conn) LocalAddr

func (c *Conn) LocalAddr() net.Addr

LocalAddr returns the local network address of the underlying connection.

func (*Conn) Read

func (c *Conn) Read(buf []byte) (read int, rerr error)

Read reads data from remote. Read returns io.EOF after an explicit close message from remote. Early hangups of the underlying connection result in an error other than io.EOF.

func (*Conn) RemoteAddr

func (c *Conn) RemoteAddr() net.Addr

RemoteAddr returns the remote network address of the underlying connection.

func (*Conn) RemoteStatic

func (c *Conn) RemoteStatic() (PublicKey, error)

RemoteStatic returns the remote's static public key. RemoteStatic ensures a handshake has been completed.

func (*Conn) SetDeadline

func (c *Conn) SetDeadline(t time.Time) error

SetDeadline calls the SetDeadline on the underlying connection.

func (*Conn) SetReadDeadline

func (c *Conn) SetReadDeadline(t time.Time) error

SetReadDeadline calls the SetReadDeadline on the underlying connection.

func (*Conn) SetWriteDeadline

func (c *Conn) SetWriteDeadline(t time.Time) error

SetWriteDeadline calls the SetWriteDeadline on the underlying connection.

func (*Conn) Write

func (c *Conn) Write(buf []byte) (written int, rerr error)

Write writes data to remote.

type PublicKey

type PublicKey []byte

PublicKey presents a 32-byte public curve25519 key, for either local or remote.

func (PublicKey) String

func (k PublicKey) String() string

String returns a base64-raw-url-encoded version of the public key.

Directories

Path Synopsis
cmd
nox
Nox is a tool for making nox connections.
Nox is a tool for making nox connections.
noxhttpget
Noxhttpget shows how to use subpackage noxhttp to create a http client that communicates over nox.
Noxhttpget shows how to use subpackage noxhttp to create a http client that communicates over nox.
noxproxy
Noxproxy is an HTTP proxy allowing incoming plain requests and making outgoing requests over nox.
Noxproxy is an HTTP proxy allowing incoming plain requests and making outgoing requests over nox.
Package noxhttp provides a http.RoundTripper for making HTTP requests over nox connections.
Package noxhttp provides a http.RoundTripper for making HTTP requests over nox connections.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL