p2pke

package
v0.0.0-...-d4b7e32 Latest Latest
Warning

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

Go to latest
Published: Aug 12, 2023 License: MPL-2.0 Imports: 25 Imported by: 0

README

P2P Key Exchange

P2P Key Exchange is an authenticated key exchange built using the Noise Protocol Framework (NPF). It depends on a message based transport beneath it, and provides security, and at-most-once delivery of messages to callers. It makes no assumptions about the underlying transport, which can be anything that delivers datagrams larger than the per packet overhead.

Goals

  • Secure any message based transport without knowing anything about the underlying transport protocol. This would make it useful on ethernet, IP, and UDP, as well as in overlay networks.
  • Be Composable. Layer well on top of protocols and beneath protocols. Never assume the protocol is running on the bottom or top of the stack. This means small headers, and minimal communication not explicitly directed by the layer above.
  • Work with signing keys.
  • Simple. Use private and public keys directly, no certificates.

Compared to Other Protocols

P2PKE WireGuard QUIC DTLS 1.2
Secure Datagrams ⚠️️ Draft Spec
P2P ⚠️ Connection IDs ❌ Client/Server
Transport Agnostic ❌ UDP ❌ UDP ❌ UDP
PKI Complexity ✅ Public Keys ✅ Public Keys ❌ x509 Certificates ❌ x509 Certificates
Algorithm Choice Signing Keys None Lots Lots
Why not {DTLS, QUIC, WireGuard} instead?
  • In practice, all of the mentioned protocols make assumptions about UDP being beneath them. Even if the spec doesn't, the implementations usually do. Wireguard for example, assumes it has access to the IP address and UDP port of incoming messages.
  • QUIC and DTLS are client server, so Wireguard is really the only protocol designed for p2p communication. QUIC does have connection IDs which solves the problem of both sides initiating connections simultaneously.
  • QUIC doesn't provide a way to send Datagrams that doesn't assume it is running on UDP. You can't send a datagram larger than the max UDP size, for example.
  • QUIC and DTLS require x509 certificates which are complicated. There are almost a dozen parameters that a user needs to understand to create a self signed cert. Contrast that with Wireguard which passes around base64 encoded public keys.

Cryptography

P2PKE uses the Noise Protocol Framework's NN Handshake with the suite (X25519, ChaCha20Poly1309, BLAKE2b) to establish a secure channel. The channel binding is signed using a long-lived public signing key to authenticate the connection. There is no way to configure the cryptography used to establish the secure session.

There is a choice of signing algorithm, a few types of signing key are supported. P2PKE uses the p2p.Sign and p2p.Verify functions, which support RSA, DSA, and Ed25519 keys. The signature scheme includes an extra layer of hashing with CSHAKE256 which includes an application specific purpose tag.

Wire Protocol

Sessions pass messages between one another consisting of a 4 byte header. The header is a single 32 bit integer containing the counter used for the message. The rest of the message is called the body herein.

Certain low counter values are reserved for the handshake messages, and the rest are used as nonces for symmetric encryption. The counter values are considered when noise calculates an authentication tag for a message. The counter values are also used for replay protection.

Message Types
InitHello

This message has a counter value of 0. The message body is an NPF handshake message containing a protocol buffer, and an appended 16 bit big endian integer which is the length of the protocol buffer data. This makes it possible to parse an InitHello from the end of the message regardless of the size of the public key which noise has prepended.

The protocol buffer contains the initiators signing key, and a signature of a timestamp.

RespHello

This message has a counter value of 1. The message body is an NPF handshake message containing a protocol buffer.

The protocol buffer contains the responders signing key, and a signature of the channel binding.

InitDone

This message has a counter value of 2. The message body is an NPF symmetric message containing a protocol buffer.

The protocol buffer contains a signature of the channel binding from the initiator.

RespDone

This message has a counter value of 3. The message body is an NPF symmetric message containing an empty payload.

Data

Data messages have counter values >= 16 and <= 2^32 - 2. The non-counter portion is an NPF message containing application data.

Sessions

A Session encapsulates the handshake state machine, the symmetric ciphers, outbound counter, and replay filter. The idea is that you can just keep delivering messages to a session and it will either give you an error, or some data to send back, or data to deliver to the application.

The session has no internal timers or background goroutines. It only knows what time it is based on what the caller tells it. Sessions have a tree-like state machine, without any cycles, starting in an initial state and eventually expiring due to a handshake failure, or message count or time conditions. Sessions have a short lifecycle. They are bounded both by a maximum number of messages (2^32-2) and a maximum age (3 minutes).

Handshake

The Session handshake state machine looks like this:

                INITIATOR                       RESPONDER
0   SendingInitHello/AwaitingRespHello          AwaitingInitHello
1                                               SendingResponseHello/AwaitingInitDone
2   SendingInitDone/AwaitingRespDone
3                                               HandshakeComplete
4   HandshakeComplete

In each of these states except HandshakeComplete, the sessions will respond to the preceeding handshake messages with the next handshake message, even if they have already done so. This allows for retries.

The Initiator has authenticated the Responder after RespHello is received, but it cannot send until the Responder has authenticated it, or it risks having its messages dropped. The Responder will not buffer messages which it cannot authenticate. So the Initiator must wait until it receives application data or (more likely) the RespDone message.

The Responder has authenticated the Initiator after it has received the InitDone, but will respond to handshake messages until application data has been sent.

Channels

Channels are a long lived secure channel between two parties. Channels manage creating and discarding sessions as they expire. Once a channel has established a session all future sessions must have the same remote key.

The Channel API:

    // NewChannel creates a channel, there is no distinction between initiator or responder
    NewChannel(ChannelConfig) *Channel
    // Send is used to send an encrypted message across the channel, it may need to create a session.
    Send(ctx context.Context, x []byte) error
    // Deliver is used to deliver inbound messages to the channel.
    Deliver(out, x []byte) ([]byte, error)
Parameters (Required)
  • PrivateKey: PrivateKey The signing key to use when authenticating the Channel.
  • Send: func([]byte) A function which is called by the Channel to send data.
  • AcceptKey: func(PublicKey) bool A function which determines whether to connect to a party identifying as a given public key.

Versions

P2PKE is a versioned protocol; the InitHello message has a version field. The version determines all the cryptographic parameters in the protocol. There is no version negotiation. The initiator dictates what protocol to use, and the responder can respond with an error if it does not support that protocol. So if and when a version 2 shows up, peers will prefer version 2, but accept version 1, and then eventually reject version 1 completely.

Documentation

Index

Constants

View Source
const (
	// Overhead is the per message overhead taken up by P2PKE.
	Overhead = 4 + 16
	// MaxMessageLen is the maximum message size that applications can send through the channel.
	MaxMessageLen = noise.MaxMsgLen - Overhead

	// MaxNonce is the maxmium number of messages that can be sent through a channel.
	MaxNonce = math.MaxUint32 - 1
	// RekeyAfterTime is the default.
	RekeyAfterTime = 120 * time.Second
	// RejectAfterTime is the default.
	RejectAfterTime = 180 * time.Second
	// RekeyAfterMessages is the number of messages that can be sent over a session before a rekey is triggered.
	RekeyAfterMessages = MaxNonce / 2

	// KeepAliveTimeout is the default.
	KeepAliveTimeout = 15 * time.Second
	// HandshakeBackoff is the default.
	HandshakeBackoff = 250 * time.Millisecond
)
View Source
const (
	InitToResp = Direction(iota)
	RespToInit
)

Variables

View Source
var File_p2pke_proto protoreflect.FileDescriptor

Functions

func IsHello

func IsHello(x []byte) bool

func IsInitHello

func IsInitHello(x []byte) bool

IsInitHello returns true if x contains an InitHello message

func IsPostHandshake

func IsPostHandshake(x []byte) bool

func IsRespHello

func IsRespHello(x []byte) bool

IsRespHello returns true if x contains a RespHello message

func PrettyPrint

func PrettyPrint(msg Message) string

Types

type Channel

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

func NewChannel

func NewChannel(params ChannelConfig) *Channel

func (*Channel) Close

func (c *Channel) Close() error

Close releases all resources associated with the channel. send will not be called after Close completes.

func (*Channel) Deliver

func (c *Channel) Deliver(out, x []byte) ([]byte, error)

Deliver decrypts the payload in x if it contains application data, and appends it to out. if err != nil, then an error occured. The Channel is capable of recovering. if out != nil, then it is application data. if out == nil, then the message was either invalid or contained a handshake message and there is nothing more for the caller to do.

e.g. out, err := c.Deliver(nil, input)

if err != nil {
  // handle the error
} else if out != nil {

  // deliver application data
} else {

  // nothing to do
}

func (*Channel) LastReceived

func (c *Channel) LastReceived() time.Time

LastReceived returns the time that a message was received

func (*Channel) LastSent

func (c *Channel) LastSent() time.Time

LastSent returns the time that a message was last sent

func (*Channel) LocalKey

func (c *Channel) LocalKey() x509.PublicKey

LocalKey returns the public key used by the local party to authenticate. It will correspond to the private key passed to NewChannel.

func (*Channel) RemoteKey

func (c *Channel) RemoteKey() x509.PublicKey

RemoteKey returns the public key used by the remote party to authenticate. It can be nil, if there has been no successful handshake.

func (*Channel) Send

func (c *Channel) Send(ctx context.Context, x p2p.IOVec) error

Send will send an encrypted message containing x It may also send a handshake message. Send blocks until a Session has been established and the message can be sent or the context is cancelled.

func (*Channel) WaitReady

func (c *Channel) WaitReady(ctx context.Context) error

WaitReady blocks until a session has been established. It will initiate a session if none exists. After WaitReady returns: RemoteKey() != nil

type ChannelConfig

type ChannelConfig struct {
	Registry x509.Registry
	// PrivateKey is the signing key used to prove identity to the other party in the channel.
	// *REQUIRED*
	PrivateKey x509.PrivateKey
	// Send is used to send p2pke protocol messages including ciphertexts and handshake messages.
	// *REQUIRED*
	Send SendFunc
	// AcceptKey is used to check if a key is allowed before connecting
	// *REQUIRED*.
	AcceptKey func(*x509.PublicKey) bool
	// Logger is used for logging, nil disables logs.
	Logger *zap.Logger

	// KeepAliveTimeout is the amount of time to consider a session alive wihtout receiving a message
	// through it.
	KeepAliveTimeout time.Duration
	// HandshakeBackoff is the amount of time to wait between sending handshake messages.
	HandshakeBackoff time.Duration
	// RekeyAfterTime is the amount of time between rekeying a session.
	RekeyAfterTime time.Duration
	// RejectAfterTime is the duration after session creation when the session will send and
	// received messages.
	RejectAfterTime time.Duration
}

type Direction

type Direction uint8

func (Direction) String

func (d Direction) String() string

type ErrDecryptionFailure

type ErrDecryptionFailure struct {
	Nonce    uint32
	NoiseErr error
}

ErrDecryptionFailure is returned by a Session when a message failed to decrypt.

func (ErrDecryptionFailure) Error

func (e ErrDecryptionFailure) Error() string

type ErrEarlyData

type ErrEarlyData struct {
	State uint8
	Nonce uint32
}

ErrEarlyData is returned by the session when application data arrives early. There is no way to verify this data without a

func (ErrEarlyData) Error

func (e ErrEarlyData) Error() string

type ErrSessionExpired

type ErrSessionExpired struct {
	ExpiredAt time.Time
}

ErrSessionExpired is returned when the session is too old to be used anymore and needs to be put down.

func (ErrSessionExpired) Error

func (e ErrSessionExpired) Error() string

type InitDone

type InitDone struct {
	Sig []byte `protobuf:"bytes,1,opt,name=sig,proto3" json:"sig,omitempty"`
	// contains filtered or unexported fields
}

func (*InitDone) Descriptor deprecated

func (*InitDone) Descriptor() ([]byte, []int)

Deprecated: Use InitDone.ProtoReflect.Descriptor instead.

func (*InitDone) GetSig

func (x *InitDone) GetSig() []byte

func (*InitDone) ProtoMessage

func (*InitDone) ProtoMessage()

func (*InitDone) ProtoReflect

func (x *InitDone) ProtoReflect() protoreflect.Message

func (*InitDone) Reset

func (x *InitDone) Reset()

func (*InitDone) String

func (x *InitDone) String() string

type InitHello

type InitHello struct {
	Version         uint32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"`
	TimestampTai64N []byte `protobuf:"bytes,2,opt,name=timestamp_tai64n,json=timestampTai64n,proto3" json:"timestamp_tai64n,omitempty"`
	KeyX509         []byte `protobuf:"bytes,3,opt,name=key_x509,json=keyX509,proto3" json:"key_x509,omitempty"`
	Sig             []byte `protobuf:"bytes,4,opt,name=sig,proto3" json:"sig,omitempty"`
	// contains filtered or unexported fields
}

func (*InitHello) Descriptor deprecated

func (*InitHello) Descriptor() ([]byte, []int)

Deprecated: Use InitHello.ProtoReflect.Descriptor instead.

func (*InitHello) GetKeyX509

func (x *InitHello) GetKeyX509() []byte

func (*InitHello) GetSig

func (x *InitHello) GetSig() []byte

func (*InitHello) GetTimestampTai64N

func (x *InitHello) GetTimestampTai64N() []byte

func (*InitHello) GetVersion

func (x *InitHello) GetVersion() uint32

func (*InitHello) ProtoMessage

func (*InitHello) ProtoMessage()

func (*InitHello) ProtoReflect

func (x *InitHello) ProtoReflect() protoreflect.Message

func (*InitHello) Reset

func (x *InitHello) Reset()

func (*InitHello) String

func (x *InitHello) String() string

type Message

type Message []byte

func ParseMessage

func ParseMessage(x []byte) (Message, error)

ParseMessage

func (Message) Body

func (m Message) Body() []byte

func (Message) GetInitHello

func (m Message) GetInitHello() (*InitHello, error)

func (Message) GetNonce

func (m Message) GetNonce() uint32

func (Message) HeaderBytes

func (m Message) HeaderBytes() []byte

func (Message) SetNonce

func (m Message) SetNonce(n uint32)

type RespHello

type RespHello struct {
	KeyX509 []byte `protobuf:"bytes,1,opt,name=key_x509,json=keyX509,proto3" json:"key_x509,omitempty"`
	Sig     []byte `protobuf:"bytes,2,opt,name=sig,proto3" json:"sig,omitempty"`
	// contains filtered or unexported fields
}

func (*RespHello) Descriptor deprecated

func (*RespHello) Descriptor() ([]byte, []int)

Deprecated: Use RespHello.ProtoReflect.Descriptor instead.

func (*RespHello) GetKeyX509

func (x *RespHello) GetKeyX509() []byte

func (*RespHello) GetSig

func (x *RespHello) GetSig() []byte

func (*RespHello) ProtoMessage

func (*RespHello) ProtoMessage()

func (*RespHello) ProtoReflect

func (x *RespHello) ProtoReflect() protoreflect.Message

func (*RespHello) Reset

func (x *RespHello) Reset()

func (*RespHello) String

func (x *RespHello) String() string

type SendFunc

type SendFunc func([]byte)

SendFunc is the type of functions called to send messages by the channel.

type Session

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

func NewSession

func NewSession(params SessionConfig) *Session

func (*Session) Deliver

func (s *Session) Deliver(out []byte, incoming []byte, now time.Time) (bool, []byte, error)

Deliver gives the session a message. If there is data in the message it will be returned as a non nil slice, appended to out. If there is not data, then a nil slice, and nil error will be returned.

isApp, out, err := s.Deliver(out, incoming, now)

if err != nil {
		// handle err
}

if !isApp && len(out) > 0 {

} else if isApp {

}

func (*Session) ExpiresAt

func (s *Session) ExpiresAt() time.Time

func (*Session) Handshake

func (s *Session) Handshake(out []byte) []byte

Handshake appends the current handshake message to out. Handshake returns nil if there is no handshake message to send.

func (*Session) InitHelloTime

func (s *Session) InitHelloTime() tai64.TAI64N

func (*Session) IsInit

func (s *Session) IsInit() bool

func (*Session) IsReady

func (s *Session) IsReady() bool

IsReady returns true if the session is ready to send data.

func (*Session) LocalKey

func (s *Session) LocalKey() x509.PublicKey

func (*Session) RemoteKey

func (s *Session) RemoteKey() x509.PublicKey

func (*Session) Send

func (s *Session) Send(out, ptext []byte, now time.Time) ([]byte, error)

Send encrypts ptext and appends the ciphertext to out, returning out. It is an error to call Send before the handshake has completed.

func (*Session) String

func (s *Session) String() string

type SessionConfig

type SessionConfig struct {
	Registry    x509.Registry
	PrivateKey  x509.PrivateKey
	IsInit      bool
	Now         time.Time
	RejectAfter time.Duration
	Logger      *zap.Logger
}

SessionConfig configures a session all the parameters are required.

type Timer

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

func (*Timer) IsPending

func (timer *Timer) IsPending() bool

func (*Timer) Reset

func (t *Timer) Reset(d time.Duration)

func (*Timer) Stop

func (t *Timer) Stop()

func (*Timer) StopSync

func (t *Timer) StopSync()

Jump to

Keyboard shortcuts

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